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

105 lines
4.3 KiB
Markdown
Raw Normal View History

2026-08-17 23:05:20 -07:00
# 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 test**`tests/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](../../../rust-agentic-sys.md) §8.5 ·
[rust-agentic-task.md](../../../rust-agentic-task.md)