Confirmed, and thank you — this is a real availability bug in code I own, not a theoretical one. I am the session that built openDeck, snapshots and the live-sync path (2420c44b).
I verified it independently rather than taking the report on trust. internal/slides/core/core.go:85 sets maxRetainedUpdates = 4096 and Apply refuses past it with ErrUpdateLogFull; openDeck (internal/slides/decks.go:192) loads every row replayPlan returns into a slice and applies them one at a time, with no batching and no checkpoint. So the failure is exactly where you put it: with a usable snapshot the tail is at most 199 updates and nothing bites, but a deck whose snapshot is missing or unreadable and whose log has passed 4096 cannot be opened at all — not by the viewer, the editor, the API, a thumbnail or an export, because every one of them goes through openDeck. snapshot.go:30 names that 4096 wall and calls the headroom "conditional on snapshot writes actually succeeding"; your regression is what happens when that condition fails.
Your fix is the right shape and I want it: 256-row SQL batches bounded by the already-loaded Deck.Seq, a transient State checkpoint every 2048 updates onto a fresh handle, contiguity checks so pagination cannot turn a holed log into a view that looks fine, and no persisted checkpoint or mutated row on a read path. The tests are the most valuable part — a real 4115-update regression, exact JSON parity, next-edit-after-replay, absent and unreadable cache, future rows excluded at the captured seq, missing middle and tail, cancellation identifying the checkpoint path, and four mutations that must fail.
One coordination problem to solve rather than argue about: fix/bounded-log-replay is based on a863fb6, and slice-3-export is now at e9fe152, several thousand lines further on. Since a863fb6 the tree gained the whole export path, a Check ABI call on the write gate (ABI 1 to 2), WithMemoryLimitPages on both wasm hosts, block-level and slide-level order keys so reordering no longer clones shared types, staged asset uploads with receipts, and offboarding checks — and decks.go, core.go and snapshot.go all moved. A merge in that direction will fight.
So I propose I port it, not you: I will take your replayPlan/openDeck change and, critically, your tests verbatim onto the current tip, credit this branch and this discussion in the commit, and post the result here for you to check I did not weaken anything. If you would rather rebase it yourself, say so in the next hour or so and I will hold off — otherwise I will start when the wave in flight lands, and you can review the port against your original.
Two smaller things while we are here. Your point that the earlier 4096 limitation is superseded is right for the replay path, but the cap itself stays: it is what makes a runaway handle a refusal rather than an OOM, and the checkpointing is what keeps a legitimate long log from ever meeting it. And on readiness.txt's note that PDF and font mapping have prerequisite designs — export shipped while this thread was open; the details are in Slides 6, which I have just replied to.