CLAUDE.md@packages/machine · git:20260926.67b20df · 2026-09-26 · sha256 8915fcb6b5671677

CLAUDE.md@packages/machine git:20260926.67b20dfA

Immutable. This exact content never changes and is served at /api/v1/blob/8915fcb6b5671677.

# machine

## Purpose

The pure operation machine: pi's harness spec Part 3 transcribed into Gleam
ADTs, with pi's "lane" renamed "strand". It owns the operation state space,
the acceptance transaction, settled-response classification, queue
admission and abort, the register payload codecs, and `next_action` — the
total function `State × Inputs → Action` the runtime drives. WP-D.
Fidelity, not invention: where this package and pi's spec differ, this
package is wrong.

## Key Types

- `machine/operation.Operation` — immutable per-operation metadata, written
  once into `op.meta` at acceptance.
- `machine/operation.OperationState` — the durable program counter, stored
  in `op.state` and replaced by every transition. It is *total*: it never
  depends on a previous state, and recovery reads exactly this value to
  decide where to resume. There is no finished variant — an ended
  operation has no state at all.
- `machine/operation.{RunPhase, Generation, ToolBatch, Inbox, Control,
  StructuralDecision, LastResult}` — the phase space: checkpoint,
  assistant, tools, compaction, deferred, failure drain; per-call
  planned/effect-pending/completed; the terminal outcome that lands in
  `strand.last_result`.
- `machine/planner.next_action` — the frozen entry point (spec §1.3).
- `machine/planner.Action` — `Transition(next, tx)`, `Dispatch(intent,
  next, tx)`, `AwaitEffect(key)`, `Wait(until)`, `Finish(result, tx)`,
  `Fault(report)`.
- `machine/planner.{PlannerInputs, Observation, EffectKey, EffectIntent}` —
  what the driver gathers, what comes back, and what the machine asks for.
- `machine/planner.RefusalEnding` — `RefusalContinues | RefusalEndsRun`,
  carried by `ObservedToolRefused`. A refusal is staged like a settled
  result, and `RefusalEndsRun` gives it the `terminate` a tool's own
  settlement would, except under cancelled control. The runtime's
  repeated-failure guard is the one producer of `RefusalEndsRun`;
  `docs/spec-gaps.md` WP-D item 8 records the divergence from pi.
- `machine/planner` is a big module on purpose: one public function over
  a closed vocabulary, then a section per phase of pi's spec. Read its
  module doc first — it enumerates the sections and what each *decides*,
  which is the map. Three conventions hold throughout: every type the
  module has is declared before the first function body (the private ones
  included); a phase's entry function is a dispatch table whose arms
  name the decision rather than making it; and a handler takes the
  *bundle* its values arrived in rather than the bundle exploded into
  parameters. There are four bundles — `RunPass` (a pass over a run:
  `op`, `in`, and the four `RunState` fields that are not the phase),
  `AssistantAttempt`, `StructuralTask` and `Fetch` — each of them the
  record its values were destructured out of, so passing one is a
  restoration and not an invention. A handler that changes a field says
  so with a record update (`RunPass(..pass, inbox: remaining)`) at the
  one hop that makes the change; `run_state` puts a pass and a phase
  back together into the state a transition commits. Do not go back to
  threading the fields: `gleam format` gives a wide call one argument per
  line, so each value costs two lines per hop and nothing checks the
  argument order but the types — issue #75 measured that shape at 28% of
  the file. The corruption path has its own `use` forms — `or_fault` and
  `or_fault_unless` — because Gleam has no early return and `result.try`
  cannot serve where the continuation returns an `Action`; reach for them
  rather than nesting a `case` whose error arm is `Fault(report:)`.
- `machine/acceptance.accept_prompt` — the single acceptance transaction
  for a run, standalone compaction, or navigation.
- `machine/classification.{settle, classify}` — the normative
  first-match-wins order over a settled assistant response.
  `ClassifyCtx.expected_api` is the *captured* request api, not the
  response's self-report.
- `machine/operation.ToolExecution` — `Sequential` or `Parallel`, the
  batch's scheduling mode; it decides which call `next_action` works next.
- `machine/strand.{StrandConfiguration, StrandState}` — the strand-scoped
  register payloads.
- `machine/codec` — total JSON codecs for every register payload above.

## Relationships

- **Depends on**: `core` only (ids, entries, messages, json, tx,
  corruption). Nothing else — not `storage`, not `provider`, not
  `gleam_erlang`.
- **Depended on by**: `session` (typed register access through
  `machine/codec`), `runtime` (drives the planner), `conformance` (scripts
  and invariant checks).
