Files
poimen/tasks/T4.2-verifierctx-and-the-snapshot-barrier.md
T

120 lines
5.2 KiB
Markdown

# 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<dyn BlobStore>,
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)