What is broken
load_slot rolls back the campaign snapshot but not the session logs. After restoring a save slot, every post-slot log entry (the discarded timeline) remains canon: it survives in read_log_all, in recap_from_store (the cold-open previously_on), and in the FTS recall ledger. An undone TPK — or any rolled-back beat — stays permanently interleaved with the new timeline in the DM's only lean-beat memory. The corruption is silent and permanent (append-only log, stable t-sort at store.py:392 keeps the two timelines stably interleaved).
Why (root cause, file:line)
save_slot (servers/engine/store.py:183-197) copies only snapshot.json.
load_slot (store.py:200-223) writes only the snapshot back; the restored session_ids point at JSONL files that still hold every post-slot entry.
- Post-slot orphan sessions (minted after the slot was saved, so absent from the restored snapshot's
session_ids) are swept back in by the leftover branch at store.py:382-385.
- Appends never truncate; nothing reconciles log length with the restored snapshot.
- The server-side
load_slot tool (server.py:7158-7169) is a thin store passthrough — no compensating caller exists.
Evidence
How to fix (the spec — verified invariant-clean)
- save_slot writes a manifest
slots/<slot>.sessions.json mapping {session_id: byte_length} alongside the snapshot copy.
- load_slot: (a) archives session files NOT in the manifest to
sessions/rolled-back-<ts>/ — invisible to read_log_all because its glob *.jsonl is non-recursive (verified at store.py:374); (b) truncates files LONGER than their manifest byte-length back to the recorded length via _atomic_write (archive the tail first).
- Degrade paths: manifest-less slots (pre-fix saves) degrade to today's behavior; a session file shorter than its manifest length (possible after an intervening restore of an older slot) must degrade to leave-as-is — never pad, never raise (skeptic spec addendum).
Invariants PASS: engine sole-writer under the caller-held lock; additive (old slots degrade gracefully); archive dir invisible to the non-recursive glob.
Test strategy
- End-to-end: save slot -> play N beats (new prose + a freshly-minted session) -> load slot -> assert
read_log_all and recap_from_store contain zero post-slot text, orphan session archived, truncated file byte-exact to manifest.
- Degrade matrix: manifest-less slot (today's behavior); shorter-than-manifest file (leave-as-is, no pad/raise); byte-identical restore (no-op).
- Extend
tests/test_slots.py with session-log assertions (currently zero).
Scorecard impact (gate/lens)
state-integrity (Angry-DM lens) + story + sat — an undone TPK persisting in recap + lean-beat memory is silent, permanent corruption of the DM's only memory of what happened; it poisons every later recap and recall on that campaign.
effort: M | confidence: high | finding-id: F08-1 | Source: docs/audits/ENGINE-AUDIT-2026-06-11.md (PR #768), unit 08
What is broken
load_slotrolls back the campaign snapshot but not the session logs. After restoring a save slot, every post-slot log entry (the discarded timeline) remains canon: it survives inread_log_all, inrecap_from_store(the cold-openpreviously_on), and in the FTS recall ledger. An undone TPK — or any rolled-back beat — stays permanently interleaved with the new timeline in the DM's only lean-beat memory. The corruption is silent and permanent (append-only log, stable t-sort at store.py:392 keeps the two timelines stably interleaved).Why (root cause, file:line)
save_slot(servers/engine/store.py:183-197) copies onlysnapshot.json.load_slot(store.py:200-223) writes only the snapshot back; the restoredsession_idspoint at JSONL files that still hold every post-slot entry.session_ids) are swept back in by the leftover branch atstore.py:382-385.load_slottool (server.py:7158-7169) is a thin store passthrough — no compensating caller exists.Evidence
skeptic_u8_probe.py, RE-RUN from a fresh state dir on HEAD a245a2c, seeded with a real 130 KB campaign):F8-1_postslot_in_log:true,F8-1_orphan_swept:true,F8-1_recap_contaminated:true— post-slot "incinerates" prose survivesload_slotin bothread_log_allANDrecap_from_store.qa/transcripts/ow-relaunch-3lens.md:27shows the DM callingsave_slotunprompted at session close;load_slotis the advertised reload path.tests/test_slots.pyis snapshot-fidelity only — zero session-log assertions.How to fix (the spec — verified invariant-clean)
slots/<slot>.sessions.jsonmapping{session_id: byte_length}alongside the snapshot copy.sessions/rolled-back-<ts>/— invisible toread_log_allbecause its glob*.jsonlis non-recursive (verified at store.py:374); (b) truncates files LONGER than their manifest byte-length back to the recorded length via_atomic_write(archive the tail first).Invariants PASS: engine sole-writer under the caller-held lock; additive (old slots degrade gracefully); archive dir invisible to the non-recursive glob.
Test strategy
read_log_allandrecap_from_storecontain zero post-slot text, orphan session archived, truncated file byte-exact to manifest.tests/test_slots.pywith session-log assertions (currently zero).Scorecard impact (gate/lens)
state-integrity (Angry-DM lens) + story + sat — an undone TPK persisting in recap + lean-beat memory is silent, permanent corruption of the DM's only memory of what happened; it poisons every later recap and recall on that campaign.
effort: M | confidence: high | finding-id: F08-1 | Source: docs/audits/ENGINE-AUDIT-2026-06-11.md (PR #768), unit 08