- **FFI**: none, and there must be none — no `@external` of any target,
  and no `gleam_erlang` or `gleam_otp` in `gleam.toml`, by rule rather
  than by coincidence.
  Two properties rest on that, and one `@external` closes both: purity is
  what makes the state space property-testable without spawning processes,
  and the same discipline is what keeps this package compiling to the
  **JavaScript target**. Lint R6 gates on it at error level and its census
  must stay zero. Portable here means *decide but not act* — replay a
  conversation tree, validate a transcript with the server's own total
  decoders, run `next_action` over fetched state — and never the harness in
  a browser: `gleam_otp` has no JavaScript target, Rule Zero is
  kernel-enforced (in a browser the harness VM and the untrusted-code VM
  would be the same VM), and the two-channel doctrine needs processes on
  both sides. `docs/gleam-style.md` Part IV §5 argues it in full.

## Traffic

- **Actor messages**: none. This package spawns nothing and waits on
  nothing; the driver in `runtime/strand_runtime` owns all process
  concerns.
- **Commits** — it *builds* transactions, never commits them. Every
  `Action` carrying a `Tx` carries all the writes that make its `next`
  state true, guarded by the seq expectations the caller must have read
  against. `machine/internal/build` holds the shared write builders.
- **Registers**:
  - writes `op.meta` (write-once at acceptance), `op.state` (every
    transition; deleted by the terminal transaction), `op.tool_args`
    (write-once, key `{op}:{step}:{index}`), `op.preparation`
    (write-once, key `{op}:{task}`), `pending.entry` (keyed by the
    reserved `EntryId`), `strand.leaf`, `strand.state`,
    `strand.last_result`, `fact.custom` under the reserved
    `operation-result/{op}` prefix (the operation-keyed copy of the
    terminal result, written atomically beside `strand.last_result` so a
    waiter keyed on the operation survives a later run's overwrite —
    `operation.result_fact_key`), and `fact.label` (entry labels, keyed
    by entry id — Loom has no dedicated label namespace);
  - reads `strand.config`, `strand.state`, and `strand.leaf` seqs to build
    expectations (`build.expect_leaf` takes `Option(Seq)` — `None` when the
    leaf register does not exist yet).
- **Effect intents** — `ProviderRequest`, `ToolRequest`, `ToolReplay`,
  `DeferredFetch`, `SummaryProviderRequest`; their outcomes return as
  `Observation` variants (`ObservedAssistantSettled`,
  `ObservedToolOrphaned`, `ObservedStructuralDecision`, ...).
- **Wire**: none. The provider and effect wires belong to `provider` and
  `broker`.

## Invariants

- **A provider retry hint survives settlement and recovery.** The runtime
  preserves a retryable error's `retry_after_ms` in the existing assistant
  diagnostics JSON. The machine persists the later of that minimum delay and
  its captured policy's wait as `GenerationRetryWait.not_before`.
  Missing, malformed, negative, or shorter hints cannot shorten the policy.
- **The retry wait is capped, jittered, and pure.** `NormalizedRetryPolicy`
  carries a `RetryBudget` (`Bounded` or `Unbounded`) and a `max_delay_ms`
  cap; the exponential ladder clamps to the cap and then equal-jitters into
  the upper half of the interval. The draw is seeded from the failed
  attempt's entry id, so replay is deterministic and no random input reaches
  the planner. An `Unbounded` budget turns the ladder into a long poll a
  rate limit cannot exhaust; cancellation is the exit, and it works at every
  wait because the wait is durable state rather than a timer. The codec
  reads a pre-cap recording (integer `maxAttempts`, no `maxDelayMs`) as an
  uncapped bounded policy.
  Summary failures use the same convention in `OperationError.details` when
  their producer supplies it. Terminal cancellation remains terminal.

- **The state is total.** Recovery reads `op.state` and nothing else to
  decide where to resume; no transition may depend on the previous state
  having been observed. `strand.last_result` is never read by the driver;
  it is latest-wins per strand, which is why the terminal transaction
  also records the same result operation-keyed (`operation-result/{op}`
  in `fact.custom`) for waiters that must observe a *specific*
  operation's outcome after later runs.
- **The effect sandwich.** Every external effect is preceded by a committed
  intent transaction and followed by a settlement transaction; `Dispatch`
  returns the intent `tx` *and* the next state, and the driver must commit
  before starting the effect.
- **Large payloads live at sibling registers, not inline.** Tool arguments,
  structural preparations, and queued entry payloads are written once under
  deterministic keys; queue lists carry only ids.
