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]>
44 lines
2.2 KiB
SQL
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);
|