Found by the 2026-06-03 VM 5-persona sweep on the built .app (build 0cc9856). The #1 cross-persona blocker holding G3-satisfaction at 5.6 (need ≥7) — hit by newbie, narrative, adversarial (3/5).
Symptom
Mid-session the Chronicle freezes (player's last action shows, no DM narration appears) and all action buttons disable with tooltip "viewing non-live campaign" / textbox "Read-only: viewing non-live campaign" — a permanent lockout with no recovery. The newbie waited 4+ min; narrative saw "Composing… completes but no new beat."
Root cause (evidence-backed)
The play surface gates can_act = live AND is_live_view, where is_live_view = live AND viewed_campaign_id == attached_campaign_id (viewer/server.py:5717, 6419). It latches False because the run accumulates multiple campaigns:
vm2-newbie-b had 4 campaigns: 1 stale (camp_a60121, 2026-06-02) + 3 today (camp_5db4e1 09:04, camp_4721ad 09:06, camp_29656 09:07).
play_party.sh mints a fresh campaign on every launch (scripts/play_party.sh:183, start_world), and part-A (.app RESUME→PLAY native bridge) + part-B (harness play_party.sh, qa/ui_playtest_app.sh:566) each start a play session → ≥2 campaigns; plus uncleaned stale leftovers.
- The viewer attaches by recency to the newest (camp_29656, day 2) while the DM's backend ran a different one (camp_4721ad, day 1) → divergence.
- The viewer's self-healing (
_live_play_view_campaign, viewer/server.py:6347-6389) deliberately refuses to follow a "parallel store" (>1 current+live campaign) to avoid misrouting writes → no recovery → permanent lockout.
This is the same can_act-wedge family as #35/#564 (single-flight)/#566 (retry-reuse).
Fix options (needs design — load-bearing state surface)
- Single-flight the campaign across part-A/part-B: part-B
play_party.sh REUSES the .app RESUME's already-live campaign instead of start_world-minting a fresh one (the cleanest — one campaign, no divergence).
- Clean the play-state campaigns dir at the start of each persona run (remove stale + cross-run) so only the cold-open's campaign exists.
- Viewer follows the move-sink's ACTUAL active campaign (the one the DM is writing beats to), not recency — disambiguating the parallel store by DM activity.
Recommend (1)+(2): prevent the multiple campaigns at the source; keep the viewer's conservative self-heal.
Acceptance
A fresh qa/ui_playtest_app.sh run produces exactly one play-store campaign; /session-surface reports is_live_view=true + can_act=true throughout; no "viewing non-live campaign" lockout; newbie/narrative/adversarial chronicles advance every beat. Verified on the NEXT built .app sweep (non-reproduction).
Found by the 2026-06-03 VM 5-persona sweep on the built .app (build 0cc9856). The #1 cross-persona blocker holding G3-satisfaction at 5.6 (need ≥7) — hit by newbie, narrative, adversarial (3/5).
Symptom
Mid-session the Chronicle freezes (player's last action shows, no DM narration appears) and all action buttons disable with tooltip "viewing non-live campaign" / textbox "Read-only: viewing non-live campaign" — a permanent lockout with no recovery. The newbie waited 4+ min; narrative saw "Composing… completes but no new beat."
Root cause (evidence-backed)
The play surface gates
can_act = live AND is_live_view, whereis_live_view = live AND viewed_campaign_id == attached_campaign_id(viewer/server.py:5717, 6419). It latches False because the run accumulates multiple campaigns:vm2-newbie-bhad 4 campaigns: 1 stale (camp_a60121, 2026-06-02) + 3 today (camp_5db4e1 09:04, camp_4721ad 09:06, camp_29656 09:07).play_party.shmints a fresh campaign on every launch (scripts/play_party.sh:183,start_world), and part-A (.app RESUME→PLAY native bridge) + part-B (harnessplay_party.sh, qa/ui_playtest_app.sh:566) each start a play session → ≥2 campaigns; plus uncleaned stale leftovers._live_play_view_campaign, viewer/server.py:6347-6389) deliberately refuses to follow a "parallel store" (>1 current+live campaign) to avoid misrouting writes → no recovery → permanent lockout.This is the same can_act-wedge family as #35/#564 (single-flight)/#566 (retry-reuse).
Fix options (needs design — load-bearing state surface)
play_party.shREUSES the .app RESUME's already-live campaign instead ofstart_world-minting a fresh one (the cleanest — one campaign, no divergence).Recommend (1)+(2): prevent the multiple campaigns at the source; keep the viewer's conservative self-heal.
Acceptance
A fresh
qa/ui_playtest_app.shrun produces exactly one play-store campaign;/session-surfacereportsis_live_view=true+can_act=truethroughout; no "viewing non-live campaign" lockout; newbie/narrative/adversarial chronicles advance every beat. Verified on the NEXT built.appsweep (non-reproduction).