Conversation
92c237b to
903d711
Compare
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
A vat's death is a set of store writes. Made from outside the run loop, they landed in whichever crank happened to be open: the run loop starts its next crank in the same turn it ends the last, so a caller that waited for a crank resumed inside the next one, and an ordinary rollback there undid a death the caller had already been told about. Control-plane `terminateVat` now queues a `terminateVat` run queue item and waits on the same per-vat RAM waiter a restart uses; the run loop carries it out, so the writes belong to the crank that made them. A restart still queued for the vat is superseded and its caller told. A teardown that fails settles the caller rather than throwing, since the run loop's catch would roll back whatever of the death did get written. `terminateSubcluster` walks persisted membership, so it now retires a member the kernel has no handle for, and keeps going when one member will not die rather than stranding the rest. `terminateAllVats` deliberately keeps writing directly: it is part of tearing the kernel down, and `reset` has to work on a kernel whose run loop has died. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ning `terminateSubcluster` depends on the run loop now, and `#terminateVatQuietly` absorbed the refusal, so on a dead loop it deleted the subcluster record, terminated nothing and reported success. `deleteSubcluster` drops every member's vat-to-subcluster mapping and `getVatSubcluster` is a `Fail`, so that broke `getStatus` for the whole kernel from then on. It now refuses at the door, tries every member, and keeps the record if any survived. `performVatTermination` no longer calls `stopVat` for a vat that is already gone: the in-crank termination path or `terminateAllVats` can get there first, and rejecting the operator's request for a vat that died exactly as asked is the wrong answer. Two comments the previous commit falsified, and a claim it overstated: a restart asked for after a termination is queued cannot be ordered against it, so only one outstanding when the termination is requested is superseded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ise test Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mber survives A failed launch's cleanup deleted the subcluster record even when a member would not die, which with termination now queued is every member once the run loop is dead. `getVatSubcluster` then fails for the survivor and `getStatus` breaks for the whole kernel. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s vat Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rries it out A termination request made while a crank is open is now held until that crank ends, as a restart request is, so an abort cannot roll it back under a waiting caller. The crank that carries it out reports itself irrevocable, so a later failure in collection or the audit cannot undo a death the caller has been told of, and a run loop that then dies no longer tells that caller the work was never done. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Carries the restart fix onto the waiter helpers this branch shares between restart and termination. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…m name terminateSubcluster dropped the system mapping before ending any member and destroyed IO channels before checking for survivors, so a member that would not die kept running without its channels, and its kept record lost the name a later boot looks it up by. The channels now go only once every member is gone, and the name once the bootstrap vat is: a kept record whose bootstrap vat died would fail the next boot's restore. The mapping is dropped after the crank ends. A failed launch's cleanup keeps the channels on the same condition. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
884fe9e to
012e7ed
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 012e7ed. Configure here.
| supersedeRestart(new VatDeletedError(vatId)); | ||
| await this.#awaitQueuedWork(this.#terminationWaiters, vatId, () => | ||
| this.#kernelQueue.enqueueTerminateVat(vatId, reason), | ||
| ); |
There was a problem hiding this comment.
Restart superseded if terminate enqueue fails
Medium Severity
terminateVat takes outstanding restart waiters and settles them with VatDeletedError before enqueueTerminateVat runs. If that enqueue throws — a dead run loop, or any later store failure — those restart callers are told the vat is gone and their queued restart is dropped as stale, while the vat is still running and was never queued for death. #awaitQueuedWork already enqueues before registering a waiter so a refusal stays that call's own error; the supersede does not follow the same order.
Reviewed by Cursor Bugbot for commit 012e7ed. Configure here.


