Skip to content

feat(qa): idle-gap companion prefetch behind WORLDOS_COMPANION_PREFETCH (Wave-2 increment 3, default OFF)#1025

Open
100yenadmin wants to merge 1 commit into
mainfrom
feat/companion-prefetch
Open

feat(qa): idle-gap companion prefetch behind WORLDOS_COMPANION_PREFETCH (Wave-2 increment 3, default OFF)#1025
100yenadmin wants to merge 1 commit into
mainfrom
feat/companion-prefetch

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 18, 2026

Copy link
Copy Markdown
Member

Wave-2 increment 3 (from the first-principles decision record). The measured latency lever for our decode-bound beats (~57 tok/s) is to overlap the companion's ~35 s decode with the human's free think-time — the gap in scripts/play_party.sh where the wrapper waits for the human to post their browser move.

Mechanism (flag-gated, DEFAULT OFF)

The instant the DM's beat-K narration is recorded, speculatively decode the single companion's reaction to that narration into its moves file (via the existing actor_move/facade path). When the human's next move arrives, if the speculative move is still valid, reuse it (skip the fresh decode); else discard + re-decode (== today). Because the human reads the narration from the browser independently of the wrapper, their think-time overlaps the decode either way → per-beat wall-clock becomes max(think, decode) instead of think + decode, saving up to one full companion decode. The kick is blocking, never &-backgrounded → exactly one claude -p runs alone during the human gap (the DM turn is done; the human is a person) → no added concurrency, no OOM risk.

Safety

  • Flag-OFF is byte-identical to today (verified via git diff + a runtime trace): actor_move gains an optional 7th skip_decode arg (existing 6-arg callers unchanged); the companion_moves reuse branch's else is today's exact cm=…; AGENT_TURNS++; all helpers (_maybe_prefetch_companion/_prefetch_valid/_discard_prefetch) early-return when the flag is off; new globals are inert.
  • Invalidation guard (conservative — re-decode when unsure): skips prefetch during combat; discards if combat started, location changed, the human's move names the companion, or the speculative decode was empty. Worst case == today's latency, never worse.
  • Invariants intact: companion only PROPOSES structured moves through the read-only facade; DM stays sole narrator/resolver; engine stays sole writer. Only the timing of the existing single companion turn changes. Scoped to play_party.sh (N=1); run_party.sh untouched (its AI player would add concurrency).
  • bash -n + shellcheck clean (only pre-existing SC1091).

Verification status

