Skip to content

fix(coldopen): guard player-PC seating + raise Part-A poll window (no_actor reliability) - #570

Merged
100yenadmin merged 1 commit into
mainfrom
fix/coldopen-reliability
Jun 2, 2026
Merged

fix(coldopen): guard player-PC seating + raise Part-A poll window (no_actor reliability)#570
100yenadmin merged 1 commit into
mainfrom
fix/coldopen-reliability

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 2, 2026

Copy link
Copy Markdown
Member

Fixes two cold-open RELIABILITY issues blocking the WorldOS .app gate.

ISSUE 1 — no_actor (no PC seated)

Root cause (forensic, from run g1): the .app drives scripts/play_party.sh (ProviderAdapters.swift:62), which pre-seeds the companions but deliberately leaves the human's PC to be created live by the DM's cold-open turn (play.sh-parity "the DM hands you a character" feel). That makes PC seating DM-stochastic. In the g1 veteran run the DM minted can_act:true early (move-sink + campaign bound) but its cold-open turn ended after start_world without ever calling create_character(kind="player") — leaving party=[], characters={NPCs only}, chat.jsonl empty. The viewer then reports readiness=degraded / failure_bucket="no_actor" ("no active player actor is seated") — an unplayable surface the persona can't escape (viewer/server.py _action_actor returns None when no party member is kind="player"). A prior newbie run DID seat (PC Rolan), confirming a stochastic miss, not a code break.

Fix (guard over prompt-only, per the brief):

  • Strengthen the cold-open prompt (play_party.sh) so seating the PC is the FIRST, mandatory tool action — create_character with kind="player" + add_to_party=true, with explicit "a cold open that ends with no seated PC is a BROKEN session" language.
  • Add a snapshot-backed seating GUARD after the cold-open DM turn that mirrors viewer/server.py _action_actor (a seated PC = a party member whose record is kind="player"). If unseated → retry the cold open ONCE on a fresh session id with a hard seat-only directive (mirrors the existing DM-turn retry pattern); if still unseated → FAIL LOUD (COLD-OPEN SEATED NO PC, exit 1) rather than hand the player a silent no_actor/degraded session.

The guard was validated against the real g1 snapshot (unseated → fires) and the g1-b snapshot (PC Zevlor seated → passes through).

ISSUE 2 — Part-A poll window too short

The native #356 / cold-open mint poll in qa/ui_playtest_app.sh was 70 × 3s = 210s, shorter than a max-effort cold open (~280–400s; WORLDOS_COLDOPEN_TIMEOUT=400) → spurious FAIL on a slow-but-healthy mint. Raised to a 420s default via a new env-overridable knob WOS_APP_PART_A_DEADLINE; the poll loop now derives its iteration count from the deadline (no more hardcoded seq 1 70).

Tests / verification

  • Two static regressions in servers/engine/tests/test_adversarial_release.py lock both fixes.
  • bash -n (both scripts) + py_compile (guard python + test file) clean.
  • Existing static assertions (test_issue50, test_dm_session_remint, test_macos_app_static) re-confirmed green.

⚠️ A live re-run must confirm the cold open now reliably seats a PC — the guard/retry/fail-loud is verified against captured snapshots and statically, but the stochastic seating itself can only be proven by a fresh built-.app cold-open run. Do not merge until that live re-run + your review confirm reliable seating.

Scope: no gateway/wire-contract changes.

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of game session initialization by ensuring the player character is properly seated before gameplay begins, with automatic retry logic and clearer error handling if setup fails.
    • Enhanced the app testing process with configurable polling timeout settings.
  • Tests

    • Added regression tests to validate session setup reliability and app testing configurations.

…_actor reliability)

