fix(openworlds): strip DM-internal leaks + animate narrating state + label action bar (#335/#336/#337) - #338
Conversation
…label action bar (Closes #335, #336, #337) Three play-surface bugs surfaced by the #324 AI playtester (run play2, newbie) on the now-reachable Table screen. All viewer-side (engine stays sole writer); no wire-contract / engine change. #335 [P1, data-integrity] DM-internal "GM Advisory" directives + bare engine tool names (e.g. "record their first memory with `remember`") leaked into the player-facing story scroll via the /chat render — the AI-DM equivalent of a system-prompt leak. Root cause: the /chat tail maps every non-player item to a narration beat with no filtering, so when the DM agent "thinks out loud" the housekeeping prose lands in the player's chronicle. - Hard guard (viewer): new sanitizeNarration() strips GM-advisory headers, the Director debt-nudge family (mirrors engine/director.py::_nudge) and bare/underscored engine-tool lines. Applied at BOTH the /chat ingestion map AND the LogEntry narration render (render-path-complete: also covers engine recentEvents). Line-oriented so a stray advisory line is removed without nuking the real prose around it; a wholly-internal beat is dropped (and no longer falsely clears the #327 pending state). - Soft guard (DM skill): a nudge in dungeon-master/SKILL.md — the advisory is for the DM, never echoed into player narration; pay the debt as fiction, then remember/add_quest silently. #336 [P1] The "DM is narrating…" state (#328) was static — 35–60s turns read as frozen. Added a LIVE elapsed-time readout (0:07 → 0:52 …, the strongest "alive" cue, works under reduced-motion since it's information), a subtle label shimmer, and a "this can take up to a minute" hint. role=status + aria-live polite announce once; the per-second counter + dots are aria-hidden so a screen reader isn't spammed. Reduced-motion stills the dots/shimmer (global token + belt-and-suspenders CSS) while keeping the timer + hint. #337 [P2] The action-bar buttons (Continue/Say/Do/Check/Save), dice (d20/d12/d8/d6), and free-text Declare were unexplained — the newbie ignored them all. Added native title= tooltips (hover + SR description, no new DOM, visible label stays the accessible name) keyed by engine action id, plus a one-line inline hint under the action bar. BrassButton now forwards an optional title. Validation: in-browser Babel transpile OK (vendored 7.29.0); sanitizeNarration logic 22/22 cases incl. the issue's verbatim leak + adversarial prose; rendered DOM confirms 0 directive/tool strings in the player chronicle while real narration survives, the narrating beat shows a ticking 0:NN timer, and all action affordances render; qa/ui_audit_health.sh --quick --axe = axe 0 across all 18 screens; no JS console errors on the real table page.
|
Caution Review failedPull request was closed or merged during review No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR addresses three playtest issues in the active play loop: prevents DM-internal directives from leaking into the player chronicle, adds progress animation to the "DM is narrating" state to prevent perceived freezing during long turns, and adds hover tooltips to action-bar buttons for new player discoverability. Changes span skill documentation, a UI component signature, and a substantial viewer-side narration handling layer. ChangesNarration safety, feedback, and affordances
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
…t pre-empted (Closes #348) (#351) 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. Co-authored-by: Eva <eva@100yen.org>
Three play-surface bugs the #324 AI playtester surfaced on the now-reachable Table/play screen (run
play2, newbie persona, baldurs-gate — the first run to sustain a play loop past the #328/#329 entrance fix). All fixes are viewer-side (screen-table.jsx+ a one-linechrome.jsxpassthrough) plus one DM-skill nudge. No engine change, no wire-contract change (engine stays sole writer;CLAWDND_*/clawdnd-*ids / bundle id untouched). Public repo → no assets committed.Closes #335
Closes #336
Closes #337
#335 — DM-internal "GM Advisory" / tool-name leak into the player story panel
[P1, data-integrity]Root cause (investigated): the
/chattail inscreen-table.jsxmapped every non-player item to anarrationbeat with no filtering and rendered it in the player's center chronicle. When the DM agent "thinks out loud" — echoing a Campaign-Director debt nudge (NPC 'Danis' … record their first memory withremember``) or a bare engine-tool name — that internal prose landed verbatim in the player surface. This is the AI-DM equivalent of a system-prompt leak. The leak vector is the/chatprojection (the DM agent's free-form prose), not the intended right-panel "GM Advisory" component (that one is a deliberate DM-facing side panel fed by `/journal-surface`).Fix — viewer-side hard guard (primary) + DM-skill soft nudge:
sanitizeNarration()strips, line-by-line: GM-advisory headers (GM Advisory…,What the campaign owes the story), the Director debt-nudge family (mirrorsservers/engine/director.py::_nudge: untracked-hook / stalled-quest / due-consequence / thread-pressure / introduced-but-silent), and bare/underscored engine-tool lines (`remember`,add_quest,world_tick, …). It is line-oriented, so a single stray advisory line inside an otherwise-real beat is removed without nuking the prose around it; a wholly-internal beat sanitizes to empty and is dropped (and no longer falsely clears the [playtest][P0] After submitting an action, no DM response + no loading state — play loop appears frozen #327 pending state)./chatingestion map and theLogEntrynarration render (the single chokepoint every narration source —/chatand enginerecentEvents— flows through).skills/dungeon-master/SKILL.md): the Director advisory is for the DM, never echoed into player narration — pay the debt as fiction, thenremember/add_questsilently.Was it viewer-side, DM-skill, or both? Both — the viewer filter is the hard guard (deterministic, can't be talked out of it); the DM-skill line reduces the rate of leaks at the source.
#336 — static "DM is narrating…" reads as frozen on 35–60s turns
[P1]#328 added the label + greyed controls but it was static. Added the missing progress signal:
0:07 → 0:52 …) — the strongest "still alive" cue, and it keeps ticking under reduced-motion (it's information, not decoration);a11y:
role="status"+aria-live="polite"announce the wait once on mount; the per-second counter and dots arearia-hiddenso a screen reader isn't spammed every tick. prefers-reduced-motion + the in-app[data-reduced-motion='on']token both still the dots/shimmer while keeping the timer + hint.#337 — unexplained action-bar buttons
[P2]The newbie ignored Continue/Say/Do/Check/Save entirely. Added discoverability with zero new DOM:
title=tooltips on the five quick actions (keyed by engine actionid), the four dice buttons, and the free-text input + Declare (hover + screen-reader description; the visible label stays the accessible name);BrassButtonnow forwards an optionaltitle.Validation
babel-standalone-7.29.0, the exact runtime):screen-table.jsx+chrome.jsxcompile clean.sanitizeNarrationlogic: 22/22 cases — incl. the issue's verbatim leak text (stripped → dropped), all five Director nudge kinds, bare/back-ticked tool lines, and adversarial prose that contains a tool word as a normal verb (tries to remember,take a long rest,travel with the caravan,"Remember the bridge") → all preserved.hasn't spoken/first memory with remember/record their first memory, while the real narration line and the player line both render. The narrating beat renders a ticking0:NNtimer +role=status/aria-live+ dots + hint. All 11 action affordances render theirtitle=.qa/ui_audit_health.sh --quick --axe→ axe total: 0 violations across all 18 screens (incl.table); all structural checks PASS.#tablepage (only Babel's standard in-browser-transformer notice).Before / After (#335 — the leak)
GM Advisory … What the campaign owes the story+… record their first memory withremember``After (#336 narrating beat, rendered)
Please don't merge — leaving for admin-merge on green CI.
Summary by CodeRabbit
Bug Fixes
New Features