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 cf18fe52fc8..0111371f9fe 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 @@ -83,6 +83,29 @@ describe('Task/Dispatch lifecycle guards', () => { } ) + it('settles a newer context-only legacy sibling after a worker report', () => { + const database = createDatabase() + const task = database.createTask({ spec: 'reversed legacy mixed split' }) + const worker = startWorker(database, task.id, 'reversed_reporter') + sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) + const contextOnly = database.createDispatchContext(task.id, 'term_reversed_context') + + expect( + database.settleWorkerReport({ + taskId: task.id, + dispatchId: worker.dispatchId, + outcome: 'succeeded', + result: 'reversed sibling completed' + }) + ).toEqual({ action: 'settled', outcome: 'succeeded', duplicate: false }) + expect(database.getTask(task.id)?.status).toBe('completed') + expect(database.getDispatchContextById(contextOnly.id)).toMatchObject({ + status: 'completed', + capability_revoked_at: expect.any(String) + }) + expect(database.getActiveDispatchForTerminal('term_reversed_context')).toBeUndefined() + }) + it('rejects generic failure while a supervised worker remains active', () => { const database = createDatabase() const task = database.createTask({ spec: 'supervised failure guard' }) diff --git a/src/main/runtime/orchestration/db.ts b/src/main/runtime/orchestration/db.ts index 6c1a12bc474..545397faf46 100644 --- a/src/main/runtime/orchestration/db.ts +++ b/src/main/runtime/orchestration/db.ts @@ -7608,15 +7608,6 @@ export class OrchestrationDb { reason: `inactive dispatch ${params.dispatchId}: it or task ${params.taskId} is already settled.` } } - const latest = this.getActiveDispatchForTask(params.taskId) - if (latest?.id !== params.dispatchId) { - return { - action: 'rejected', - code: 'stale_dispatch', - reason: `Dispatch ${params.dispatchId} is not the current dispatch for task ${params.taskId}.` - } - } - const conflictingWorker = this.db .prepare( `SELECT active.id @@ -7635,6 +7626,15 @@ export class OrchestrationDb { reason: `Task ${params.taskId} still has active supervised Dispatch ${conflictingWorker.id}; stop or settle it before completing ${params.dispatchId}.` } } + const reportingWorker = this.getWorkerDispatch(params.dispatchId) + const latest = this.getActiveDispatchForTask(params.taskId) + if (!reportingWorker && latest?.id !== params.dispatchId) { + return { + action: 'rejected', + code: 'stale_dispatch', + reason: `Dispatch ${params.dispatchId} is not the current dispatch for task ${params.taskId}.` + } + } const siblingDispatchIds = this.db .prepare( `SELECT id FROM dispatch_contexts