ADR 0060: Progress survives as minted checkpoints, never a mutated identity
Status: accepted (James, 2026-07-25). Implemented in the GBA emulator integration and the GBA MCP server, with the fail-closed paths covered by tests.
Current status (2026-08-19)
Checkpoint identity and verification remain unchanged. Under ADR 0129, Clankie's play and each GBA MCP process operate separate cores and checkpoint scopes; save/load authority comes from the owning runtime, not possession of a shared body.
Context
Every boot of the real core loads a pinned savestate and verifies it against the scenario fixture's digest, failing closed on mismatch. Play progress persists through separately minted checkpoints. The configured fixture remains immutable; the in-game save writes to process-local flash until the checkpoint serializer captures the complete core state.
The naive fix — write the current state over the configured savestate — would break the model it lives inside: the fixture's digest would fail to match, and "the pinned savestate" would quietly stop meaning anything.
Decision
A save mints a sibling identity
writeGbaCheckpoint captures the serialized core state into an operator-local
checkpoint directory alongside two documents:
- a receipt recording the checkpoint id, optional label, capture time, overworld position, and the digests of the savestate, ROM, and core wasm the running core verified at boot;
- a companion scenario — the booted route scenario with only its savestate
identity replaced (
savestateId: checkpoint:<id>, the new digest). It parses under the same schema and boots through the same fail-closed loader, so a checkpoint is a first-class pinned identity, not an exception to the rule.
The pinned fixtures stay frozen. Nothing ever overwrites an existing identity; a checkpoint is always a new sibling. Savestate bytes stay operator-local exactly like the ROM — receipts, evidence, and tool results carry digests only. Receipts also name the environment and journey when the minting runner knows them, so an operator can identify saves without opening their state bytes.
A load verifies everything before touching the core
readGbaCheckpoint refuses ids that are not directory basenames, receipts that
do not name their own directory, checkpoints taken from a different ROM or core
build, and savestate bytes that fail their recorded digest. Only then does the
core restore, resetting its derived battle bookkeeping — that bookkeeping
described the replaced timeline. Logical frame and input counters keep
counting: they order evidence within the process run, and restoring RAM does
not rewind what already happened here.
Save and load are driving
The MCP surface publishes both as lease-gated tools. gba_emulator_save_state
captures the body's complete state — more than observation exposes — and
gba_emulator_load_state rewrites the body's whole world, so both are gated by
possession exactly like acting (ADR 0053).
Load with no id lists what exists instead of guessing. Both are absent on the
deterministic double, whose determinism is its identity: there is no state to
serialize that the scenario does not already pin.
Consequences
- Hours of unreplayable free play (ADR 0049)
outlives the process: save a checkpoint, and a later boot points
CLANKIE_GBA_SAVESTATE_PATHandCLANKIE_GBA_SCENARIO_PATHat the checkpoint's pair. Mid-session, a possessor can restore without restarting. - The determinism anchors hold. Session specs bind to whatever scenario booted
them, checkpoint or fixture, through the unchanged
validateScenarioBinding. - The companion scenario's route fields (map, start, target) describe the original fixture's route, not where the checkpoint is taken; the receipt records the actual position. A checkpoint scenario is a boot anchor, not a route to replay — running the deterministic route drivers from one is not a supported use.
- Evidence emitted after a mid-session load still cites the boot savestate in its determinism anchors, which stays true — that is where this session started. The load itself is visible in the tool result and the watchers' frame stream.
- Deletion is operator-only.
/savesin the local TUI lists validated receipt directories and requires a second confirmation before removing exactly one; malformed ids, mismatched receipts, and symlinked directories are refused.
