diff --git a/src/main/runtime/orchestration/db/worker-terminal/worker-terminal-attention-query.ts b/src/main/runtime/orchestration/db/worker-terminal/worker-terminal-attention-query.ts index 36f6bd3b3fe..c007632a1c9 100644 --- a/src/main/runtime/orchestration/db/worker-terminal/worker-terminal-attention-query.ts +++ b/src/main/runtime/orchestration/db/worker-terminal/worker-terminal-attention-query.ts @@ -18,10 +18,13 @@ export type WorkerAttentionFacts = { terminationReason: TerminalExitCause['kind'] | null isRoot: boolean workerState: WorkerDispatchState | null + workerStage: string | null dispatchStatus: DispatchStatus /** Execution host of the worker's terminal resource; a remote row with no connection id is * unverifiable. `undefined` when no resource was ever materialized. */ hostScope?: string | null + /** A released resource is an execution-host confirmation that the terminal is gone. */ + releaseState?: string | null } export function getWorkerAttentionFactsForDispatches( @@ -37,8 +40,9 @@ export function getWorkerAttentionFactsForDispatches( const rows = this.db .prepare( `SELECT d.id AS dispatch_id, d.task_id, d.status AS dispatch_status, - d.termination_reason, w.state AS worker_state, t.parent_id AS parent_task_id, - r.id AS resource_id, r.host_scope, + d.termination_reason, w.state AS worker_state, w.stage AS worker_stage, + t.parent_id AS parent_task_id, + r.id AS resource_id, r.host_scope, r.release_state, EXISTS ( SELECT 1 FROM question_threads q WHERE q.dispatch_id = d.id AND q.status = 'pending' @@ -65,9 +69,11 @@ export function getWorkerAttentionFactsForDispatches( dispatch_status: DispatchStatus termination_reason: TerminalExitCause['kind'] | null worker_state: WorkerDispatchState | null + worker_stage: string | null parent_task_id: string | null resource_id: string | null host_scope: string | null + release_state: string | null pending_input: number pending_approval: number pending_guidance: number @@ -105,8 +111,11 @@ export function getWorkerAttentionFactsForDispatches( terminationReason: row.termination_reason, isRoot: row.parent_task_id === null, workerState: row.worker_state, + workerStage: row.worker_stage, dispatchStatus: row.dispatch_status, - ...(row.resource_id === null ? {} : { hostScope: row.host_scope }) + ...(row.resource_id === null + ? {} + : { hostScope: row.host_scope, releaseState: row.release_state }) } ] }) diff --git a/src/main/runtime/orchestration/worker-attention-context.test.ts b/src/main/runtime/orchestration/worker-attention-context.test.ts index 24c4b90e0d1..c25733086c6 100644 --- a/src/main/runtime/orchestration/worker-attention-context.test.ts +++ b/src/main/runtime/orchestration/worker-attention-context.test.ts @@ -15,6 +15,7 @@ function facts(overrides: Partial = {}): WorkerAttentionFa terminationReason: null, isRoot: false, workerState: 'ready', + workerStage: 'prompt_delivered', dispatchStatus: 'dispatched', ...overrides } @@ -68,6 +69,37 @@ describe('worker attention liveness', () => { expect(attention).toEqual({ categories: [], requiresAction: false }) }) + it('reads a released resource as exited, not as a stale live pane', () => { + const attention = projectWorkerAttentionContext({ + // worker-list called the same dispatch exited while this pane classified from a status. + facts: facts({ + outcome: 'outcome_unknown', + hostScope: '{"kind":"local","hostId":"local"}', + releaseState: 'released' + }), + isRoot: false, + status: status({ evidenceObservedAt: NOW - AGENT_STATUS_STALE_AFTER_MS - 60_000 }), + now: NOW + }) + + expect(attention).toEqual({ categories: [], requiresAction: false }) + }) + + it('reads a released worker stage as exited, not as a stale live pane', () => { + const attention = projectWorkerAttentionContext({ + facts: facts({ + outcome: 'outcome_unknown', + workerStage: 'released', + hostScope: '{"kind":"local","hostId":"local"}' + }), + isRoot: false, + status: status({ evidenceObservedAt: NOW - AGENT_STATUS_STALE_AFTER_MS - 60_000 }), + now: NOW + }) + + expect(attention).toEqual({ categories: [], requiresAction: false }) + }) + it('treats a settled worker stop as exited rather than unverifiable', () => { const attention = projectWorkerAttentionContext({ facts: facts({ workerState: 'stopped', outcome: 'in_progress' }), diff --git a/src/main/runtime/orchestration/worker-attention-context.ts b/src/main/runtime/orchestration/worker-attention-context.ts index cae42edbb6e..c252f0c4be1 100644 --- a/src/main/runtime/orchestration/worker-attention-context.ts +++ b/src/main/runtime/orchestration/worker-attention-context.ts @@ -52,8 +52,12 @@ export function projectWorkerAttentionContext(args: { liveness: projectLiveness( { workerState: args.facts.workerState, + workerStage: args.facts.workerStage, terminationReason: args.facts.terminationReason, - resource: args.facts.hostScope === undefined ? null : { hostScope: args.facts.hostScope } + resource: + args.facts.hostScope === undefined + ? null + : { hostScope: args.facts.hostScope, releaseState: args.facts.releaseState } }, args.status, args.now