Skip to content

fix(openworlds): strip DM-internal leaks + animate narrating state + label action bar (#335/#336/#337) - #338

Merged
100yenadmin merged 1 commit into
mainfrom
fix/335-336-337-play-surface
May 29, 2026
Merged

fix(openworlds): strip DM-internal leaks + animate narrating state + label action bar (#335/#336/#337)#338
100yenadmin merged 1 commit into
mainfrom
fix/335-336-337-play-surface

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented May 29, 2026

Copy link
Copy Markdown
Member

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-line chrome.jsx passthrough) 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 /chat tail in screen-table.jsx mapped every non-player item to a narration beat 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 with remember``) 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 /chat projection (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:

  • New sanitizeNarration() strips, line-by-line: GM-advisory headers (GM Advisory…, What the campaign owes the story), the Director debt-nudge family (mirrors servers/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).
  • Applied at two points so the guard is render-path-complete: the /chat ingestion map and the LogEntry narration render (the single chokepoint every narration source — /chat and engine recentEvents — flows through).
  • DM-skill nudge (skills/dungeon-master/SKILL.md): the Director advisory is for the DM, never echoed into player narration — pay the debt as fiction, then remember/add_quest silently.

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:

a11y: role="status" + aria-live="polite" announce the wait once on mount; the per-second counter and dots are aria-hidden so 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:

  • native title= tooltips on the five quick actions (keyed by engine action id), the four dice buttons, and the free-text input + Declare (hover + screen-reader description; the visible label stays the accessible name);
  • a one-line inline hint under the action bar distinguishing free-text + Declare from the quick actions.
  • BrassButton now forwards an optional title.

Validation

  • In-browser Babel transpile (vendored babel-standalone-7.29.0, the exact runtime): screen-table.jsx + chrome.jsx compile clean.
  • sanitizeNarration logic: 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.
  • Rendered-DOM proof (headless Chrome against the real bundle): in the player chronicle's visible text — 0 occurrences of 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 ticking 0:NN timer + role=status/aria-live + dots + hint. All 11 action affordances render their title=.
  • a11y gate: qa/ui_audit_health.sh --quick --axeaxe total: 0 violations across all 18 screens (incl. table); all structural checks PASS.
  • Console: no JS errors on the real #table page (only Babel's standard in-browser-transformer notice).

Before / After (#335 — the leak)

Player chronicle content
Before DM beat rendered verbatim incl. GM Advisory … What the campaign owes the story + … record their first memory with remember``
After Only the real prose renders: "The lantern-light pools on the wet cobbles … Danis the fletcher finally lifts his eyes to meet yours." — advisory + tool-name stripped (DOM-verified: 0 directive strings in visible text)

After (#336 narrating beat, rendered)

The Dungeon Master is narrating • • • 0:52
Weaving the next beat — this can take up to a minute.

Please don't merge — leaving for admin-merge on green CI.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed internal DM directives and tool references from leaking into player-facing narration.
  • New Features

    • Added tooltip hints to action buttons and dice rolls for better guidance.
    • Enhanced "DM is narrating" indicator with elapsed time display and animated feedback.

Review Change Stack

…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.
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

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: 8565c967-8881-4030-89a3-1dded2e21ae5

📥 Commits

Reviewing files that changed from the base of the PR and between 7611fbe and 7329b20.

📒 Files selected for processing (3)
  • skills/dungeon-master/SKILL.md
  • viewer/openworlds/chrome.jsx
  • viewer/openworlds/screen-table.jsx

📝 Walkthrough

Walkthrough

This 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.

Changes

Narration safety, feedback, and affordances

Layer / File(s) Summary
Narration sanitization contract and implementation
viewer/openworlds/screen-table.jsx
sanitizeNarration(text) with regex utilities filters out internal "GM Advisory" directive lines and bare engine tool tokens (e.g., remember, add_quest), collapsing excess whitespace and returning empty string when a beat becomes entirely internal, preventing any leak into player view.
Sanitization enforcement: chat processing and render
viewer/openworlds/screen-table.jsx
Live chat tail processing runs DM narration through sanitizeNarration before emission; LogEntry hardens render path by sanitizing at display time and returning null for empty-after-sanitization beats, ensuring internal-only beats never surface.
UI affordance hints: action buttons, dice, and Declare
viewer/openworlds/chrome.jsx, viewer/openworlds/screen-table.jsx
Action bar (Continue/Say/Do/Check/Save), dice buttons (d20/d12/d8/d6), and Declare input receive hover title tooltips explaining their purpose; BrassButton component signature expands to accept optional title prop and forward it to the underlying button element; EncounterButton accepts hint prop wired from action cards.
DM narration progress feedback with elapsed time
viewer/openworlds/screen-table.jsx
DmNarratingBeat component refactored to accept since timestamp, track elapsed time via interval, render animated progress dots and mm:ss label with reduced-motion-aware CSS keyframe animation; pending-state renderer passes since to enable "alive" feedback during long DM turn resolution.
Skill documentation: anti-leak contract
skills/dungeon-master/SKILL.md
"Re-ground" beat cycle instructions updated to explicitly document that director/event debts must be paid as fiction while silently updating memory/quests, and never echo director nudges, advisory text, or engine tool names into player-facing narration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • electricsheephq/WorldOS#328: Both PRs modify viewer/openworlds/screen-table.jsx's DM narration affordance—expanding ScreenTable/LogEntry narration handling and updating DmNarratingBeat/pending-state behavior—so the changes are directly related at the same code level.
  • electricsheephq/WorldOS#154: The main PR's updated "Re-ground" beat instructions and viewer narration sanitization explicitly constrain how get_campaign_director "DIRECTOR/GM Advisory" debts are surfaced (paid silently, never echoed), which directly relates to the retrieved PR wiring the Campaign Director advisory into the DM beat loop and DM prompt.

Poem

🐰 A rabbit's ode to cleaner screens:

No "GM Advisory" leaks for newfound dreams,
The DM narrates while dots dance and gleam,
Tooltips now whisper what buttons all mean—
Best playtest ever! the player now beams. 🎭✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 PR title clearly summarizes the main change: fixing DM-internal leaks, animating the narrating state, and labeling the action bar across the three linked issues.
Description check ✅ Passed The PR description is comprehensive, covering all three issues, validation steps, and before/after evidence, though the CLA checklist is not explicitly marked.
Linked Issues check ✅ Passed Code changes fully address all three linked issues: #335 (sanitizeNarration filter strips DM leaks), #336 (narrating beat now shows elapsed time + animation + hint), #337 (action buttons and dice now have title tooltips).
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the three linked issues. The modifications to screen-table.jsx, chrome.jsx, and SKILL.md are directly addressing the identified bugs with no unrelated changes.

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

@100yenadmin
100yenadmin merged commit 5b8ab51 into main May 29, 2026
6 of 7 checks passed
@100yenadmin
100yenadmin deleted the fix/335-336-337-play-surface branch May 29, 2026 21:32
100yenadmin added a commit that referenced this pull request May 30, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant