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
+27
View File
@@ -18,6 +18,18 @@ pub fn claw_alias(claw_id: Uuid) -> String {
format!("claw_{}", claw_id.simple())
}
/// The claw behind a runtime alias, or `None` if it is not one of ours.
///
/// The inverse of [`claw_alias`], and it lives beside it so the two cannot
/// drift — a changed prefix breaks the round-trip test rather than quietly
/// returning `None` for every agent and dropping their attribution.
///
/// `None` is the honest answer for `scout` and the other configured aliases
/// that are not claws: they have no row in `agents` to point at.
pub fn claw_from_alias(alias: &str) -> Option<Uuid> {
Uuid::parse_str(alias.trim().strip_prefix("claw_")?).ok()
}
/// Map a claw's chosen model to a configured provider alias.
///
/// Claude models resolve to `claude_cli.default`, which spawns the real
@@ -435,4 +447,19 @@ mod tests {
let id = Uuid::nil();
assert_eq!(claw_alias(id), "claw_00000000000000000000000000000000");
}
/// The alias must round-trip, and must NOT invent a claw for one of the
/// configured non-claw aliases.
///
/// The failure this guards is silent both ways: a broken round-trip drops
/// every tool call's agent attribution (files appear, nobody moves), and a
/// too-eager parse would attribute work to a claw id that matches no row.
#[test]
fn an_alias_round_trips_to_its_claw_and_nothing_else_does() {
let id = Uuid::from_u128(0x0198_2f11_7ac0_7d51_9c3e_44a1_09b2_5e77);
assert_eq!(claw_from_alias(&claw_alias(id)), Some(id));
assert_eq!(claw_from_alias("scout"), None);
assert_eq!(claw_from_alias("claude_cli.default"), None);
assert_eq!(claw_from_alias("claw_not-a-uuid"), None);
}
}