Skip to content

fix(viewer): session-scope chronicle seq — render post-move DM narration after session rotation (BUG2) - #569

Merged
100yenadmin merged 1 commit into
mainfrom
fix/chronicle-seq-session-scope
Jun 2, 2026
Merged

fix(viewer): session-scope chronicle seq — render post-move DM narration after session rotation (BUG2)#569
100yenadmin merged 1 commit into
mainfrom
fix/chronicle-seq-session-scope

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 2, 2026

Copy link
Copy Markdown
Member

BUG2 (critical)

In the WorldOS .app, the DM's narration stopped rendering in the Chronicle after a player move — the player saw "Composing…" then nothing.

Root cause

Narration is deduped + ordered by seq, a bare per-session-log line index with NO session-id scoping (both server and client). When the engine rotates the session log (cold-open start_session + the DM-turn-retry re-mint, 5e71f77), the new session's narration restarts at seq 0,1,2 — the same values the cold-open already claimed. The post-move narration was then both suppressed by claimNarrationSeq and dropped by buildChronicleLog's seq match.

No cross-session test existed to catch this — every prior seq test was single-session.

Fix

Namespace seq with the session id (composite ${sid}:${seq}) so it is globally unique across rotations, while preserving within-session monotonicity for the order tiebreak.

Server (viewer/server.py)

  • /events response now carries the resolved sid (new helper _active_session_id).
  • _session_event_tail_from_dir + _session_recent_events carry sid on recentEvents rows.
  • seq stays a bare int — _read_events's 2-tuple + bare-int contract is unchanged (test_read_events_stamps_absolute_seq_across_polls and test_session_event_tail_stamps_stable_absolute_seq stay green).

Client (viewer/openworlds/app.jsx)

  • The /events poll composes ${sid}:${seq} and keys claimNarrationSeq/seenSeq + the beat's orderSeq by the composite (empty sid → :N, still unique within the single session it serves).

Client (viewer/openworlds/screen-table.jsx)

  • buildChronicleLog mirrors the composite via a seqKeyOf helper (the liveSeqs set + the recentEvents dedup).
  • compareChronicle parses the composite's numeric tail for the within-session tiebreak, falling to creation-order (.at) across sessions. eventAt remains the primary sort.

Test

Adds test_new_session_seq_collision_after_rotation_still_renders to viewer/tests/test_live_narration_stream.py — the cross-session twin of test_same_seq_reingested_is_shown_once: a new session's seq 0,1 after a rotation renders (live beats and assembled chronicle), not suppressed by collision with the prior session's seq 0,1.

The #405 same-session re-ingest collapse (test_same_seq_reingested_is_shown_once) still passes — same sid:seq still collapses to one row.

Verification

  • python3 -m py_compile viewer/server.py
  • JSX transpile sanity check (vendored Babel) on both .jsx files ✅
  • Locally (sequential, lightweight): all 34 test_live_narration_stream tests pass (33 existing + the new one), plus the two _read_events / event-tail server tests. CI viewer-tests runs the full suite.

⚠️ Do not merge yet — a live built-.app re-run (1 persona) must confirm the DM reply now renders in the Chronicle post-move. The unit tests prove the dedup/order logic; they don't exercise the real .app.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where live narration and chronicle entries would be incorrectly suppressed or fail to appear when sessions rotated or restarted. The system now properly handles narration across session transitions, ensuring all content displays correctly.

…ion after session rotation (BUG2)

In the WorldOS .app the DM's narration stopped rendering in the Chronicle
after a player move ("Composing…" then nothing).

ROOT CAUSE: narration is deduped + ordered by `seq`, a BARE per-session-log
line index with NO session-id scoping (both server and client). When the
engine ROTATES the session log (cold-open start_session + DM-turn-retry
re-mint, 5e71f77), the new session's narration restarts at seq 0,1,2 — the
same values the cold-open already claimed. The post-move narration was then
BOTH suppressed by claimNarrationSeq AND dropped by buildChronicleLog's seq
match.

FIX: namespace `seq` with the session id (composite `${sid}:${seq}`) so it is
globally unique across rotations, while preserving within-session monotonicity
for the order tiebreak.

- server.py /events response now carries the resolved `sid` (new helper
  `_active_session_id`); `_session_event_tail_from_dir` + `_session_recent_events`
  carry `sid` on recentEvents rows. `seq` stays the bare int (contract intact).
- app.jsx composes `${sid}:${seq}` for claimNarrationSeq/seenSeq and orderSeq.
- screen-table.jsx buildChronicleLog mirrors the composite via a seqKeyOf
  helper (liveSeqs + recentEvents dedup); compareChronicle parses the numeric
  tail for the within-session tiebreak, falling to creation-order across
  sessions (eventAt remains the primary sort).

