@@ -17,11 +17,13 @@ import {
1717 inArray ,
1818 isNotNull ,
1919 isNull ,
20+ lt ,
2021 notInArray ,
2122 or ,
2223 type SQL ,
2324 sql ,
2425} from 'drizzle-orm'
26+ import { SIM_TOOL_EXECUTION_VERSION } from '@/lib/mothership/async-runs/lifecycle'
2527import { publishChatStatusChanged } from '@/lib/mothership/chat-status'
2628import { findStreamsWithReplay } from '@/lib/mothership/request/session/buffer'
2729import { findStreamsHoldingChatLock } from '@/lib/mothership/request/session/controller-lease'
@@ -50,14 +52,16 @@ const UNFINISHED_RUN_STATUSES: CopilotRunStatus[] = [
5052export const ORPHANED_RUN_GRACE_MS = 60 * 60 * 1000
5153
5254/**
53- * Runs admitted without a chat lease (headless turns and rows from before the lease
54- * protocol) have no liveness signal, so only an age far past any process lifetime
55- * proves them dead.
55+ * Runs admitted by code predating the current tool-execution protocol. Every run the
56+ * current code admits records the current version, so once a deploy has replaced the
57+ * processes that admitted these, none can be live; the age only leaves room for a
58+ * rollout. A current run without a lease (a headless turn) is never swept: it has no
59+ * liveness signal and its own lifecycle always settles it.
5660 */
57- export const UNLEASED_RUN_GRACE_MS = 24 * 60 * 60 * 1000
61+ export const LEGACY_RUN_GRACE_MS = 24 * 60 * 60 * 1000
5862
5963export const ORPHANED_RUN_ERROR = 'This response was interrupted before it finished.'
60- export const UNLEASED_RUN_ERROR = 'Run was never finalized (no controller lease).'
64+ export const LEGACY_RUN_ERROR = 'Run was never finalized (pre- lease run ).'
6165
6266const SWEEP_BATCH_SIZE = 500
6367const SWEEP_MAX_ROWS_PER_RUN = 5_000
@@ -71,8 +75,12 @@ function idleFor(ms: number): SQL {
7175}
7276
7377const leasedRunIdle = and ( isNotNull ( controllerToken ) , idleFor ( ORPHANED_RUN_GRACE_MS ) )
74- const unleasedRunIdle = and ( isNull ( controllerToken ) , idleFor ( UNLEASED_RUN_GRACE_MS ) )
75- const orphanIdle = or ( leasedRunIdle , unleasedRunIdle )
78+ const legacyRunIdle = and (
79+ isNull ( controllerToken ) ,
80+ lt ( copilotRuns . toolExecutionVersion , SIM_TOOL_EXECUTION_VERSION ) ,
81+ idleFor ( LEGACY_RUN_GRACE_MS )
82+ )
83+ const orphanIdle = or ( leasedRunIdle , legacyRunIdle )
7684
7785/** The user pressed Stop on this stream; a newer turn also closes tool admission, without one. */
7886const stopRequested = sql `(EXISTS (SELECT 1 FROM ${ copilotRequestStops } s
@@ -105,11 +113,11 @@ const unownedRunColumns = {
105113type Transaction = Parameters < Parameters < typeof db . transaction > [ 0 ] > [ 0 ]
106114
107115/** A stopped run ends cancelled; the sweep ends it cancelled only if its user pressed Stop. */
108- function terminalValues ( reason : 'stopped' | 'orphaned' | 'unleased ' ) {
116+ function terminalValues ( reason : 'stopped' | 'orphaned' | 'legacy ' ) {
109117 if ( reason === 'stopped' ) {
110118 return { status : sql `'cancelled'::copilot_run_status` , error : sql `NULL::text` }
111119 }
112- const error = reason === 'orphaned' ? ORPHANED_RUN_ERROR : UNLEASED_RUN_ERROR
120+ const error = reason === 'orphaned' ? ORPHANED_RUN_ERROR : LEGACY_RUN_ERROR
113121 return {
114122 status : sql `(CASE WHEN ${ stopRequested } THEN 'cancelled' ELSE 'error' END)::copilot_run_status` ,
115123 error : sql `CASE WHEN ${ stopRequested } THEN NULL ELSE ${ error } ::text END` ,
@@ -122,8 +130,8 @@ function terminalValues(reason: 'stopped' | 'orphaned' | 'unleased') {
122130 * which write the same row, wins or loses atomically against it.
123131 *
124132 * Chat rows are locked first, in id order, as a controller's claim does, so the two
125- * never wait on each other in opposite orders. A run without a lease keeps its last
126- * write as its completion and retention time. The chat marker is released without
133+ * never wait on each other in opposite orders. A legacy run keeps its last write as its
134+ * completion and retention time. The chat marker is released without
127135 * touching the chat's ordering timestamp.
128136 */
129137async function settleRuns (
@@ -169,7 +177,7 @@ async function settleRuns(
169177 await apply (
170178 runs . filter ( ( run ) => run . controllerToken === null ) ,
171179 isNull ( controllerToken ) ,
172- { ...terminalValues ( 'unleased ' ) , completedAt : sql `${ copilotRuns . updatedAt } ` }
180+ { ...terminalValues ( 'legacy ' ) , completedAt : sql `${ copilotRuns . updatedAt } ` }
173181 )
174182 for ( const run of runs ) {
175183 if ( run . controllerToken === null ) continue
@@ -212,28 +220,28 @@ function announceSettled(runs: UnownedRun[]): void {
212220/** The candidates no controller owns; leased runs are skipped when ownership is unreadable. */
213221async function withoutOwners ( candidates : UnownedRun [ ] ) : Promise < UnownedRun [ ] > {
214222 const leased = candidates . filter ( ( run ) => run . controllerToken !== null )
215- const unleased = candidates . filter ( ( run ) => run . controllerToken === null )
216- if ( leased . length === 0 ) return unleased
223+ const legacy = candidates . filter ( ( run ) => run . controllerToken === null )
224+ if ( leased . length === 0 ) return legacy
217225 try {
218226 const [ locked , replayable ] = await Promise . all ( [
219227 findStreamsHoldingChatLock ( leased ) ,
220228 findStreamsWithReplay ( leased . map ( ( run ) => run . streamId ) ) ,
221229 ] )
222- return unleased . concat (
230+ return legacy . concat (
223231 leased . filter ( ( run ) => ! locked . has ( run . streamId ) && ! replayable . has ( run . streamId ) )
224232 )
225233 } catch ( error ) {
226234 logger . warn ( 'Chat stream ownership is unreadable; leaving leased runs for a later sweep' , {
227235 error : getErrorMessage ( error ) ,
228236 } )
229- return unleased
237+ return legacy
230238 }
231239}
232240
233241/**
234242 * Settles runs that no controller will ever finish: a leased run whose stream holds no
235- * chat lock and has no replay buffer left, idle past the recovery window, and a run
236- * without a lease idle past any process lifetime . A failed batch is logged and skipped.
243+ * chat lock and has no replay buffer left, idle past the recovery window, and a legacy
244+ * run from before the current protocol . A failed batch is logged and skipped.
237245 */
238246export async function sweepOrphanedRuns ( ) : Promise < { settledRunIds : string [ ] } > {
239247 const settledRunIds : string [ ] = [ ]
0 commit comments