Whole-branch review — Claude Fable 5.1 (AGENTS.md §Review)
Read as one diff, origin/main...origin/fix/r1-native-version-contract (a37c032, cut from main bf59232), in a scratch worktree, against every store.Open caller and every path that consumes a version stamp.
VERDICT: APPROVE
Open change is safe for every caller. nextVersion is rebuilt from max(tab.content_version, every cell's InputVersion and CacheVersion) + 1, and content_version is advanced inside the same transaction as every stamp (setContent, setBlank, persistTouched, all via advanceContentVersion), so a re-opened handle never re-issues a stamp even when the stamped row was deleted by a clear. The Open stamp was never what kept versions monotonic. Recovery is untouched: stale is computed the same way, recoverStale with an empty list is a no-op (the loop never runs), and with a non-empty list the stamp is taken exactly as before. No caller can get a stale handle without recovery. Callers checked: openTab (cells, batch, format, size, comments), openTabFor (table, history), export, xlsx import (h.Set, its own stamps), and SetBatch's staged buildHandle inside the transaction, which before this branch quietly added a stamp to every batch (N+1) and now does not.
stamps on every write path — enumerated: /cells (SetReport, 1), /cells/batch (SetBatch, len(writes)), the table-view cell write and history undo/restore (applyRaw) both fan out through publishReport with rep.Stamps = 1. Format, size and GET carry none and report Handle.Version(), the next stamp. Stamp consumers with no response — import's h.Set, Open's crash-recovery stamp when something is stale, and a batch whose staged handle recovered stale cells (then Stamps undercounts by one) — all err on the side of a refetch, never a silent apply.
The Go test uses real responses and each part is load-bearing. TestNativeVersionContractAgainstRealResponses goes through postCell/postFormat/postSize and the real handlers. Flipped in a scratch copy: main's handle_open.go → 5 refetches with nothing missed (cell, batch, size, clear, batch); json:"-" on Stamps → both batches refetch; result.Stamps = 1 in SetBatch → both batches refetch. Baseline passes. make test green with the pinned rastrillo CLI (race step included). swift test: 16 Swift Testing + 4 XCTest pass, stampedBatchAppliesWithoutRefetch among them.
Web unaffected. Against a local server built from this branch: drive.mjs LIVE-ACROSS-TABS: YES, chrome.mjs all checks passed; same on a main build. clipboard.mjs: the same five failures on both (headless clipboard sentinel, pre-existing). One branch run also failed B3 shows 60 — got "" at the fixed 900 ms wait and passed on rerun; timing, not the contract (grid.js is byte-identical and only drops version < held).
Findings, none blocking:
- [nit, acknowledged in the discussion] A single write missed immediately after a stampless response (snapshot, format, size) is still undetectable, because those report the NEXT stamp and the rule allows one. The test's "one write missed" cases all start from a write's version, so this hole is not covered. Step (c) is the right follow-up and its own change; until then a one-line comment in the test naming the uncovered case would keep it honest.
- [nit]
handle_write_batch.go:95—result.Stamps = len(writes)is one short when the stagedbuildHandletook a recovery stamp. Safe direction (native refetches once), but the comment "one SetReport, so one stamp, per write" reads as exact. Suggest: "...plus, rarely, one recovery stamp the staged open took, which only makes a client refetch." - [nit]
SheetWire.swiftgainedstampsonGridResponseandSheetModel.acceptreads it;SheetLive.receivedstill callsrefresh()when a stream payload fails to decode. Since the new field is optional that path is unaffected. Nothing to do.
Merge when CI on a37c032 is green; fix/r1-native-version-gap (#35) should be closed unmerged as stated.