fix(agent-hooks): stop Claude agent-hook runs from minting phantom subagent rows (#33298)

Claude tool events from agent-hook runs (`hook-agent-<uuid>` ids, which never send SubagentStop) no longer mint phantom "Working - Agent" subagent rows. Fixes #27775. Supersedes #27776, #15326.

Co-authored-by: DamHV <251416543+DamHV@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-24 16:55:06 -07:00
committed by GitHub
co-authored by DamHV
parent a02a635764
commit 2a4f8e65d8
2 changed files with 34 additions and 1 deletions
@@ -221,6 +221,35 @@ describe('shared agent-hook-listener', () => {
expect(stopped?.payload.state).toBe('done')
})
it('does not mint rows for agent-hook runs on the lead or inside a subagent', () => {
claudeEvent({ hook_event_name: 'UserPromptSubmit', prompt: 'go' })
// Why: payloads captured from Claude Code 2.1.310; a hook run inside a subagent inherits its agent_type.
const leadHook = claudeEvent({
hook_event_name: 'PreToolUse',
agent_id: 'hook-agent-f91f48da-b4db-45e3-a8a1-3debdaa4f2f5',
tool_name: 'Read',
tool_input: { file_path: '/tmp/README.md' }
})
expect(leadHook?.payload.state).toBe('working')
expect(leadHook?.payload.subagents ?? []).toEqual([])
claudeEvent({
hook_event_name: 'SubagentStart',
agent_id: 'aa34e9e23451d1f6b',
agent_type: 'general-purpose'
})
const childHook = claudeEvent({
hook_event_name: 'PreToolUse',
agent_id: 'hook-agent-5179ea04-58c0-440a-b861-04ead9f87aaa',
agent_type: 'general-purpose',
tool_name: 'Read',
tool_input: { file_path: '/tmp/README.md' }
})
expect(childHook?.payload.subagents).toEqual([
expect.objectContaining({ id: 'aa34e9e23451d1f6b' })
])
})
it('parks a teammate as a persistent idle row across its stop/idle/lead-Stop cycle', () => {
// Why: the interactive agent-teams shape observed live on 2.1.217 —
// lifecycle events use `a<name>-<hex>` agent ids while background_tasks
@@ -33,6 +33,8 @@ import { buildClaudeStatusPayload } from './claude-status-build'
import { trackClaudeTaskNotificationDelivery } from './claude-task-notification-delivery'
import { recordClaudeTeammateSpawn } from './claude-teammate-spawn'
const CLAUDE_HOOK_AGENT_ID_PREFIX = 'hook-agent-'
export function normalizeClaudeEvent(
state: HookListenerState,
eventName: unknown,
@@ -179,7 +181,9 @@ export function normalizeClaudeEvent(
eventName === 'PostToolUseFailure')
? eventAgentId
: undefined
if (eventAgentId && (subagentOriginId || isWaitingInducing)) {
// Why: `"type": "agent"` hook runs emit tool hooks (agent_type only if inherited) but never SubagentStop, so their row would stay working forever; real children may be untyped and start-less.
const isClaudeHookAgent = eventAgentId?.startsWith(CLAUDE_HOOK_AGENT_ID_PREFIX) === true
if (eventAgentId && (subagentOriginId || isWaitingInducing) && !isClaudeHookAgent) {
upsertWorkingClaudeSubagent(
getOrCreateClaudeSubagentRoster(state, paneKey),
eventAgentId,