From 50fb3486f35dfef2494ba3565e0aec1d7ff49a24 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sun, 16 Aug 2026 23:37:29 -0400 Subject: [PATCH] fix(orchestration): restore siblings after uncertain stop --- .../db-task-dispatch-lifecycle-guards.test.ts | 22 +++++++++++++++++++ src/main/runtime/orchestration/db.ts | 19 ++++++++-------- 2 files changed, 32 insertions(+), 9 deletions(-) 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 0111371f9fe..b4839b67aeb 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 @@ -296,6 +296,28 @@ describe('Task/Dispatch lifecycle guards', () => { expect(database.getDispatchContextById(abandoned.dispatchId)?.status).toBe('failed') }) + it('restores a live sibling after stopping an uncertain worker start', () => { + const database = createDatabase() + const task = database.createTask({ spec: 'uncertain legacy worker split' }) + const live = startWorker(database, task.id, 'uncertain_live') + sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) + const uncertain = database.createStartingWorkerDispatch({ taskId: task.id, startOptions: {} }) + database.markWorkerStartUnknown(uncertain.dispatch.id, 'agent_readiness', 'outcome unknown') + expect(database.getTask(task.id)?.status).toBe('blocked') + + expect(database.beginWorkerStop(uncertain.dispatch.id).disposition).toBe('stopping') + expect(database.settleWorkerStop(uncertain.dispatch.id).state).toBe('stopped') + expect(database.getTask(task.id)?.status).toBe('dispatched') + expect( + database.settleWorkerReport({ + taskId: task.id, + dispatchId: live.dispatchId, + outcome: 'succeeded', + result: 'live sibling completed' + }) + ).toEqual({ action: 'settled', outcome: 'succeeded', duplicate: false }) + }) + 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 60eafd9cd8f..e4a9cbc2be0 100644 --- a/src/main/runtime/orchestration/db.ts +++ b/src/main/runtime/orchestration/db.ts @@ -6418,7 +6418,7 @@ export class OrchestrationDb { WHERE id = ?` ) .run(dispatchId) - this.blockTaskWithoutActiveSibling(dispatch.task_id, dispatchId) + this.reconcileTaskAfterDispatchInterruption(dispatch.task_id, dispatchId) this.closeQuestionsForDispatch(dispatchId) this.db.exec('COMMIT') return { @@ -6454,7 +6454,7 @@ export class OrchestrationDb { WHERE id = ? AND status IN ('pending', 'dispatched')` ) .run(dispatchId) - this.blockTaskWithoutActiveSibling(dispatch.task_id, dispatchId) + this.reconcileTaskAfterDispatchInterruption(dispatch.task_id, dispatchId) this.db.exec('COMMIT') return this.getWorkerDispatch(dispatchId) as WorkerDispatchRow } catch (error) { @@ -6500,7 +6500,7 @@ export class OrchestrationDb { WHERE id = ? AND status IN ('pending', 'dispatched')` ) .run(dispatchId) - this.blockTaskWithoutActiveSibling(dispatch.task_id, dispatchId) + this.reconcileTaskAfterDispatchInterruption(dispatch.task_id, dispatchId) this.db.exec('COMMIT') return this.getWorkerDispatch(dispatchId) as WorkerDispatchRow } catch (error) { @@ -6598,7 +6598,7 @@ export class OrchestrationDb { WHERE id = ?` ) .run(dispatchId) - this.blockTaskWithoutActiveSibling(dispatch.task_id, dispatchId) + this.reconcileTaskAfterDispatchInterruption(dispatch.task_id, dispatchId) this.closeQuestionsForDispatch(dispatchId) this.db.exec('COMMIT') return { @@ -7330,16 +7330,17 @@ export class OrchestrationDb { .get(taskId) as DispatchContextRow | undefined } - private blockTaskWithoutActiveSibling(taskId: string, dispatchId: string): void { + private reconcileTaskAfterDispatchInterruption(taskId: string, dispatchId: string): void { this.db .prepare( - `UPDATE tasks SET status = 'blocked' - WHERE id = ? AND NOT EXISTS ( + `UPDATE tasks + SET status = CASE WHEN EXISTS ( SELECT 1 FROM dispatch_contexts WHERE task_id = tasks.id AND id != ? AND status IN ('pending', 'dispatched') - )` + ) THEN 'dispatched' ELSE 'blocked' END + WHERE id = ? AND status IN ('dispatched', 'blocked')` ) - .run(taskId, dispatchId) + .run(dispatchId, taskId) } getDispatchContextById(dispatchId: string): DispatchContextRow | undefined {