Skip to content

fix(engine): load_slot rolls back session logs (F08-1) + lookup_lore reachable for natural queries (F10-1) - #849

Merged
100yenadmin merged 1 commit into
mainfrom
fix/f08-1-f10-1-slot-logs-lore-queries
Jun 14, 2026
Merged

fix(engine): load_slot rolls back session logs (F08-1) + lookup_lore reachable for natural queries (F10-1)#849
100yenadmin merged 1 commit into
mainfrom
fix/f08-1-f10-1-slot-logs-lore-queries

Conversation

@100yenadmin

Copy link
Copy Markdown
Member

Root cause

F08-1 (#779) — load_slot rolls back the snapshot but NOT the session logs.
save_slot copied only snapshot.json; load_slot wrote only the snapshot back. The append-only sessions/*.jsonl logs were never reconciled, so a discarded timeline — an undone TPK appended to the same session, or a whole post-slot orphan session — stayed canon in read_log_all and recap_from_store, the DM's lean-beat memory. The #749/#763 decontamination filtered only exact-match wrapper-heartbeat lines; rolled-back story prose passed straight through. Permanent, silent corruption (two timelines interleaved under the stable t-sort).

F10-1 (#782) — lookup_lore buries the dedicated lore page for natural queries.
Two classes of natural query couldn't reach the dedicated page in the 356-page corpus:

  • Possessive: "Wyrm's Crossing" tokenized to ["Wyrm","s","Crossing"]; the bare 1-char "s" matched every page's apostrophe-s and dragged unrelated pages above wyrm-s-crossing.md.
  • Stopword-heavy: "the Counting House" — three authored (tier-0) pages matched ONLY the stopword "the" at noise rank (|rank| ≈ 2e-6), yet tier-0's absolute precedence let them fill the cap and bury the genuinely-matching counting-house-baldur-s-gate.md (a tier-1 wiki page at |rank| ≈ 8.97).

Fix

F08-1 (store.py): save_slot writes a session-log manifest ({filename: byte_length}) to a sidecar slots/.manifests/<slot>.json (a dedicated subdir, invisible to list_slots' non-recursive slots/*.json glob). load_slot reconciles the logs to that manifest AFTER restoring the snapshot, under the same campaign_lock:

  • session NOT in the manifest (post-slot orphan) → archived whole;
  • session LONGER than its manifest length → discarded tail archived, live file truncated back (atomic temp+fsync+os.replace);
  • session SHORTER/equal → left as-is (degrade, never pad/raise);
  • manifest-less (legacy) slot → no-op (today's behavior).

Discarded entries are archived under sessions/rolled-back-<ts>/ (recoverable, and invisible to read_log_all's non-recursive *.jsonl glob), never deleted. Truncation lands on a newline boundary (append_log writes entry+"\n"), so neither head nor archived tail is ever a torn JSONL line.

F10-1 (lorebook.py):

  • _safe_match drops sub-2-char tokens (unless that empties the match → keeps them so a degenerate query still searches).
  • Tier-0 precedence gains a relative noise floor: an authored page keeps absolute precedence only if its |rank| ≥ 0.01 × best_match_overall. Noise-rank stopword matches (≈6 orders of magnitude below a genuine hit) are DEMOTED below the wiki tier (kept as a fallback, never dropped); genuine authored matches still lead. The floor is relative, not a fixed epsilon, because bm25 magnitudes are corpus-size-dependent (a tiny corpus ranks every match ≈2e-6).

Invariants

  • Sole writer / atomic: all writes are _atomic_write or temp+fsync+os.replace, under the caller's campaign_lock.
  • Additive / round-trip: no model field added (F08-1 manifest is a filesystem sidecar); old snapshots and legacy slots round-trip; F10-1 has no model/wire change.
  • Frozen wire: no tool params, clawdnd-* ids, or CLAWDND_* env touched.
  • Token-mass: F10-1 keeps the same hit count (re-orders only); F08-1 REMOVES contaminants from the DM surface.

Tests (red-first, all green)

  • test_slots.py +12: grown-session truncate, orphan-session archive, tail archive, manifest-less degrade, shorter-than-manifest leave-as-is, snapshot byte-identical, manifest-not-listed, no-sessions-dir safe, end-to-end tool rollback.
  • test_lorebook.py +7: _safe_match token-drop, possessive + stopword reachability (synthetic + real-corpus repros), genuine-tie still wins authored, clean-query byte-identity (0/18 shipped clean queries changed).

Verification

  • Full engine suite: 2097 passed.
  • qa/fast_gate.sh Tier-0: PASS (188 deterministic).
  • F08-1 repro (undone TPK + orphan): before → both replay in read_log_all/recap; after → neither.
  • F10-1 repros on shipped corpus: possessive → wyrm-s-crossing.md at Epic 2: Characters & progression #3; stopword → counting-house-baldur-s-gate.md at Epic 2: Characters & progression #3 with genuine baldurs-gate.md/factions.md still leading.

Closes #779
Closes #782
Source: docs/audits/ENGINE-AUDIT-2026-06-11.md (F08-1, F10-1)

… natural queries (F08-1, F10-1)

F08-1 (issue #779): load_slot rolled back only snapshot.json, leaving a discarded
timeline (an undone TPK, a post-slot orphan session) permanently canon in
read_log_all / recap — the DM's lean-beat memory replayed events the player undid.
save_slot now captures a SESSION-LOG manifest (each sessions/*.jsonl byte length at
slot time) into a sidecar (slots/.manifests/<slot>.json, invisible to list_slots'
non-recursive slots/*.json glob). load_slot reconciles the logs to that manifest
AFTER restoring the snapshot, under the same campaign_lock: post-slot orphan sessions
are archived whole, sessions that grew past their slot-time length are truncated back
(archiving the discarded tail) under sessions/rolled-back-<ts>/ — invisible to
read_log_all's non-recursive *.jsonl glob, so recap/lean-memory match the rolled-back
snapshot. The discarded timeline is ARCHIVED (recoverable), never deleted. Manifest-less
(legacy) slots degrade to today (snapshot restored, logs untouched); a session SHORTER
than its manifest length is left as-is (never padded/raised). Truncation is at a
newline boundary (append_log writes entry+"\n"), so neither the head nor the archived
tail is ever a torn JSONL line. Additive: no model field added; old snapshots/slots
round-trip; engine stays sole writer (atomic temp+os.replace).

F10-1 (issue #782): lookup_lore buried the dedicated lore page for natural queries.
(a) _safe_match dropped sub-2-char tokens (unless that empties the match) — the 1-char
"s" of a possessive query ("Wyrm's Crossing") matched every page's apostrophe-s and
buried the dedicated page. (b) Tier-0 authored-canon precedence gained a NOISE FLOOR:
an authored page keeps absolute precedence over the 351-page wiki tier only if its FTS
|rank| is within a small fraction (0.01) of the best match overall — so a stopword-only
match ("the Counting House" → authored pages matching only "the" at |rank|≈2e-6, six
orders of magnitude below the genuine wiki hit) is demoted below the wiki tier (kept as
a fallback, never dropped) while genuine authored matches still lead. The floor is
relative (not a fixed epsilon) because bm25 magnitudes are corpus-size-dependent. Clean
queries leave the weak set empty → ordering reduces to today's tier-0-then-tier-1
(byte-identical; verified 0/18 clean-query outputs changed on the shipped corpus).

Tests (red-first):
- test_slots.py +12: grown-session truncate, orphan-session archive, tail archive,
  manifest-less degrade, shorter-than-manifest leave-as-is, snapshot byte-identical,
  manifest-not-listed, no-sessions-dir safe, end-to-end tool rollback.
- test_lorebook.py +7: possessive token-drop, stopword noise-floor (synthetic + real
  corpus repros), genuine-tie still wins authored, clean-query byte-identity.
Full engine suite 2097 passed; fast_gate Tier-0 PASS.

Closes #779
Closes #782
Source: docs/audits/ENGINE-AUDIT-2026-06-11.md (F08-1, F10-1)
@100yenadmin 100yenadmin added this to the v1.0.5 milestone Jun 14, 2026
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@100yenadmin, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 57 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 337587ec-47dc-4874-9e4f-17420677efe4

📥 Commits

Reviewing files that changed from the base of the PR and between 7dfef61 and 6653709.

📒 Files selected for processing (4)
  • servers/engine/lorebook.py
  • servers/engine/store.py
  • servers/engine/tests/test_lorebook.py
  • servers/engine/tests/test_slots.py

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 665370930d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread servers/engine/store.py
Comment on lines +276 to +277
if cur_len <= kept_len:
continue # unchanged or shorter — leave as-is (never pad/raise)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore logs for newer slots after earlier rollbacks

When a player saves slot A, keeps playing and saves slot B, then loads A, this rollback truncates the live session file and archives B's tail. If they later load B, the file is now shorter than B's manifest, and this branch silently leaves the shortened log instead of restoring the saved bytes. The snapshot returns to B, but read_log_all/session recap lose all narration between A and B, so named save slots are not actually restorable after an earlier rollback; the slot needs to store/restore log content (or revive the archived tail) rather than only byte lengths.

Useful? React with 👍 / 👎.

@100yenadmin
100yenadmin merged commit a8e36d3 into main Jun 14, 2026
16 checks passed
@100yenadmin
100yenadmin deleted the fix/f08-1-f10-1-slot-logs-lore-queries branch June 14, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant