Skip to content

fix(qa+engine): make the beat heartbeat real — flip progress at ingest, decontaminate recap/FTS/lean-tail/fallback (#749) - #763

Merged
100yenadmin merged 1 commit into
mainfrom
fix/749-heartbeat-repair
Jun 10, 2026
Merged

fix(qa+engine): make the beat heartbeat real — flip progress at ingest, decontaminate recap/FTS/lean-tail/fallback (#749)#763
100yenadmin merged 1 commit into
mainfrom
fix/749-heartbeat-repair

Conversation

@100yenadmin

Copy link
Copy Markdown
Member

Closes #749. Refs #743 #727 #623 #357 #720.

The two audit-verified roots

  1. The fix(qa): guarantee a per-beat progress signal so a slow DM beat is never perceived as dropped/hung (closes #623) #743 heartbeat was a NO-OP for the player. The heartbeat texts are byte-identical to _WRAPPER_PROGRESS_LINES in viewer/openworlds/screen-table.jsx, and the /events ingest in viewer/openworlds/app.jsx ran const clean = sanitize(...); if (!clean) return null; BEFORE notePendingProgress — the row was silently swallowed and the spinner never flipped. Zero player benefit.
  2. The heartbeat rows contaminated engine memory. They are real kind=narration session-log rows and no engine consumer filtered them: recap.py recited the filler in "Previously on…", ledger.py indexed it for FTS recall, server.py::_scene_recent_narration fed it to the lean re-ground (the DM was told filler is its own canon), and qa/dm_narration_fallback.py recovered it — masking fully-dead beats with stale prose + filler.

The repair (keep-and-repair; additive; engine = sole writer)

(a) Client — flip progress at ingest. app.jsx's /events ingest special-cases the exact wrapper lines BEFORE the sanitize-drop: notePendingProgress() (flips streaming, re-arms the adaptive recovery window) + return null (never renders). It deliberately does NOT set eventsStreamedThisTurnRef/dmBeatCountRef, so a dead-DM beat whose only stream was the heartbeat still renders its recovered /chat text. The predicate + constant are shared from screen-table.jsx (window.isWrapperProgressLine / window.WRAPPER_PROGRESS_LINES), with a graceful fall-through to today's sanitize-drop if absent.

(b) ONE python source of truth + sync test + 4 filters. Canonical constants live in servers/engine/wrapper_progress.py (pure, dependency-free — the engine's 3 consumers import it directly and keep filtering even without qa/); qa/wrapper_progress_lines.py (the spec-named module) re-exports it by file-path load for the stdlib-only qa scripts. tests/test_wrapper_progress_sync.py regex-parses the jsx Set AND BOTH sh emit rotations (qa/lib_beat_driver.sh and scripts/play_codex_dm.sh — the audit cited one; both exist and both are now pinned) and fails on the first divergent byte. Exact-trimmed-match filters added in:

(c) Fallback honesty. New clawdnd_resolve_dm_reply (a DIRECT-call front door over clawdnd_dm_narration_or_fallback — a command-substitution subshell could never export the flag) sets CLAWDND_FALLBACK_RECOVERED=1 iff the #357 fallback recovered the prose. record_dm_reply (both branches) and the new clawdnd_chatlog_dm stamp {"fallback_recovered":true} on the dm chat row, consume-once. All 9 runner call sites converted (scripts/play.sh ×2, scripts/play_party.sh ×4, qa/run_duo.sh ×2 + chatlog, qa/run_party.sh ×2 + chatlog, qa/ui_playtest.sh — whose dm_turn now returns the raw result so the caller can resolve+flag). Flags are jsonl-side payload only; assert_behavioral semantics unchanged in this PR.

(d) Dedup verdict: REAL collision, fixed by exemption. #727's last-8 substring guard in lib_beat_driver.sh::log_engine_narration provably swallows cadence-aligned heartbeats: a run of dead beats logs ONLY heartbeats, so with a 4-line rotation, beat N+4's text always sits inside the last-8 tail → dropped → no /events row → no flip, exactly when the heartbeat is the only life sign. Chosen fix: wrapper lines are EXEMPT from the dedup (always append) rather than uniquifying the text with a beat counter — this keeps EXACT-match semantics on every filter surface (no prefix-matching contract to keep in sync), and the repeated rows are inert (never rendered; filtered from recap/FTS/lean tail/fallback). scripts/play_codex_dm.sh's own log_engine_narration has no dedup guard, so no change needed there. The intact direction is pinned: the DM's own echoed prose still dedups to one row.

Tests (red-first; red proven before implementation)

  • servers/engine/tests/test_wrapper_progress_sync.py (7) — python==jsx==both-sh, shim re-export, window export, exact-trim semantics.
  • servers/engine/tests/test_heartbeat_repair.py (10) — recap (store-backed + unit + all-filler→new-adventure), FTS recall exclusion, scene_context lean-tail exclusion, dedup heartbeat-repeat-survives + real-prose-still-dedups (real bash + uv engine), fallback_recovered stamps (recovery/no-recovery/failure-path/success-path, consume-once).
  • servers/engine/tests/test_dm_narration_fallback.py (+2) — heartbeat-only dead beat recovers nothing; prose after a heartbeat survives.
  • viewer/tests/test_live_narration_stream.py (+2, node harness over the real transpiled jsx) — a wrapper /events row flips pending.streaming WITHOUT rendering; a heartbeat-only stream does not suppress a later /chat-only beat.
  • servers/engine/tests/test_dm_session_remint.py — the ui_playtest static guard updated to track the new front door (same intent, stronger: both turns + honesty stamp).

Verification

  • Engine: uv run --directory servers/engine python -m pytest tests -q -p no:xdist1783 passed
  • Viewer: python3 -m pytest viewer/tests -q -p no:xdist488 passed, 6 skipped
  • bash qa/fast_gate.shPASS (188 passed)
  • /bin/bash -n clean on all 7 touched shell scripts (macOS system bash 3.2)

Untouched (per constraints)

#746/#761 stuck-backstop code, #740 chronicle sanitizer's handling of real prose, wire contracts, _StrictModel schemas. bash 3.2-clean throughout.

…t, decontaminate recap/FTS/lean-tail/fallback (#749)

The #743 wrapper heartbeat was (1) a NO-OP for the player — its texts are
byte-identical to screen-table.jsx's _WRAPPER_PROGRESS_LINES and app.jsx
sanitized the /events row to "" BEFORE notePendingProgress, so the spinner
never flipped — and (2) real kind=narration rows that NO engine consumer
filtered: recap recited the filler, the FTS ledger indexed it, the lean
re-ground tail fed it back to the DM as canon, and dm_narration_fallback
recovered it (masking fully-dead beats with stale prose + filler).

Keep-and-repair, additive; engine stays the sole writer; old snapshots
round-trip:

(a) CLIENT: app.jsx /events ingest special-cases exact wrapper lines BEFORE
    the sanitize-drop — notePendingProgress() (flip streaming) + return null
    (never render; never sets eventsStreamedThisTurnRef so a dead beat's
    recovered /chat text still renders). Predicate + constant shared from
    screen-table.jsx as window-guarded globals.
(b) ONE python source of truth: servers/engine/wrapper_progress.py
    (qa/wrapper_progress_lines.py re-exports it for stdlib-only qa scripts)
    + tests/test_wrapper_progress_sync.py regex-pins the jsx Set and BOTH sh
    emit rotations (lib_beat_driver.sh + play_codex_dm.sh) byte-identical.
    Exact-match filters in recap.format_recap, ledger.backfill (FTS),
    server._scene_recent_narration, and qa/dm_narration_fallback.py (where a
    heartbeat BREAKS the trailing block, so a heartbeat-only dead beat
    honestly recovers nothing).
(c) FALLBACK HONESTY: clawdnd_resolve_dm_reply (direct-call front door) flags
    a #357 recovery; record_dm_reply / clawdnd_chatlog_dm stamp
    {"fallback_recovered":true} on the dm chat row (consume-once). All 9
    runner call sites converted. assert_behavioral semantics unchanged.
(d) DEDUP: #727's last-8 substring guard provably swallowed cadence-aligned
    heartbeats (a run of dead beats logs ONLY heartbeats — the 4-line
    rotation repeats inside the window). Wrapper lines are now EXEMPT from
    the dedup (always append); the repeats are inert (never rendered, fully
    filtered), keeping the exact-match design on every side.

Tests (red-first): sync test, recap/FTS/lean-tail exclusion, fallback
wrapper-skip + fallback_recovered stamps, dedup bypass + intact prose dedup,
viewer node-harness heartbeat-flips-without-rendering + no-suppression guard.
Engine 1783 passed, viewer 488 passed, fast_gate PASS, bash -n clean (3.2).

Closes #749. Refs #743 #727 #623 #357 #720.
@100yenadmin 100yenadmin added this to the v1.0.4 milestone Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@100yenadmin, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 14 minutes and 17 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 24f8356f-3975-4da0-b19c-e1054c219d90

📥 Commits

Reviewing files that changed from the base of the PR and between 6a8297b and 11f2cb5.

📒 Files selected for processing (19)
  • qa/dm_narration_fallback.py
  • qa/lib_beat_driver.sh
  • qa/run_duo.sh
  • qa/run_party.sh
  • qa/ui_playtest.sh
  • qa/wrapper_progress_lines.py
  • scripts/play.sh
  • scripts/play_party.sh
  • servers/engine/ledger.py
  • servers/engine/recap.py
  • servers/engine/server.py
  • servers/engine/tests/test_dm_narration_fallback.py
  • servers/engine/tests/test_dm_session_remint.py
  • servers/engine/tests/test_heartbeat_repair.py
  • servers/engine/tests/test_wrapper_progress_sync.py
  • servers/engine/wrapper_progress.py
  • viewer/openworlds/app.jsx
  • viewer/openworlds/screen-table.jsx
  • viewer/tests/test_live_narration_stream.py

Comment @coderabbitai help to get the list of available commands and usage tips.

@100yenadmin
100yenadmin merged commit a245a2c into main Jun 10, 2026
17 checks passed
@100yenadmin
100yenadmin deleted the fix/749-heartbeat-repair branch June 10, 2026 11:50
100yenadmin added a commit that referenced this pull request Jun 11, 2026
… timeout(1) shim, cold-open guards, party heartbeat + soft-tick (audit F12-1, F12-3, F12-4, F12-5, F12-8) (#828)

The five wrapper-reliability findings from docs/audits/ENGINE-AUDIT-2026-06-11.md
unit 12 (skeptic-verified), fixed together because they share files:

* F12-1 (enriches #753): routine beat deadline 200s -> 360s in clawdnd_dm_timeout
  (measured: routine p90=224s max=360s — the flat 200s killed ~18% of HEALTHY
  beats), and BOTH dm_turn paths (play.sh + play_party.sh) now recompute the
  retry deadline via the new clawdnd_dm_retry_timeout (attempt 2 escalates to
  the model-aware cold-open tier, never de-escalates, never reuses verbatim).
* F12-8 (closes #787): worldos_timeout shim in qa/lib_beat_driver.sh —
  timeout(1) when present, else a python3 subprocess fallback preserving
  rc=124/127/126 semantics — swapped into both play-lane _dm_invoke call sites;
  launch_common.sh preflight now warns (non-fatal, the shim covers absence)
  with the brew-install-coreutils hint.
* F12-3 (closes #777): play.sh cold open now ABORTS NON-ZERO on an empty
  opening DMSG and on a no-campaign mint (was: unflagged empty chat row + an
  indefinitely-"running" unplayable session), and runs the seating guard with
  one reseat retry then a loud abort. pc_seated() factored into the lib as
  clawdnd_pc_seated (snapshot-read-only, viewer _action_actor contract);
  play_party.sh now REUSES the shared helper instead of its local copy.
* F12-4 (closes #790, completes #623): play_party.sh emits the
  model-independent progress heartbeat — before the cold-open turn (campaign id
  is pre-seeded in this lane) and after the human move BEFORE companion_moves —
  via the same shared helper play.sh calls (post-#763 contamination-safe).
* F12-5 (closes #791): play_party.sh beat loop captures PREV_DAY/PREV_TOD
  pre-beat and runs clawdnd_soft_tick after record_dm_reply (mirror of
  play.sh:475-478/504) — it was the only beat loop without the backstop.

Tests: servers/engine/tests/test_wrapper_reliability.py (22 new, red-first) —
worldos_timeout behavioral matrix (rc pass-through, rc=124 deadline, PATH-
stripped fallback, native-binary preference), retry-deadline escalation,
clawdnd_pc_seated behavioral matrix, static anti-drift asserts on all call
sites, bash -n on every touched script. test_adversarial_release.py seating
assert updated to track the factored helper (intent unchanged). Full engine
suite 1805 passed; qa/fast_gate.sh PASS.

Engine stays the sole writer (guards are snapshot-read-only; heartbeat/tick
route through log_engine_narration/advance_time). Wire contracts frozen
(CLAWDND_* names kept; no new env names). Bash-3.2-clean.

Co-authored-by: Eva <arncalso@gmail.com>
100yenadmin added a commit that referenced this pull request Jun 14, 2026
…F07-1, F14-3) (#847)

F07-1 (#772): the cold-open "previously on" recap and the FTS recall ledger were
contaminated by combat/system BOOKKEEPING — every run recited "Tough 1 takes 5
force damage / Turn advances to Tough 2" and a recall('Rolan') probe returned 4 of
6 top hits as bookkeeping. Distinct from #749/#763, which exact-matched only the
wrapper-heartbeat line. Engine combat-event rows carry payload schema
clawdnd.combat_event.v1 (via _log_combat_event); the two session markers are
"Session N began" / "Session ended.". recap.format_recap now drops schema-stamped
combat rows (narrative combat with no/other payload stays); ledger.backfill skips
schema-stamped combat rows AND the two session markers by exact prefix — while
keeping every OTHER kind=system row indexed, preserving the documented
DM-authored-system-note->recall path (SKILL.md:47). Derived-index-only; no schema
change.

F14-3 (#795): persist_beat (the every-beat write) burned ~2 beats in a real gate
run. Four defects fixed by validate-then-apply: (1) decision chosen=null (and
summary/rationale) crashed pydantic string_type -> None-coerce every Decision str
field; (2) mem["character_id"] bare-KeyError'd -> resolve via the F14-8 _char
resolver (#786) with id/npc_id aliases and an actionable "memories index N: ..."
error carrying a did-you-mean; (3) events were appended to the session jsonl BEFORE
memories/decision validation (a crash left a partial chronicle, a retry duplicated
it) -> the whole batch is now validated BEFORE the first append_log (events-only
non-atomic window closed); (4) the quadratic remembered echo (whole growing memory
list per item) is now the slim {"id","fact","memory_count"}. advance stays its own
sequential locked call; engine-sole-writer + one-lock-one-save preserved.

Tests: +3 recap (drop schema-stamped combat; keep narrative/unrelated-payload
combat), +3 ledger (skip combat-events + session markers; KEEP DM system note),
+8 persist_beat (chosen:null; null str fields; actionable bad-id not bare KeyError;
events-not-applied-on-later-failure atomicity; text alias honored; empty rejected;
id alias resolves; remembered O(items) not quadratic). Full engine suite 2096
passed; fast_gate Tier-0 PASS.

Closes #772
Closes #795
Source: docs/audits/ENGINE-AUDIT-2026-06-11.md

Co-authored-by: Eva <arncalso@gmail.com>
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.

[qa][engine] #743 heartbeat is invisible to the player AND contaminates engine memory (recap/FTS/lean tail)

1 participant