the deploy train vs LIVING software
Everywhere else, "update the running system" means a deploy train: rebuild the image, restart the process, reconnect the clients, and pray nothing was mid-flight when the lights went out. For ordinary software that's merely annoying. For living software — agents working continuously, inside an engine you and they both touch — it's lethal twice over.
It's lethal the first way when an agent authors a tool mid-session. It just wrote a command it needs now, three steps into a task. If getting that tool live means a CI run and a restart, the agent's work is gone before the tool arrives. Improvement that can't land without stopping the worker isn't improvement; it's a context switch with extra steps.
It's lethal the second way when a human pushes a fix to a system an agent is actively working in. The old answer was a deploy: rebuild, restart — and drop whatever the agent had in flight. The cowboy answer was worse — tar the new files over SSH, straight onto the box, and clobber the agent's half-written state. One path drops the work; the other overwrites it. The reader has been burned by both, and has learned to fear the exact thing living software needs most: changing a running system without breaking the work inside it.
the DEFINITION
1. a loaded artifact — a toolkit command, a kernel, an agent definition, content — replaced in a running Nexus by repointing a name at new content-addressed bytes; in-flight work finishes on the old bytes, and nothing reboots.
The lineage is Erlang's hot code loading — the famous trick of replacing a module in a live VM without dropping calls. The Nexus gets the same effect one level up: not on VM modules, but on the artifacts the engine loads. Same spirit, different layer — and content addressing does the job BEAM magic did below it.
the binding is the whole TRICK
Here is the entire idea, and it's smaller than you expect. The "live system" is just a tiny map from names to files. When you run a command by name, the engine looks the name up in that map and runs the file it points at. That map — the command registry — is the only thing a swap touches.
The registry stores name → build/commands/<sha>.wasm in a
:persistent_term — Erlang's "rare write, fast read" table, so the
lookup on every run is a bare memory read with no process in the
path. Re-registering a name that already exists doesn't append or version — it
replaces the binding in place. The source even labels it, verbatim:
HOT-SWAP. And there's an observation hook: current(name)
returns the live spec bound to a name, so you can verify a swap actually
landed.
The money question is in-flight semantics, so watch one trace carefully. Caller A invokes a command and the engine resolves its path at call time; it now holds the old file. Mid-run, a re-register lands a new sha for the same name. Caller A keeps going — and finishes on the old bytes, because old content-addressed files are never deleted on re-register; a new sha is simply a new file. The next caller, B, invokes the same name and resolves the new sha. Same name, two immutable files, the map moved between them:
sequenceDiagram
participant A as caller A (mid-flight)
participant R as registry (:persistent_term)
participant B as caller B (later)
A->>R: run("slugify")
Note over R: resolves at CALL TIME → sha-OLD
R-->>A: build/commands/9f2c…wasm
Note over R: re-register "slugify" → sha-NEW lands
A-->>A: finishes on sha-OLD (old file never deleted)
B->>R: run("slugify")
R-->>B: build/commands/4b7e…wasm (sha-NEW)
Note over A,B: same name · two immutable files · the map moved
That's the confidence claim, exactly: you can change a live system an agent depends on without breaking its work, because a call resolves the file when it starts and keeps that file to the end. Resolution happens at call time, not hold time — so a swap is invisible to anything already running, and live for everything that starts after.
why the swap is SAFE
The reason "repoint a name" doesn't corrupt anything is that the files it
points at are immutable and self-naming. Content addressing is the load-bearing
property: hash the bytes with sha256, write
build/commands/<sha>.wasm — and only if that path is absent.
This is idempotent. The same source produces the same bytes, the same hash, the
same path; re-registering identical source is a no-op write that lands on the
same file. Different source is a different hash, which means a different file —
so a swap can never overwrite the bytes another call is reading.
That last sentence closes a real attack window. The filename being the
sha256 of the trusted bytes means tampering is detectable, and the check runs
at run time, not just at register time — the registry re-hashes the
file when it loads it and refuses a mismatch. So even if something altered the
bytes on disk between registration and use (the classic TOCTOU race), the swap
fails closed rather than running poisoned code. Registration is fenced too:
the engine refuses any wasm path that doesn't canonicalize strictly inside
build/commands/, so a registration can't point the map at a wasm
planted somewhere else.
And the swaps survive a reboot without rebuilding anything. A tiny manifest,
build/commands/registry.json, records name → addressed path plus
mode. On boot, reload_persisted re-registers every entry from that
manifest — no rebuild, no fetch — skipping reserved or malformed names and any
artifact that's gone missing. The content-addressed store is the cache; the
manifest is the index into it.
swap by SELF-AUTHORING
One swap primitive, three ways to drive it. The first is the in-session swap: an agent re-registering a command it just rebuilt, in seconds. The whole cycle — write the source, build it, content-address it, register it — runs entirely inside the wasm sandbox; even the compiler is wasm. Six languages are inline-buildable: rust, c, zig, js, ts, go. And the built command is capped by the Instance's Policy profile — self-authoring can never widen the granted capability set. An agent can write itself a sharper tool; it can't write itself a bigger blast radius.
You can drive it by hand to feel it. Build a command from a file:
$ wb toolkit build-inline slugify rust slugify.rs built + registered command `slugify` (rust) → build/commands/9f2c…81d4.wasm run it via the Dock: run-command slugify
Now the swap. Edit slugify.rs, re-run the same command. The build
produces different bytes, so a different sha — say
build/commands/4b7e…0a92.wasm — and the registry repoints
slugify at it. Ask the engine what's bound now and
CommandRegistry.current("slugify") shows the new path. The old
9f2c…81d4.wasm is still on disk: a call that was mid-flight when you
swapped finished on it, exactly as the trace above promised. Two immutable files,
one name, the map moved. (Try the same trick on a built-in like jq
and you get {:error, :reserved_name} — more on that under the
guardrails.)
A command an agent builds mid-session lives in the session by default. The
lifecycle ladder is session → workspace → registry:
promote materializes an inline command as a real toolkit directory —
a manifest, the source, a skill stub — persisting the source, not just
the bytes, then packing and installing it like any other
promoted toolkit. The swap gets you fast; promotion
gets you durable.
swap by PUSH
The second vehicle is a git push that goes live within one tick. Turn on
WB_GITOPS=1 and, at the top of every keeper
tick — before the agent runs — the engine reconciles the tenant
repo: a push to GitHub becomes live within one tick, tracked and versioned. The
critical word is integrate, not overwrite. The reconcile snapshots any
dirty agent work first, fetches origin, then does a real
git merge --no-edit. It never blows away the agent's in-progress
state to take the upstream change.
Walk one push end to end. A human fixes the SEO skill and runs
git push origin main, touching skills/seo.org. On the
next tick, the runtime snapshots dirty work as wip: snapshot before
reconcile, fetches, merges, and returns
{:ok, {:applied, %{commits: 1, files: ["skills/seo.org"]}}}; the log
reads Keeper: GitOps — pulled 1 upstream commit(s); 1 file(s):
skills/seo.org. The very next agent run reads the new skill. Here is that
whole flow, including the branch nobody wants but everybody needs:
flowchart TD
push["human: git push (skills/seo.org)"] --> tick["top of next keeper tick"]
tick --> snap["snapshot dirty agent work — wip commit"]
snap --> fetch["fetch origin"]
fetch --> merge{"git merge --no-edit"}
merge -- "clean" --> apply["{:ok, {:applied, …}}"]
apply --> pub["publish content + deploy dist"]
pub --> live["live · next agent run reads it"]
merge -- "same file both sides" --> abort["merge --abort · {:conflict, [files]}"]
abort --> human["reported · rolled back · left for a human"]
style push fill:#aee5c2,stroke:#121316,stroke-width:2.5px
style live fill:#13d943,stroke:#121316,stroke-width:2.5px
style abort fill:#f3c5a3,stroke:#121316
Two things make that merge clean in practice. First, the CODE/DATA path
split: by convention, CODE — the app's src/, the agent def,
design.org, skills/ — is what humans and CI push, while
DATA — content/, blog/, the board,
rem/ — is what the agent commits. Disjoint paths mean the human's
change and the agent's commits almost never touch the same file, so the replay is
clean. Second, when they do collide — say the agent also edited
skills/seo.org — the merge aborts, rolls back, and returns
{:conflict, ["skills/seo.org"]}. A real same-file divergence is
reported and left for a human; it is never silently overwritten. The agent's data
commits in content/ merge cleanly regardless, because of that very
split.
And "live" here is literal and atomic. The runtime publishes inside the commit
path, not as a separate step — the lander once shipped blogs that were committed
but served a 404 because the run died between commit and a later publish, so the
commit and the publish became one operation. On merge, the engine republishes
content/** and blog/** (pure file ops, no shell,
path-contained, junk filtered) and ships a built app from dist/ to
the site root. This page owns the swap semantics; the
gitops lesson owns the tenant-repo plumbing underneath
it.
the zero-mechanism SWAP
The third vehicle has no mechanism at all, which is the point. The keeper
re-reads the agent definition file every tick —
File.read!(path), fresh, inside the tick. There is no cache to
invalidate. The def is parsed fresh per run — its :ID:,
:MODEL:, :TOOLKITS:, and the system prompt under its
heading — and the toolkit index is injected per run from what the def declares.
So editing the .org def, or a git pull replacing it, is live on the
next tick with nothing restarting. "Deploying" a new agent personality is
saving a file.
That gives the page its boundary map — the small table the reader will screenshot. When does a change land, and by what mechanism?
| artifact | the swap lands | mechanism |
|---|---|---|
| toolkit command (wasm) | next call | registry repoint — in-flight safe |
| agent def / skill (.org) | next tick | re-read per run — no cache |
| content / blog | on commit | commit ⇒ publish, atomic |
Three artifacts, three latencies, one idea: the live thing is a pointer, and the pointer is cheap to move. A command swaps the instant the next call resolves it; a def swaps the instant the next tick reads it; content swaps the instant the commit publishes. Nothing in that table involves a restart, because nothing in it is the engine.
the GUARDRAILS
Depth rung — skippable. Casual swapping is only safe because a handful of rules make the dangerous swaps impossible rather than discouraged. Four of them carry the weight.
- Built-ins are unshadowable. Trusted built-in names —
jq,grep,upper— are reserved, and the registry merges built-ins last at lookup. So even a poisoned dynamic key bound tojqloses: a swap can never replace a trusted built-in. That's the{:error, :reserved_name}you saw earlier. - A hard cap, for a real reason. The dynamic registry tops out at 4096
entries. Not arbitrary: every
:persistent_term.puttriggers a global garbage collection, so an unbounded registration storm is a denial-of- service. The cap turns "swap freely" into "swap freely, within a ceiling that can't be weaponized." - Name hygiene. Names must match a strict character set; empty or exotic names are refused at the door, before they can become a lookup surprise.
- Provenance gates the swap-in. A third-party toolkit with invalid
provenance never builds — the engine refuses it outright and tells the author to
wb toolkit signit first. And trust posture drives the isolation tier at run time: a third-party command is routed to a separate BEAM VM, with graceful fallback to local. You can swap in someone else's code, but only signed, and it runs further from the core than your own.
None of these slow down the common case — your own signed tools, swapping in seconds. They exist so that the freedom to swap can't be turned against the engine that grants it.
the same pattern ELSEWHERE
Depth rung — skippable. Once you see "name → content-addressed bytes, replace in place," you start seeing it everywhere in the engine, because it's one idea reused, not three coincidences.
The pallet — the prebuilt WASI artifacts — re-seeds
idempotently at the registry level: re-seeding replaces the binding in place, the
same hot-swap, with sha-pinned artifacts fetched once into the content-addressed
store. The KernelRegistry is the kernel-shaped twin of the command
registry in forty-five lines: name → content-addressed bytes-to-bytes kernel
wasm, the same :persistent_term pattern, the same 4096 ceiling, the
same Map.put that replaces in place. Commands and kernels swap by
the identical move.
And the autopoet rides this as its entire job. It's the agent whose work is implementing capability issues filed by other agents — by editing the declarative config layer (toolkits, skills, defs, the capability registry), never native code. Its edits land as org files, build through the same gates, and swap in live. The autopoet is what hot-swap looks like as a standing practice instead of a one-off.
where it STOPS
Honesty section. The most important limit is the most reassuring one: the
host layer does not hot-swap. Engine code — the .ex under
runtime/host/ — ships via the CI-built runtime image. That's a
redeploy, not a swap. This isn't a gap; it's the design's spine. Loaded
artifacts can be edited freely because the engine that loads them is
fixed beneath them — agent self-modification is safe precisely because the agent
can only ever touch the loaded layer, never the floor it stands on.
The rest of the boundary, stated plainly:
| change | lands | how |
|---|---|---|
| toolkit command (wasm) | next call | registry repoint, in-flight safe |
| agent def / skill (.org) | next tick | re-read per run |
| content / blog | on commit | commit ⇒ publish, atomic |
app dist/ | on pull | deploy_app → site root |
app src/ needing vite | after CI / committed dist | the build seam — honest gap |
| host engine (.ex) | redeploy | CI runtime image, never hot |
| wbx itself | next invocation | wbx upgrade binary rename |
Read the bottom four rows together — they're where a push won't just
go live, and knowing them saves you the confusion of pushing the wrong kind of
change and wondering why nothing happened. The build seam is the
project's own honestly-named gap: a pull integrates the repo, but an app that
needs vite build can't be built on the BEAM box, so app
src/ rides either a committed dist/ or CI. GitOps is
live today for the build-free surface — the agent def, design.org,
skills/, and content/.
Two more honest notes. The reconcile trigger is a poll at each keeper
tick, not a webhook — a dedicated reconcile endpoint is explicitly the next step,
not a shipped one. And the toolkit rebuild is a verb, not a daemon: a git
pull republishes content and deploys a committed dist/, but I found
no surface that automatically rebuilds a compiled command when a pull changes its
source — swapping in new compiled bytes still takes an explicit build-and-register
call or an agent action.
For contrast, hold this up against the one thing nearby that isn't a
registry repoint: wbx upgrade swaps the CLI's own native binary by
downloading a new one and renaming it into place — a host-binary swap that takes
effect on the next invocation, not a live repoint of a running map. Same word,
different layer. The registry swap is live and in-flight-safe; the binary swap
waits for you to run the tool again.
questions people actually ASK
Does an in-flight call see the new version?
No. A call resolves its file at call time and keeps that file to the end. Old content-addressed bytes are never deleted on re-register, so anything already running finishes on what it started with. New calls — and only new calls — get the new artifact.
Can a swap replace jq or grep?
No. Built-in names are reserved, and the registry merges built-ins last at
lookup, so even a dynamic key bound to jq loses. Trying it returns
{:error, :reserved_name}. A trusted built-in is unshadowable by
construction, not by policy.
What survives a reboot?
The content-addressed store plus a tiny registry.json manifest
of name → addressed path. On boot, reload_persisted re-registers
every entry — no rebuild, no fetch. Your swaps come back as bindings without
rebuilding any bytes.
Can someone swap a tampered artifact in?
The filename is the sha256 of the trusted bytes, and the registry re-hashes
the file at run time and refuses a mismatch — so a byte altered after
registration fails closed instead of running. Registration also refuses any
path that doesn't canonicalize inside build/commands/.
Why didn't my pushed Svelte change go live?
The build seam. A pull integrates the repo and deploys a committed
dist/, but it can't run vite build on the BEAM box.
Commit the built dist/, or let CI build it — then the deploy lands
on pull like everything else.
Is this just Erlang hot code loading?
Same spirit, different layer. Erlang swaps modules inside the VM; the Nexus swaps the artifacts the engine loads — commands, kernels, defs, content — using content addressing where BEAM used VM machinery. The engine's own code, notably, does not hot-swap; that's a redeploy, by design.
keep GOING
Hot-swap is the artifact-level corollary of the engine you own — start with the parent, then follow the vehicles.
Going deeper on the parts: toolkits are what gets swapped, lanes is the compiler-in-sandbox that builds them, promotion is the session→workspace→registry ladder, and upgrades covers how a live plan swaps under its own subtyping discipline.