Structural only (host just OOM'd; no heavy runs). The measured latency win + the companion-quality check are deferred: ship-dark (flag default OFF) until (a) the increment-1 Tier-1-vs-Tier-2 quality/cost A/B runs on the support VM, and (b) an interactive play_party session with the flag ON confirms the ~one-decode wall-clock drop with identical story score and clean budget accounting.

Summary by CodeRabbit

  • Refactor
    • Internal optimization of companion action handling and caching mechanisms to improve system efficiency.

Note: These changes are primarily internal improvements with no direct impact on user-facing gameplay features.

…CH (Wave-2 increment 3, default OFF)

Per-beat latency in scripts/play_party.sh is OUTPUT-DECODE-bound (~57 tok/s; a
single Sonnet companion turn is ~35s of decode), and today that decode only
STARTS after the human posts their move. But between the DM narrating beat-K and
the human posting, the wrapper is just WAITING in the idle `sleep 2` loop while
the human reads + decides — dead decode time.

This reclaims that free think-gap: the instant beat-K's narration is recorded
(after record_dm_reply + soft_tick), speculatively kick the single companion's
beat-(K+1) reaction — reacting to beat-K's NARRATION ONLY (it cannot see the
human's not-yet-posted move) — into its own moves file via the existing
`turn actor` facade path, OVERLAPPING the ~35s decode with the human's think-time.
When the human's move lands, if a valid prefetch is present we REUSE it (skip the
fresh decode → the ~35s was already spent during think-time); otherwise we decode
normally (== today's latency, the worst case).

Invalidation guard (correctness > speed; when unsure, RE-DECODE):
  - NON-COMBAT only — combat is initiative-sequential, so a companion can't
    pre-decide its turn before the human's; we never prefetch during/into combat.
  - SINGLE companion (N==1) only — no multi-companion ordering speculation.
  - Re-decode (discard) if, between the kick and the human's move: combat started,
    the current location changed, the human's move addresses the companion by name,
    or the speculative turn produced no moves. Discard truncates the stale
    speculative lines back out of the moves file so they are NEVER relayed.

Cost/turn accounting: the speculative turn runs through the SAME `turn actor`
path (stream → $COMBINED) and counts AGENT_TURNS exactly once at kick time —
whether later reused OR discarded (its decode was spent honestly either way); the
reuse path does not re-count. One writer at a time: the prefetch is a lone
`claude -p` issued in the think-gap when nothing else runs (no DM turn; the human
is a person) — never &-backgrounded, no added concurrency, no OOM risk.

Flag DEFAULT OFF and flag-OFF path byte-identical: all new logic is behind
`[ "${WORLDOS_COMPANION_PREFETCH:-0}" = 1 ]` guards (or inert new globals).
actor_move gains an optional 7th skip_decode arg (default empty → today's decode
for every 6-arg caller); companion_moves' new reuse branch is unreachable unless
the flag is set AND a valid prefetch exists. Verified: bash -n clean, shellcheck
clean (only the 2 pre-existing SC1091 notes), and a runtime trace shows the
flag-off companion_moves/actor_move path is byte-identical to origin/main
(same relayed block, same AGENT_TURNS, same single `turn actor` decode).

Out of scope (unchanged): qa/run_party.sh (its "player" is an AI agent, so a
prefetch there would add real concurrency). Engine remains sole writer; DM
remains sole narrator/resolver; the prefetch changes only WHEN the existing
single companion turn is issued, never the trust boundary.
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

scripts/play_party.sh gains a speculative idle-gap companion prefetch mechanism gated by WORLDOS_COMPANION_PREFETCH. During the human player's think-time, a companion reaction turn is pre-computed and stored; on the next human move, the beat loop either reuses or discards that result based on combat status, location, and whether the human addressed the companion directly.

Changes

Speculative Companion Prefetch

Layer / File(s) Summary
Prefetch subsystem: state, kick, validate, discard
scripts/play_party.sh
Adds global PREFETCH_* state and four new functions: _prefetch_situation reads combat/location snapshot, _maybe_prefetch_companion fires a speculative turn actor in idle time and saves cursor/location, _prefetch_valid decides reuse vs discard based on move production, combat/location stability, and companion-name targeting, and _discard_prefetch truncates the moves file to the saved cursor.
actor_move skip_decode + companion_moves reuse logic
scripts/play_party.sh
actor_move accepts an optional skip_decode seventh argument that harvests already-written move lines past the cursor without re-invoking the model. companion_moves sets skip_decode=1 when PREFETCH_REUSE matches the companion index and omits the AGENT_TURNS increment for replayed turns.
Beat loop: initial kick, per-beat validate/reset, next-beat kick
scripts/play_party.sh
Calls _maybe_prefetch_companion after cold-open narration for the first think-gap. Each beat validates the pending prefetch before companion_moves, resets PREFETCH_REUSE after the DM resolves the beat, and kicks the next speculative prefetch after worldos_soft_tick.

Sequence Diagram

sequenceDiagram
  participant Human
  participant BeatLoop
  participant _maybe_prefetch_companion
  participant _prefetch_valid
  participant actor_move

  rect rgba(100, 149, 237, 0.5)
    note over BeatLoop,_maybe_prefetch_companion: Cold-open / beat end
    BeatLoop->>_maybe_prefetch_companion: kick speculative turn (non-combat, single companion)
    _maybe_prefetch_companion->>actor_move: turn actor (background, saves cursor+location)
    actor_move-->>_maybe_prefetch_companion: moves written to moves file
  end

  Human->>BeatLoop: submit move (pmsg)

  rect rgba(144, 238, 144, 0.5)
    note over BeatLoop,actor_move: Validate and consume or discard
    BeatLoop->>_prefetch_valid: pmsg
    alt valid (no combat/location change, companion not addressed)
      _prefetch_valid-->>BeatLoop: PREFETCH_REUSE set
      BeatLoop->>actor_move: skip_decode=1 (harvest prefetched moves)
    else invalid
      _prefetch_valid->>BeatLoop: discard (truncate moves file)
      BeatLoop->>actor_move: full decode
    end
  end

  BeatLoop->>BeatLoop: reset PREFETCH_REUSE after DM beat
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hop, hop — while you think, I act ahead,
Speculative ears perked, moves pre-read.
If the world shifts, I toss my guess away,
But if all is calm, my moves hold sway!
A rabbit's trick: prefetch the next refrain,
And save the idle gaps from going in vain.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description comprehensively covers mechanism, safety guarantees, and verification status, but omits the required CLA and validation checkboxes from the repository template. Complete the Licensing/CLA section with required checkboxes and add a Validation section listing checks run (e.g., bash -n, shellcheck results).
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature added: idle-gap companion prefetch mechanism, the controlling flag, and the release context (Wave-2 increment 3) with default state.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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

🤖 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 `@scripts/play_party.sh`:
- Around line 577-581: The speculative pre-move turn on line 577 uses the
canonical companion session (COMP_SIDS[0]), but the discard mechanism
(_discard_prefetch) only truncates the moves file without fully resetting the
session state, leaving stale speculative prompts in the transcript. Replace the
use of the canonical session (COMP_SIDS[0]) with a separate disposable or
promotable actor session for the prefetch turn, ensuring that
invalidation/discarding can cleanly reset state without affecting the canonical
transcript. This change should also be applied to the other speculative turn
calls mentioned in the comment (around lines 625-640).
- Around line 608-616: The prefetch invalidation logic has two issues: the
location check uses a condition that skips validation when PREFETCH_LOC is empty
(should fail closed instead), and the grep command treats the companion name as
a regex pattern which can cause false matches if the name contains
metacharacters. Fix the first condition around the location check to remove the
`[ -n "$PREFETCH_LOC" ]` guard so the check happens even when location is
unknown and invalidates the prefetch. Replace the `grep -qi --
"${COMP_NAMES[0]}"` call with `grep -qFi -- "${COMP_NAMES[0]}"` to use
fixed-string matching (the -F flag) instead of regex pattern matching.
🪄 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: fc298782-2b77-4c41-9889-e44e902e1674

📥 Commits

Reviewing files that changed from the base of the PR and between b81dd8a and 405fd9c.

📒 Files selected for processing (1)
  • scripts/play_party.sh

Comment thread scripts/play_party.sh
Comment on lines +577 to +581
turn actor "${COMP_SIDS[0]}" 0 "The DM says:

$dm_says

This is a SPECULATIVE pre-move: the human player has not acted yet. React to the scene as it stands — take your next action(s) using your tools (say / do / request_check / cast_spell / use_item / attack; look or my_sheet first if useful). Tools only, no narration." "${COMP_CFGS[0]}" >/dev/null

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 | 🟠 Major | 🏗️ Heavy lift

Isolate speculative actor sessions before discard can be trusted.

Line 577 resumes the canonical companion session for the speculative turn (first=0 maps to --resume "$sid" in turn()), but _discard_prefetch only truncates the moves file. After any invalidation, the “fresh” decode still resumes a transcript that already contains the stale speculative prompt/tool turn, so discard is not equivalent to the non-prefetch path. Use a disposable/promotable actor session for prefetch, or add explicit rollback/commit semantics before enabling invalidation.

Also applies to: 625-640

🤖 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 `@scripts/play_party.sh` around lines 577 - 581, The speculative pre-move turn
on line 577 uses the canonical companion session (COMP_SIDS[0]), but the discard
mechanism (_discard_prefetch) only truncates the moves file without fully
resetting the session state, leaving stale speculative prompts in the
transcript. Replace the use of the canonical session (COMP_SIDS[0]) with a
separate disposable or promotable actor session for the prefetch turn, ensuring
that invalidation/discarding can cleanly reset state without affecting the
canonical transcript. This change should also be applied to the other
speculative turn calls mentioned in the comment (around lines 625-640).

Comment thread scripts/play_party.sh
Comment on lines +608 to +616
if [ -n "$PREFETCH_LOC" ] && [ "$loc" != "$PREFETCH_LOC" ]; then
echo "[play-party] prefetch: INVALID — location changed ($PREFETCH_LOC → $loc) since the prefetch; re-decoding." >&2
return 1
fi
# The human's move directly addresses/targets THIS companion → the speculative reaction (to narration,
# not to the human) is likely stale. Case-insensitive whole-name substring match on the companion's name.
if printf '%s' "$pmsg" | grep -qi -- "${COMP_NAMES[0]}"; then
echo "[play-party] prefetch: INVALID — your move addresses ${COMP_NAMES[0]} directly; re-decoding so it reacts to you." >&2
return 1

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

Make prefetch invalidation fail closed and match names literally.

The location check skips invalidation when the kick-time location is empty, and grep -qi treats the companion name as a regex. That can reuse a stale prefetch when location is unknown or when a name contains regex metacharacters. Prefer failing closed on missing locations and using fixed-string matching for names.

Proposed fix
-  if [ -n "$PREFETCH_LOC" ] && [ "$loc" != "$PREFETCH_LOC" ]; then
+  if [ -z "${PREFETCH_LOC:-}" ] || [ -z "${loc:-}" ] || [ "$loc" != "$PREFETCH_LOC" ]; then
     echo "[play-party] prefetch: INVALID — location changed ($PREFETCH_LOC → $loc) since the prefetch; re-decoding." >&2
     return 1
   fi
@@
-  if printf '%s' "$pmsg" | grep -qi -- "${COMP_NAMES[0]}"; then
+  if [ -n "${COMP_NAMES[0]:-}" ] && printf '%s' "$pmsg" | grep -Fqi -- "${COMP_NAMES[0]}"; then
     echo "[play-party] prefetch: INVALID — your move addresses ${COMP_NAMES[0]} directly; re-decoding so it reacts to you." >&2
     return 1
   fi
📝 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
if [ -n "$PREFETCH_LOC" ] && [ "$loc" != "$PREFETCH_LOC" ]; then
echo "[play-party] prefetch: INVALID — location changed ($PREFETCH_LOC$loc) since the prefetch; re-decoding." >&2
return 1
fi
# The human's move directly addresses/targets THIS companion → the speculative reaction (to narration,
# not to the human) is likely stale. Case-insensitive whole-name substring match on the companion's name.
if printf '%s' "$pmsg" | grep -qi -- "${COMP_NAMES[0]}"; then
echo "[play-party] prefetch: INVALID — your move addresses ${COMP_NAMES[0]} directly; re-decoding so it reacts to you." >&2
return 1
if [ -z "${PREFETCH_LOC:-}" ] || [ -z "${loc:-}" ] || [ "$loc" != "$PREFETCH_LOC" ]; then
echo "[play-party] prefetch: INVALID — location changed ($PREFETCH_LOC$loc) since the prefetch; re-decoding." >&2
return 1
fi
# The human's move directly addresses/targets THIS companion → the speculative reaction (to narration,
# not to the human) is likely stale. Case-insensitive whole-name substring match on the companion's name.
if [ -n "${COMP_NAMES[0]:-}" ] && printf '%s' "$pmsg" | grep -Fqi -- "${COMP_NAMES[0]}"; then
echo "[play-party] prefetch: INVALID — your move addresses ${COMP_NAMES[0]} directly; re-decoding so it reacts to you." >&2
return 1
🤖 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 `@scripts/play_party.sh` around lines 608 - 616, The prefetch invalidation
logic has two issues: the location check uses a condition that skips validation
when PREFETCH_LOC is empty (should fail closed instead), and the grep command
treats the companion name as a regex pattern which can cause false matches if
the name contains metacharacters. Fix the first condition around the location
check to remove the `[ -n "$PREFETCH_LOC" ]` guard so the check happens even
when location is unknown and invalidates the prefetch. Replace the `grep -qi --
"${COMP_NAMES[0]}"` call with `grep -qFi -- "${COMP_NAMES[0]}"` to use
fixed-string matching (the -F flag) instead of regex pattern matching.

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