fix(orchestration): give the per-pane attention verdict the full liveness subject

The attention context passed no workerStage and no resource releaseState to
projectLiveness, so a released worker's pane could classify live from a
stale agent status while worker-list called the same dispatch exited.
This commit is contained in:
Jinwoo-H
2026-09-04 15:32:59 -04:00
parent 5f92f8c220
commit 118228bc0d
3 changed files with 49 additions and 4 deletions
@@ -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 })
}
]
})
@@ -15,6 +15,7 @@ function facts(overrides: Partial<WorkerAttentionFacts> = {}): 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' }),
@@ -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