feat(viz): what the agents actually did, as structured events

The World could draw a mission's shape but nothing about the work. The
detail existed only as prose in checkpoint.log and model output, where a
tool name is indistinguishable from an agent *talking about* a tool — so
it was never parsed, deliberately. `mission_events` is the structured
channel that replaces it.

Three taps, one table:

- Container tier: the `_ => {}` at the end of topology_exec's typed frame
  stream now matches `tool_call` and reads the tool's JSON ARGUMENTS for a
  path. Never the prose summary — a path scraped from a sentence would put
  files on the map that no agent opened, and the test proves a Grep whose
  summary says "src/main.rs" produces no file touch. The frame name itself
  is unverified, so the same commit ships an unmatched-frame-type
  histogram: a tap that matches nothing looks exactly like a mission that
  used no tools, and this is how one gw-04 run names the real frame.

- microVM tier: a `PostToolUse` hook, the seam vm_stop_gate already proved
  fires under `claude -p`. It copies stdin to /root/tap and exits 0
  unconditionally — a non-zero PostToolUse hook talks back to the model,
  which would turn the observer into a participant. Drained before collect,
  since the VM is destroyed moments later.

- Phase transitions: five identical copies of the pending→running UPDATE
  became one `mark_phase_running`, and `close_finished_phases` grew
  RETURNING. Its CASE decides each phase's status inside SQL from rows the
  statement does not change, so it cannot be re-derived afterwards without
  writing that CASE twice — without RETURNING it emits zero phase.completed
  and reports success.

The settings.json hazard the plan called out: the stop gate wrote the
WHOLE document, so a second hook writer would have silently erased it and
a coding phase would then complete having written nothing — the exact
failure the gate exists to catch. There is now one composer,
`vm_tool_tap::guest_settings`, one writer, and a source-walk test that
fails if anything else writes a settings document.

`mission_events.run_id` carries no FK on purpose: phase_runner DELETEs
topology_runs on retry, and a cascade would erase a phase's whole history
the moment it retried — silently, since a cascade is not an error.

world.rs streams it with a cursor that separates backfill from motion.
Everything already in the table when a subscriber arrives is drawn as
settled history; only what lands afterwards animates. Otherwise opening a
finished mission replays an hour of tool calls as a burst storm.

Bounded twice: 400 events per phase (enforced inside the INSERT, since
two concurrent taps would each read a count below the cap) and a 7-day
retention sweep in mission_gc.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-11 09:16:50 -07:00
co-authored by Claude Opus 5
parent c2fa8067e1
commit 9e61e3ba35
13 changed files with 1257 additions and 101 deletions
+28 -11
View File
@@ -185,23 +185,33 @@ async fn run_job(
// the missions row; else fall back to the shared env-derived
// gateway (pre-C3 missions + non-mission runs). This is what
// isolates agents' workspace filesystem to that mission's repo.
let mission_binding: Option<(Option<String>, Option<String>)> =
sqlx::query_as::<_, (Option<String>, Option<String>)>(
"SELECT m.runtime_endpoint, m.runtime_pairing_code
type MissionBinding = (Option<String>, Option<String>, Uuid, Option<Uuid>);
let mission_binding: Option<MissionBinding> = sqlx::query_as::<_, MissionBinding>(
"SELECT m.runtime_endpoint, m.runtime_pairing_code, m.id, r.mission_phase_id
FROM topology_runs r
JOIN missions m ON m.id = r.mission_id
WHERE r.id = $1",
)
.bind(id)
.fetch_optional(pool)
.await
.ok()
.flatten();
)
.bind(id)
.fetch_optional(pool)
.await
.ok()
.flatten();
// What this run's turns will be attributed to. `None` when the run belongs
// to no mission — a bare topology run has no phase to hang tool calls on.
let tap = mission_binding
.as_ref()
.map(|(_, _, mission_id, phase_id)| crate::topology_exec::MissionTap {
pool: pool.clone(),
mission_id: *mission_id,
phase_id: *phase_id,
run_id: Some(id),
});
let leaf_result = match mission_binding {
Some((Some(url), Some(code))) => {
Some((Some(url), Some(code), _, _)) => {
ZeroClawDriveExecutor::from_env_for_gateway_with_code(url, code)
}
Some((Some(url), None)) => ZeroClawDriveExecutor::from_env_for_gateway(url),
Some((Some(url), None, _, _)) => ZeroClawDriveExecutor::from_env_for_gateway(url),
_ => ZeroClawDriveExecutor::from_env(),
};
let leaf = match leaf_result {
@@ -211,6 +221,13 @@ async fn run_job(
return;
}
};
// The tap rides on the leaf executor, so the recursive tiers get it too:
// they drive the same leaf all the way down, and a company-tier mission's
// tool calls belong to its phase exactly as a team-tier one's do.
let leaf = match tap {
Some(t) => leaf.with_tap(t),
None => leaf,
};
// Select the executor by deploy tier: `team` drives claws directly; the
// upper tiers drive the recursive sub-topology executor (which runs each