Repairs the Cut 8a play loop against the cut map's own fix batch:
- PA.f61: `communicate` leaves PLAY_TOOLS (24 tools, matching table.rs's own
pinned list); `set_persona_material` is refused on an existing subject
and stands only for a subject this same authoring run declares, using
the new `WorldPatch::sets_persona_of_existing_subject`.
- Actor prefix: `execute_actor_call` passes the real handle prefix and the
full, unstripped call name to `decode_actor_call`, so the library's own
prefix strip is the only one; `round_tools`/`execute_actor_call` now
share one `actor_prefix` helper so generation and decode can never drift.
- PA.f54: `commit_authoring_run` decodes a run through the library's
`decode_authoring_calls`, commits the whole run as one shared patch under
one command id (derived from the run's own anchor slot), and a
`PatchRejected` refusal is described through the run's own `DecodedBatch`
site map so it names the exact failing call. `execute_authoring_call` is
now dead and removed.
- PA.f62/PA.f63: turn identity (`turn_id`) is separated from the request
key. `PlayTurn.applied_keys` records every key this turn has answered to;
a replayed key (open or closed) is a no-op making zero inference calls,
a fresh key on an open turn continues it, and a fresh key with no open
turn (none stored, or the stored one closed) opens a new turn under it.
- PA.f64: a dispatched Persona's own prose is the dispatch call's own tool
result (labelled by subject), not "{label} acted" — `rebuild_conversation`
was already generic over every call's recorded result, so this is the
whole fix.
- PA.f65: an empty/whitespace answer is refused before anything is
recorded; an inference exhausted beyond its retry budget closes the turn
with `PlayTurn.fault` recorded instead of leaving it `Running`.
- PA.f66: (a) the round budget closes the turn and narrates through the
same door `end_turn` uses; (b) a kernel `Invariant` fault closes the turn
with the fault recorded, set directly in `execute_round` at each of its
three poisoning sites; (c) `dispatched_subjects` was already
record-derived in call order on every path — pinned with a direct test,
no code change needed; (d) `dispatch_command_id`/`dispatch_command_id_for_handle`
replace `dispatch_slot`, deriving from the dispatch call's own slot and
the subject rather than the subject's position in that call's own JSON
array, so two subjects or two dispatch calls in one round can never
collide.
- PA.f69: strengthens `an_act_for_an_undispatched_subject_is_refused` (S2)
and `the_turn_closes_with_the_players_narration` to assert real outcomes;
adds a Persona-exact-quote-accepted test (S1); pins
`commit_authoring_run`'s record-before-submit ordering at the source
level (S4, no runtime seam exists to interrupt one synchronous call);
and adds a fresh-turn-after-close test proving no transcript crosses
turns, checked against the actual seen inference request (S7 / invariant
1). S6 was already covered by the existing restart test.
Dungeon bin: 63 -> 78 tests. Library: 620 -> 621 tests (the new
WorldPatch predicate's own unit test, previous commit). Every mutation in
the cut map's fix list was reverted/loosened against the final code and
confirmed to fail, then restored; see the session report for the full list.
Discrepancy not covered by this batch: `round_tools` did not append
HANDLE_SEPARATOR to the prefix it handed `actor_tools`, which would have
made every real (non-scripted) actor tool name undecodable against the
`<handle>__<kind>` convention this file's own instructions document. Fixed
alongside the actor-prefix cut as a necessary companion, since decode and
generation must share one prefix by construction.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>