Repository navigation
S134 — Saves and branches carry their epochs - #592
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).
resolveSaveEnvelopecan take an epoch resolver as a fourth argument. A save whose epochengine.contentstill resolves, and whosekindVersionis the registered kind's, loads on that epoch. It is not migrated and stays replay-compatible.campaignVersionon a log that already has an epoch entry, the load appends{ seq, system: "migration", from, to }, so C26 holds.campaignVersionmust now match the embedded state's.saveGamestamps the version of the epoch the state is on.loadGamereaches its adoption point and persists the result in the new session's first write.branchSessionreplays the retained prefix from the log's starting version:submitAction, content entries throughadoptContent;atActionCountcounts entries of every type;branchSessionchecks every version the replay depends on before creating anything, so a refusal writes nothing:invalid_state;unknown_campaign;invalid_state.Criteria
S134.1 — a save on a held epoch loads as it was made(3 tests).replayCompatibletrue, log[a0],formatVersion1, and it can branch.S134.2 — every other save migrates, and a restamp over a crossed log is logged(3 tests).{seq: 2, migration, 2→4}, keepsformatVersion2, and passesdeserialize. Saving and loading it again appends no second entry.formatVersion1.kindVersion-only migration appends nothing.envelope.test.ts"a wrapper campaignVersion that disagrees with the embedded state's is invalid_state".S134.3 / S134.5 — a branch replays across an adoption, byte-identically.[a0, c1, a2].atActionCount4 →invalid_fork_point.S134.4 — a branch that cannot be reproduced refuses, with nothing written(4 tests).replayCompatible: truerecord →invalid_state.invalid_state, while a fork before it still branches.unknown_campaign.[a0, c1→2, a2, c3→1]under a v1-only archive: forks at 2 and 4 →unknown_campaign; a fork at 1 branches.Differs from design
Nothing differs from the contract. The judgement calls are recorded in
90-decisions.md, 2026-10-09:kindVersionmatches. Otherwise it takes the registry path.campaignVersionis cross-checked against the state's.fromis the state'scampaignVersion, and it is appended only after the kind accepts the migrated state.branchSessionchecks every version it depends on before creating anything. It reads a migration entry from the log itself, not only fromreplayCompatible.unknown_campaigntest. 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.long-horizon.replay.test.ts"long-horizon-win: every state it reaches round-trips through deserialize", timing out at 5 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 -BuildOnlywas 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.tsplusenvelope.test.tswere run (162 tests).campaignVersionchecksaveGamestamps the state's epochloadGameadoption pointadoptContent