From 3c6c1f80b2fd37344a072189f9ad08a6d72ec3f4 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:09:52 -0700 Subject: [PATCH] fix(agent-status): let a stop's own inventory veto removal-driven done MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review finding round 5: after a restart the listener can miss a child's start; another child's stop still lists it running in background_tasks. A running non-teammate task or live shell/cron now vetoes the no-lead removal->done into working. Veto only — folding the inventory at stop time would reap a just-parked teammate row before its TeammateIdle confirmation arrives. --- src/shared/agent-hook-listener.test.ts | 34 ++++++++++++++++++++++++++ src/shared/agent-hook-listener.ts | 10 +++++++- 2 files changed, 43 insertions(+), 1 deletion(-) 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 }) }