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

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. 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 testtests/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 §10.1, §10.2 · rust-agentic-task.md