Skip to content

fix(viewer): building-universe overlay can never wedge — escape hatch + streaming polish (#405, #406) - #409

Merged
100yenadmin merged 1 commit into
mainfrom
fix/405-overlay-escape-hatch
May 30, 2026
Merged

fix(viewer): building-universe overlay can never wedge — escape hatch + streaming polish (#405, #406)#409
100yenadmin merged 1 commit into
mainfrom
fix/405-overlay-escape-hatch

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented May 30, 2026

Copy link
Copy Markdown
Member

Closes #405. Addresses #406.

Do NOT close on merge — verify on the next full-arc playtest.

#405 (CRITICAL) — the overlay could wedge full-screen for 12 minutes

The "building your universe" cover (z-index: 9000) only cleared via the first-narration handoff or a 12-minute backstop, and its dismiss() callback had zero call sites. On a DM cold-open that errors or stalls (no narration ever arrives), the cover sat over everything — including the table's own "Try again" recovery — for up to 12 minutes. A blind playtester quits.

Product decision implemented

The overlay is only a cover for the legitimately-slow cold-open (~55s). The table already has robust handling (live streaming narration, a 180s "DM is narrating…" timeout, a "Try again" recovery), so the cover now yields to the table on ANY of:

  1. Hard error — a cold-open / session error reported on the native bridge (appStatus.lastError, mirrored to nativeState.error) dismisses the overlay. Honored after a 3s grace so a stale leftover error at a fresh build's click instant (pre-reload) can't nuke a just-started build; a genuine failure (seen on a later poll, past the grace) still dismisses promptly. (Pre-reload mint failures were already torn down by screen-launcher/screen-create; this covers the post-reload cold-open error.)
  2. Stall ceiling — replaced the 12-min backstop with a FIXED ~120s ceiling measured from the build's start (covers the real ~55s cold-open + margin). On hit → dismiss → the table's 180s narrating + Try-again take over.
  3. Manual escape — an "Enter anyway →" button appears after ~15s, wired to dismiss() (App passes onEnterAnyway={building.dismiss}).

After any dismiss the table is reachable (the z-9000 cover unmounts) and usable — on a cold-open the table arms no pending, so its action bar is enabled and the player can act (their move then arms the table's own narrating/stuck-recovery).

The 12-min sessionStorage self-heal in read() is kept as a separate net for the reload path only (a stale flag can't re-enter the overlay) — it no longer governs the live overlay.

Latent handoff bug found + fixed (by the new tests)

The handoff flourish armed its 1400ms dismiss() timer in the same effect that flipped handoff — an effect whose deps include handoff and whose cleanup clears the timer. React re-runs the effect when handoff flips → cleanup cancels the just-armed timer → the re-run early-returns → the dismiss never fired. So even the canonical exit (first narration) never auto-dismissed the cover; it relied on nothing. This shipped green only because the unit tests stub useEffect to a no-op. Fixed by splitting into (a) flip handoff on first narration and (b) a separate handoff-keyed effect that arms the dismiss timer (cleanup only on unmount/record swap). Confirmed empirically: test_first_narration_beat_hands_off_and_clears FAILS on the single-effect version, PASSES after.

#406 — streaming/overlay polish bundle

# Item Disposition
1 False aria-modal w/ no focus management Fixed — dropped role=dialog/aria-modal (no focus trap exists); root is now an aria-busy labeled container; the dedicated role=status announcement is kept.
2 12-min backstop re-arms on every streamed beat FixednotePendingProgress re-arms only the adaptive stuck recovery timer (split out clearRecoveryTimer); the absolute backstop is armed once in armPending, so a streaming-but-unresolved turn can't defer the 12-min cap forever.
3 Narration-dedup scope #407 resolved the canonical path; a residual remained → fixed. The seq-keyed /events dedup (#407) does NOT suppress legitimately-repeated lines (different seq). But the TEXT-key fallback (seenText, used only for /chat-only beats) was run-long, so a repeated short line on a later /chat-only turn (a catchphrase, a repeated "Yes.") was permanently suppressed. seenText now resets when a turn resolves (the gap it must span is one turn's /events/chat).
4 Retried cold-open drops 4min → 180s FixedfirstBeat now keys off a new resolvedTurnsRef (bumped only on /chat turn-resolution), not dmBeatCountRef (bumped per streamed paragraph). A retried cold-open whose first turn never resolved keeps the generous 4-min window.
5 Headline freezes after 38.5s Fixed — added BUILDING_FLAVOR_LATE, a calmer "still composing — a rich opening is worth the wait" pool used past ~42s, so the headline keeps fresh, on-arc copy for the full overlay lifetime (still rotates → proof-of-life).
6 Untested lifecycle + wrong comment Fixed — added an effect-running test harness + the lifecycle tests below; rewrote the misleading "min-display floor" comment (no such floor exists).

How I verified the wedge is gone

  • New effect-running lifecycle harness (test_building_universe.py) drives the real useBuildingUniverse hook with a minimal working React (real useState/useEffect+cleanup/useRef/useCallback) + a controllable clock/timers. Asserts: handoff-on-narration clears the cover; dismiss-on-error (after grace) clears it; a stale error within grace does NOT dismiss (and a persisting one does); the ~120s ceiling dismisses (and does NOT re-arm on re-renders); the "Enter anyway" affordance arms after ~15s and dismiss() clears the flag; and a negative-disclosure test that no-beat-within-ceiling legitimately stays up.
  • Component render tests confirm the root is no longer a false aria-modal dialog and the escape renders as a real wired <button>.
  • Streaming-hook regression guards (test_live_narration_stream.py): a repeated /chat-only line renders on a later turn (chore(openworlds): building-universe + live-session streaming edge cases (a11y modal, backstop re-arm, dedup scope, headline freeze, untested lifecycle) #406.3); a streamed-but-unresolved cold-open keeps the first-beat window (chore(openworlds): building-universe + live-session streaming edge cases (a11y modal, backstop re-arm, dedup scope, headline freeze, untested lifecycle) #406.4). Both were confirmed to FAIL on pre-fix behavior and pass after.
  • Booted the viewer (viewer/server.py "" 8884) and confirmed it serves the changed bundle. (Browser-eyeball wasn't possible this session — the Preview tool kept binding to a different local checkout and Claude-in-Chrome was disconnected — so the lifecycle harness is the behavioral proof.)

Scope

Viewer-only: viewer/openworlds/{building-universe,app}.jsx, styles.css, + the two test files. No DM skill / --resume / persist_beat / engine / wire-contract changes.

CI runs the viewer suite (viewer-tests job, pytest viewer/tests -q -p no:xdist); the new lifecycle tests run there.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added "Enter anyway" escape button to the loading overlay, allowing manual dismissal after a brief wait period.
    • Enhanced loading overlay with better error handling and accessibility improvements.
  • Bug Fixes

    • Improved stability and recovery behavior for live sessions.
    • Added automatic timeout protection for the loading overlay to prevent indefinite hangs.
  • Tests

    • Expanded test coverage for overlay lifecycle, error handling, and narration behavior.

Review Change Stack

…n never wedge (#405) + streaming polish (#406)

#405 (CRITICAL): the "building your universe" cover (z-9000) only cleared via the
first-narration handoff or a 12-min backstop; its dismiss() callback had ZERO call
sites. On a stalled/errored cold-open it wedged full-screen over the table's own
recovery for up to 12 minutes. The overlay is only a COVER for the slow cold-open, so
it now YIELDS to the table (live streaming + 180s narrating timeout + "Try again") on
ANY of: a hard cold-open/session error (dismiss after a 3s stale-error grace), a FIXED
~120s stall ceiling (not the 12-min wall, not re-armed per beat), or a manual "Enter
anyway ->" button surfaced after ~15s. dismiss() is wired in App (onEnterAnyway) and
passed the bridge error. After dismiss the table is reachable + usable (cold-open action
bar is enabled).

Found + fixed a latent bug the new tests surfaced: the handoff flourish armed its
1400ms dismiss timer in the SAME effect that flipped `handoff`, whose cleanup cancelled
the timer on the re-run -> the overlay never auto-dismissed on first narration. Split
into flip + a separate handoff-keyed arm effect.

#406 polish:
 1. aria-modal: dropped the false role=dialog/aria-modal (no focus trap) -> aria-busy
    labeled container + the existing role=status announcement.
 2. backstop re-arm: notePendingProgress now re-arms only the 'stuck' recovery timer,
    not the absolute backstop (armed once in armPending) -> a streaming-but-unresolved
    turn can't defer the 12-min cap forever.
 3. dedup scope: #407 fixed the canonical seq-keyed path; a residual TEXT-key
    suppression remained for the /chat-only fallback across turns -> seenText now resets
    per turn (a repeated short line on a later turn renders again).
 4. retried cold-open window: firstBeat now keys off resolvedTurnsRef (bumped only on
    /chat resolution), not streamed paragraphs -> a retried cold-open keeps the 4-min
    window instead of dropping to 180s.
 5. headline freeze: added BUILDING_FLAVOR_LATE (calmer pool past ~42s) so the headline
    keeps fresh, on-arc copy for the full overlay lifetime.
 6. untested lifecycle: added an effect-running harness + lifecycle tests (handoff,
    dismiss-on-error+grace, dismiss-on-ceiling, manual escape, table-reachable, negative
    disclosure) and 2 streaming-hook regression guards; rewrote the misleading
    min-display-floor comment.
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f4d4cc0c-3757-415a-97b4-d6a396e2aa46

📥 Commits

Reviewing files that changed from the base of the PR and between 7221f7b and 760b943.

📒 Files selected for processing (5)
  • viewer/openworlds/app.jsx
  • viewer/openworlds/building-universe.jsx
  • viewer/openworlds/styles.css
  • viewer/tests/test_building_universe.py
  • viewer/tests/test_live_narration_stream.py

📝 Walkthrough

Walkthrough

This PR refactors the "DM is narrating" recovery timing from per-paragraph beats to per-resolved-turn tracking, resets text deduplication per-turn to prevent suppressing repeated lines across turns, and extends the building-universe loading overlay with explicit multi-exit paths (error dismissal, stall ceiling, manual escape, handoff flourish) to prevent getting stuck.

Changes

Never-Wedge Overlay and Turn-Based Recovery

Layer / File(s) Summary
Turn-Based Recovery Timing and Text Dedup Reset
viewer/openworlds/app.jsx
resolvedTurnsRef tracks /chat turn-END events and replaces per-paragraph beat counting for first-beat recovery window. seenText is reset per-resolved-turn instead of run-long, preventing repeated narration from being suppressed across later turns.
Split Timer Re-arming for Streamed Progress
viewer/openworlds/app.jsx
Timer cleanup is split so streamed-beat progress re-arms only the adaptive recovery timer (not the absolute backstop), preventing unintended window resets on incremental prose arrival.
Building Universe Multi-Exit Lifecycle
viewer/openworlds/building-universe.jsx
useBuildingUniverse hook accepts sessionError and manages multiple coordinated exit paths: error grace (~3s), stall ceiling (~120s), manual escape (~15s), and handoff flourish. Hook returns expanded shape with escapable, handoff, and unified dismiss() callback.
Overlay Accessibility Semantics and Escape Button
viewer/openworlds/building-universe.jsx
Removes false modal semantics (role="dialog") and relies on aria-busy plus aria-live="polite" for announcements. Adds late-phase headline pool and conditional "Enter anyway →" manual escape button that renders only when escapable is true.
Error State Integration and Overlay Wiring
viewer/openworlds/app.jsx
Derives coldOpenError from native bridge and passes it to the overlay. BuildingUniverse component receives escapable and onEnterAnyway props for dismissal integration.
Escape Button Styling and Motion Support
viewer/openworlds/styles.css
New CSS rules for .building-universe .bu-escape button with hover/focus states, fade-in animation, and reduced-motion overrides.
Accessibility and Component Rendering Tests
viewer/tests/test_building_universe.py
Node harness gains utilities to validate overlay root ARIA attributes and escape button wiring. Tests assert overlay is not a false modal and escape button renders/wires conditionally.
Lifecycle and Multi-Exit Effect Tests
viewer/tests/test_building_universe.py
New effect-executing harness with controllable clock drives real useBuildingUniverse and validates all exit paths: handoff, error grace, ceiling, manual escape, and active-without-exit coverage.
Edge Case Tests for Dedup and First-Beat Window
viewer/tests/test_live_narration_stream.py
Tests confirm per-turn text dedup does not suppress repeated lines on later turns, and unresolved cold-open prose maintains first-beat recovery window until /chat resolves.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • electricsheephq/WorldOS#406: Directly addresses the per-turn text deduplication reset (seenText), turn-resolution-based first-beat recovery window (resolvedTurnsRef), and split timer re-arming behavior described in the issue.
  • electricsheephq/WorldOS#405: Directly fixes the overlay wedge by adding escapable/onEnterAnyway wiring, error-grace dismissal, fixed stall ceiling, and manual escape affordance—all core missing pieces described in the issue.

Possibly related PRs

  • electricsheephq/WorldOS#351: Changes to adaptive stuck recovery timing in app.jsx directly overlap with PR #351's first vs later narration beat distinction.
  • electricsheephq/WorldOS#396: Main PR extends the building-universe overlay lifecycle and useBuildingUniverse hook behavior introduced in PR #396.
  • electricsheephq/WorldOS#343: Both PRs modify the pending/turn-resolution logic in app.jsx around /chat turn-END and recovery timing.

Poem

🐰 A universe built with care, no longer stuck in repair,
The overlay now knows its way: escape at fifteen, or wait for the day.
On turns resolved, the prose finds grace—per-turn, not lost to the race,
With ceiling high and errors seen, this never-wedge shall reign supreme! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically addresses the main changes: fixing a critical issue (#405) where the building-universe overlay could wedge for 12 minutes, plus adding an escape hatch mechanism and streaming polish (#406).
Description check ✅ Passed The description is comprehensive, well-structured, and covers the rationale, implementation details, verification steps, and scope. However, the CLA checklist section from the template is not explicitly completed.
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.

@100yenadmin
100yenadmin merged commit aaf1e19 into main May 30, 2026
9 checks passed
@100yenadmin
100yenadmin deleted the fix/405-overlay-escape-hatch branch May 30, 2026 19:28
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.

fix(openworlds): 'building your universe' overlay can wedge full-screen 12 min on a DM cold-open error (no escape; dismiss() unwired)

1 participant