# openquiver-runtime
The executor: exec tokens, the value store, and run resumption.
## How execution works
An exec token walks the white wires from an event node. At each node the runtime
resolves that node's inputs, runs it, records the outputs, and continues from the
execution output the node chose. Traversal was already done at compile time, so
this is a walk over a precomputed plan rather than a search.
`step` runs exactly one node, and `run` is a loop over `step`. That is not a
convenience: a node is the unit a run is interrupted at, and it is also the unit
a debugger steps by, because a breakpoint and a crash leave the journal in
exactly the same shape.
## The journal is the state
Nothing is held anywhere else. Each node writes `node.entered` before it touches
anything and `node.exited` — with its outputs and the execution output it chose —
afterwards. Resuming replays those records forward: nodes that recorded an exit
are not run again, their outputs are read back from what they wrote, and work
continues from the front.
The vocabulary lives here rather than in `openquiver-store`, which deliberately does
not know what an event means. That is what lets the set grow without a schema
migration each time.
| `run.started` | `workflow` |
| `node.entered` | `node` |
| `node.exited` | `node`, `outputs`, `exec` |
| `print` | `text` |
| `run.finished` | — |
An entry whose kind this build does not recognise is refused rather than skipped.
Skipping means replaying a journal minus the part you did not understand and
continuing from a state that never existed. The same reasoning refuses a journal
that never started, one that starts twice, and one that continues past its own
end: read leniently, each of those is a run that finished having done nothing,
and it would be recorded as a success.
`run.started` carries a **fingerprint of the plan**, and resuming refuses a
journal whose fingerprint does not match. Without it, editing a workflow while a
run is suspended is silently destructive: a renamed node fails loudly, but a
*deleted* wire makes the resumed run stop early and report success.
A session appears in the journal as its **handle**, never as the object: the
agent process does not survive a reboot, so resuming re-opens the conversation
from the handle instead of starting a new one. That is why ACP was the right
substrate — session resumption is in the protocol rather than bolted on.
## The honest limit
A node that died between `node.entered` and `node.exited` is in an unknown state:
the issue in the tracker may or may not have been created, and no journal can
tell you which. Every node type therefore declares a recovery policy, and the
safe answer is the default — a type has to claim the cheap one deliberately.
- `Recovery::Retry` — safe to run again. Resuming just runs it.
- `Recovery::Ask` — only a human can say. Resuming refuses and names the node;
resuming again with `InDoubt::Retry` runs it, accepting the double effect.
`acp.prompt` is the one node in this milestone that is not `Retry`. A turn
interrupted halfway may have been answered, thought about and paid for, and
nothing distinguishes that from a turn the agent never saw.
`Retry` is not free either, and the tests say so out loud: a repeated `io.print`
really does print twice. It is chosen where repeating is cheap, not where it is
invisible.
A run stopped by something that will fail identically next time — a graph the
runtime cannot walk, a journal it cannot read — is recorded as **failed**, not
suspended. Left suspended it would sit in the list of runs to pick up, and every
attempt would append another `node.entered` to a journal that can never reach an
end.
## Where a run stops
Execution ends when a node leaves by an execution output with nothing wired to
it. That is ordinary Blueprint semantics — an unwired `false` on a Branch means
"do nothing in that case" — but it deserves attention on `acp.prompt`, whose four
outputs exist precisely so a refusal is not mistaken for an answer. A graph that
wires only `end_turn` treats a refusal as the end of the workflow. The journal
records which output was taken, so the run is readable; nothing warns you at
compile time yet.
## Not guarded against
**Two runtimes on one run.** `resume` accepts any run the store calls resumable,
and there is no lease. Two processes resuming the same run will both walk it and
both prompt the agent. Status alone cannot fix this — a crashed run is left
`Running`, so refusing `Running` would refuse exactly the case resumption exists
for. The answer is a lease held by the daemon, which is where runs will live.
## Determinism is a load-bearing requirement
Because resumption replays the journal, replay must produce the same thing.
Ambient nondeterminism is therefore banned from pure nodes: randomness comes from
a seed, and the clock is a `Now` node whose result is journaled rather than read
again at replay time. `PureNode` will enforce this structurally by never handing
out an I/O context.
## Not built yet
- **Pure nodes and per-tick memoization.** Every node this milestone ships is
impure, so there is nothing to memoize. It arrives with the first pure node.
- **Parallelism.** A run has one frontier. `Parallel` and `Join` nodes fan out
and collect explicitly — inferring concurrency from data dependencies would
make it impossible to reason about how many agents are running and what they
cost — but neither node exists yet, and one frontier is the whole of M1.
- **Snapshots.** Resumption replays the journal from the start, which is free at
the length an M1 run reaches. Snapshots every N events are what keeps that free
once runs are long.
- **Pause and cancel.** Suspension already works — it is what a killed process
leaves behind, and what a resumable run is picked up from. Soft pause and
cascading cancellation with `session/cancel` arrive with the daemon, which is
where a run that nobody is watching will live.
- **Several event nodes in one graph.** A run begins at one, and choosing which
is a question for the Run panel.