Skip to content

fix(reliability): cold-open retry resumes the minted campaign, not a second one (#719) - #726

Merged
100yenadmin merged 1 commit into
mainfrom
fix/719-coldopen-campaign-remint
Jun 9, 2026
Merged

fix(reliability): cold-open retry resumes the minted campaign, not a second one (#719)#726
100yenadmin merged 1 commit into
mainfrom
fix/719-coldopen-campaign-remint

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 9, 2026

Copy link
Copy Markdown
Member

Closes #719.

The bug (adversarially verified, RRI 2026-06-09)

The shared engine root of two RRI criticals — the hero-bind party-wipe and the input-lock/no-spinner. The DEFAULT cold-open prompt (scripts/play.sh) says start_world("<world>") + "if it returns existing_campaigns, start fresh". When attempt-1 times out (timeout(1) rc=124, >400s) after already minting+seeding a campaign, play.sh's one-shot retry re-runs that identical prompt → the DM mints a SECOND, party-less campaign. The viewer auto-follows the newer empty orphan → "wiped" party + a frozen, input-locked screen.

PR #717 mitigated the read-side (viewer demotes party-less orphans) and #718 added the cold-open spinner — but the orphan was still created (a wasted cold-open + a data-loss footgun). This is the engine-side prevention.

The fix (additive, invariant-safe — engine stays sole writer)

New shared helper clawdnd_coldopen_retry_msg (qa/lib_beat_driver.sh) returns a RESUME directiveget_state(<id>) first, DO NOT start_world, seat a canon PC only if the party is empty (never invent), then open the 2nd-person scene — when and only when this is a DEFAULT cold-open RETRY (first=1, no authored hero) and attempt-1 left a live campaign (clawdnd_live_campaign_id). Otherwise it echoes the base message UNCHANGED:

  • first=0 (continuing beat) — never a cold-open re-seed risk;
  • HERO_CAMP set — the authored-hero opener already opens on the existing campaign;
  • no live campaign — attempt-1 minted nothing, so the normal cold-open must run.

Wired into dm_turn's retry in play.sh. Byte-identical on every path except the one bug. Read-only (asks the engine for the live save) and only on the slow retry path. bash-3.2-clean (printf, no heredoc-in-$() — the documented macOS-system-bash footgun).

The engine seam this relies on already exists + is tested: start_world(resume=)test_start_world_resume_continues_instead_of_orphaning (test_content.py).

Tests (deterministic, no LLM/network/engine)

5 new shell-helper tests in test_dm_session_remint.py (the pytest-via-/bin/bash pattern):

  • resumes the existing campaign on the bug path (campaign_id + DO NOT call start_world + get_state, canon-PC-if-empty; NOT the fresh cold-open prompt);
  • UNCHANGED for no-prior-campaign / continuing-beat / authored-hero;
  • play.sh wiring anti-drift.

✅ 15/15 in-file · ✅ fast_gate.sh 188 engine + seat-path · ✅ bash -n clean on both scripts.

Moves which RRI gates

zero_critical (the party-wipe + input-lock criticals at their root) → toward no_give_up + cross_persona_sat 4.4→7. Verify on the next VM part-B sweep.

Summary by CodeRabbit

  • Bug Fixes

    • Campaign retries now properly resume existing campaigns instead of starting fresh, maintaining party and world state continuity during retry attempts.
  • Tests

    • Added comprehensive test coverage for cold-open retry behavior, validating proper campaign resumption and state preservation.

…second one (#719)

The DEFAULT cold-open prompt (scripts/play.sh) instructs start_world + 'if it
returns existing_campaigns, start fresh'. When attempt-1 times out (rc=124) AFTER
already minting+seeding a campaign, play.sh's one-shot retry re-runs that same
prompt -> the DM mints a SECOND, party-less campaign. The viewer auto-follows the
newer empty orphan -> party-wipe + a frozen/input-locked screen. This is the
adversarially-verified engine root shared by two RRI criticals (hero-bind party-wipe
+ input-lock); PR #717/#718 mitigated the read-side + added the spinner, but the
orphan was still CREATED.

Fix (additive, invariant-safe; engine stays sole writer): new shared helper
clawdnd_coldopen_retry_msg (qa/lib_beat_driver.sh) returns a RESUME directive
(get_state on the existing id, DO NOT start_world, seat a canon PC only if the party
is empty) for a DEFAULT cold-open RETRY (first=1, no authored hero) where attempt-1
left a live campaign; otherwise it echoes the base message UNCHANGED (continuing
beat / authored hero / fresh first attempt with nothing to resume). Wired into
dm_turn's retry in play.sh. Byte-identical on every path except the bug; read-only
(asks the engine for the live save) and only on the slow retry path. 3.2-clean
(printf, no heredoc-in-$()).

The engine seam already exists + is tested (start_world(resume=) ->
test_start_world_resume_continues_instead_of_orphaning, test_content.py).

Tests: 5 new deterministic shell-helper tests in test_dm_session_remint.py (resume
on the bug path; unchanged for no-prior-campaign / continuing-beat / authored-hero;
play.sh wiring anti-drift). 15/15 in-file + fast_gate 188 green.
@coderabbitai

coderabbitai Bot commented Jun 9, 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: 29ec4ab1-6ce4-4cb1-804b-b41d68ebab7b

📥 Commits

Reviewing files that changed from the base of the PR and between 52e74e2 and a4c5131.

📒 Files selected for processing (3)
  • qa/lib_beat_driver.sh
  • scripts/play.sh
  • servers/engine/tests/test_dm_session_remint.py

📝 Walkthrough

Walkthrough

This PR prevents campaign double-minting during cold-open beat retries by detecting existing campaign state and resuming it instead of re-seeding. A new helper function conditions the retry message based on retry type, existing campaigns, and authored hero presence; play.sh's retry path integrates the helper; comprehensive tests validate all conditional paths and wiring.

Changes

Cold-open retry campaign resumption

Layer / File(s) Summary
Cold-open retry message generator
qa/lib_beat_driver.sh
New clawdnd_coldopen_retry_msg() function conditionally outputs a resume directive when retrying a cold-open with an existing campaign and no authored hero, instructing the DM to call get_state, seat a canon hero only if the party is empty, enforce solo-session constraints, and produce opening prose—otherwise returns the base message unchanged.
Retry wiring and validation
scripts/play.sh, servers/engine/tests/test_dm_session_remint.py
dm_turn()'s retry block now queries the live campaign id from engine state and rewrites the cold-open retry message via the helper to resume the existing campaign. Five test cases validate the helper's conditional logic (existing campaign, no campaign, continuing beat, authored hero, wiring) to ensure retries correctly resume and abandon the re-seed path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • electricsheephq/WorldOS#412: Both PRs adjust the cold-open Dungeon Master prompt in scripts/play.sh to ensure solo runs do not auto-recruit a companion, overlapping on shared solo-session instruction logic.

Poem

A rabbit hops through retry lands,
Where campaigns once were double-minted by careless hands.
Now one resume whispers true:
"Keep the state, seat the hero anew—
No orphans left behind, just me and you! 🐰"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is comprehensive and detailed but is missing the required CLA and licensing checkboxes from the template, though it does address the summary and validation sections. Add the licensing/CLA checkbox section with the three required statements (CLA agreement, no confidential data, third-party material attribution) even if unchecked.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing a cold-open retry bug where the minted campaign is resumed instead of re-minting a new one, directly addressing issue #719.
Linked Issues check ✅ Passed The PR successfully implements the fix specified in issue #719: adding clawdnd_coldopen_retry_msg helper to resume existing campaigns on cold-open retries and wiring it into play.sh, with comprehensive deterministic tests covering all paths.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the cold-open retry double-minting bug: the helper function, its wiring in play.sh, and deterministic tests validating the fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 66688d7 into main Jun 9, 2026
15 checks passed
100yenadmin pushed a commit that referenced this pull request Jun 9, 2026
…dup to /events

Adversarial review (data-confirmed on real VM runs): the CLAUDE DM frequently logs the
opening/beat narration to the engine session log DURING its turn. record_dm_reply's
UNCONDITIONAL re-log would then put the prose in the log TWICE -> a SECOND /events row
(the viewer keys /events by line-index seq, not text) -> the duplicate is RELOCATED
(/events-vs-/events), not fixed.

log_engine_narration now appends ONLY when the prose is not already in the recent
session-log narration (whitespace-normalized substring, covering single-blob AND
per-paragraph logging shapes), but STILL returns success so record_dm_reply stamps
engine_logged. Net: the prose lives in the engine log EXACTLY ONCE (for /events +
recap/memory) and the redundant /chat blob is dropped -> rendered once. The CODEX DM
(which does not self-log) still gets the canonical append.

Tests (+3): does-not-double-log-when-already-logged, idempotent-across-per-paragraph
(real newlines via $'...'), appends-canonical-when-absent. 12/12 in-file + remint 15 +
fast_gate 188 + client contract 3 green. Rebased onto main (incl #719/#726).
100yenadmin added a commit that referenced this pull request Jun 9, 2026
… kill the duplicate opening (#720) (#727)

* fix(reliability): stamp engine_logged on DM opening/beat chat rows to kill the duplicate opening (#720)

The cold-open opening narration rendered TWICE in the OpenWorlds chronicle
(vm2-newbie/narrative/veteran sweep): the opening prose lands in two
viewer-read sources — the engine per-session log (per-paragraph, fed to
/events) AND chat.jsonl (the whole opening as one blob, written by the play
wrappers' `chatlog` with no flag). The client's mid-session de-dup
(eventsStreamedThisTurnRef) does not guard the cold-open blob (the opening is
already complete pre-mount), so it shows up twice.

The codex DM path (scripts/play_codex_dm.sh) already solves this with three
pieces: a 3-arg `chatlog` (optional extra-JSON merged into the row), a
`log_engine_narration` helper, and a `record_dm_reply` helper that stamps
{"engine_logged":true} on the chat row IFF the prose was also logged to the
engine session log. The client already honors that marker
(viewer/openworlds/app.jsx: `if (it.engine_logged === true) return null;`).

This ports that PROVEN idiom into the two CLAUDE-DM viewer-backed wrappers.
Both play.sh and play_party.sh already source qa/lib_beat_driver.sh, so the
three helpers live ONCE in that shared lib (DRY, mirroring the shared
clawdnd_dm_remint_session_on_retry) and both wrappers call them:

  - qa/lib_beat_driver.sh: add chatlog (3-arg) + log_engine_narration +
    record_dm_reply (read $CHAT/$STATE_DIR/$ROOT from the caller, as the codex
    versions read $CHAT/$RUN_DIR/$ROOT). On engine-log SUCCESS -> flagged row;
    on FAILURE -> unflagged row (byte-identical to today; the client's
    eventsStreamedThisTurnRef backstop still applies). The flag is NEVER
    stamped unconditionally (a legit /chat-only beat must still render).
  - scripts/play.sh: drop the one-line chatlog (now shared); MOVE the
    CAMPAIGN_ID resolution to BEFORE the opening write (record_dm_reply needs
    the id; the campaign already exists post cold-open dm_turn); route the
    opening + per-move DM writes through record_dm_reply.
  - scripts/play_party.sh: drop the one-line chatlog; route the opening,
    reseat, after-intros, and per-beat DM writes through record_dm_reply.
    Player/companion rows are left as plain chatlog calls.

Wrapper-only (the viewer side is already done + tested). bash 3.2 safe:
the heredoc-bearing helpers are only ever called directly, never inside $(...).

Tests: new servers/engine/tests/test_dm_reply_engine_logged.py (9 tests,
behavioral + static anti-drift) green; the existing remint anti-drift test
(10) and the client-side engine_logged de-dup + regression tests (3) stay
green; fast_gate Tier-0 (188) passes.

* fix(#720): make log_engine_narration IDEMPOTENT — don't relocate the dup to /events

Adversarial review (data-confirmed on real VM runs): the CLAUDE DM frequently logs the
opening/beat narration to the engine session log DURING its turn. record_dm_reply's
UNCONDITIONAL re-log would then put the prose in the log TWICE -> a SECOND /events row
(the viewer keys /events by line-index seq, not text) -> the duplicate is RELOCATED
(/events-vs-/events), not fixed.

log_engine_narration now appends ONLY when the prose is not already in the recent
session-log narration (whitespace-normalized substring, covering single-blob AND
per-paragraph logging shapes), but STILL returns success so record_dm_reply stamps
engine_logged. Net: the prose lives in the engine log EXACTLY ONCE (for /events +
recap/memory) and the redundant /chat blob is dropped -> rendered once. The CODEX DM
(which does not self-log) still gets the canonical append.

Tests (+3): does-not-double-log-when-already-logged, idempotent-across-per-paragraph
(real newlines via $'...'), appends-canonical-when-absent. 12/12 in-file + remint 15 +
fast_gate 188 + client contract 3 green. Rebased onto main (incl #719/#726).

---------

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.

[reliability] Cold-open retry double-mints a campaign — resume instead of re-seed (party-wipe + input-lock root)

1 participant