Files
clawmates/migrations/0075_mission_events.sql
T
Omar SobhandClaude Opus 5 9e61e3ba35 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]>
2026-08-11 09:16:50 -07:00

44 lines
2.2 KiB
SQL

-- Structured mission activity: what an agent actually DID, as data.
--
-- Until now the World could show a mission's shape (phases, status, who is on
-- which station) but nothing about the work itself. The detail existed only as
-- prose in `checkpoint.log` and agent output, where a tool name is
-- indistinguishable from an agent *talking about* a tool — so it was never
-- parsed, deliberately. This table is the structured channel that replaces it.
--
-- `run_events` could not be reused: 0003 declares `run_id REFERENCES
-- agent_runs(id)`, and mission phases insert into `topology_runs`. Those are
-- independent id spaces, so every write would have been an FK violation.
CREATE TABLE IF NOT EXISTS mission_events (
id BIGSERIAL PRIMARY KEY,
mission_id UUID NOT NULL REFERENCES missions(id) ON DELETE CASCADE,
-- No FK. `mission_phases` rows survive, but a phase that loops is re-run
-- and this column is only ever read as a grouping key.
phase_id UUID,
-- Deliberately NO FK, and this is the load-bearing decision in the file:
-- `phase_runner` DELETEs the `topology_runs` row on every retry. With a
-- cascading FK, a phase's whole history would vanish the moment it retried
-- — silently, since a cascade is not an error. The id is kept as a plain
-- correlation value.
run_id UUID,
-- The platform agent, when there is one. NULL for a microVM phase, which
-- has no platform agent at all — that is the truth, not missing data.
agent_id UUID,
-- phase.started | phase.completed | tool.call | file.touch
-- | finding.raised | benchmark.delta
kind TEXT NOT NULL,
-- The tool name, or the file path — whatever the event is *about*.
target TEXT,
detail JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- The World reads by mission, newest last, and by phase for the per-phase cap.
CREATE INDEX IF NOT EXISTS mission_events_mission_idx
ON mission_events (mission_id, id);
CREATE INDEX IF NOT EXISTS mission_events_phase_idx
ON mission_events (phase_id, id);
-- The retention sweep in `mission_gc` scans by age.
CREATE INDEX IF NOT EXISTS mission_events_created_idx
ON mission_events (created_at);