fix(tap): a subagent's tool calls are no longer credited to its parent
The previous commit's message ended "mission agents are spawning subagents and
nothing in our design accounts for it." Twelve spawns across the two production
missions, all of them used as a fetch mechanism — container missions have no
`WebFetch` or `WebSearch`, so they reach the network through `Bash` + `curl`,
and 151 of 158 Bash calls are exactly that.
Measured against the real claude 2.1.246 binary rather than reasoned about,
because the containers were reaped and the question had three possible answers:
1. A subagent's tool calls DO fire both hooks. `PostToolUse` records them, and
`PreToolUse` blocked a subagent's denied curl and got the reason back to
it. `Agent` is not a gate bypass — worth knowing before shipping the rule
in the previous commit.
2. They carry the PARENT's session_id. One parent plus one subagent produced
three events on one id. This is why attribution resolved 119/119: a
subagent never adds a session, so attribute_sessions' exact count holds.
3. Only `agent_type` / `agent_id` tell them apart — present on a subagent's
payload, absent on the parent's own.
`hook_script` appends the raw payload, so both fields were already on disk in
every production run. `parse()` read past them. The guest was never the lossy
half, so nothing container-side changes and no redeploy of the image is needed.
`Observed.subagent` / `.subagent_id` now carry them into `mission_events.detail`.
A blank `agent_type` reads as "the turn's own agent", because absence IS the
signal here and a subagent named "" is not a thing.
Same defect class as the tap discarding tool ARGUMENTS until 2026-08-21: the
record looked complete while being wrong about who did the work.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
co-authored by
Claude Opus 5
parent
5a2ed8fb42
commit
fe5c7d2c87
@@ -320,6 +320,8 @@ mod attribution_tests {
|
||||
tool: "Bash".into(),
|
||||
path: None,
|
||||
session: session.map(str::to_string),
|
||||
subagent: None,
|
||||
subagent_id: None,
|
||||
input: serde_json::json!({"command": "ls"}),
|
||||
response: serde_json::Value::Null,
|
||||
}
|
||||
@@ -2250,6 +2252,12 @@ pub(crate) async fn record_vm_tools(
|
||||
// Only commands carry one; `bounded_response` returns null for
|
||||
// everything else, and a null key here is noise.
|
||||
"response": t.response,
|
||||
// Null unless a SUBAGENT made this call. It shares its parent's
|
||||
// session id, so `agent_id` above names the agent that spawned
|
||||
// it and this is the only thing saying the parent did not run
|
||||
// it itself.
|
||||
"subagent": t.subagent,
|
||||
"subagent_id": t.subagent_id,
|
||||
}),
|
||||
});
|
||||
if let Some(path) = &t.path {
|
||||
|
||||
Reference in New Issue
Block a user