Files
poimen/tasks/T2.3-rewind-as-fork.md
T

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 Committed intent 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 BranchId while verifiers hold a view of the old one.

Steps

  1. Allocate the next BranchId for the run. Record a fork event carrying (parent_branch, parent_lsn) — the fork point is data, not inference.
  2. 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.
  3. 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.
  4. Mark which branch is live. Default every query to it; expose an explicit all-branches mode for grading (T1.7).
  5. Restrict export to the live branch — check this where the outbox entry is built (T0.6), not at the relay.
  6. Before rewinding past a Committed intent, 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 testtests/it_rewind_fork.rs:

  1. Run 6 steps to a failure at step 6. Snapshot branch 0's serialized records.
  2. Rewind to step 3; run a different path on branch 1.
  3. Assert branch 0's records are byte-identical to the snapshot — nothing truncated, nothing rewritten.
  4. Assert branch 1's LSNs start at 0 and its fork event carries (parent_branch: 0, parent_lsn: 3).
  5. Assert the default query returns only branch 1's path; the all-branches query returns both.
  6. Assert branch 1 does not contain copies of branch 0's records — count records per branch and compare against expected.
  7. Export path: assert outbox entries exist only for branch 1.
  8. Compensation flag: rewind past a Committed non-idempotent intent; assert the fork event carries the compensation-required flag and the rewind still proceeds.
  9. assert_refold_identical on 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