What I'm trying to do
Enforce "one git write per assistant response" with a preToolCall hook. Tool calls within a single response run concurrently, so a git write batched with any other git command contends on index.lock, and a read-back issued alongside its write can report pre-write state. Both fail silently, which is what makes a hook the right tool: the failure is invisible in the transcript, so a rule in the system prompt doesn't survive contact with an instruction to batch independent calls.
The gap
To decide whether two calls belong to the same response, the hook needs to know which response each came from. Nothing in the input says.
Documented chat-scoped input gives chat_id, session_id, agent, behavior, full_model, variant, and preToolCall adds tool_name, server, tool_input, tool_call_id, approval. tool_call_id is per call, chat_id is per conversation. There is no identifier at the granularity in between.
Why a timestamp heuristic doesn't substitute
Correction, see the comment below. The 3562ms row in this table was not a measurement and is retracted. The real picture is worse for heuristics, not better: same-response gaps reach 10717ms, because the gap includes however long the model spent emitting the second call. The two distributions overlap completely rather than nearly touching, so no threshold exists at all.
I tried a time window, on the assumption that calls within one response dispatch milliseconds apart while consecutive responses are separated by an LLM round-trip. Measured gaps between consecutive preToolCall firings, one session:
| calls |
same response |
gap |
two eca__git reads |
yes |
62ms |
two eca__shell_command |
yes |
64ms |
git commit then git log |
yes |
3562ms |
| assorted |
no |
5008ms, 6628ms, 9007ms |
Same-response gaps span 62ms to 3562ms and cross-response gaps start at 5008ms, so the distributions nearly touch. Any threshold either misses real batches or blocks the first call of the next response. I confirmed the concurrency is real rather than an artefact: two eca__shell_command calls each running sleep 3 overlapped for the full three seconds.
Proposal
Add a per-response identifier to chat-scoped hook input, something like request_id or turn_id, stable across every hook firing within one assistant response and different for the next. A hook can then group calls exactly, with no heuristic.
This would also give tool hooks a reliable key for per-turn state. agent distinguishes a subagent from the primary agent, but not one of its turns from the next, so any hook keeping state across calls currently has the same problem.
Alternative considered
read-chat can reach the chat history from a hook, but it runs synchronously inside a 30s budget on every matching tool call, which is a lot of work to answer "which response is this". A field in the payload is far cheaper.
What I'm trying to do
Enforce "one git write per assistant response" with a
preToolCallhook. Tool calls within a single response run concurrently, so a git write batched with any other git command contends onindex.lock, and a read-back issued alongside its write can report pre-write state. Both fail silently, which is what makes a hook the right tool: the failure is invisible in the transcript, so a rule in the system prompt doesn't survive contact with an instruction to batch independent calls.The gap
To decide whether two calls belong to the same response, the hook needs to know which response each came from. Nothing in the input says.
Documented chat-scoped input gives
chat_id,session_id,agent,behavior,full_model,variant, andpreToolCalladdstool_name,server,tool_input,tool_call_id,approval.tool_call_idis per call,chat_idis per conversation. There is no identifier at the granularity in between.Why a timestamp heuristic doesn't substitute
I tried a time window, on the assumption that calls within one response dispatch milliseconds apart while consecutive responses are separated by an LLM round-trip. Measured gaps between consecutive
preToolCallfirings, one session:eca__gitreadseca__shell_commandgit committhengit logSame-response gaps span 62ms to 3562ms and cross-response gaps start at 5008ms, so the distributions nearly touch. Any threshold either misses real batches or blocks the first call of the next response. I confirmed the concurrency is real rather than an artefact: two
eca__shell_commandcalls each runningsleep 3overlapped for the full three seconds.Proposal
Add a per-response identifier to chat-scoped hook input, something like
request_idorturn_id, stable across every hook firing within one assistant response and different for the next. A hook can then group calls exactly, with no heuristic.This would also give tool hooks a reliable key for per-turn state.
agentdistinguishes a subagent from the primary agent, but not one of its turns from the next, so any hook keeping state across calls currently has the same problem.Alternative considered
read-chatcan reach the chat history from a hook, but it runs synchronously inside a 30s budget on every matching tool call, which is a lot of work to answer "which response is this". A field in the payload is far cheaper.