fix(orchestration): restore siblings after uncertain stop

This commit is contained in:
Jinwoo-H
2026-08-16 23:37:29 -04:00
parent b484d7e77e
commit 50fb3486f3
2 changed files with 32 additions and 9 deletions
@@ -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' })
+10 -9
View File
@@ -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 {