Conversation
… behind A rollback reverts the database and nothing else. Every cached stored value closes over the last value written through it, so the kernel went on reading the abandoned crank's terminated vats and GC actions, and the next `set` wrote them back. `maybeFreeKrefs` lives only in RAM, so a promise the rollback deleted stayed a collection candidate and the next crank's `collectGarbage` died reading it. Savepoints now carry a snapshot of the candidate set, restored rather than cleared: candidates added while no crank was open are still owed a collection and must survive an unrelated crank's rollback. Closes #1071 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…failed rollback Review follow-up. `clear()` left every cache pointing at rows it had deleted, so the next dequeue killed the run loop — the same defect one function over, now that there is something to call. A failed rollback discards the whole transaction, so RAM goes back to the outermost savepoint rather than the named one. The candidate set is restored first, being the one step of the revert that cannot fail, and `CACHED_VALUES` is keyed by the context's own cached fields so a value added to one and not the other does not compile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sirtimid
added this pull request to stack #1107
September 15, 2026 22:37
This was referenced Sep 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A crank rollback reverts the database and nothing else. Two kinds of state sit
above it and stayed as the abandoned crank left them.
Every cached stored value closes over the last value written through it, so
after a rollback the kernel went on reading the terminated vats and GC actions
the crank had recorded, and the next
setwrote them straight back to adatabase that no longer had them.
maybeFreeKrefslives only in RAM. A promise the rollback deleted stayed acollection candidate, so the next crank's
collectGarbageread a promise thatwas not there and killed the run loop over work that no longer existed.
revertStateBeneathRollbackhandles both. The candidate set is restored from asnapshot the savepoint carries, not cleared: only
collectGarbageempties it,so a candidate added while no crank was open — a peer restart abandoning every
export it held — is still owed a collection and has to survive an unrelated
crank's rollback.
The same staleness was reachable from
Kernel.clearStorage(), which emptied thedatabase and left every cache and the run queue head naming deleted rows. Now
that there is something to call, it calls it.
This is one PR in the split of #1021–#1023 into single-defect changes.
Changes
store/index.ts: oneCACHED_VALUEStable declaring every cached storedvalue, keyed by the context's own cached fields so a value added to one and
not the other does not compile, and
context.refreshCachedValues()over it.store/types.ts:savepointsbecomesSavepoint[], each carrying thecollection-candidate set as it stood when the savepoint was taken.
store/methods/crank.ts:revertStateBeneathRollbackon both rollback paths.A failed rollback discards the whole transaction, so RAM goes back to the
outermost savepoint rather than the named one; the candidate restore runs
first, being the one step that cannot itself fail.
store/index.ts:clear()andreset()share onediscardCachedState().@metamask/ocap-kernelchangelog entries under Fixed.Deliberately not here: reverting the caches when
releaseAllSavepointsfails(round-3 finding 8). That rests on a failed
RELEASEdiscarding thetransaction, which is a driver change landing separately; folding it in now
would assert behaviour
maindoes not have.Testing
store/methods/crank.rollback.test.tsis the regression guard and runs againstreal nodejs SQLite
:memory:— the map store'srollbackSavepointis a no-opand so cannot tell a reverted cache from a stale one. All three cases fail with
the production hunk reverted; removing only
refreshCachedValues()fails thetwo cache cases and leaves the candidate case green.
store/methods/crank.cross-crank-gc.test.tsis adopted from @grypez's #1039claim 4. These pass on
main— they are not a repro, they guard the newmechanism against over-correcting. Replacing the snapshot restore with a bare
clear()fails two of the three.store/methods/crank.test.tsgains three cases over a mocked database, for thepaths a real database will not produce on demand: which savepoint's snapshot is
restored, what happens when the rollback itself throws, and what the error says
when the revert then fails too.
store/index.test.tsgains one forclear(),which had no unit test at all; without the fix it dies with
No value found for key queue.run.1.Full
@metamask/ocap-kernelsuite green locally; eslint,constraintsandchangelog:validateclean.Closes #1071
🤖 Generated with Claude Code
Note
High Risk
Touches crank transaction boundaries, GC collection, and run-queue behavior—bugs here can kill or strand the kernel run loop after rollback or storage clear.
Overview
Crank rollback and storage clear now discard in-memory kernel-store caches that DB rollback alone cannot undo, preventing the run loop from acting on terminated vats, queued GC actions, or collection candidates for promises that were rolled back.
On
rollbackCrank, savepoints snapshotmaybeFreeKrefs;revertStateBeneathRollbackrestores that set (so peer-restart orphans still collect later), refreshes the run queue and length cache, andrefreshCachedValues()re-reads everyStoredValuecache from a singleCACHED_VALUEStable so closures cannot write abandoned state back. Failed DB rollbacks revert RAM to the outermost savepoint.clear()/reset()sharediscardCachedState()so an emptied database no longer leaves a stale run-queue head.Tests cover SQLite rollback regressions, cross-crank GC candidate survival, mocked rollback failure paths, and
clear()dequeue after wipe.Reviewed by Cursor Bugbot for commit 7da43da. Bugbot is set up for automated code reviews on this repo. Configure here.