Skip to content

[persistence][P1] load_slot rolls back the snapshot but not session logs — discarded timeline stays canon (audit F08-1) #779

Description

@100yenadmin

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)

  1. save_slot writes a manifest slots/<slot>.sessions.json mapping {session_id: byte_length} alongside the snapshot copy.
  2. 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).
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    engine-auditFinding from the 2026-06-11 full-engine adversarial audit

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions