Skip to content

fix(viewer): budget-aware stuck-backstop — stop false-firing 'stuck' on healthy slow turns (refs #745, follow-up to #746) - #761

Merged
100yenadmin merged 1 commit into
mainfrom
fix/746-stuck-backstop-budget-aware
Jun 10, 2026
Merged

fix(viewer): budget-aware stuck-backstop — stop false-firing 'stuck' on healthy slow turns (refs #745, follow-up to #746)#761
100yenadmin merged 1 commit into
mainfrom
fix/746-stuck-backstop-budget-aware

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 10, 2026

Copy link
Copy Markdown
Member

P0 — the #746 stuck-backstop false-fires on healthy slow turns

Refs #745 (the mid-stream-stall lockout); follow-up to the merged #746 (which shipped the ceiling this PR retunes). Audit-verified root cause — sanity-rechecked against the cited lines before editing:

  • viewer/openworlds/app.jsx: PENDING_STUCK_BACKSTOP_MS = 5 * 60 * 1000 (flat 300s), armed once at submit in armPending, NOT firstBeat-aware; progress does not reset it (by design, [reliability][P0] Stalled-mid-stream DM beat locks the player out with no recovery (the v1.0.4-rc2 holdout) #745).
  • But the system's own healthy budgets exceed 300s:
    • cold open: ~300s measured with a 400–500s deadline (qa/lib_beat_driver.sh clawdnd_dm_timeout — 500s for Opus; verified at lines ~441–447);
    • continuing beat: scripts/play.sh CLAWDND_BEAT_TIMEOUT=200s + ONE retry ⇒ a healthy beat can run ~400s.
  • When the ceiling fires falsely mid-flight: pendingActive flips false → the action bar re-opens (screen-table.jsx ~614), the "DM seems stuck" toast fires (~796–798), and retryStuck re-POSTs the move (~875+) → the same intent resolves twice once the in-flight beat lands.

Fix (audit option (a), minimal)

Budget-aware ceiling by turn position via a pure stuckBackstopMs(firstBeat) selector (mirrors recoveryWindowMs, exported for tests):

branch ceiling clears budget
first beat (cold open) PENDING_STUCK_BACKSTOP_FIRST_MS = 9 min (540s) ≥ 500s Opus cold-open deadline
later beats PENDING_STUCK_BACKSTOP_MS = 7 min (420s) ≥ ~400s timeout + one-retry

Ordering invariant preserved per branch: position recovery (180s/240s, resettable) < stuck ceiling (420s/540s, hard, recoverable) < 12-min null-backstop. firstBeat detection unchanged (resolvedTurnsRef.current === 0, the #406 semantics).

Option (b) (arm the ceiling only on the first streamed paragraph) deliberately NOT implemented: with the ceiling submit-anchored (required to keep it under the null-backstop) and strictly above the recovery windows, arming at submit vs. at first paragraph is observationally identical — a never-streaming turn is flagged stuck by the adaptive recovery window (180s/240s) long before the ceiling, and a streaming turn arms it anyway. (b) would add arm-state in the progress path for zero behavioral delta, so it fails the "only if it stays small" bar on the value side.

Tests (red-first; viewer/tests/test_recovery_timing.py)

Harness upgrade: the recovery-timing harness now runs real effects + a scripted /chat poll (ported from test_live_narration_stream.py, keeping the deterministic fake-timer queue). Resolving a turn via a real /chat dm line is the only writer of resolvedTurnsRef, so tests can now exercise the later-beat branch of the real hook — previously unreachable (the old harness's documented limitation).

Proven RED on main @ f24a102 (5 failed, 30 passed), then GREEN after the fix:

  1. The missing bandtest_healthy_first_beat_in_the_cold_open_budget_band_never_shows_stuck: healthy first beat streams every 30s, resolves ~430s. RED on main: false-fired at exactly 300s (assertion message captured [300]s).
  2. Later beattest_healthy_later_beat_resolving_within_the_retry_budget_never_shows_stuck: turn 1 resolved via the live /chat path (firstBeat=false precondition asserted), turn 2 streams + resolves ~380s. RED on main.
  3. [reliability][P0] Stalled-mid-stream DM beat locks the player out with no recovery (the v1.0.4-rc2 holdout) #745 protection retainedtest_multi_partial_trickle_then_freeze_recovers_to_stuck_not_null (recovers to recoverable stuck, never the silent null clear, bounded ≤ the first-beat ceiling) and test_stuck_backstop_is_not_reset_by_progress rewritten at the new 9-min value (trickle to 510s, freeze, stuck at 545s — progress cannot defer the submit-anchored ceiling).
  4. Contracttest_stuck_backstop_constants_exported_and_strictly_ordered (per-branch ordering + explicit ≥500s / ≥400s budget floors) and test_stuck_backstop_selector_both_branches.

Verification (honest)

  • python3 -m pytest viewer/tests/test_recovery_timing.py -q -p no:xdist35 passed (was 5 failed / 30 passed pre-fix — red-first proven).
  • Full viewer suite python3 -m pytest viewer/tests -q -p no:xdist486 passed, 6 skipped, 48 subtests passed.
  • bash qa/fast_gate.shPASS (188 passed, deterministic engine tier).
  • Not verified live: an end-to-end slow-beat run in the app (the 5–9-minute timer band isn't practical to observe in a quick manual session); the Node harness drives the real transpiled hook code as the proxy.

Invariants

  • Viewer-only; engine untouched (sole-writer unaffected). No wire-contract changes. __PENDING_TIMING__ change is additive (new stuckBackstopFirstMs key; existing keys keep meaning).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements

    • Refined stuck recovery timeout behavior to adapt dynamically based on turn position, providing more intelligent recovery windows for the live session indicator.
  • Tests

    • Updated recovery timing test coverage to validate the new adaptive timeout behavior across different turn states and recovery scenarios.

…on healthy slow turns (refs #745, follow-up to #746)

The #746 hard stuck ceiling (PENDING_STUCK_BACKSTOP_MS) was a flat 5 min,
armed once at submit and not firstBeat-aware — but the system's own HEALTHY
turn budgets exceed it: the cold open measures ~300s with a 400-500s deadline
(qa/lib_beat_driver.sh clawdnd_dm_timeout, 500s for Opus), and a continuing
beat can legitimately run ~400s (scripts/play.sh CLAWDND_BEAT_TIMEOUT=200s +
ONE retry). When the ceiling fired mid-flight on a working turn:
pendingActive flipped false (action bar re-opened, screen-table.jsx), the
"DM seems stuck" toast fired, and retryStuck re-POSTed the move - the SAME
intent resolved TWICE once the in-flight beat landed.

Fix (minimal, option (a) of the audit): make the ceiling budget-aware by
turn position via a pure stuckBackstopMs(firstBeat) selector (mirrors
recoveryWindowMs): firstBeat => 9 min (>= the 500s cold-open budget), later
beats => 7 min (>= the ~400s timeout+retry budget). Both stay strictly under
the 12-min null-backstop, so the #745 ordering holds: position recovery
(resettable) < stuck ceiling (hard, recoverable) < null-backstop.

Tests (red-first against main): the recovery-timing harness now runs real
effects + a scripted /chat poll (ported from test_live_narration_stream.py),
so a test can RESOLVE a turn - flipping firstBeat - and exercise the
later-beat ceiling branch of the real hook. New: the missing-band first-beat
test (healthy stream, resolves ~430s - false-fired at exactly 300s on main),
the later-beat ~380s retry-budget test, the budget-aware ordering/selector
contract; updated: progress-does-not-defer-the-ceiling at the new 9-min
value; retained: trickle-then-freeze still recovers to recoverable stuck
(the #745 protection).
@100yenadmin 100yenadmin added this to the v1.0.4 milestone Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

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: bbbef02e-a76f-4fa7-8909-cf0c89f885c8

📥 Commits

Reviewing files that changed from the base of the PR and between f24a102 and 3c1a628.

📒 Files selected for processing (2)
  • viewer/openworlds/app.jsx
  • viewer/tests/test_recovery_timing.py

📝 Walkthrough

Walkthrough

This PR updates the live in-flight turn indicator's stuck-recovery timing to be turn-position-aware: first beats (cold opens) use a 9-minute ceiling, later beats use 7 minutes. It replaces a fixed timeout with an adaptive stuckBackstopMs(firstBeat) helper, exposes the new timing surface to tests via window.__PENDING_TIMING__, and upgrades the test harness with proper React effect simulation and /chat scripting to validate the multi-beat behavior.

Changes

Adaptive stuck-backstop timing by turn position

Layer / File(s) Summary
Stuck-backstop constants and position-aware helper
viewer/openworlds/app.jsx
Introduces new stuck-backstop configuration constants (7 min for later beats, 9 min for first beat) and adds a pure stuckBackstopMs(firstBeat) helper that selects the correct duration based on turn position.
Integrate adaptive timeout into pending/arm logic
viewer/openworlds/app.jsx
Updates the pending/arm timeout scheduling for the hard stuck ceiling to use stuckBackstopMs(firstBeat) (adaptive by turn position) instead of the previous fixed timeout constant.
Public timing surface export
viewer/openworlds/app.jsx
Extends the public timing/debug surface by exporting window.stuckBackstopMs function and augmenting window.__PENDING_TIMING__ with stuckBackstopFirstMs constant and the adaptive stuckBackstopMs value so unit tests and devtools can inspect the position-aware contract.
Test harness React upgrade with effect support
viewer/tests/test_recovery_timing.py
Replaces the minimal effect-less React hook stub with a more accurate hook/effect implementation that persists state/ref/callback slots, supports dependency-based useCallback memoization, and queues useEffect execution with cleanup handling. Expands the VM sandbox with DOM/document stubs and global constructors. Includes documentation updates describing the new budget-aware stuck-ceiling semantics and expanded test coverage.
Test scripting surface for live /chat resolution
viewer/tests/test_recovery_timing.py
Adds hook mounting using the upgraded React harness, introduces chatNext and a scripted resolveTurn that enqueues /chat responses and drains async pollers, and defines the richer h interface used by async test scripts to drive first-beat and later-beat behavior.
Timing assertion updates for individual test cases
viewer/tests/test_recovery_timing.py
Updates return shapes and assertions across the recovery-window, first-beat survival, backstop, clearPending, and selector-gate tests to match the upgraded harness output formatting and to validate the new position-aware timing contract (first vs later recoveryWindowMs, budget-aware stuck ceilings, etc.).
Mid-stream stall test rewrite with live polling validation
viewer/tests/test_recovery_timing.py
Reworks the MidStreamStallTests suite to use the live /chat resolution path instead of synthetic state transitions. Validates new stuck/backstop constant ordering and budget-aware ceiling thresholds, exercises healthy first-beat and healthy later-beat flows without stuck, and validates that trickle-then-freeze scenarios recover to a bounded non-null stuck state with submit-anchored hard backstop not reset by progress.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • electricsheephq/WorldOS#748: Main changes implement the budget-aware stuck-backstop ceilings (firstBeat 540s, later 420s) in viewer/openworlds/app.jsx—the same fix called for in this issue.

Possibly related PRs

  • electricsheephq/WorldOS#746: Both PRs modify viewer/openworlds/app.jsx and its recovery/timing test surface around the stuck-backstop logic (stuckBackstopMs / first-vs-later stuck ceilings and related test assertions).
  • electricsheephq/WorldOS#343: Both PRs modify the viewer's in-flight turn stuck/recovery timing contract—earlier PR adds the stuck/pending recovery window and 12-min backstop via useLiveSession, and this PR refines that same stuck-backstop behavior with stuckBackstopMs(firstBeat) plus updated timing tests.
  • electricsheephq/WorldOS#666: Both PRs modify viewer/openworlds/app.jsx's window.__PENDING_TIMING__ and test coverage around the pending-life-cycle—this PR updates the stuck/backstop timing contract while the earlier PR adds clearPending's arm-grace behavior, and this PR's tests include the same "spurious same-tick clear doesn't wipe a just-armed spinner" expectations.

Poem

🐰 With beats that race and pendants pending,
We time the stuck with wisdom blending—
Nine minutes first, then seven's way,
The harness hops through /chat all day!
Position-aware, the backstop grows,
Let tests confirm how timing flows. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description is comprehensive, addressing root cause, fix rationale, test coverage, and verification. However, it does not include the required licensing/CLA checklist from the template. Add the required 'Licensing / CLA' checklist section with the three mandatory acknowledgments as specified in the repository template.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: fixing a budget-aware stuck-backstop ceiling to prevent false positives on healthy slow turns, with relevant issue references.
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.

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