5.2 KiB
5.2 KiB
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)
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.
Verifyingis 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:- a rewind forking a new
BranchIdwhile verifiers hold a view of the old one; - a cancel, which is legal from
Verifying; - recovery resolving a
Dispatchedintent left by an earlier crash, which writes an outcome into an attempt a verifier is already reading.
- a rewind forking a new
- Each is rare enough to be missed in testing and ordinary enough to happen in production.
- This is why
Verifyingmust be a real resting state and not a synchronous branch (T1.3).
Steps
- Define
VerifierCtxas above.EpisodeViewis an owned snapshot or an immutableArc— never a handle that re-reads the store. - Build the snapshot at the transition into
Verifying: capture the branch, the attempts (including failed ones), refs, partitions, usage, at a fixed LSN. - 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.
- 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. - Pass
blobsas a handle, not resolved bodies — laziness is T4.3. - 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 duringVerifying, and recovery resolving aDispatchedintent into an attempt the verifier is reading. - Explicitly not a concurrent-retry test — every step is terminal before
Verifyingis 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:
- Enter
Verifying. Park the verifier. Snapshotserialize(cx.episode)from inside it. - Mutator A — rewind: fork a new
BranchIdfrom outside the run. Release the verifier. Assert its view is byte-identical to the snapshot and still points at the old branch. - Mutator B — cancel: cancel the run during
Verifying. Assert the held view is unchanged and the verifier still completes. - Mutator C — recovery: have recovery resolve a
Dispatchedintent 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. - Assert
cxrecords the snapshot LSN, and that a view built at that LSN by cold re-fold equals the held view. - 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.
Verifyingis 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
EpisodeViewthat 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
EpisodeViewthat 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 §10.1, §10.2 · rust-agentic-task.md