KeiSeiKit-1.0/hooks/agent-event-spawn.sh
Parfii-bot 45555bc4aa fix(live-graph): tool_use events properly attribute to spawning agent
User pushback: live-graph showed only "main" node, no pulses on agents.
Root cause: hook stdin doesn't carry parent_tool_use_id for sub-agent
tool calls — we only get the sub-agent's own session_id, which doesn't
link back to the spawn's tool_use_id.

Sequential heuristic via shared state file:
  - agent-event-spawn.sh appends tool_use_id to /tmp/kei-active-children.tsv
  - tool-use-event.sh reads the LAST line of that file → uses that
    tool_use_id as agent_id for the emitted event
  - agent-event-done.sh removes the spawn's line (grep -v + atomic mv)

Verified end-to-end: a code-implementer agent ran 5 Bash calls during
its lifetime — all 5 tool_use events were correctly attributed to the
spawn's tool_use_id. After agent_done, subsequent orchestrator-direct
tool calls correctly fall back to agent_id="main".

Limitation: parallel agents may misattribute. The "most recent live
spawn" heuristic works for single-agent-at-a-time which is the common
case. Parallel spawns share /tmp/kei-active-children.tsv and a sub-
agent's tool calls all attribute to whichever spawn appended last.
Acceptable for v1 demo; proper parent-tool-use-id propagation requires
Claude Code to expose it in sub-agent stdin (upstream change).

The `mv` after `grep -v` runs UNCONDITIONALLY (not gated on grep's
exit code) — grep -v returns 1 when ALL lines match, which would
otherwise leave the stale file in place.

Bypass: `KEI_EVENTS_BYPASS=1` (existing) covers all 3 hooks.
Override path: `KEI_ACTIVE_SPAWNS_FILE=/path/to/file`.

=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: NOT-RUN
behaviour-verified: yes
follow-up-required:
  - Parallel-agent attribution would need parent_tool_use_id from
    Claude Code sub-agent stdin (not currently exposed).
  - Race condition window between spawn append and done remove is
    millisecond-scale; observed clean in single-agent demo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:43:42 +08:00

56 lines
2 KiB
Bash
Executable file

#!/bin/sh
# agent-event-spawn.sh — PreToolUse:Agent hook.
#
# Emits `agent_spawn` event to ~/.claude/memory/agent-events.jsonl
# AND records the tool_use_id in /tmp/kei-active-children.tsv so
# tool-use-event.sh can attribute incoming sub-agent tool calls
# to this spawn (sub-agent stdin lacks parent_tool_use_id).
#
# Defensive: never blocks, exits 0 on every path.
# Bypass via `KEI_EVENTS_BYPASS=1`.
set -u
[ "${KEI_EVENTS_BYPASS:-0}" = "1" ] && exit 0
command -v jq >/dev/null 2>&1 || exit 0
PAYLOAD=$(cat 2>/dev/null || true)
[ -n "$PAYLOAD" ] || exit 0
TOOL=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
[ "$TOOL" = "Agent" ] || exit 0
EVENTS_FILE="$HOME/.claude/memory/agent-events.jsonl"
mkdir -p "$(dirname "$EVENTS_FILE")" 2>/dev/null || true
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null)
printf '%s' "$PAYLOAD" | jq -c \
--arg ts "$TS" \
'{
ts: $ts,
event: "agent_spawn",
id: (.tool_use_id // .toolUseId // "unknown"),
parent_id: (.session_id // null),
subagent_type: (.tool_input.subagent_type // null),
model: (.tool_input.model // null),
branch: (.tool_input.isolation // null),
prompt_preview: (
(.tool_input.prompt // "")
| gsub("[\"\\n\\r\\t]"; " ")
| .[0:80]
)
}' \
>> "$EVENTS_FILE" 2>/dev/null || true
# Active-spawn ledger for tool-use attribution. Sub-agent's hook stdin
# carries no parent_tool_use_id, so we maintain a small TSV of currently
# alive spawns; tool-use-event.sh attributes incoming tool_use events to
# the MOST RECENT live spawn (sequential heuristic — works for the common
# single-agent-at-a-time case; parallel agents may misattribute).
TOOL_USE_ID=$(printf '%s' "$PAYLOAD" | jq -r '.tool_use_id // .toolUseId // empty' 2>/dev/null)
ACTIVE_FILE="${KEI_ACTIVE_SPAWNS_FILE:-/tmp/kei-active-children.tsv}"
if [ -n "$TOOL_USE_ID" ]; then
printf '%s\t%s\n' "$(date +%s)" "$TOOL_USE_ID" >> "$ACTIVE_FILE" 2>/dev/null || true
fi
exit 0