Skip to content

fix(viewer): #826 nav-away mid-narration resilience + #825 full chronicle narration - #869

Merged
100yenadmin merged 1 commit into
mainfrom
fix/viewer-826-825-nav-chronicle
Jun 14, 2026
Merged

fix(viewer): #826 nav-away mid-narration resilience + #825 full chronicle narration#869
100yenadmin merged 1 commit into
mainfrom
fix/viewer-826-825-nav-chronicle

Conversation

@100yenadmin

Copy link
Copy Markdown
Member

Fixes the two v1.0.4 viewer orphans flagged by the engine-audit Section-4 coverage check (rc1+rc2 persona complaints with no prior issue). Both are viewer-scope only — the engine stays sole writer (viewer/server.py is the read-only bridge projecting state the engine already wrote; no campaign-state writes, no wire-contract changes).

Source: docs/audits/ENGINE-AUDIT-2026-06-11.md (Section-4 orphans #1 + #5). Refs #826, #825.


#826 — Navigating away mid-narration corrupts session state + freezes the scene image (rc2 adversarial)

A user-initiated nav during an in-flight beat — distinct from the #745/#648 stalled-beat trigger. Two independent legs:

1. State corruption (double-fire). postMove armed the one-move gate only after the /move POST resolved. If the player navigated away (Table→Map) during the in-flight POST and back, ScreenTable remounted with a fresh submittingRef=false and pending=null → the action bar re-opened → a second move could double-fire the one-move-at-a-time lane.

2. Scene-image freeze (dead handle). chrome.jsx Img latched failed on the first onError and only ever cleared it on a scope change — so a #399 fire-and-forget scene that 404s while its art is still being generated stayed frozen on the placeholder forever, even once the same-scope image became servable.

  • Fix: bounded, backed-off, cache-busted retries so the component recovers when the pending art lands (and stops after the budget for a genuinely-missing image). No dead latch.

#825 — Chronicle truncates DM narration mid-word at a fixed ceiling (3 personas: rc2 adversarial+narrative, rc1 veteran)

The chronicle's leading history band (_session_recent_events) hard-cut every row to text[:1000], slicing a long DM beat mid-word with no ellipsis/expand — the remainder unreadable.


Tests (TDD via the JSX behaviour harness — test_recovery_timing.py pattern)

New viewer/tests/test_nav_chronicle_resilience.py (7 tests, all green):

Verification:

  • New file: 7 passed.
  • Full viewer suite: 505 passed, 1 skipped.
  • qa/fast_gate.sh Tier-0: 195 passed ✅.

Invariants held: engine sole-writer untouched; additive-by-default (abandonPending is a new exported callback; old clearPending fallback kept for older bundles; Img URL keeps the /image?scope= shape on the happy path); wire contracts frozen.

Final GUI confirmation comes from the re-measure sweep — this PR is the sound fix + the deterministic JSX-harness proof. Do not merge pending that sweep.

…icle narration

Two v1.0.4 viewer orphans from the engine-audit Section-4 coverage check
(rc1+rc2 persona evidence with no prior issue). Viewer-scope only; the engine
stays sole writer — viewer/server.py is the read-only bridge projecting state
the engine already wrote.

#826 — Navigating away mid-narration corrupts session state + permanently
freezes the scene image (rc2 adversarial). A user-initiated nav DURING an
in-flight beat (distinct from the #745/#648 stalled-beat trigger). Two legs:

  * State corruption (double-fire): postMove armed the one-move gate only AFTER
    the /move POST resolved, leaving a window where a nav-away/nav-back remounted
    ScreenTable (fresh submittingRef=false, pending=null) and re-opened the bar —
    a second move could double-fire the one-move-at-a-time lane. Fix: arm
    OPTIMISTICALLY before the await (the App-level useLiveSession pending state
    survives the unmount), and on a POST rejection roll it back via a new,
    surgical abandonPending that bypasses the #648 arm-grace (an authoritative
    server rejection is not a spurious clear; it only clears the move we armed,
    never a newer live turn). Preserves the #745/#746 recovery contract and the
    #399/#406 first-beat window (armPending/notePendingProgress untouched).

  * Scene-image freeze (dead handle): chrome.jsx Img latched `failed` on the
    first onError and only ever cleared it on a SCOPE CHANGE — so a #399
    fire-and-forget scene that 404s while its art is still being generated stayed
    frozen on the placeholder forever, even once the same-scope image became
    servable. Fix: bounded, backed-off, cache-busted retries so the component
    RECOVERS when the pending art lands (and stops after the budget for a
    genuinely-missing image). No dead latch.

#825 — Chronicle truncates DM narration mid-word at a fixed ceiling (3 personas:
rc2 adversarial+narrative, rc1 veteran). The chronicle's leading history band
(_session_recent_events) hard-cut every row to text[:1000], slicing a long DM
beat mid-word with no ellipsis/expand. Fix: render the FULL beat
(_bounded_chronicle_text) — the render region is already a scrollable role="log"
and the #752 a11y bound is the ROW cap (CHRONICLE_RENDER_CAP/MAX_LIVE_BEATS), not
a per-row char ceiling, so removing the cut does not reintroduce the #752 a11y
flood. Only a generous DoS guard remains (64 KB, far above any real beat), and
even it cuts on a word boundary.

TDD via the JSX behaviour harness (test_recovery_timing.py pattern):
viewer/tests/test_nav_chronicle_resilience.py — a nav-away-during-pending /
optimistic-arm-survival + authoritative-abandon test, an Img dead-handle-recovery
test, and a long-narration no-mid-word-truncation test (7 tests, all green).
Full viewer suite 505 passed / 1 skipped; fast_gate Tier-0 195 passed.

Source: docs/audits/ENGINE-AUDIT-2026-06-11.md (Section-4 orphans #1 + #5)
Refs #826, #825
@100yenadmin 100yenadmin added this to the v1.0.4 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 1 hour, 26 minutes, and 8 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: e9965d03-a27e-4a71-9710-4f62f9e5a65f

📥 Commits

Reviewing files that changed from the base of the PR and between c148efd and 7a4d2d9.

📒 Files selected for processing (5)
  • viewer/openworlds/app.jsx
  • viewer/openworlds/chrome.jsx
  • viewer/openworlds/screen-table.jsx
  • viewer/server.py
  • viewer/tests/test_nav_chronicle_resilience.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: 7a4d2d9d21

ℹ️ 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".

// with a fresh submittingRef=false AND pending=null — the bar re-opened and a SECOND move could
// double-fire the one-move-at-a-time lane (the #826 state corruption). recordPlayerEcho is #399-
// idempotent, so re-POST/retry paths don't duplicate the echo.
recordPlayerEcho(hero.name, text, cleanMove);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Roll back optimistic echoes when /move is rejected

This now appends the player's action to the Chronicle before the /move POST is accepted, but the rejection path only clears the optimistic pending state and never removes that echo. When the server rejects a move (for example a stale action, dead move sink, or payload.ok === false), the player gets a "Move not sent" toast while the Chronicle still shows the action as if it happened, and a later retry can leave duplicate/false history in the visible session.

Useful? React with 👍 / 👎.

@100yenadmin
100yenadmin merged commit faef1ec into main Jun 14, 2026
16 checks passed
@100yenadmin
100yenadmin deleted the fix/viewer-826-825-nav-chronicle branch June 14, 2026 11:00
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