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
- 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.
- A visible escape control — "Enter anyway" (route to table) and/or "Back to chronicles" (route to launcher) — wired to the already-exported
dismiss() callback.
- The 12-min hard backstop remains as the final net.
- 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.
- 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 path — building-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
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 isOW_BUILDING_BACKSTOP_MS = 12 * 60 * 1000(12 min).building-universe.jsx:151-153— the only other exit is the first{kind:"narration"}beat appearing inliveSession.chatBeats.building-universe.jsx:174-181— the hook returns adismisscallback…app.jsx:741-742— …but the render site is{building.active && <BuildingUniverse record={building.record} handoff={building.handoff} />}— nodismissprop passed, noonClick, no Escape handler. Verified:grep -n "\.dismiss\|dismiss(" building-universe.jsx app.jsxreturns zero call sites outside the definition.styles.css:969— the overlay isz-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.jsxstuck-recovery arming — recovery arms viaarmPending, which fires when the player submits a move. On a cold-open the DM opens first (no player move), sopendingis likely null and the stuck-recovery may not arm at all for the cold open.Why it ships green
test_building_universe.py:45stubsuseEffectto 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
PENDING_RECOVERY_FIRST_MS), the overlay mutates to a recoverable state.dismiss()callback.useEffect(){}stub) that asserts: overlay with no narration beat and elapsed past the recovery threshold surfaces the escape control; and clicking it clearsbuilding.active+ routes correctly.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 path —
building-universe.jsx+app.jsxare 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 ofdismiss()is trivial; the behavior is the call.Out of scope
aria-modala11y gap on the same overlay — filed separately in the Loop-11 polish bundle.