feat(missions): keep the tool's arguments, not just its name

The container tier's first measured mission recorded `Bash × 6` and not
one of them said what it ran. Every behavioural question about the phase
— did it run the tests, did it commit, did it call an API a skill forbids
— was unanswerable from a record that looked complete.

`vm_tool_tap::parse` already read `tool_input` to pull the path out of it,
then dropped the rest on the floor. It now keeps it, bounded: file bodies
(`content`, `new_string`, `old_string`, `edits`) become a byte count, and
any other over-long string is truncated with a marker saying so. Bounded
rather than whitelisted, because a whitelist silently loses the one
argument that matters the first time a tool grows a field.

`file.touch` keeps the absolute path in `detail.abs` alongside the
repo-relative `target`. Normalising is what the map needs and exactly what
destroys "did this write land outside the checkout".

`tool.call` also gains `detail.path`, which the World's SSE has been
reading and getting a null from on every container-tier call.

`mission_events::tool_evidence_for_mission` is the reader — the
counterpart to `narrative_for_mission`, and the reason it exists: the
narrative is what an agent SAID it did.

Host-side only. No image rebuild: the arguments were always in the tap
file, the first parse threw them away.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9
This commit is contained in:
Omar Sobh
2026-08-21 08:23:42 -07:00
co-authored by Claude Opus 5
parent 0b4d91889a
commit 8cb38d1320
4 changed files with 210 additions and 7 deletions
+7 -3
View File
@@ -1114,9 +1114,13 @@ pub async fn world_live(
// the durable source: `reasoning` rows carry the agent's own step
// output, `tool.call` rows its actions.
//
// NOTE: on the container tier `tool.call` legitimately stays empty —
// those agents are tool-free behind the §15 door. Reasoning flows on
// every tier; tool lines appear where agents actually hold tools.
// This used to say the container tier "legitimately stays empty —
// those agents are tool-free". That was wrong, and it was wrong in
// the most expensive way: it explained the silence, so nobody
// looked. Those agents call `Bash` and `Write` constantly; the
// calls happen inside claude's own subprocess and so never reached
// ZeroClaw's executor. `container_tool_hooks` records them now, and
// `tool.call` is populated on both tiers.
if agent_ev_cursor < 0 {
agent_ev_cursor = sqlx::query_scalar(
"SELECT coalesce(max(id), 0) FROM mission_events",