Your plugin system couldn't replace plugins. So here's the transaction that you're missing. A developer has released @moult/runtime 0.1.1, a plugin runtime that treats plugin replacement as a transactional protocol rather than a simple assignment, so a failed upgrade leaves the previous version active and usable. The project ships with 145 tests run under both Node and a DOM environment (290 runs) and a comparison harness showing it survives a failed upgrade with zero leaked resources, unlike a naive registry. The developer notes the safety costs roughly 0.16ms per replace in the benchmark environment, calling it environment-specific evidence rather than a performance promise. What if an upgrade fails halfway through activating? The question would be: what is still running? In most plugin systems, the answer is: none. And in registries, the common bug could be as follows: // the old plugin is already gone before the new one exists registry.set 'storage', await next.setup ; If setup throws, the old working value is destroyed, and the new one never arrives. Now, your host is missing a capability it had a millisecond ago. In a daemon, that's an outage, not an error. This is the failure mode that matters most for systems which can't restart: long-running CLI daemons, editors, dev tools, and the new wave of AI-agent extension hosts, where shipping an upgrade should be routine, not a maintenance window. Moult treats it like one. A plugin is a versioned capability provider with an owned resource scope, and replacing it runs a protocol, not an assignment: If anything fails before the commit, the candidate scope is disposed, and the failure stays invisible: the previous generation remains active and usable. Like a crab, the system only sheds its shell once the new one is ready. js import { capability, createRuntime } from '@moult/runtime'; const storage = capability<{ get k: string : string | undefined } 'storage', '1.0.0' ; const runtime = createRuntime ; runtime.install { id: 'memory.storage', version: '1.0.0', provides: { capability: storage } , setup: ctx = { const map = new Map 'k', 'v1' ; ctx.provide storage, { get: k = map.get k } ; }, } ; await runtime.start 'memory.storage' ; // A broken replacement is rejected — and v1 keeps serving: await runtime.replace { id: 'memory.storage', version: '2.0.0', provides: { capability: storage } , setup: = { throw new Error 'bug in the new version' ; }, } .catch e = console.log e.code ; // REPLACEMENT FAILED // Usable, not just active: a consumer installed after the failure // still binds the old generation's value. let seen: string | undefined; runtime.install { id: 'reader', version: '1.0.0', requires: { capability: storage, range: '^1.0.0' } , setup: ctx = { seen = ctx.require storage .get 'k' ; }, } ; await runtime.start 'reader' ; console.log seen ; // 'v1' — the failed v2 never existed to readers That is the entire guarantee, runnable: the rejection is a structured MoltError , and a consumer installed after the failure still reads v1's value usable, not just active. There's a second half to "versioned capability provider" that the snippet hides: capabilities carry a semver version, consumers declare semver ranges, and each token declares whether it accepts exactly one provider or aggregates many multiple: true . Resolution runs during preparation; a candidate whose requirements don't resolve fails before setup, so a version mismatch can never become visible either. It's what's enforced. Nine replacement transaction tests cover failed setup, failed validation, disposal ordering, and resource cleanup, on top of property-based and stress suites: 145 tests, each run under both Node and a DOM environment — 290 runs, all green. The 15 invariants are written down in docs/guarantees.md , and the repo's comparison harness runs the same failed-upgrade scenario against a naive registry, cordis, and Moult, publishing the raw numbers in demo/comparison/RESULTS.md : | Runner | Survives | Leaked | |---|---|---| | naive registry | no | 102 | | cordis 4.0.0-rc.9 | no | 0 | | @moult/runtime 0.1.1 | yes | 0 | Excerpt — the full table adds the blocked-stop diagnostic and the average-ms column. Their caveat, which I'll repeat: environment-specific evidence, not a performance promise. That safety isn't free: the whole benchmark scenario — install, one failed replace, and one hundred successful replaces — averages ~16ms against ~0.14ms for the naive registry roughly ~0.16ms per replace in that environment, not ~16ms; scenario average, not per-call . You pay it per replace, never per capability read. A note on the neighbours: HMR reloads modules, and Module Federation shares them. Both are code-delivery mechanisms — neither promises that an upgrade either fully applies or fully rolls back. Moult owns a different layer: the replacement transaction itself. And if you use Vite, @moult/vite routes module updates through the same replace path, so import/setup failures preserve the old generation and the bridge doesn't commit anything itself; the runtime's transaction does. The honest limits, because a runtime that hides its limits can't be trusted with your uptime: Moult is not a sandbox — plugins are trusted code, and it governs lifecycle and capability visibility, not permissions. It's not a loader or bundler; there's no global registry. And v1 refuses to silently rebind dependents — replacing a provider with active dependents is rejected outright with a structured REPLACEMENT FAILED error that names the dependent path, instead of quietly re-wiring them. Moult preserves service, not state: every generation gets a fresh scope, so in-memory handles don't migrate — React component state is explicitly not promised to survive replacement, and durable state belongs in a host-provided capability. The success path is just as strict: replaced fires before the old scope is disposed, old resources release in LIFO order, and if the old generation's disposal fails after commit, the failure is recorded and inspectable — the replacement stands; there is no rollback. Around the runtime there's a small family: @moult/events generation-scoped typed events , @moult/react bindings for committed contributions , @moult/test utilities for proving ownership, replacement, and leak behaviour , and @moult/vite Vite HMR bindings that route module updates through the same transactional lifecycle . npm install @moult/runtime Node 22 or newer Repo: github.com/neryva-lab/moult https://github.com/neryva-lab/moult — if you have an upgrade scenario your system can't survive, open an issue with the repro. I'd genuinely like to see it fail. docs/guarantees.md https://github.com/neryva-lab/moult/blob/main/docs/guarantees.md demo/comparison/RESULTS.md https://github.com/neryva-lab/moult/blob/main/demo/comparison/RESULTS.md @moult/runtime https://www.npmjs.com/package/@moult/runtime