Adds a cross-session regression test: a NEW session's seq 0,1 after a rotation
RENDERS (not suppressed by collision with a prior session's seq 0,1). The #405
same-session re-ingest collapse (test_same_seq_reingested_is_shown_once) still
passes.
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes a narration deduplication bug where sequence indices (seq) collided across session rotations. The fix introduces a session-scoped composite key ${sid}:${seq} throughout the narration pipeline, from server resolution through client deduplication and chronicle rendering.

Changes

Session-scoped narration dedup fix

Layer / File(s) Summary
Server-side session id resolution and /events response
viewer/server.py
_active_session_id() helper resolves active session from snapshot; _session_recent_events and _session_event_tail_from_dir preserve sid on event rows; /events endpoint includes sid in JSON response payload.
Client-side composite dedup key construction
viewer/openworlds/app.jsx
Client reads sid from /events response; narration claim callback accepts string composite keys; /events entry loop builds ${sid}:${seq} keys and dedupes narration using composite identity instead of bare seq.
Chronicle log ordering and deduplication with session-scoped keys
viewer/openworlds/screen-table.jsx
seqKeyOf helper derives ${sid}:${seq} identity from multiple input shapes; orderOf parses composite keys for within-session tie-breaking; liveSeqs and dedupedRecent filter use seqKeyOf() to operate on session-scoped keys instead of bare seq or orderSeq.
Test: narration rendering after session rotation
viewer/tests/test_live_narration_stream.py
Test test_new_session_seq_collision_after_rotation_still_renders enqueues overlapping seq values across different sids and asserts both narration and chronicle include all beats without cross-session suppression.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • electricsheephq/WorldOS#407: Modifies the same narration dedup/chronicle ordering logic in app.jsx and screen-table.jsx, extending the dedup key to ${sid}:${seq} to prevent cross-session collisions.
  • electricsheephq/WorldOS#394: Modifies the narration dedup/"claim" and chronicle path in app.jsx / screen-table.jsx, but focuses on dedup across /events vs /chat rather than session-scoped keying.

Poem

🐰 A session spins, and sequences reset, /
Two sids arrive, yet seq stays the same— /
Composite keys now lock the right beat, /
No cross-rotation narration shame! /
Dedup once blind, now sees through the session,
One chronicle flows through each rotation's progression. 🌀

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is comprehensive, explaining the root cause, the fix across all three files, tests, and verification steps. However, the Licensing/CLA section is entirely unchecked, which is a required section of the template. Check the two CLA-related checkboxes and the confidentiality checkbox to confirm compliance with the Licensing/CLA template section.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title precisely describes the fix (session-scoped composite seq keying) and correctly references the bug (BUG2) and its symptom (post-move DM narration after session rotation).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@viewer/server.py`:
- Around line 6601-6606: The entries and response sid are read from two separate
snapshots causing a race; ensure both come from the same session snapshot by
reading the active session id once and using that same id when fetching entries
(or modify _read_events to return the session id along with entries and nxt).
Concretely, call _active_session_id(view_cid) first (or update
_read_events(view_cid, since) to return (entries, nxt, sid)) and then use that
single sid value in the JSON response instead of calling _active_session_id
again.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bac45102-b064-4798-9516-ef36afd3e866

📥 Commits

Reviewing files that changed from the base of the PR and between 444f3aa and ff3f5e3.

📒 Files selected for processing (4)
  • viewer/openworlds/app.jsx
  • viewer/openworlds/screen-table.jsx
  • viewer/server.py
  • viewer/tests/test_live_narration_stream.py

Comment thread viewer/server.py
Comment on lines +6601 to +6606
entries, nxt = _read_events(view_cid, since)
# BUG2: include the resolved session id so the client composes a globally-unique
# `${sid}:${seq}` dedup/order key — a bare per-session line index collides across a
# session rotation (cold-open + DM-turn-retry re-mint), suppressing the new session's
# post-move narration (seq 0,1,2 already claimed by the prior session's cold-open).
self._json({"entries": entries, "next": nxt, "sid": _active_session_id(view_cid)})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use one session-id source for entries and response sid

Line 6601 and Line 6606 resolve session state via separate snapshot reads, so a rotation between them can return entries from one session and sid from another. That miskeys ${sid}:${seq} on the client and can still drop/duplicate narration around rotation boundaries.

Suggested fix
-def _read_events(campaign_id: str, since: int) -> tuple[list[dict], int]:
+def _read_events(campaign_id: str, since: int) -> tuple[list[dict], int, str]:
@@
-    if not sid:
-        return [], since
+    if not sid:
+        return [], since, ""
@@
-    if not log.exists():
-        return [], since
+    if not log.exists():
+        return [], since, sid
@@
-    return out, consumed
+    return out, consumed, sid
-            entries, nxt = _read_events(view_cid, since)
+            entries, nxt, sid = _read_events(view_cid, since)
@@
-            self._json({"entries": entries, "next": nxt, "sid": _active_session_id(view_cid)})
+            self._json({"entries": entries, "next": nxt, "sid": sid})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@viewer/server.py` around lines 6601 - 6606, The entries and response sid are
read from two separate snapshots causing a race; ensure both come from the same
session snapshot by reading the active session id once and using that same id
when fetching entries (or modify _read_events to return the session id along
with entries and nxt). Concretely, call _active_session_id(view_cid) first (or
update _read_events(view_cid, since) to return (entries, nxt, sid)) and then use
that single sid value in the JSON response instead of calling _active_session_id
again.

@100yenadmin
100yenadmin merged commit d7c36a1 into main Jun 2, 2026
14 checks passed
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