ISSUE 1 (no_actor): the .app drives scripts/play_party.sh, which pre-seeds
companions but leaves the human's PC to the DM's cold-open turn — DM-stochastic.
Forensic run g1 (veteran persona) minted can_act:true but the DM ended after
start_world without ever calling create_character(kind=player): party=[],
characters={NPCs only}, chat empty → viewer readiness=degraded /
failure_bucket=no_actor ("no active player actor is seated"), an unplayable
surface the persona can't escape. (A prior newbie run DID seat — stochastic miss.)
Fix: (a) strengthen the cold-open prompt to make seating the PC the FIRST,
mandatory tool action (create_character kind=player, add_to_party); (b) add a
snapshot-backed seating GUARD after the cold open that mirrors viewer/server.py
_action_actor (a party member whose record is kind=player) — if unseated, retry
the cold open ONCE on a fresh session with a hard seat directive, then FAIL LOUD
("COLD-OPEN SEATED NO PC", exit 1) rather than hand a silent no_actor session.

ISSUE 2 (Part-A poll too short): the #356 mint poll was 70 x 3s = 210s, shorter
than a max-effort cold open (~280-400s, WORLDOS_COLDOPEN_TIMEOUT=400) → spurious
FAIL on a slow-but-healthy mint. Raise to a 420s-default, env-overridable knob
(WOS_APP_PART_A_DEADLINE); the poll loop now derives its iteration count from it.

Tests: two static regressions in test_adversarial_release.py lock both fixes.
bash -n + py_compile clean; guard validated against the real g1 (unseated) and
g1-b (seated, PC Zevlor) snapshots.
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR strengthens the playtest setup reliability by making the mint-polling window deadline-driven (instead of fixed) and ensuring the player character is always seated during cold-open with automatic retry and explicit failure handling.

Changes

Playtest Session Initialization Reliability

Layer / File(s) Summary
Part A mint-poll deadline configuration and implementation
qa/ui_playtest_app.sh
Introduces PART_A_DEADLINE (default 420s, overridable via WOS_APP_PART_A_DEADLINE) to replace the prior fixed polling window. Poll iteration count is derived from PART_A_DEADLINE / 3 (minimum 1). Updated Part A log messages report the deadline and computed poll count.
Player PC seating requirement and guard
scripts/play_party.sh
DM cold-open prompt is updated to require seating the player character first. A new seating guard checks snapshot.json for a kind="player" member; if missing, the script mints a fresh DSID and retries cold-open with a seat-only directive. If retry still lacks a seated player PC, the script aborts with a non-silent error.
Regression tests for playtest reliability features
servers/engine/tests/test_adversarial_release.py
Two new pytest tests verify that play_party.sh implements player PC seating verification with guard-and-retry behavior, and that ui_playtest_app.sh Part A uses deadline-driven polling (420s default, environment-overridable) rather than hardcoded iterations.

Possibly Related PRs

  • electricsheephq/WorldOS#374: Directly modifies the same qa/ui_playtest_app.sh Part A harness that this PR updates from hardcoded polling to deadline-driven polling.
  • electricsheephq/WorldOS#329: Also strengthens player PC seating validation in the DM cold-open flow, adding corpse/dead restrictions and canon gating alongside this PR's seating guard and retry logic.

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A deadline polled with grace, no false alarm,
The player PC safely seated, snug and warm,
Retry once if needed, then speak the truth—
Playtest reliability blooms in its youth!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description comprehensively covers both issues with root cause analysis, fixes, and validation. However, the required CLA checklist section from the template is completely missing. Add the Licensing/CLA checklist section with appropriate checkboxes and declarations as specified in the repository template.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the two main fixes: guarding player-PC seating in cold-open and raising the Part-A poll window deadline, directly reflecting the changeset's core objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
servers/engine/tests/test_adversarial_release.py (1)

193-199: ⚡ Quick win

Tighten this regression around the actual guard branch.

These assertions only prove the file contains the substrings somewhere. The test can still pass if the loud-fail branch loses its exit 1, or if the seated-PC check drifts away from party membership while another get("kind") == "player" remains elsewhere.

Suggested fix
+import re
+
 def test_coldopen_play_party_guards_player_pc_seating():
