Fix batch 8a-fix3: Dungeon play loop (PA.f98-PA.f109, PA.f111)

- PA.f98 (high): dispatch takes each subject by its full id, exactly as
  table_view prints it in brackets, never the short <handle>__ actor-tool
  prefix. parse_dispatch now matches on a new full_id_text (mirrors the
  library's own id_text, undocumented-but-identical, without handle_for's
  8-hex truncation) and returns unmatched ids separately; the dispatch call
  site folds them into the result as a named refusal ("`<text>` names no
  dispatchable subject") instead of dropping them. Actor tool descriptions
  now carry their own subject's label and full id (round_tools annotates the
  library's own actor_tools output in place, so table.rs stays untouched).
  dispatch's own tool description and PLAY_INSTRUCTIONS restated to match.
- PA.f104 (medium): dispatched is no longer snapshotted once before a
  round's own calls execute; an actor call now derives it fresh from the
  turn's own record at the point of resolution, so a same-round
  [dispatch(Mara), <mara>__speak, end_turn] resolves speak identically on a
  first pass and on resume.
- PA.f99 (medium, test gap): added a before-submit test hook
  (#[cfg(test)] only) fired right before each of the three submit doors
  (commit_authoring_run, execute_advance_time, execute_actor_call). It hands
  the hook a fresh PlayTurnStore::pull_current_from_disk_for_test() read —
  a fresh `pull_all` against the store's own already-owned redb Database,
  never the cached self.state and never a second store (CultCache's
  single-owner lock refuses a second OwnedRedbMessagePackBackingStore::new
  on the same path outright; discovered building this seam). Three new
  tests observe the durable body at the moment of submission.
- PA.f100 (medium, test gap): the same request key, reused only after
  eviction from the KeyLedger window, must mint two distinct fresh turn_ids.
- PA.f101 (medium, test gap): answers: None while a question is open is
  refused (StaleAnswer) and records nothing.
- PA.f102 (medium, test gap): split the old "exhausted beyond retries" test,
  which actually only ever drove RecoveryRequired, into an honestly-named
  RecoveryRequired test (asserts exactly one call) plus a new Retryable
  budget-exhaustion test (asserts exactly ROUND_BUDGET + 1 calls, pinned to
  a literal, not to the ROUND_RETRY_BUDGET symbol, so a budget mutation
  can't drag the expectation along with it). retry_delay's zero-under-test
  behavior is now its own small #[cfg(test)] bool (skip_retry_delay_in_test)
  rather than a #[cfg(test)] swap over the whole formula; the formula itself
  (backoff_delay) is a free function, always compiled, with its own unit
  test.
- PA.f103 (medium, test gap): added tests for a narrate failure right after
  end_turn and at the round budget, both closing with the fault recorded.
  The snapshot-failure site in execute_round is reported, not covered: no
  deterministic way to fail WorldMailbox::snapshot exists in this crate's
  test harness without tearing down the mailbox's own background actor
  independently of the fixture; documented in place.
- PA.f105 (low-medium): the resume path through execute_round now runs the
  same handle-collision check round_tools runs (it never did). Added a real
  test forcing a genuine collision between two real SubjectIds (spliced via
  serde_json, since SubjectId is #[serde(transparent)]) and calling
  round_tools directly — this was wrongly reported infeasible in the prior
  pass. The resume-path (execute_round) wiring itself has no live-collision
  test: forcing one needs the kernel's own snapshot to already carry one,
  and the kernel mints every SubjectId internally; documented alongside the
  precedent this codebase already uses for the same shape of gap
  (a_refused_player_act_sets_the_refusal_line).
- PA.f108 (low): empty or whitespace-only text with no turn open to
  continue (none at all, or Closed) is refused (PlayError::EmptyOpening)
  before a fresh turn opens, making zero inference calls.
- PA.f109 (low): run's outcome is now RunOutcome::Ran or
  Replayed { turn_id }; a replay reads ClosedTurnKeys::turn_id for the
  first time anywhere in production (via a new turn_id_for_key on the
  store and turn_id_for on the ledger).
- PA.f111: PLAY_INSTRUCTIONS' own doc comment rewritten off the "fresh
  table_view per round" framing into the recorded-opening rule (S6).

Every mutation named in the task spec (delete/move each of the three
PA.f99 persists, turn_id=hash(key), delete the round_tools collision call,
accept a None answer, accept an empty opening, make RecoveryRequired retry,
raise the retry budget, remove the backoff cap, turn each of the two
testable close-with-fault sites back into a bare `?`) was applied, run
against its matching test, confirmed to fail, and reverted.

Test counts: Dungeon bin 98 -> 113, lib 622 -> 622 (one test renamed and
reshaped, no net add), persona-projection 30 -> 32.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 19:25:58 +02:00
commit 754c7fe288

File diff suppressed because it is too large Load diff