From 2b3a6aa91de81524e38139bf840e6e256b973da2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sun, 16 Aug 2026 22:56:24 -0400 Subject: [PATCH] fix(orchestration): release final active context --- .../context-only-dispatch-release.ts | 13 ++++---- .../db-task-dispatch-lifecycle-guards.test.ts | 33 +++++++++++++++++++ src/main/runtime/orchestration/db.ts | 14 ++------ 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/main/runtime/orchestration/context-only-dispatch-release.ts b/src/main/runtime/orchestration/context-only-dispatch-release.ts index 966684df4d7..f293e5d57ec 100644 --- a/src/main/runtime/orchestration/context-only-dispatch-release.ts +++ b/src/main/runtime/orchestration/context-only-dispatch-release.ts @@ -12,14 +12,13 @@ export type ContextOnlyDispatchReleaseResult = { export function releaseContextOnlyDispatch( db: Database.Database, dispatch: DispatchContextRow, - latestDispatchId: string | undefined, requestedState: 'abandoned' | 'stopped' ): ContextOnlyDispatchReleaseResult { if (dispatch.status !== 'pending' && dispatch.status !== 'dispatched') { return { state: persistedReleaseState(dispatch), alreadySettled: true, - releasedCurrentTask: latestDispatchId === dispatch.id + releasedCurrentTask: false } } @@ -36,10 +35,12 @@ export function releaseContextOnlyDispatch( WHERE task_id = ? AND status IN ('pending', 'dispatched') LIMIT 1` ) .get(dispatch.task_id) - const releasedCurrentTask = latestDispatchId === dispatch.id && !remaining - if (releasedCurrentTask) { - db.prepare("UPDATE tasks SET status = 'blocked' WHERE id = ?").run(dispatch.task_id) - } + const releasedCurrentTask = Boolean( + !remaining && + db + .prepare("UPDATE tasks SET status = 'blocked' WHERE id = ? AND status = 'dispatched'") + .run(dispatch.task_id).changes + ) return { state: requestedState, alreadySettled: false, releasedCurrentTask } } diff --git a/src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts b/src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts index b156865c2ae..f57bc0e55c6 100644 --- a/src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts +++ b/src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts @@ -191,6 +191,39 @@ describe('Task/Dispatch lifecycle guards', () => { } ) + it.each(['stop', 'abandon'] as const)( + '%s releases the last context-only sibling after a newer worker start fails', + (operation) => { + const database = createDatabase() + const task = database.createTask({ spec: `${operation} historical sibling` }) + const contextOnly = database.createDispatchContext(task.id, `term_${operation}`) + sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) + const failed = database.createStartingWorkerDispatch({ taskId: task.id, startOptions: {} }) + + database.failWorkerStart(failed.dispatch.id, 'start_failed', 'worker failed to start') + expect(database.getTask(task.id)?.status).toBe('dispatched') + + const released = + operation === 'stop' + ? database.beginWorkerStop(contextOnly.id) + : database.abandonWorkerDispatch(contextOnly.id) + expect(released).toMatchObject({ + disposition: 'context_only', + alreadySettled: false, + releasedCurrentTask: true + }) + expect(database.getTask(task.id)?.status).toBe('blocked') + expect(database.getDispatchContextById(contextOnly.id)?.status).toBe('failed') + expect(database.getActiveDispatchForTerminal(`term_${operation}`)).toBeUndefined() + expect(() => + database.createDispatchContext( + database.createTask({ spec: `${operation} later work` }).id, + `term_${operation}` + ) + ).not.toThrow() + } + ) + it('rejects gate creation while a supervised worker remains active', () => { const database = createDatabase() const task = database.createTask({ spec: 'worker gate guard' }) diff --git a/src/main/runtime/orchestration/db.ts b/src/main/runtime/orchestration/db.ts index 70312d2a4b7..1f0f7528d4a 100644 --- a/src/main/runtime/orchestration/db.ts +++ b/src/main/runtime/orchestration/db.ts @@ -6383,12 +6383,7 @@ export class OrchestrationDb { throw new OrchestrationError('dispatch_not_found', `Dispatch ${dispatchId} was not found.`) } if (!worker) { - const released = releaseContextOnlyDispatch( - this.db, - dispatch, - this.getDispatchContext(dispatch.task_id)?.id, - 'stopped' - ) + const released = releaseContextOnlyDispatch(this.db, dispatch, 'stopped') if (!released.alreadySettled) { this.closeQuestionsForDispatch(dispatchId) } @@ -6560,12 +6555,7 @@ export class OrchestrationDb { throw new OrchestrationError('dispatch_not_found', `Dispatch ${dispatchId} was not found.`) } if (!worker) { - const released = releaseContextOnlyDispatch( - this.db, - dispatch, - this.getDispatchContext(dispatch.task_id)?.id, - 'abandoned' - ) + const released = releaseContextOnlyDispatch(this.db, dispatch, 'abandoned') if (!released.alreadySettled) { this.closeQuestionsForDispatch(dispatchId) }