# T4.2 — `VerifierCtx` and the snapshot barrier | Field | Value | |---|---| | Phase | P4 — Verification | | Size | M — 1 to 3 days | | Status | Not started | | Flags | — | | Spec | inlined below | | Blocks | — | ## Goal Freeze the episode view on entry to `Verifying`, so state stops moving beneath a verifier that is mid-flight. ## Facts (inlined — no spec read needed) ```rust pub struct VerifierCtx { pub run: RunId, pub step: StepId, pub attempt: AttemptNo, /// Frozen on entry to `Verifying`. Never mutates while a verifier holds it. pub episode: EpisodeView, /// Lazy. Verifiers needing no text never pay for it. pub blobs: Arc, pub deadline: Instant, } ``` - A verifier seeing only identifiers can answer "did it work". Answering "did the agent have what it needed" requires the **context and the prompt** — so the view carries the context partition inline and prompt/output by reference. - **Include failed attempts.** "Retried three times because context was missing X" is the learning signal; shipping only the winning attempt discards it. - **What the barrier defends against is not a concurrent retry.** `Verifying` is entered only when every step is terminal, so no attempt can still be running. A test built around a concurrent retry passes against no barrier at all. The three real mutators arrive from **outside** the run's own execution: 1. a **rewind** forking a new `BranchId` while verifiers hold a view of the old one; 2. a **cancel**, which is legal from `Verifying`; 3. **recovery** resolving a `Dispatched` intent left by an earlier crash, which writes an outcome into an attempt a verifier is already reading. - Each is rare enough to be missed in testing and ordinary enough to happen in production. - This is why `Verifying` must be a real resting state and not a synchronous branch (T1.3). ## Steps 1. Define `VerifierCtx` as above. `EpisodeView` is an owned snapshot or an immutable `Arc` — never a handle that re-reads the store. 2. Build the snapshot at the transition into `Verifying`: capture the branch, the attempts (including failed ones), refs, partitions, usage, at a fixed LSN. 3. Record the snapshot LSN in the context. A verifier's answer is about a specific point in the log, and later analysis needs to know which. 4. Ensure the three mutators cannot reach into a held view: rewind writes to a new `BranchId` (T2.3), cancel changes run state not the snapshot, recovery appends new records above the snapshot LSN. 5. Pass `blobs` as a handle, not resolved bodies — laziness is T4.3. 6. Write one test per mutator, each firing while a verifier is deliberately parked mid-`verify`, asserting the view is unchanged. ## Acceptance - A verifier holding a context sees an unchanged view across each of the three mutators: a rewind forking a new `BranchId`, a cancel arriving during `Verifying`, and recovery resolving a `Dispatched` intent into an attempt the verifier is reading. - Explicitly **not** a concurrent-retry test — every step is terminal before `Verifying` is entered, so that test would pass against no barrier at all. ## Verify **Harness:** a verifier that parks on a channel mid-`verify`, so a mutator can be fired while it demonstrably holds the context. One test per mutator — they are different code paths and a single combined test proves the weakest. **Integration test** — `tests/it_snapshot_barrier.rs`: 1. Enter `Verifying`. Park the verifier. Snapshot `serialize(cx.episode)` from inside it. 2. **Mutator A — rewind:** fork a new `BranchId` from outside the run. Release the verifier. Assert its view is byte-identical to the snapshot and still points at the old branch. 3. **Mutator B — cancel:** cancel the run during `Verifying`. Assert the held view is unchanged and the verifier still completes. 4. **Mutator C — recovery:** have recovery resolve a `Dispatched` intent into an attempt the verifier is reading. Assert the held view is unchanged, and that the new outcome **is** visible to a freshly built view. 5. Assert `cx` records the snapshot LSN, and that a view built at that LSN by cold re-fold equals the held view. 6. Assert failed attempts are present in the snapshot — count them. **Command:** `cargo test -p verify snapshot_barrier -- --test-threads=1` **False pass:** - **A concurrent-retry test.** `Verifying` is entered only when every step is terminal, so no attempt can still be running — that test passes against no barrier at all. It is the obvious test to write and it verifies nothing. All three mutators must come from outside the run's own execution. - An `EpisodeView` that lazily queries state: the snapshot comparison passes if the mutator happens to touch a different key. Step 4 targets the same attempt the verifier is reading, on purpose. - Firing the mutator before the verifier has actually parked. Use a rendezvous channel, not a sleep. ## Traps - An `EpisodeView` that lazily queries materialized state. It looks frozen and is not. - Filtering failed attempts out of the view. --- Background (not required to do this task): [rust-agentic-sys.md](../../../rust-agentic-sys.md) §10.1, §10.2 · [rust-agentic-task.md](../../../rust-agentic-task.md)