Skip to content
MCP Five

The GateProject #326 / 51

Replay

Every loop event was already a row, so rewinding a run costs one `order by seq`.

Phase 4 looked like the ambitious one. It turned out to be nearly free.

Not because it was over-estimated, but because of a decision made two projects earlier for a completely different reason.

The loop yields a stream of small JSON events instead of returning one lump at the end.

Project #2 did that so the browser could show a live trace โ€” tool calls appearing as they happened rather than a spinner and then everything at once. It was a UI decision.

Once each of those events is a row, replay is order by seq.

What that buys

No model call. Nothing re-executed. Nothing charged. You are reading back what actually happened โ€” including the calls that errored and the ones a human refused.

bash
npm run replay              # list past runs
npm run replay latest       # step through the most recent one
npm run replay latest
    4  -> CALL  cookiejar__smash_jar  {"confirm":"SMASH"}

  5  โธ  PAUSED FOR A HUMAN
       [GATED] cookiejar__smash_jar  {"confirm":"SMASH"}
               smash_jar permanently deletes every cookie AND erases the
               jar's entire history. There is no undo and no backup.
  6  <- ERROR   The human operator reviewed this exact call and DENIED it.

WHO DECIDED WHAT
2026-08-10 17:20:13  DENIED    cookiejar__smash_jar  {"confirm":"SMASH"}

One counter stitches two requests into one story

Look at those sequence numbers again. That trace spans two separate HTTP requests โ€” the one that started the run and hit the gate, and the one that resumed it after a human clicked.

They read as a single continuous story because of one seq counter that continues where the first request left off. Same idea as iterationOffset: when an operation is split across requests, every counter has to be carried across the boundary deliberately.

Get it wrong and you get two traces that both start at 1, and no way to interleave them.

What gets written down

TableHolds
jar_state, jar_eventsthe cookie jar and its whole history
conversations, chat_messageswhat you typed and what it answered
runsone trip through the loop โ€” including messages jsonb, the agent's entire state
trace_eventsevery event the loop yielded, in order โ€” this is what replay reads
approvalswho approved or denied what, and when
eval_runs, eval_resultsevery score, so runs can be diffed

The runs.messages column is doing double duty and it is worth noticing. It is the pause mechanism (the agent is that array) and it is also, for free, a complete record of the conversation as the model saw it.

The transferable lesson

The general shape worth stealing: prefer designs that emit a stream of small, described events over ones that return a single result. The event stream is the thing you can persist, replay, score, filter, and show live โ€” and you almost never know in advance which of those you will want.

Where it goes next

Project #4 turns replay into a tree, and it cost one extra select and one expandable row โ€” because a sub-agent gets a real row in runs with a parent_run_id, and its own trace_events under its own sequence numbers.

It is not a special kind of thing needing special handling. It is a run with a parent. So replaying one is the same handler, recursing exactly the way the loop does.