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
+12 -2
View File
@@ -2077,7 +2077,11 @@ pub(crate) async fn record_vm_tools(
agent_id: None,
kind: crate::mission_events::TOOL_CALL.to_string(),
target: Some(t.tool.clone()),
detail: serde_json::Value::Null,
// `path` because the World's SSE reads `detail.path` for this kind
// and was handed a null on every container-tier call; `input`
// because the tool name alone cannot answer a single behavioural
// question about the phase.
detail: serde_json::json!({ "path": t.path, "input": t.input }),
});
if let Some(path) = &t.path {
events.push(crate::mission_events::MissionEvent {
@@ -2090,7 +2094,13 @@ pub(crate) async fn record_vm_tools(
path,
&["/mission/repo", "/workspace"],
)),
detail: serde_json::json!({ "tool": t.tool }),
// The ABSOLUTE path as well as the repo-relative one. `target`
// is normalised for the map, where a `mission` → `repo` pair of
// directory orbs means nothing to a reader — but normalising is
// exactly what destroys the question "did this write land
// outside the checkout", which is the one boundary a skill can
// be scored on.
detail: serde_json::json!({ "tool": t.tool, "abs": path }),
});
}
}