Skip to content
Developer Preview — APIs and language features may change before 1.0

Reading a Run

The classic advice for debugging is: don’t panic, treat the bug as a puzzle, and read the error message before you touch any code. That advice assumes you have to go looking for what happened. In mashin you don’t. Every run is recorded in full as it executes, so by the time something fails, the evidence already exists. Reading a run is the whole first act of debugging.

Every run has a page. From Running in the sidebar, pick a run, or follow a link from a machine’s Runs tab. The page opens in Trace view by default; switch to Cockpit debugger view with the toggle at the top for the layout this page describes (a run URL looks like /running/<run_id>?view=cockpit, so it’s a link you can send someone).

If the run failed, a banner sits at the top of the cockpit before you scroll past a single step. This is the failure envelope, and it exists so you never have to reconstruct what went wrong from a stack trace:

Field What it tells you
outcome How the run ended
error_category / error_code What kind of failure this was
error_message The failure in words
retryable Whether trying again could plausibly help
root_cause_intent_ref The exact step that caused it
denial_stage / denial_reason If governance stopped it, which stage and why

That last pair matters: a permission denial is not an error the machine produced. It’s a decision the runtime made on purpose, and it is reported as one. You are never left guessing whether something broke or something was refused.

Below the envelope is the spine: one row per step, in order. Each row carries:

  • A type glyph (compute, reason, call, decide, await, remember), so you can see the shape of the run without reading names.
  • A governance tick (allow, hold, or deny) in the left gutter, for any step that went through a directive.
  • A heat bar on the right showing cost and latency, so an expensive or slow step stands out before you click into it.
  • A status color: completed, failed, denied, or running.

Click any row to move the playhead there. The playhead is the same thing a debugger’s current line is, except it never has to stop and wait for you: the run already finished, so moving the playhead just changes what you’re looking at.

A transport bar above the spine gives you four ways to move it: step forward or back, jump to the next failed step, jump to the next denied step, or jump to the next cost or latency outlier. A run minimap along the top lets you click straight to any step by position. None of this replays anything. It’s navigation over a record that already exists.

Selecting a step opens its variable panel: the exact environment at that point in the run; input, prior step outputs by name, state, and output so far. This is the answer to “what did this step actually see,” without adding a single log statement anywhere.

Two details make it more than a JSON dump:

  • Provenance. An entry in the step scope is clickable, and clicking it moves the playhead back to the step that produced it. You can walk a value’s history backward, one producer at a time.
  • Redaction. A field the machine declared sensitive renders as a locked REDACTED chip, never the raw value. The inspector shows you everything you’re allowed to see and nothing you aren’t, in the same view.

If any step in the run was denied, a panel above the spine lists each one in plain language, for example: “network.http.post to api.payments.example is outside the wall declared in permissions_wall_denied.” Each entry jumps the playhead straight to the step it happened at. A run with a clean governance record shows nothing here. There is nothing to scroll past.

Here is a real machine, denied on purpose, so you can see what its run actually looks like in the cockpit rather than take the description on faith.

machine permissions_wall_denied
accepts
amount as number, default: 100
responds with
result as text
behaves
ask hop, from: "@mashin/actions/service/call"
endpoint: "https://api.payments.example/charge"
payload: {amount: input.amount}
compute done
{result: steps.hop.status ?? "unreached"}
ensures
permissions
allowed to network.http to "api.weather.example"
verifies
test "INTENTIONAL DENIAL: the callee's request to a host outside the composer's wall is refused"
given {amount: 100}
expect denied

permissions_wall_denied declares that it may reach api.weather.example and nothing else. Its one step, hop, calls the standard library’s @mashin/actions/service/call, an ordinary machine that would happily make this request on its own. Called from inside this wall, its request to api.payments.example is outside the boundary this machine drew, so it is refused: a composer can narrow what it calls, never widen it.

Open this run in the cockpit and you’d see:

  • The failure envelope reads outcome: denied, with denial_stage and denial_reason naming the exact constraint that fired.
  • The spine shows one row for hop, marked denied, governance tick showing a refusal rather than an allow.
  • The denials panel carries one entry: which host, which policy line, and a jump straight to hop.
  • The variable inspector on hop shows input.amount and nothing past it, because nothing past it ran.

That is the whole investigation. No reproduction step, because the run is the reproduction. No log statement, because the ledger already recorded the decision. The only thing left to do is decide, as a human, whether the wall should widen or the call should go.

The mechanical parts of classic debugging (reproduce the bug, read the error, gather evidence) exist in other tools because the runtime forgets. mashin’s runtime doesn’t. The behavioral ledger is the log you didn’t have to write, and a run’s cockpit view is that log, laid out to read. What’s left is the part no substrate can do for you: deciding what the failure means and what to change. That’s the subject of the next page.