fix(viz): read the gateway's real tool_call keys, and correct the record
ci / gates (push) Failing after 6s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped

The frame is `{"type":"tool_call","id","name","args"}` — zeroclaw-gateway
/src/ws.rs. The tap read `tool` then `name`, and `arguments` then `input`.
`name` happened to be in the fallback chain; `args` was not in it at all,
so a container-tier tool call would have been recorded with its name and
NO path — a tool that reads as having touched nothing. `tool` and
`arguments` belong to `approval_request`, which is where they came from.

Also corrects what the histogram was read as saying. A mission turn on
gw-04 carried only chunk/done/session_start, and the first reading was
"there is no tool_call frame". Wrong: `grep -c tool_call` on the deployed
0.8.3 binary returns 46. Container-tier agents are provisioned tool-free
behind the MCP door (§15), so they call nothing — there is nothing to
observe on that tier, and nothing is broken.

That distinction is exactly what the histogram was shipped to make
possible: a tap matching no frame is otherwise indistinguishable from a
mission that used no tools.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-11 12:22:43 -07:00
co-authored by Claude Opus 5
parent 31158467f4
commit d810fc0a86
+35 -15
View File
@@ -87,12 +87,19 @@ pub struct ToolTrace {
pub calls: Vec<ToolCall>,
/// Frame `type` values this drain did not recognise, counted.
///
/// Shipped in the same change as the tap on purpose: the frame name
/// `tool_call` is taken from a comment in this file, not from a captured
/// frame. If the runtime calls it something else, the tap records nothing
/// and nothing anywhere errors — the World simply stays as sparse as it was
/// before. This histogram is how one gw-04 run names the real frame instead
/// of a bisect.
/// Shipped in the same change as the tap on purpose: the frame name was
/// taken from a comment in this file rather than from a captured frame. If
/// the runtime called it something else, the tap would record nothing and
/// nothing anywhere would error — the World would simply stay as sparse as
/// it was before.
///
/// MEASURED on gw-04 (v0.8.3, 2026-08-11): a mission turn's stream carried
/// `chunk`, `done` and `session_start` and no tool frames at all. That is
/// not a protocol mismatch — `tool_call` is in the deployed binary
/// (`zeroclaw-gateway/src/ws.rs` emits `{"type":"tool_call","id","name",
/// "args"}`) — it is §15: these agents are provisioned tool-free behind the
/// MCP door, so they call nothing. The histogram is what let us tell those
/// two apart, which was its whole purpose.
pub unmatched: std::collections::BTreeMap<String, u32>,
}
@@ -511,16 +518,27 @@ impl ZeroClawDriveExecutor {
// often enough to be believed and wrong often enough to
// put files on the map that nobody edited.
"tool_call" => {
// `name` is what the gateway sends; `tool` is
// what `approval_request` uses, kept as a fallback.
let tool = v
.get("tool")
.or_else(|| v.get("name"))
.get("name")
.or_else(|| v.get("tool"))
.and_then(|t| t.as_str())
.unwrap_or("")
.trim()
.to_string();
if !tool.is_empty() {
// `args` FIRST: that is what the gateway
// actually sends (`{"type":"tool_call","id",
// "name","args"}` — zeroclaw-gateway/src/ws.rs).
// The others were guesses, and a guess that
// never matches costs the file path silently:
// the tool call is still recorded, with no
// target, and reads as a tool that touched
// nothing.
let args = v
.get("arguments")
.get("args")
.or_else(|| v.get("arguments"))
.or_else(|| v.get("input"))
.cloned()
.unwrap_or(serde_json::Value::Null);
@@ -670,15 +688,17 @@ mod tests {
let _ = socket.recv().await;
for f in [
json!({"type": "session_start"}),
json!({"type": "tool_call", "tool": "Read",
"arguments": {"file_path": "/mission/repo/src/a.rs"}}),
// The REAL frame shape, copied from the gateway:
// {"type":"tool_call","id","name","args"}.
json!({"type": "tool_call", "id": "t1", "name": "Read",
"args": {"file_path": "/mission/repo/src/a.rs"}}),
// A tool whose arguments name no path at all.
json!({"type": "tool_call", "tool": "Bash",
"arguments": {"command": "cargo test"}}),
json!({"type": "tool_call", "id": "t2", "name": "Bash",
"args": {"command": "cargo test"}}),
// Prose that MENTIONS a path. It must not become a file touch.
json!({"type": "tool_call", "tool": "Grep",
json!({"type": "tool_call", "id": "t3", "name": "Grep",
"arguments_summary": "searching src/main.rs",
"arguments": {"pattern": "fn main"}}),
"args": {"pattern": "fn main"}}),
json!({"type": "a_frame_we_have_never_seen"}),
json!({"type": "a_frame_we_have_never_seen"}),
json!({"type": "done", "input_tokens": 1, "output_tokens": 1}),