Second PR of the "only the run loop writes the store" work, stacked on #1096. Read this diff as
git diff sirtimid/restart-vat-run-queue-item...sirtimid/terminate-vat-through-run-loop.A vat's death is a set of store writes. Made from outside the run loop they land in whichever crank happens to be open: the loop starts its next crank in the same turn it ends the last, so a caller that waited for a crank resumed inside the next one, and an ordinary rollback there undid a death the caller had already been told about.
Control-plane
terminateVatnow queues aterminateVatrun queue item and waits on the same per-vat RAM waiter a restart uses — the queue-and-wait shape from #1096, factored out so both share it. The run loop carries the request out, so#retireVat's writes belong to the crank that made them. A teardown that fails settles the caller rather than throwing, since the run loop's catch would roll back whatever of the death did get written and restore the request.Unlike a restart, a termination with no waiter left is still carried out: it is an instruction rather than a request, and a vat the next boot has already relaunched is exactly the one it names. A vat something else killed first — the in-crank
crankResult.terminatepath, orterminateAllVats— resolves its caller rather than reporting a failure for a vat that died as asked.terminateAllVatsdeliberately keeps writing directly. It is part of tearing the kernel down, andresethas to work on a kernel whose run loop has died, which a queued request never could be. The narrow "run loop is not running, so direct writes are legal" mode that makes that safe is the last of the control-plane moves, not this one.terminateSubclusterwalks persisted membership, so it now retires a member the kernel has no handle for rather than skipping it and stranding the rest — the half of @grypez's #1030 that #1093 did not reach. It tries every member and, if any survived, keeps the subcluster record and says so:deleteSubclusterdrops every member's vat-to-subcluster mapping, andgetVatSubclusteris aFail, so deleting the record over a live member would breakgetStatusfor the whole kernel from then on. It also refuses outright on a dead run loop, aslaunchSubclusteralready does. A failedlaunchSubclusterkeeps the record on the same terms: its cleanup terminates each member it launched, and with termination queued, a run loop that died mid-launch leaves every one of them running.Two limits worth stating. A restart asked for after a termination is queued cannot be ordered against it — waiters are pooled per vat, not bound to their item — so only one outstanding when the termination is requested is superseded; the other direction, a termination undone by a later restart, cannot happen. And
resetcan still destroy a queued item and strand its waiter; that belongs to the PR that stops the run loop before those writes.Synchronous is still not atomic: a
stopVatthat fails part-way commits what it wrote. Now that the work happens inside a crank, a nested savepoint could roll just that back — the first time that has been possible — but the crank's savepoint structure is being reworked in a parallel PR, so it is left for afterwards.Changes
terminateVatrun queue item, dispatched throughKernelRouterto a newVatManager.performVatTerminationVatManager.terminateVatqueues one and waits; the queue-and-wait and settle-the-waiters shapes are shared with restartterminateSubclusterrefuses on a dead run loop, retires a member with no handle, and keeps the record if a member surviveslaunchSubcluster's failure cleanup keeps the record if a member survivesKernelQueuegainsenqueueTerminateVatTesting
VatManager.test.tscovers the queued path, the superseded restart, a teardown failure settling rather than throwing, a vat something else already killed, a request nobody is waiting for, and thatterminateAllVatsdoes not queue.SubclusterManager.test.tscovers the no-handle member, the surviving member (record kept, error raised), the dead run loop, a failed launch whose cleanup leaves a member running, and that cleanup waiting for the crank before it deletes the record.Kernel.test.ts's mockedKernelQueuenow captures thedeliverfunction the kernel hands torun(), soenqueueRestartVat/enqueueTerminateVatdispatch the item through the realKernelRouteron a later turn. That is what lets its existing end-to-end terminate andterminateSubclustercases keep working through the new route, and it restores therestarts a vatcase #1096 had to delete.Twelve mutations checked, each failing only its own cases:
terminateVatwriting directly,performVatTerminationrethrowing, the already-dead guard removed,terminateAllVatsrouted through the queue, the queued restart not superseded,terminateSubclusterskipping a no-handle member, propagating one member's failure, deleting the record over a survivor in eitherterminateSubclusteror a failed launch's cleanup, that cleanup deleting the record without waiting for the crank to end, not checking the run loop, andenqueueTerminateVatalways settingreason(whichtoHaveBeenCalledWithcannot see, since it ignoresundefined-valued keys — the item'sreasonisexactOptional).Full
@metamask/ocap-kernelsuite green after the rebase onto the current #1096. Before it, kernel-testvat-lifecycle,subclusters,cluster-launchandresumewere green against a freshdist. The full kernel-test suite aborts in a native better-sqlite3 teardown in this environment, which reproduces unchanged onorigin/main.Carries draft findings A2, A5 and A13.
🤖 Generated with Claude Code
Note
High Risk
Changes core vat lifecycle and crank transaction semantics (irrevocable terminations, subcluster teardown); incorrect rollback or waiter handling could leave inconsistent store state or stranded subclusters.
Overview
Control-plane
terminateVatnow queues aterminateVatrun queue item (mirroringrestartVat) so vat teardown store writes run inside the crank that performs them, not in whatever crank happens to be open when the control plane calls—avoiding rollbacks that undo a death the caller was already told succeeded.VatManagerenqueues viaenqueueTerminateVat, waits with shared queue-and-wait helpers, andperformVatTerminationruns on the run loop; teardown failures settle callers instead of rejecting the crank, and successful terminations returnCrankResult.irrevocableso later crank failures do not roll back the death. Pending restart requests for the same vat are superseded when termination is requested.terminateAllVatsstill callsstopVatdirectly (no queue) so kernel teardown works when the run loop is dead.terminateSubclusterand failedlaunchSubclustercleanup are tightened: refuse a dead run loop, retire persisted members without a live handle, try every member, and keep the subcluster record (and IO/name where appropriate) if any vat survives termination—deleting the record over live members would break kernel status.Tests extend
KernelQueue,KernelRouter,VatManager,SubclusterManager, andKernel.testmocks so queued terminate/restart items dispatch through the router like a real crank.Reviewed by Cursor Bugbot for commit 012e7ed. Bugbot is set up for automated code reviews on this repo. Configure here.