Skip to content

fix(openworlds): adaptive 'stuck' recovery so a slow Act-opening isn't pre-empted (Closes #348) - #351

Merged
100yenadmin merged 1 commit into
mainfrom
fix/348-recovery-timeout
May 30, 2026
Merged

fix(openworlds): adaptive 'stuck' recovery so a slow Act-opening isn't pre-empted (Closes #348)#351
100yenadmin merged 1 commit into
mainfrom
fix/348-recovery-timeout

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented May 30, 2026

Copy link
Copy Markdown
Member

Closes #348.

The bug

The #324 narrative persona (nar1) hit "The Dungeon Master seems stuck — No reply came back in time" with NO narration produced, at a dramatic cliffhanger. The #342/#344 recovery fired mechanically (good), but the DM's legitimate Act-opening can take several minutes (an earlier blind-newbie run saw the cold-open take 5–8 min and succeed). The recovery's fixed 90s wall-clock from submit therefore false-fired 'stuck' mid-opening → the still-coming narration + the momentum were lost.

STEP 1 finding — the beat arrives ALL-AT-ONCE (no streaming)

/chat carries no streaming / partial chunks / heartbeat. Both runner paths confirm it:

  • qa/run_duo.sh (lines 95–101) and qa/play_human.sh (lines 42–44, 60, 89) capture the DM turn via claude -p … --output-format stream-json > out, then extract only the final result text after the turn completes (jq -rs 'map(select(.type=="result"))[-1].result') and append one {"role":"dm","text":…} line to <run>.chat.jsonl.
  • viewer/server.py _read_chat (line 5153) + the /chat route (line 5956) tail that file line-by-line. So the poll returns zero new items for the whole turn, then the one complete beat.

There is no in-flight progress to reset on → a progress-aware timer is impossible without a wire change (out of scope). So the fix is a threshold-raise.

STEP 2 chosen approach — adaptive (turn-position-aware) threshold, viewer-only

viewer/openworlds/app.jsx useLiveSession — the recovery 'stuck' window is now chosen by recoveryWindowMs(firstBeat):

