Skip to content

S134 — Saves and branches carry their epochs - #592

Merged
The-Running-Dev merged 1 commit into
mainfrom
slice/S134-saves-branches
Oct 9, 2026
Merged

The-Running-Dev merged 1 commit into
mainfrom
slice/S134-saves-branches

Conversation

@The-Running-Dev

Copy link
Copy Markdown
Owner

What

Saves and branches now carry the epochs a session ran on (16 §3.5, §5.3, §5.6, §7; 20-contract §7.4, §10.2, B1).

  • resolveSaveEnvelope can take an epoch resolver as a fourth argument. A save whose epoch engine.content still resolves, and whose kindVersion is the registered kind's, loads on that epoch. It is not migrated and stays replay-compatible.
  • Any other save migrates onto the registry's version. If that changes campaignVersion on a log that already has an epoch entry, the load appends { seq, system: "migration", from, to }, so C26 holds.
  • The envelope's campaignVersion must now match the embedded state's.
  • saveGame stamps the version of the epoch the state is on.
  • loadGame reaches its adoption point and persists the result in the new session's first write.
  • branchSession replays the retained prefix from the log's starting version:
    • actions go through submitAction, content entries through adoptContent;
    • atActionCount counts entries of every type;
    • the branch then reaches its own adoption point in its first write.
  • branchSession checks every version the replay depends on before creating anything, so a refusal writes nothing:
    • a migration entry → invalid_state;
    • a starting or adopted version that is no longer held → unknown_campaign;
    • a recorded adoption the kind now refuses → invalid_state.

Criteria

  • S134.1 — met: S134.1 — a save on a held epoch loads as it was made (3 tests).
    • A v1 save loads under a v2 registry with no migration: replayCompatible true, log [a0], formatVersion 1, and it can branch.
    • With an offer, it adopts in exactly one write at revision 0, with the new session's scope.
    • With a refused offer, it stays pinned.
  • S134.2 — met: S134.2 — every other save migrates, and a restamp over a crossed log is logged (3 tests).
    • A crossed save migrating onto v4 appends {seq: 2, migration, 2→4}, keeps formatVersion 2, and passes deserialize. Saving and loading it again appends no second entry.
    • An uncrossed save appends nothing and keeps formatVersion 1.
    • A kindVersion-only migration appends nothing.
    • Also: envelope.test.ts "a wrapper campaignVersion that disagrees with the embedded state's is invalid_state".
  • S134.3 — met: S134.3 / S134.5 — a branch replays across an adoption, byte-identically.
    • Forks at 0–3 of [a0, c1, a2].
    • A branch's own adoption happens in a single write.
    • atActionCount 4 → invalid_fork_point.
  • S134.4 — met: S134.4 — a branch that cannot be reproduced refuses, with nothing written (4 tests).
    • A migration entry on a replayCompatible: true record → invalid_state.
    • An adoption a refusing kind now rejects → invalid_state, while a fork before it still branches.
    • A starting version that is no longer held → unknown_campaign.
    • A rolled-back log [a0, c1→2, a2, c3→1] under a v1-only archive: forks at 2 and 4 → unknown_campaign; a fork at 1 branches.
    • Zero session writes in every refusal.
  • S134.5 — met: same describe as S134.3. With no offer, branches at 3, 2, 1 and 0 serialize byte-identically to the source's stored blob after the adoption and after the last action. Forks at 1 and 0 match the blob of a session that never adopted.

Differs from design

Nothing differs from the contract. The judgement calls are recorded in 90-decisions.md, 2026-10-09:

  • A save loads on its held epoch only when its kindVersion matches. Otherwise it takes the registry path.
  • The wrapper campaignVersion is cross-checked against the state's.
  • A migration entry's from is the state's campaignVersion, and it is appended only after the kind accepts the migrated state.
  • branchSession checks every version it depends on before creating anything. It reads a migration entry from the log itself, not only from replayCompatible.
  • Two older store tests are superseded and removed:
    • S130's interim crossed-log refusal.
    • W99's mutated-registry unknown_campaign test. The engine's archive now decides, so mutating the store's registry no longer reaches a branch.

Verified

  • npm run typecheck, npm run lint: clean.
  • npm test: 119 files; 2329 of 2330 tests passed in each of three full runs.
    • The one failure each time was long-horizon.replay.test.ts "long-horizon-win: every state it reaches round-trips through deserialize", timing out at 5 s.
    • Alone it passes in 2.9 s.
    • It exercises only the kernel's deserialize, which this change does not touch. It is the timing flake already noted on S133 — Running sessions pick up new content #591.
  • ./build/Test-Documentation.ps1: passed (19 generated pages, 135 Markdown files).
  • git diff --check: clean.
  • ./docs.ps1 -BuildOnly was not run because it is not installed locally. CI's Verify Documentation Build covers it.

Revert verification: each fix was reverted alone, and store.test.ts plus envelope.test.ts were run (162 tests).

Reverted Failing tests
envelope loads on the held epoch 3
envelope appends the migration entry 1
envelope wrapper/state campaignVersion check 1
saveGame stamps the state's epoch 2
loadGame adoption point 1
branch refuses a migration entry 1
branch starts on the log's starting version 1
branch replays content entries through adoptContent 2
branch refuses a recorded adoption that now refuses 1
branch checks every named version first 2
branch adoption point 1

A save on a held epoch loads unmigrated and replayable, then reaches loadGame's adoption point; a migration that restamps a crossed log appends a migration entry. branchSession replays content entries through adoptContent from the log's starting version, checking every named epoch before writing anything.
@The-Running-Dev
The-Running-Dev merged commit 3674b97 into main Oct 9, 2026
8 checks passed
@The-Running-Dev
The-Running-Dev deleted the slice/S134-saves-branches branch October 9, 2026 11:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant