fix(agent-status): canonicalize lifecycle and tool agent ids like inventory ids

Review finding round 15: inventory ids trim at parse but lifecycle/tool
agent_id stayed raw, so a padded stop missed the trimmed roster key and
the row could never drain. All id reads now canonicalize identically.
This commit is contained in:
Brennan Benson
2026-08-01 10:16:22 -07:00
parent b4af7f39f0
commit 90c8125794
2 changed files with 17 additions and 2 deletions
+11
View File
@@ -3354,6 +3354,17 @@ describe('shared agent-hook-listener', () => {
expect(stopped?.payload.interactivePrompt).toBeUndefined()
})
it('canonicalizes padded lifecycle ids so starts and stops target one row', () => {
claudeEvent({
hook_event_name: 'SubagentStart',
agent_id: ' a2 ',
agent_type: 'general-purpose'
})
const stopped = claudeEvent({ hook_event_name: 'SubagentStop', agent_id: ' a2 ' })
expect(stopped?.payload.state).toBe('done')
expect(stopped?.payload.subagents).toBeUndefined()
})
it('tracks whitespace-padded inventory ids canonically so their stops can drain them', () => {
const first = claudeEvent({
hook_event_name: 'SubagentStop',
+6 -2
View File
@@ -2429,7 +2429,9 @@ function normalizeClaudeSubagentLifecycleEvent(
hookPayload: Record<string, unknown>
): ParsedAgentStatusPayload | null {
const lifecycleField = eventName === 'TeammateIdle' ? 'teammate_name' : 'agent_id'
const lifecycleId = readString(hookPayload, lifecycleField)
// Why: inventory ids are trimmed at parse — lifecycle ids must canonicalize identically or a
// padded stop misses the trimmed roster key and the row can never drain.
const lifecycleId = readString(hookPayload, lifecycleField)?.trim()
if (!lifecycleId) {
return null
}
@@ -2730,7 +2732,9 @@ function normalizeClaudeEvent(
(eventName === 'Stop' || eventName === 'StopFailure') &&
hasActiveClaudeNonAgentBackgroundWork(hookPayload)
const eventAgentId = readString(hookPayload, 'agent_id')
// Why: canonicalize like the inventory parser and lifecycle handler — a padded id must hit
// the same roster key and wait ownership as its trimmed twin.
const eventAgentId = readString(hookPayload, 'agent_id')?.trim() || undefined
// Why: subagent/teammate events carry `agent_id` (lead's don't); child tool activity keeps its row live but must not become the lead's state or overwrite its tool/prompt caches (a live card would vanish).
// Two exceptions take the full path below: waiting-inducing events (a child needs human attention on this pane) and the blocked child's own next tool event (approval granted — clear the wait as for the lead).
const isWaitingInducing = stateName === 'waiting'