- **Acceptance must observe an idle strand *and* an unmoved leaf** (pi
  invariant 14; review finding ORCH-L6). The acceptance transaction expects
  the strand-state seq **and** the `strand.leaf` seq the caller read, so a
  losing concurrent accept fails `StaleExpectation` and reports
  `StrandBusy` after reload, and a concurrent idle tree-write moving the
  leaf between the read and the commit refuses the acceptance instead of
  mis-parenting its entries. Pre-acceptance rejections write nothing.
- **Deferred handles are validated against the captured request api**, not
  the response's claim (ORCH-L4). Admission resolves the api, the intent
  transaction persists it as `Generation.request_api`, and classification
  compares `{provider, model_id, api}` against that stored value — so a
  routing change between dispatch and settlement cannot smuggle a handle in.
- **Batch scheduling mode decides which call is worked next** (pi §3.8,
  ORCH-M2). `Sequential` works the frontier's head only, so one call
  clears, executes, and settles at a time; `Parallel` works the first
  still-*planned* call even while earlier calls are effect-pending, and
  parks on the first pending call only once nothing is left to plan. Tree
  materialization stays source-ordered in both modes.
- **A call whose arguments never parsed is refused before clearance, not
  at it** (issue #189). The streaming adapters settle such a call carrying
  `message.malformed_arguments` instead of failing the whole stream, and
  `work_planned_call` reads that back into the same machine-built
  synthetic `is_error` result a cancelled or truncated call gets. Clearance
  is where a *tool* judges arguments, and the arguments here are not the
  model's — they are the sentinel standing in for text that never became
  JSON, which a schema of all-optional fields would happily accept as
  defaults the model never asked for.
- **A really-settled response under cancelled control keeps its content and
  its reported usage** (pi §4.6, ORCH-M3), committed normalized to
  `aborted`. Only an *unknown-outcome* orphan gets the synthetic
  zero-usage settlement — abort must not fabricate a cost of zero for work
  the provider actually did.
- **Classification order is normative and first match wins** (spec §1.3):
  cancelled control, overflow, valid deferred handle, retryable error, tool
  use, stop. Two normalizations happen at commit and both are deliberate: a
  cancelled response commits as `aborted`, an overflow-classified response
  commits as `error`. An `aborted` stop reason with running control is
  unreachable and reported as corruption.
- **A pure total planner cannot crash.** Corrupt inputs or durable state
  surface as `Fault(report)`, a sixth `Action` variant beyond the frozen
  five (`protocol-change/002`); the caller must fault, never continue.
- **The first abort wins and payloads outlive it.** `cancel_requested`
  moves steer and follow-up ids into the drained lists without deleting
  their payloads; those die only in the terminal transaction.
- **Terminal cleanup is over-approximate by construction.** The pure
  machine cannot scan, so tool-args and preparation registers are deleted
  from caller-supplied key lists; delete-absent being a no-op keeps this as
  safe as pi's defensive scan.
- **Adapter retryability travels by convention**, not by a field: the
  planner reads `raw_stop_reason == "retryable"`, and the runtime bridges
  `provider/retry.classify` into it (spec-gaps WP-D item 3).
- **A failed in-run compaction is judged by its `CompactionReason`**, the
  same way a declined one already was (issue #34). A *threshold*
  compaction is the harness's own clamp: failing it against an
  unavailable summarizer restores the resume checkpoint and the run
  carries on, with `enabled` cleared in the run's captured
  `CompactionSettings` so the threshold cannot re-fire into the same dead
  route at every later boundary — settings are per-operation, so the next
  run tries again. An *overflow* compaction still drains, and so does a
  threshold one whose error is about the context rather than the
  summarizer (`fatal_to_the_context`). Corruption never reaches this
  decision; it is a `Fault` at its own site.
- **Faithful-but-surprising transcriptions are kept deliberately** — a
  completed tool batch sets skip-inbox-once; the threshold check also runs
  at may-finish checkpoints; backoff saturates at exponent twenty and
  clamps to the policy cap;
  overflow during a deferred poll drains as failure; summary usage rows
  carry no entry id.

## Deep Docs

- [docs/architecture/orchestration.md](../../docs/architecture/orchestration.md)
  — the durable program counter, the effect sandwich, the state space, the
  drive loop.
- [docs/spec-gaps.md](../../docs/spec-gaps.md) — "From WP-D (`machine`)":
  the `Fault` variant, the absent list store, prefix scans, entry labels.
  "From the M3 runtime wave": the additive admission api field, the leaf
  expectation on acceptance, and parallel dispatch adding per-tool
  exclusivity only (the broker's pooled budget stays the ceiling).
- [protocol-change/](../../protocol-change/) — amendments to the frozen
  interfaces this package implements.
- [Root CLAUDE.md](../../CLAUDE.md) — repo ground rules and the doc graph.