@@ -209,7 +209,7 @@ export async function applyStripeFundedKiloClawPeriod(params: {
209209 const commitEndsAt = plan === 'commit' ? periodEnd : null ;
210210
211211 // If the row doesn't exist yet (settlement arrived before subscription.created),
212- // look up the user's active instance so the INSERT path can populate instance_id.
212+ // look up the user's active instance so we can populate instance_id.
213213 let instanceId = existingRow ?. instance_id ?? null ;
214214 if ( ! existingRow ) {
215215 const [ activeInstance ] = await tx
@@ -218,9 +218,57 @@ export async function applyStripeFundedKiloClawPeriod(params: {
218218 . where ( and ( eq ( kiloclaw_instances . user_id , userId ) , isNull ( kiloclaw_instances . destroyed_at ) ) )
219219 . limit ( 1 ) ;
220220 instanceId = activeInstance ?. id ?? null ;
221+
222+ // The instance may already have a subscription row (e.g. a trial, or a row
223+ // inserted by subscription.created arriving just before us). Inserting a new
224+ // row keyed on stripe_subscription_id would violate the partial unique index
225+ // UQ_kiloclaw_subscriptions_instance. Update the existing instance row
226+ // in-place instead, setting the stripe_subscription_id and converting it to
227+ // hybrid state. See KILOCODE-WEB-1JJF.
228+ if ( instanceId ) {
229+ const [ instanceRow ] = await tx
230+ . select ( {
231+ suspended_at : kiloclaw_subscriptions . suspended_at ,
232+ scheduled_plan : kiloclaw_subscriptions . scheduled_plan ,
233+ } )
234+ . from ( kiloclaw_subscriptions )
235+ . where ( eq ( kiloclaw_subscriptions . instance_id , instanceId ) )
236+ . limit ( 1 ) ;
237+
238+ if ( instanceRow ) {
239+ wasSuspended = ! ! instanceRow . suspended_at ;
240+ resolvedInstanceId = instanceId ;
241+ const shouldClearSchedule = instanceRow . scheduled_plan === plan ;
242+
243+ await tx
244+ . update ( kiloclaw_subscriptions )
245+ . set ( {
246+ stripe_subscription_id : stripeSubscriptionId ,
247+ payment_source : 'credits' ,
248+ status : 'active' ,
249+ plan,
250+ current_period_start : periodStart ,
251+ current_period_end : periodEnd ,
252+ credit_renewal_at : periodEnd ,
253+ commit_ends_at : commitEndsAt ,
254+ past_due_since : null ,
255+ auto_top_up_triggered_for_period : null ,
256+ ...( shouldClearSchedule
257+ ? { scheduled_plan : null , scheduled_by : null , stripe_schedule_id : null }
258+ : { } ) ,
259+ } )
260+ . where ( eq ( kiloclaw_subscriptions . instance_id , instanceId ) ) ;
261+
262+ return ;
263+ }
264+ }
221265 }
222266
223267 // Upsert the subscription row to hybrid state, keyed on stripe_subscription_id.
268+ // Reached when: (a) a row already exists for this stripe_subscription_id (normal
269+ // renewal), or (b) no row exists for either stripe_subscription_id or instance_id
270+ // (first settlement with no prior instance row — rare but possible if the instance
271+ // was destroyed between checkout and webhook delivery).
224272 await tx
225273 . insert ( kiloclaw_subscriptions )
226274 . values ( {
0 commit comments