Skip to content

Commit b14f97c

Browse files
authored
fix(kiloclaw): replace provision locks with registry admission (#3611)
* fix(kiloclaw): replace provision locks with registry admission * fix(kiloclaw): recover delayed reservation release * fix(kiloclaw): preserve legacy registry routing * test(kiloclaw): stabilize date-sensitive fixtures * fix(kiloclaw): keep legacy registry fallback routable * docs(kiloclaw): document provision admission state * fix(kiloclaw): map missing provision instances
1 parent aca1e0f commit b14f97c

30 files changed

Lines changed: 2661 additions & 554 deletions

.specs/kiloclaw-datamodel.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -392,11 +392,6 @@ not yet enforced in the current codebase:
392392
across all services that mutate subscription records. Some
393393
subscription-creation paths may already write change-log entries;
394394
complete cross-service coverage remains the intended invariant.
395-
4. Fresh Provision Admission SHOULD be implemented in the Registry-backed
396-
Worker admission flow before the existing web advisory lock is removed.
397-
(Currently, web requests use transitional PostgreSQL advisory-lock
398-
coordination that is being replaced because it is unsafe through
399-
transaction-pooled production connections.)
400395

401396
## Changelog
402397

apps/web/src/app/(app)/claw/components/billing/ReferralRewardStatusCard.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ describe('ReferralRewardStatusCard', () => {
8181
application: {
8282
appliedAt: '2026-04-10T00:05:00.000Z',
8383
subscriptionId: '11111111-1111-4111-8111-111111111111',
84-
previousRenewalBoundary: '2026-05-01T00:00:00.000Z',
85-
newRenewalBoundary: '2026-06-01T00:00:00.000Z',
84+
previousRenewalBoundary: '2026-05-01T12:00:00.000Z',
85+
newRenewalBoundary: '2026-06-01T12:00:00.000Z',
8686
},
8787
},
8888
{

apps/web/src/app/(app)/claw/components/billing/ReferralRewardsSummary.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ describe('ReferralRewardsSummary', () => {
3030
role: 'referrer',
3131
appliedAt: '2026-04-10T00:05:00.000Z',
3232
monthsGranted: 1,
33-
previousRenewalBoundary: '2026-05-01T00:00:00.000Z',
34-
newRenewalBoundary: '2026-06-01T00:00:00.000Z',
33+
previousRenewalBoundary: '2026-05-01T12:00:00.000Z',
34+
newRenewalBoundary: '2026-06-01T12:00:00.000Z',
3535
},
3636
{
3737
role: 'referee',
3838
appliedAt: '2026-04-11T00:05:00.000Z',
3939
monthsGranted: 1,
40-
previousRenewalBoundary: '2026-06-01T00:00:00.000Z',
41-
newRenewalBoundary: '2026-07-01T00:00:00.000Z',
40+
previousRenewalBoundary: '2026-06-01T12:00:00.000Z',
41+
newRenewalBoundary: '2026-07-01T12:00:00.000Z',
4242
},
4343
],
4444
},

apps/web/src/app/admin/components/KiloclawReferralsInvestigation.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ function referralRow(params: {
4646
id: `${params.referralId}-application`,
4747
beneficiaryUserId: 'referrer-1',
4848
subscriptionId: '55555555-5555-4555-8555-555555555555',
49-
previousRenewalBoundary: '2026-05-01T00:00:00.000Z',
50-
newRenewalBoundary: '2026-06-01T00:00:00.000Z',
49+
previousRenewalBoundary: '2026-05-01T12:00:00.000Z',
50+
newRenewalBoundary: '2026-06-01T12:00:00.000Z',
5151
appliedAt: '2026-04-10T00:05:00.000Z',
5252
},
5353
]

apps/web/src/lib/impact/kiloclaw-referrals.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ async function insertActivePersonalSubscription(
145145
plan: 'standard',
146146
status: 'active',
147147
current_period_start: '2026-04-01T00:00:00.000Z',
148-
current_period_end: '2026-05-01T00:00:00.000Z',
149-
credit_renewal_at: '2026-05-01T00:00:00.000Z',
148+
current_period_end: '2026-05-01T12:00:00.000Z',
149+
credit_renewal_at: '2026-05-01T12:00:00.000Z',
150150
cancel_at_period_end: false,
151151
...overrides,
152152
})
@@ -636,7 +636,7 @@ describe('kiloclaw referrals', () => {
636636
expect(applications).toHaveLength(2);
637637
expect(
638638
applications.map(application => String(application.new_renewal_boundary)).sort()
639-
).toEqual(['2026-06-01 00:00:00+00', '2026-06-01 00:00:00+00']);
639+
).toEqual(['2026-06-01 12:00:00+00', '2026-06-01 12:00:00+00']);
640640

641641
const subscriptions = await db
642642
.select({
@@ -650,13 +650,13 @@ describe('kiloclaw referrals', () => {
650650
expect.arrayContaining([
651651
expect.objectContaining({
652652
userId: referrer.id,
653-
currentPeriodEnd: '2026-06-01 00:00:00+00',
654-
creditRenewalAt: '2026-06-01 00:00:00+00',
653+
currentPeriodEnd: '2026-06-01 12:00:00+00',
654+
creditRenewalAt: '2026-06-01 12:00:00+00',
655655
}),
656656
expect.objectContaining({
657657
userId: referee.id,
658-
currentPeriodEnd: '2026-06-01 00:00:00+00',
659-
creditRenewalAt: '2026-06-01 00:00:00+00',
658+
currentPeriodEnd: '2026-06-01 12:00:00+00',
659+
creditRenewalAt: '2026-06-01 12:00:00+00',
660660
}),
661661
])
662662
);
@@ -1573,7 +1573,7 @@ describe('kiloclaw referrals', () => {
15731573
.select()
15741574
.from(kiloclaw_subscriptions)
15751575
.where(eq(kiloclaw_subscriptions.user_id, referee.id));
1576-
expect(subscription.current_period_end).toBe('2026-05-01 00:00:00+00');
1576+
expect(subscription.current_period_end).toBe('2026-05-01 12:00:00+00');
15771577

15781578
const refereeRewards = await db
15791579
.select({
@@ -1646,7 +1646,7 @@ describe('kiloclaw referrals', () => {
16461646
'sub_referee_123',
16471647
expect.objectContaining({
16481648
proration_behavior: 'none',
1649-
trial_end: Math.floor(new Date('2026-06-01T00:00:00.000Z').getTime() / 1000),
1649+
trial_end: Math.floor(new Date('2026-06-01T12:00:00.000Z').getTime() / 1000),
16501650
}),
16511651
expect.objectContaining({
16521652
idempotencyKey: expect.stringContaining('stripe-apply'),

apps/web/src/lib/kilo-pass/apple-store-notifications.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function transaction(
5555
bundleId: 'com.kilocode.kiloapp',
5656
productId: 'kilopass.tier19.monthly.v1',
5757
purchaseDate: 1_777_626_000_000,
58-
expiresDate: 1_780_218_000_000,
58+
expiresDate: Date.parse('2030-06-01T00:00:00.000Z'),
5959
environment: 'Sandbox',
6060
rawPayload: { test: true },
6161
...overrides,
@@ -1152,8 +1152,8 @@ describe('processAppStoreKiloPassNotification', () => {
11521152
transactionId: tx1,
11531153
productId: 'kilopass.tier19.monthly.v1',
11541154
appAccountToken: user.app_store_account_token,
1155-
purchaseDate: Date.parse('2026-05-01T00:00:00.000Z'),
1156-
expiresDate: Date.parse('2026-05-31T00:00:00.000Z'),
1155+
purchaseDate: Date.parse('2026-06-01T00:00:00.000Z'),
1156+
expiresDate: Date.parse('2026-07-01T00:00:00.000Z'),
11571157
currency: 'USD',
11581158
price: 19000,
11591159
}),
@@ -1173,8 +1173,8 @@ describe('processAppStoreKiloPassNotification', () => {
11731173
transactionId: tx2,
11741174
productId: 'kilopass.tier49.monthly.v1',
11751175
appAccountToken: user.app_store_account_token,
1176-
purchaseDate: Date.parse('2026-05-16T00:00:00.000Z'),
1177-
expiresDate: Date.parse('2026-06-16T00:00:00.000Z'),
1176+
purchaseDate: Date.parse('2026-06-16T00:00:00.000Z'),
1177+
expiresDate: Date.parse('2026-07-16T00:00:00.000Z'),
11781178
currency: 'USD',
11791179
price: 49000,
11801180
}),
@@ -1188,7 +1188,7 @@ describe('processAppStoreKiloPassNotification', () => {
11881188
const issuance = await db.query.kilo_pass_issuances.findFirst({
11891189
where: and(
11901190
eq(kilo_pass_issuances.kilo_pass_subscription_id, subscription?.id ?? ''),
1191-
eq(kilo_pass_issuances.issue_month, '2026-05-01')
1191+
eq(kilo_pass_issuances.issue_month, '2026-06-01')
11921192
),
11931193
});
11941194
expect(issuance).toBeDefined();

apps/web/src/lib/kiloclaw/kiloclaw-internal-client.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,21 @@ export class KiloClawInternalClient {
303303
);
304304
}
305305

306+
async repairProvisionReservation(
307+
userId: string,
308+
instanceId: string,
309+
orgId?: string
310+
): Promise<{ ok: true }> {
311+
return this.request(
312+
'/api/platform/provision/repair-reservation',
313+
{
314+
method: 'POST',
315+
body: JSON.stringify({ userId, instanceId, orgId }),
316+
},
317+
{ userId }
318+
);
319+
}
320+
306321
async start(
307322
userId: string,
308323
instanceId?: string,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { TRPCError } from '@trpc/server';
2+
import { UpstreamApiError } from '@/lib/trpc/init';
3+
import { KiloClawApiError } from './kiloclaw-internal-client';
4+
5+
type ProvisionErrorPayload = { message?: string; code?: string };
6+
7+
type ProvisionErrorPayloadReader = (err: KiloClawApiError) => ProvisionErrorPayload;
8+
9+
export function handleProvisionError(err: unknown, getPayload: ProvisionErrorPayloadReader): never {
10+
if (err instanceof KiloClawApiError) {
11+
const { message, code } = getPayload(err);
12+
if (
13+
(err.statusCode === 409 || err.statusCode === 503) &&
14+
(code === 'provision_in_progress' ||
15+
code === 'provision_completion_pending' ||
16+
code === 'instance_already_active' ||
17+
code === 'instance_destroyed')
18+
) {
19+
throw new TRPCError({
20+
code: 'CONFLICT',
21+
message:
22+
message ??
23+
'An instance is already being created. Wait for setup to finish, then try again.',
24+
cause: new UpstreamApiError(code),
25+
});
26+
}
27+
if (err.statusCode === 404 && code === 'instance_not_found') {
28+
throw new TRPCError({
29+
code: 'NOT_FOUND',
30+
message: message ?? 'No active KiloClaw instance found',
31+
cause: new UpstreamApiError(code),
32+
});
33+
}
34+
}
35+
throw err;
36+
}

0 commit comments

Comments
 (0)