Skip to content

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

Description

@100yenadmin

TL;DR

The new "building your universe" loading overlay (PR #396) can wedge full-screen for 12 minutes with no user escape if the DM cold-open errors or produces no narration line. The overlay's only exits are (a) the first narration beat landing, or (b) a 12-minute internal backstop. There is no Cancel / Escape / click-to-dismiss, and the dismiss() callback the hook exports is never wired to any UI. This is strictly worse than the abrupt read-only flash it replaced — and it's exactly the failure that makes a blind AI-playtester (or a real player) give up.

Provenance: Loop 11 parallel-review of the main agent's 3-hour cold-open/streaming sprint (2026-05-30). Verified by code-read on main @ 6310750.

File:line evidence

  • building-universe.jsx:133-146 — the only timed exit is OW_BUILDING_BACKSTOP_MS = 12 * 60 * 1000 (12 min).
  • building-universe.jsx:151-153 — the only other exit is the first {kind:"narration"} beat appearing in liveSession.chatBeats.
  • building-universe.jsx:174-181 — the hook returns a dismiss callback…
  • app.jsx:741-742 — …but the render site is {building.active && <BuildingUniverse record={building.record} handoff={building.handoff} />}no dismiss prop passed, no onClick, no Escape handler. Verified: grep -n "\.dismiss\|dismiss(" building-universe.jsx app.jsx returns zero call sites outside the definition.
  • styles.css:969 — the overlay is z-index: 9000 ("above the window frame, the tweaks panel, toasts").
  • screen-table.jsx:412 — the cold-open's own "DM seems stuck → Try again" recovery renders underneath the z-9000 overlay, so even when it fires the player can neither see nor reach it.
  • screen-table.jsx stuck-recovery arming — recovery arms via armPending, which fires when the player submits a move. On a cold-open the DM opens first (no player move), so pending is likely null and the stuck-recovery may not arm at all for the cold open.

Why it ships green

test_building_universe.py:45 stubs useEffect to a no-op (function useEffect() {}), so the entire effect-driven lifecycle — the first-narration handoff (:155-167), the backstop timer (:135-146), the begin-event listener (:124-131) — has zero test coverage. The file docstring asserts "the 12-min backstop guarantees the overlay can never wedge forever," but "doesn't wedge forever" still means "wedges for 12 minutes," and that prose claim is never tested.

Repro

In the native app, press Bind/Play and have the DM provider fail or stall its first turn (bad API key, crash, empty completion, or simply the 60-90s × N cold-open that a blind newbie run has seen take 5-8 min — see #393). The overlay shows "composing · M:SS" climbing; the player has no feedback that anything is wrong and no way out until the 12-minute backstop finally clears to a bare table.

Acceptance criteria

  1. After a "something may be wrong" threshold well before 12 min (suggest 90-120s, matching PENDING_RECOVERY_FIRST_MS), the overlay mutates to a recoverable state.
  2. A visible escape control — "Enter anyway" (route to table) and/or "Back to chronicles" (route to launcher) — wired to the already-exported dismiss() callback.
  3. The 12-min hard backstop remains as the final net.
  4. A test (with effects actually running, not the useEffect(){} stub) that asserts: overlay with no narration beat and elapsed past the recovery threshold surfaces the escape control; and clicking it clears building.active + routes correctly.
  5. Decide the interaction with the table's own stuck-recovery so the two don't both fire confusingly.

Severity rationale

Critical. A 12-minute inescapable full-screen overlay on a DM failure is a hard dead-end on the primary entry path (every new game and every Resume routes through it). It directly produces the "looks broken → quit" behavior the entire cold-open/streaming sprint (#385/#388/#393/#394/#401) is trying to eliminate. It is also invisible to CI because the lifecycle is untested.

Suggested fix / PR-readiness

The escape-hatch UX (where "Enter anyway" routes, whether to reuse the cold-open stuck copy, how it composes with the table recovery) is a product decision in the main agent's hot pathbuilding-universe.jsx + app.jsx are files they're actively iterating. Recommend the main agent fold this into the streaming work rather than a parallel PR, to avoid divergence. The mechanical wiring of dismiss() is trivial; the behavior is the call.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    accessibilityWCAG / reduced-motion / contrast / keyboard navui-auditFrom the 2026-05-29 UI/UX audit (docs/ui-audit/)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions