4.3 KiB
4.3 KiB
T2.3 — Rewind as fork
| Field | Value |
|---|---|
| Phase | P2 — Durability hard parts |
| Size | L — over 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | — |
Goal
Rewind allocates a new BranchId and starts its LSNs at zero. Nothing is
deleted.
Facts (inlined — no spec read needed)
lsn 0 ─ 1 ─ 2 ─ 3 ─ 4 ─ 5 ─ 6(failed) branch 0, retained
└─ 0 ─ 1 ─ 2 ─ ... branch 1, forked at (0, 3)
- Same rule as attempt N+1 never mutating attempt N, for the same reason: the discarded branch is the evidence. Truncating it destroys the failure that motivated the rewind, which is what the learning loop exists to consume.
- Queries default to the live branch; grading may read all of them. Only the live branch is exported.
- Rewinding past a
Committedintent with a non-idempotent effect is a compensation problem, not a replay problem. The log records what happened; it cannot un-happen it. Flag rather than pretend. - A rewind is also one of the three mutators the verifier snapshot barrier
defends against (T4.2) — it can fork a new
BranchIdwhile verifiers hold a view of the old one.
Steps
- Allocate the next
BranchIdfor the run. Record a fork event carrying(parent_branch, parent_lsn)— the fork point is data, not inference. - Start the new branch's LSNs at 0. They are per branch already (T0.3), so this falls out of the key shape rather than needing a reset.
- Seed the new branch's state by folding the parent branch up to the fork LSN. Do not copy the parent's records into the new branch.
- Mark which branch is live. Default every query to it; expose an explicit all-branches mode for grading (T1.7).
- Restrict export to the live branch — check this where the outbox entry is built (T0.6), not at the relay.
- Before rewinding past a
Committedintent, look up the effect class. If it is not idempotent, emit a compensation-required flag on the fork event and surface it. Do not block the rewind and do not silently proceed.
Acceptance
- Rewind a 6-step run to step 3, run a different path.
- The original branch is fully readable.
- The live-branch query returns only the new path.
Verify
Harness: a 6-step run driven by the stub model, plus the raw log reader so branch 0 can be inspected directly rather than through the query surface.
Integration test — tests/it_rewind_fork.rs:
- Run 6 steps to a failure at step 6. Snapshot branch 0's serialized records.
- Rewind to step 3; run a different path on branch 1.
- Assert branch 0's records are byte-identical to the snapshot — nothing truncated, nothing rewritten.
- Assert branch 1's LSNs start at 0 and its fork event carries
(parent_branch: 0, parent_lsn: 3). - Assert the default query returns only branch 1's path; the all-branches query returns both.
- Assert branch 1 does not contain copies of branch 0's records — count records per branch and compare against expected.
- Export path: assert outbox entries exist only for branch 1.
- Compensation flag: rewind past a
Committednon-idempotent intent; assert the fork event carries the compensation-required flag and the rewind still proceeds. assert_refold_identicalon both branches.
Command: cargo test -p durability rewind
False pass:
- Step 3 checked through the query surface, which defaults to the live branch and will happily report "branch 0 unchanged" without reading it. Read the raw log.
- Step 6 omitted: an implementation that copies parent records into the child passes every other assertion here and makes every later "what happened" query ambiguous.
- Asserting the fork point by re-deriving it from LSNs rather than reading the recorded fork event.
Traps
- Truncating the log at the fork point "since that history is dead". It is the evidence.
- Copying parent records into the child branch. It duplicates history and makes every later "what actually happened" query ambiguous.
- Exporting all branches. Downstream consumers then see two contradictory timelines for one run.
Background (not required to do this task): rust-agentic-sys.md §8.5 · rust-agentic-task.md