콘텐츠로 이동
Developer Preview — APIs and language features may change before 1.0

Forking and Replaying a Run

이 콘텐츠는 아직 번역되지 않았습니다.

Classic debugging advice says: reproduce the problem first, then break it into smaller pieces, and let every bug make you better. In mashin the reproduction step is already done (the run you’re reading is the reproduction), so what’s left is narrower and more useful: testing a hypothesis against a real run, without touching anything real to do it.

This page covers the three ways to do that from a run’s cockpit view: fork it, sweep an edit across history, and replay it against a different model.

The playhead you use to read a run (see Reading a Run) is already a form of bisection: narrowing a failure down by moving through time instead of re-executing anything. Jump to the next failed step, the next denied step, or the next cost or latency outlier, and you’ve cut the run in half without writing a single print. Classic bisection cuts a search space by re-running the program; here the whole timeline already exists, so cutting it is just navigation.

Below the variable panel, the Fork this run control does one specific thing: it takes the step you’re currently looking at and asks, “if the machine kept running from right here, what happens?”

Press Fork at this step and the runtime re-executes the tail of the run starting after the step you’re on, using everything before it exactly as recorded. Nothing before the fork point is re-run for real; every governed action downstream is served from the original recording unless the fork’s tail reaches somewhere the recording never went, in which case it stops there rather than doing something nobody approved:

Forked at step 4, and it ran to completed. It answered {"result": "ok"}. This fork touched nothing real: everything downstream came from the recording.

Or, if the new tail reaches an action the original run never took, the fork stops there instead of guessing: “step ‘charge_card’ (call) is an action the origin run never recorded. A fork does not exercise an unrecorded capability by surprise.”

That halt is itself an answer. It tells you the fork’s path diverges from the original somewhere real is involved, which is exactly the thing you’d want to know before letting it go further.

This is the most direct way to ask “was this step the problem, or something before it”: fork earlier and later in the same run and compare where the outcome changes.

Once you have a fork whose outcome is the one you wanted, Promote to a test turns it into a permanent verifies case on the machine, in one action. The fork already carries everything a test needs: the prefix state, the input, and the expected output. Promoting writes a new machine version with that case added, through the evolution ledger, and reports whether it’s already present or genuinely new:

The debugger wrote your test, and the machine is now v14. It passes 12/12.

This is the ninth classic principle (every bug makes you better) as a mechanism rather than a habit. The fix and its oracle are the same action, so a debugged run doesn’t just get closed, it becomes something that can never silently regress again.

Sweep every recorded run answers a different, sharper question: of every run this machine has actually done, how many would this fork’s edit change? The result is a count, unchanged, diverged, skipped (not replayable), and indeterminate, plus a per-run line naming exactly where the first divergence happens:

3 of 40 recorded runs change under this edit, 37 unchanged.

That’s regression testing against real production history, with no fixture anyone had to write by hand. The unchanged count matters as much as the diverged one: a sweep that reports “everything changed” and one that reports “nothing changed” would look identical without it.

Everything above works on the machine’s own logic. Model migration replay (the third view toggle on a run page, next to Trace and Cockpit) answers a different question: would a different model have decided this the same way?

Trigger it with a target model, and the runtime genuinely re-invokes that model under governance and mints its own governed run with its own receipt. The original run is never modified. What comes back is a structured diff, not a raw comparison you have to eyeball:

  • Per-reasoning-step output diffs, original next to replay.
  • The governance diff. A denial added or removed by the new model is reported as a first-class result, never a footnote: the panel marks the decision profile changed and lists each added or removed denial by step name and stage.
  • Cost and latency deltas, per step and in total.
  • The final output diff, with the specific fields that changed.

If the target model would have been refused where the original wasn’t (or the reverse), that shows up as a governance diff before you ever compare outputs. That is often the answer by itself.

Everything on this page happens from a run’s own page. There is a fourth, broader form of replay: faithful replay of a run exactly as it happened, or replay against your machine’s current governance policy to see whether a rule you changed would have changed the outcome. That form is reached from the CLI, documented on the tooling reference under Debugging: mashin replay <run_id> and mashin replay <run_id> --reevaluate.