diff --git a/src/shared/agent-hook-listener.test.ts b/src/shared/agent-hook-listener.test.ts index 699bc639841..0e327a886d2 100644 --- a/src/shared/agent-hook-listener.test.ts +++ b/src/shared/agent-hook-listener.test.ts @@ -3177,6 +3177,40 @@ describe('shared agent-hook-listener', () => { expect(duplicatePark).toBeNull() }) + it('keeps working when a no-lead stop carries an inventory with another live child', () => { + // Why: after a restart the listener can miss a2's start; a1's stop still + // lists a2 running in background_tasks — the removal must not retire it. + claudeEvent({ + hook_event_name: 'SubagentStart', + agent_id: 'a1', + agent_type: 'general-purpose' + }) + const stopped = claudeEvent({ + hook_event_name: 'SubagentStop', + agent_id: 'a1', + background_tasks: [ + { id: 'a2', type: 'subagent', status: 'running', agent_type: 'general-purpose' } + ] + }) + // Why: veto only — a2's row materializes from its own events or the next + // lead Stop's fold; the stop must merely not retire the pane. + expect(stopped?.payload.state).toBe('working') + }) + + it('keeps working when a no-lead drain leaves a live shell in the inventory', () => { + claudeEvent({ + hook_event_name: 'SubagentStart', + agent_id: 'a1', + agent_type: 'general-purpose' + }) + const stopped = claudeEvent({ + hook_event_name: 'SubagentStop', + agent_id: 'a1', + background_tasks: [{ id: 's1', type: 'shell', status: 'running' }] + }) + expect(stopped?.payload.state).toBe('working') + }) + it('never claims done from a no-lead TeammateIdle park', () => { claudeEvent({ hook_event_name: 'SubagentStart', diff --git a/src/shared/agent-hook-listener.ts b/src/shared/agent-hook-listener.ts index be3a106c0a9..f2bc13795fa 100644 --- a/src/shared/agent-hook-listener.ts +++ b/src/shared/agent-hook-listener.ts @@ -2594,8 +2594,16 @@ function buildClaudeChildDrivenStatusPayload( }) } if (evidence.removedRow) { + // Why: the stop's own inventory can list work this listener never saw start (post-restart); + // a running non-teammate task or live shell/cron vetoes done. Teammate-typed entries prove + // nothing ("running" even while idle), and full folding here would reap a just-parked + // teammate row before its TeammateIdle confirmation arrives — veto only, no reconcile. + const inventory = readClaudeBackgroundAgentTasks(hookPayload) + const inventoryShowsLiveWork = + inventory.tasks.some((task) => task.running && !task.teammate) || + hasActiveClaudeNonAgentBackgroundWork(hookPayload) return buildClaudeStatusPayload(state, eventName, '', paneKey, hookPayload, { - stateName: 'done', + stateName: inventoryShowsLiveWork ? 'working' : 'done', updateToolSnapshot: false }) }