@@
-    assert 'get("kind") == "player"' in text
+    assert re.search(
+        r"pc_seated\(\).*?party.*?get\(\"kind\"\)\s*==\s*\"player\"",
+        text,
+        re.S,
+    )
@@
-    assert "COLD-OPEN SEATED NO PC" in text and "exit 1" in text
+    assert re.search(r"COLD-OPEN SEATED NO PC.*?exit 1", text, re.S)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@servers/engine/tests/test_adversarial_release.py` around lines 193 - 199, The
current assertions only check for scattered substrings; instead locate the
actual guard branch in the generated text and assert the required tokens appear
together and in order. For example, find the contiguous guard block that
contains "pc_seated()", then assert within that block you see 'get("kind") ==
"player"', that the block contains "retrying the cold open ONCE" followed by
"COLD-OPEN SEATED NO PC", and that "exit 1" appears inside the same block (or
within a small proximity window). Use an indexed search or a regex that matches
the sequence ("pc_seated" ... 'get("kind") == "player"' ... "retrying the cold
open ONCE" ... "COLD-OPEN SEATED NO PC" ... "exit 1") to ensure the test fails
if the loud-fail branch or the seated-PC check drifts away.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@qa/ui_playtest_app.sh`:
- Line 78: PART_A_DEADLINE is taken directly from WOS_APP_PART_A_DEADLINE and
used in arithmetic to compute part_a_polls, which can become 0 if the env var is
non-numeric or non-positive; validate and sanitize the override by parsing it as
an integer (e.g., use a safe integer parse/regex) and ensure it is >0 before
using it, otherwise fall back to the default 420; then use the
validated/normalized PART_A_DEADLINE value in the existing part_a_polls
calculation and subsequent clamp so Part A cannot collapse to a single poll.

