Skip to content

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

Description

@100yenadmin

Root cause (adversarially verified, RRI 2026-06-09)

The shared engine-root of two critical bugs — the hero-bind party-wipe (vm2-opuslean-narr2) and the input-lock/no-spinner (vm2-newbie). When a cold-open beat times out (timeout(1) rc=124, >WORLDOS_COLDOPEN_TIMEOUT 400s), play.sh's one-shot retry re-mints only the --session-id and re-invokes the identical cold-open prompt — which again instructs start_world("<world>"). The engine has no resume context, so it mints a SECOND, fresh campaign (party-less — load_canon_character never ran), orphaning attempt-1's seated run. The viewer then auto-follows the newer empty orphan → "wiped" party + a frozen empty screen.

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

Fix (additive, invariant-safe; engine stays sole writer)

In scripts/play.sh dm_turn()'s retry block (~287-296): before re-invoking _dm_invoke on a cold-open (first=1) turn with no HERO_CAMP, detect the campaign attempt-1 already minted — clawdnd_live_campaign_id "$ROOT" "$STATE_DIR" "$WORLD" (the engine's authoritative most-recent save, qa/lib_beat_driver.sh) — and if non-empty, switch the retry $msg to the EXISTING-CAMPAIGN directive (mirror the HERO_CAMP branch text, play.sh:360-373): campaign_id=<id> ALREADY EXISTS — call get_state(<id>) FIRST, DO NOT call start_world (it would mint a NEW campaign and ORPHAN this save); seat the PC if absent, then open the scene. Byte-unchanged on a fresh first attempt (no prior campaign) and on continuing beats.

The engine seam already exists: start_world(world_id, resume=<id>) at servers/engine/server.py:493-512 returns resumed:True + the same campaign; load_canon_character is idempotent on an already-seated party.

Tests (deterministic, no LLM)

  • play.sh: extract the retry's campaign-selection into a sourceable shell fn; seed a STATE_DIR with one engine-minted seated campaign, assert a first=1 RETRY selects the EXISTING campaign + emits a directive containing campaign_id=<existing> + DO NOT call start_world, NOT the start_world("<world>") cold-open text. RED today.
  • engine contract (servers/engine/tests/test_dm_session_remint.py): start_world(world)→mint A→seat PC = ONE dir; start_world(world, resume=A) returns resumed:True, campaign==A, NO second dir; start_world(world) with no resume DOES mint a second dir (documents the orphaning to avoid).

Verified plan in the workflow output (session scratchpad references it). Load-bearing beat-driver change — TDD it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions