fix(agent-status): resolve identity-matched child drains to done without a lead record

Review finding round 3: rosterChanged let a known-child drain bypass the
no-evidence guard into the unconditional 'working' default — sticky until
the stale sweeper when no lead hook ever arrives. With no lead record the
pane state now follows tracked children: live child -> working, identity-
matched drain/park -> done. Duplicate stale stops stay suppressed.
This commit is contained in:
Brennan Benson
2026-08-01 07:46:35 -07:00
parent 6afef239c4
commit 98943de98b
2 changed files with 31 additions and 5 deletions
+24
View File
@@ -3132,6 +3132,30 @@ describe('shared agent-hook-listener', () => {
expect(state.claudeSubagentRosterByPaneKey.has(PANE_KEY)).toBe(false)
})
it('resolves a known-child drain to done instead of sticky working when no lead ever spoke', () => {
claudeEvent({
hook_event_name: 'SubagentStart',
agent_id: 'a1',
agent_type: 'general-purpose'
})
// Why: the child's own identity-matched stop drains the roster; the old no-lead
// 'working' default left this pane spinning until the stale sweeper.
const drained = claudeEvent({ hook_event_name: 'SubagentStop', agent_id: 'a1' })
expect(drained?.payload.state).toBe('done')
expect(drained?.payload.subagents).toBeUndefined()
})
it('suppresses a duplicate stale stop after a no-lead drain', () => {
claudeEvent({
hook_event_name: 'SubagentStart',
agent_id: 'a1',
agent_type: 'general-purpose'
})
claudeEvent({ hook_event_name: 'SubagentStop', agent_id: 'a1' })
const duplicate = claudeEvent({ hook_event_name: 'SubagentStop', agent_id: 'a1' })
expect(duplicate).toBeNull()
})
it('re-emits the lead done state on a start-less SubagentStop after a completed turn', () => {
claudeEvent({ hook_event_name: 'UserPromptSubmit', prompt: 'go' })
claudeEvent({ hook_event_name: 'Stop', background_tasks: [] })
+7 -5
View File
@@ -2573,14 +2573,16 @@ function buildClaudeChildDrivenStatusPayload(
// Why: no lead record, no live child, and an unchanged roster mean the event proves nothing is
// running. Compact's start-less SubagentStop is exactly that; minting the default below left a
// sticky 'working' no Stop ever clears on freshly resumed panes (STA-2915).
if (!lead && !rosterChanged && !claudeRosterHasWorkingSubagent(roster)) {
const rosterHasWorking = claudeRosterHasWorkingSubagent(roster)
if (!lead && !rosterChanged && !rosterHasWorking) {
return null
}
// Why: default 'working' — a spawn proves activity even before the lead's first state-bearing event (e.g. Orca restarted mid-session).
const leadState = lead?.state ?? 'working'
// Why: with no lead record the pane state is inferred from tracked children only — a live child
// shows working, while an identity-matched drain/park resolves to done. The old unconditional
// 'working' default stuck until the stale sweeper once the last child left (STA-2915 review).
const leadState = lead?.state ?? (rosterHasWorking ? 'working' : 'done')
return buildClaudeStatusPayload(state, eventName, '', paneKey, hookPayload, {
stateName:
leadState === 'done' && claudeRosterHasWorkingSubagent(roster) ? 'working' : leadState,
stateName: leadState === 'done' && rosterHasWorking ? 'working' : leadState,
updateToolSnapshot: false,
interrupted: lead?.interrupted
})