turn window const
first beat of a session (cold-open / Act-opening) 4 min PENDING_RECOVERY_FIRST_MS
later beats (the ~35–60s norm) 90s (unchanged) PENDING_RECOVERY_MS
  • "first beat?" = no DM narration has arrived this session yet (dmBeatCountRef === 0, already reset to 0 on every run change).
  • The 12-min hard backstop (fix(openworlds): strip DM-internal leaks + animate narrating state + label action bar (#335/#336/#337) #338) is UNCHANGED — a turn that blows even the first-beat window still gets force-cleared.
  • The "DM is narrating…" affordance is now honest (screen-table.jsx DmNarratingBeat): the first beat reads "Setting the opening scene — the first beat of a session can take a few minutes"; later beats keep "up to a minute".

Preserved invariants

Evidence

Scope: viewer-only; worktree off origin/main; no engine / wire-contract change (engine stays sole writer; /chat + /move + the chat-log contract untouched). No run-artifacts committed.

Summary by CodeRabbit

  • Bug Fixes

    • Improved recovery from slow DM narrations with adaptive timeout windows that account for session context. Updated messaging to set appropriate wait-time expectations for players.
  • Tests

    • Added comprehensive test coverage for DM narration recovery behavior, including timeout window selection, recovery state transitions, and recovery mechanism validation.

Review Change Stack

…t pre-empted (Closes #348)

The #324 narrative persona hit "The Dungeon Master seems stuck — No reply came
back in time" with NO narration at a cliffhanger: the #342 recovery fired at a
fixed 90s from submit, but the DM's legit cold-open (Act-opening) can take SEVERAL
minutes (a blind newbie run saw 5–8 min and succeed). The 90s wall-clock false-fired
'stuck' mid-opening → the still-coming narration + momentum were lost.

Finding (STEP 1): the DM beat lands ALL-AT-ONCE — no streaming/partial/heartbeat.
Both the duo runner (qa/run_duo.sh) and the human runner (qa/play_human.sh) capture
the DM turn via `claude -p --output-format stream-json > out`, then extract only the
final `result` text AFTER the turn completes and append ONE {"role":"dm",...} line
to <run>.chat.jsonl. The /chat poll therefore returns zero new items for the entire
turn, then the complete beat — there is NO in-flight progress to reset on. So the fix
is a THRESHOLD-RAISE, not progress-aware (a progress-aware reset would require a wire
change, which is out of scope).

Fix (viewer-only, no engine/wire change):
- The recovery 'stuck' window is now turn-position-aware (recoveryWindowMs):
  • FIRST beat of a session (cold-open): 4 min  (PENDING_RECOVERY_FIRST_MS)
  • LATER beats (~35–60s norm):          90s   (PENDING_RECOVERY_MS, unchanged)
  "first beat?" = no DM narration has arrived this session yet (dmBeatCountRef === 0,
  already reset to 0 on every run change).
- The 12-min hard backstop (#338) is UNCHANGED — a turn that blows even the first-beat
  window still gets force-cleared.
- The "DM is narrating…" affordance is now HONEST: the first beat reads "Setting the
  opening scene — the first beat of a session can take a few minutes"; later beats keep
  "up to a minute". (#336 animation/elapsed/reduced-motion fully intact.)

Preserved invariants:
- #340 — the /chat poll still clearPending()s on ANY narration beat, so a beat that
  lands after 'stuck' still renders into the log + clears the indicator (untouched path).
- #344 — armPending/clearPending signatures + retryStuck/lastMoveRef unchanged; timers
  disarm cleanly so 'Try again' re-arms.
- #336 — DmNarratingBeat dots/elapsed/reduced-motion preserved; firstBeat defaults falsy
  (older pending objects show the original copy) — additive-by-default.

Tests: viewer/tests/test_recovery_timing.py transpiles the REAL app.jsx with the bundled
Babel and drives useLiveSession under Node with a deterministic clock + fake timers
(mirrors test_sanitize_narration.py). Proves: first beat is NOT stuck at 91s (the bug)
yet still recovers after 4 min; later-beat window stays 90s; the 12-min backstop still
force-clears; clearPending disarms timers. axe = 0 across all 17 screens; both JSX files
transpile.
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The PR extends the DM narration "stuck" recovery behavior to use adaptive timeouts: first-beat turns get a longer window (4 minutes) before flagging as stuck, while subsequent beats use the original 90-second window. The pending state now tracks firstBeat, UI messaging adjusts accordingly, and recovery timing constants are exposed for testing.

Changes

Adaptive First-Beat Recovery Timing

Layer / File(s) Summary
Recovery timing contract and adaptive selector
viewer/openworlds/app.jsx
Recovery window constants (recoveryMs: 90s, recoveryFirstMs: 4min, backstopMs: 12min) and a recoveryWindowMs(firstBeat) selector are introduced; both are exposed on window for test/devtools introspection.
Pending state with firstBeat tracking
viewer/openworlds/app.jsx
armPending computes whether the pending turn is the first DM beat (via dmBeatCountRef), stores firstBeat on the pending object, and applies adaptive recovery timing using the selector.
UI messaging adapted for first beat
viewer/openworlds/screen-table.jsx
DmNarratingBeat accepts firstBeat and conditionally selects "a few minutes" (first beat) or "up to a minute" (later beats) for the wait hint; ScreenTable passes the flag from pending.
Recovery timing test suite
viewer/tests/test_recovery_timing.py
End-to-end tests validate timing constants, selector logic, first-beat non-stuck at 91s, eventual stuck transition at 4min, 12-minute backstop clearing, and clearPending immunity to timer resurrection.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

  • electricsheephq/WorldOS#328: Introduced the initial pending and DmNarratingBeat affordance; this PR extends it with firstBeat-aware timing and messaging.
  • electricsheephq/WorldOS#343: Established the core pending/recovery timer and "stuck" logic in useLiveSession; this PR adds adaptive timeout behavior to that recovery window.

Poem

🐰✨ A rabbit's tale of timing true—
The first beat waits a minute more (or few),
While later beats keep pace at ninety strong,
And constant windows guide the song.
Tests ensure no false alarm's drawn,
Recovery blooms when reset's dawn! 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% 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 clearly and specifically summarizes the primary change: fixing the recovery timeout to use adaptive windows so first DM beats are not prematurely flagged as stuck.
Description check ✅ Passed The description comprehensively documents the bug, root cause, solution, design decisions, preserved invariants, and validation evidence. However, the CLA checklist items are not explicitly checked (no checkmarks shown).
Linked Issues check ✅ Passed The PR fully addresses #348 requirements: investigates the root cause (no streaming), implements an adaptive timeout approach (4-min first beat, 90s later beats), maintains genuine stall recovery with 12-min backstop, and provides comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the #348 objectives: UI recovery timeout adjustment, pending state tracking, recovery window selection logic, updated narration messaging, and comprehensive tests. No unrelated changes detected.

✏️ 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.

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.

[playtest][P1] DM first-response ~60s NO narration → recovery fired; recovery-timeout may pre-empt the legit multi-minute DM opening

1 participant