In `@servers/engine/tests/test_adversarial_release.py`:
- Around line 203-205: The module docstring contains ambiguous Unicode
punctuation (an en dash and a multiplication sign) causing RUF002; replace the
en dash in "280–400s" with an ASCII hyphen ("280-400s") and replace the
multiplication sign in "70 × 3s" with an ASCII "x" or plain asterisk (e.g., "70
x 3s" or "70 * 3s") so the docstring in test_adversarial_release.py uses only
ASCII punctuation.

---

Nitpick comments:
In `@servers/engine/tests/test_adversarial_release.py`:
- Around line 193-199: The current assertions only check for scattered
substrings; instead locate the actual guard branch in the generated text and
assert the required tokens appear together and in order. For example, find the
contiguous guard block that contains "pc_seated()", then assert within that
block you see 'get("kind") == "player"', that the block contains "retrying the
cold open ONCE" followed by "COLD-OPEN SEATED NO PC", and that "exit 1" appears
inside the same block (or within a small proximity window). Use an indexed
search or a regex that matches the sequence ("pc_seated" ... 'get("kind") ==
"player"' ... "retrying the cold open ONCE" ... "COLD-OPEN SEATED NO PC" ...
"exit 1") to ensure the test fails if the loud-fail branch or the seated-PC
check drifts away.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5cbaf806-5c04-4b1e-9493-28c59243d40b

📥 Commits

Reviewing files that changed from the base of the PR and between d7c36a1 and 013998f.

📒 Files selected for processing (3)
  • qa/ui_playtest_app.sh
  • scripts/play_party.sh
  • servers/engine/tests/test_adversarial_release.py

Comment thread qa/ui_playtest_app.sh
# the old 210s poll (70 × 3s) was SHORTER than a max-effort cold open, so a slow-but-healthy
# mint timed out as a spurious FAIL. Give the poll a 420s budget (just past the cold-open
# deadline), env-overridable for fast inner loops.
PART_A_DEADLINE="${WOS_APP_PART_A_DEADLINE:-420}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate WOS_APP_PART_A_DEADLINE before using it in arithmetic.

A non-numeric or non-positive override can collapse part_a_polls to 0, and the clamp then turns Part A into a single poll. That reintroduces the same spurious FAIL mode this change is fixing.

Suggested fix
 PART_A_DEADLINE="${WOS_APP_PART_A_DEADLINE:-420}"
+case "$PART_A_DEADLINE" in
+  ''|*[!0-9]*)
+    printf '[uipt-app] WOS_APP_PART_A_DEADLINE must be a positive integer (got %s)\n' "$PART_A_DEADLINE" >&2
+    exit 2
+    ;;
+esac
+[ "$PART_A_DEADLINE" -ge 1 ] || {
+  printf '[uipt-app] WOS_APP_PART_A_DEADLINE must be >= 1 (got %s)\n' "$PART_A_DEADLINE" >&2
+  exit 2
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PART_A_DEADLINE="${WOS_APP_PART_A_DEADLINE:-420}"
PART_A_DEADLINE="${WOS_APP_PART_A_DEADLINE:-420}"
case "$PART_A_DEADLINE" in
''|*[!0-9]*)
printf '[uipt-app] WOS_APP_PART_A_DEADLINE must be a positive integer (got %s)\n' "$PART_A_DEADLINE" >&2
exit 2
;;
esac
[ "$PART_A_DEADLINE" -ge 1 ] || {
printf '[uipt-app] WOS_APP_PART_A_DEADLINE must be >= 1 (got %s)\n' "$PART_A_DEADLINE" >&2
exit 2
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/ui_playtest_app.sh` at line 78, PART_A_DEADLINE is taken directly from
WOS_APP_PART_A_DEADLINE and used in arithmetic to compute part_a_polls, which
can become 0 if the env var is non-numeric or non-positive; validate and
sanitize the override by parsing it as an integer (e.g., use a safe integer
parse/regex) and ensure it is >0 before using it, otherwise fall back to the
default 420; then use the validated/normalized PART_A_DEADLINE value in the
existing part_a_polls calculation and subsequent clamp so Part A cannot collapse
to a single poll.

Comment on lines +203 to +205
"""Cold-open reliability: the Part-A (#356) mint poll must outlast the max-effort cold open
(~280–400s). The old 210s window (70 × 3s) was a spurious FAIL; the deadline is now a
420s-default, env-overridable knob."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace the ambiguous unicode punctuation in the docstring.

Ruff is already flagging the en dash and multiplication sign here (RUF002), so this adds avoidable lint noise.

Suggested fix
-    """Cold-open reliability: the Part-A (`#356`) mint poll must outlast the max-effort cold open
-    (~280–400s). The old 210s window (70 × 3s) was a spurious FAIL; the deadline is now a
+    """Cold-open reliability: the Part-A (`#356`) mint poll must outlast the max-effort cold open
+    (~280-400s). The old 210s window (70 x 3s) was a spurious FAIL; the deadline is now a
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""Cold-open reliability: the Part-A (#356) mint poll must outlast the max-effort cold open
(~280400s). The old 210s window (70 × 3s) was a spurious FAIL; the deadline is now a
420s-default, env-overridable knob."""
"""Cold-open reliability: the Part-A (`#356`) mint poll must outlast the max-effort cold open
(~280-400s). The old 210s window (70 x 3s) was a spurious FAIL; the deadline is now a
420s-default, env-overridable knob."""
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 204-204: Docstring contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)


[warning] 204-204: Docstring contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF002)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@servers/engine/tests/test_adversarial_release.py` around lines 203 - 205, The
module docstring contains ambiguous Unicode punctuation (an en dash and a
multiplication sign) causing RUF002; replace the en dash in "280–400s" with an
ASCII hyphen ("280-400s") and replace the multiplication sign in "70 × 3s" with
an ASCII "x" or plain asterisk (e.g., "70 x 3s" or "70 * 3s") so the docstring
in test_adversarial_release.py uses only ASCII punctuation.

@100yenadmin
100yenadmin merged commit 12207a9 into main Jun 2, 2026
15 checks passed
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.

1 participant