How to recycle and upgrade a daemon
When you ship a new daemon build, an older one may still be holding the gate with
clients attached. converge() detects the mismatch, decides what to do by your
declared policy, and enacts it through the endpoint — without the client noticing
more than a brief reconnect.
1. Declare a policy
A ConvergencePolicy<Cap> is the consumer’s entire convergence surface — who I
am (baked) + how I converge per trigger (contract skew vs same-contract build
change) +, when drainable, the Cap-gated drainBudget. The two shipped shapes:
recycle-on-skew(kaval): a mismatched daemon is killed and respawned. No budget — a not-drainable policy cannot spell one.drain-newer-else-refuse(padi): the daemon with the strictly-newer contract version wins — it is drained in and replaces the old; an older or equal contract refuses. Budget survives adopts so a cross-supervisor fight terminates ascross-supervisor, not a livelock.
2. Converge before you connect
State the policy (and probe) on createEndpoint, then call converge(endpoint)
— the only boot verb:
// The only boot verb — policy (who I am + how I converge) is fixed on the endpoint.
const outcome = yield* converge(endpoint);
converge runs three steps: probe the running identity (over the
version-agnostic channel, so a skew can’t hide it), decide with the pure
decide fold (zero I/O — the policy table), and enact through the endpoint’s
private boot methods (budget-gated when drainable). It returns a
ConvergenceOutcome with an optional ConvergenceAnomaly; wire it to your own
status surface and logs. Connector arms use convergeAdmit with the same policy.
probeDaemonIdentity({ capability: "not-drainable" })(socketPath) owns a fresh
local socket dial; the drainable form also requires drainCeilingMs. A connector
that has already dialed a combined client uses probeDaemonIdentityFrom
instead, supplying that client, its disposer, and the transport’s stronger exit
oracle. Both forms assemble the same ConvergenceProbe; the client form never
turns a lost link into honest daemon absence.
3. What the live recycle does underneath
When the decision is to recycle, the endpoint runs:
- read the gate under the pid-first law (
readGateIdentity+ injectedreadProcessIdentity, ±2 s start-time match; one-field legacy falls back toisHolderLive+ socket accept) — is a live survivor holding it? - kill it, then
waitForPidGone— block until the pid is reaped, so the respawn never races a live gate holder; driver.spawn()— a survivable spawn (systemd-run --user, or detached+unref);waitForSocket, thenconnect()and the identity handshake.
For a connected client, the daemon is replaced underneath and the client’s
session loop reconnects — re-running converge — with an onStatus(degraded)
bridging the gap. recycle(endpoint, steps) wraps this as
capture → drain → recycle → reattach, so a session can be captured and
re-adopted live across the kill.