Summary
The PreToolUse hook enqueues a {toolUseId, agentId} entry keyed by tool name for every tool call, but the matching dequeue happens only in canUseTool — which the Agent SDK does not invoke for auto-allowed tools. Any allow-rule desyncs the FIFO and corrupts tool-call correlation for the rest of the session.
Location
src/daemon/providers/claude/index.ts:297-303 (enqueue in PreToolUse)
src/daemon/providers/claude/index.ts:350-359 (dequeue in canUseTool)
allowedTools at :262-264; settingSources: ["project"] at :296 (loads .claude/settings.json permissions.allow)
- consumer correlation in
src/daemon/session.ts:1937-1948
Root cause
canUseTool is skipped by the SDK for tools matched by allowedTools or by a project permissions.allow rule. The hook still pushed an entry for those, so the queue holds stale entries that the next gated tool pops.
Failure scenario
Repo has .claude/settings.json → "allow": ["Bash(git status:*)"]. Agent runs git status (hook enqueues under "Bash", canUseTool never fires), then runs a gated rm -rf build. canUseTool pops the stale git status entry → tool_start emitted with the wrong sdkToolUseId → #toolUseIdToMessageId mismatches → the real tool_complete finds no message (tool shows running forever) and later output can attach to the wrong tool message in scrollback and canonical history.
Secondary defect (same site)
Entries for always-allowed tools (e.g. mcp__codeoid_memory__recall) are never popped, so #pendingToolUse grows unbounded over an hours-long session.
Fix
Correlate by the SDK's real tool_use_id end-to-end rather than a name-keyed FIFO, or also drain the queue from the PostToolUse/result path so auto-allowed tools don't leave stale heads. Bound the map regardless.
Summary
The PreToolUse hook enqueues a
{toolUseId, agentId}entry keyed by tool name for every tool call, but the matching dequeue happens only incanUseTool— which the Agent SDK does not invoke for auto-allowed tools. Any allow-rule desyncs the FIFO and corrupts tool-call correlation for the rest of the session.Location
src/daemon/providers/claude/index.ts:297-303(enqueue in PreToolUse)src/daemon/providers/claude/index.ts:350-359(dequeue in canUseTool)allowedToolsat:262-264;settingSources: ["project"]at:296(loads.claude/settings.jsonpermissions.allow)src/daemon/session.ts:1937-1948Root cause
canUseToolis skipped by the SDK for tools matched byallowedToolsor by a projectpermissions.allowrule. The hook still pushed an entry for those, so the queue holds stale entries that the next gated tool pops.Failure scenario
Repo has
.claude/settings.json→"allow": ["Bash(git status:*)"]. Agent runsgit status(hook enqueues under"Bash",canUseToolnever fires), then runs a gatedrm -rf build.canUseToolpops the stalegit statusentry →tool_startemitted with the wrongsdkToolUseId→#toolUseIdToMessageIdmismatches → the realtool_completefinds no message (tool shows running forever) and later output can attach to the wrong tool message in scrollback and canonical history.Secondary defect (same site)
Entries for always-allowed tools (e.g.
mcp__codeoid_memory__recall) are never popped, so#pendingToolUsegrows unbounded over an hours-long session.Fix
Correlate by the SDK's real
tool_use_idend-to-end rather than a name-keyed FIFO, or also drain the queue from the PostToolUse/result path so auto-allowed tools don't leave stale heads. Bound the map regardless.