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> says what to do per trigger — a contract skew versus a
same-contract build change. The two shipped shapes:
recycle-on-skew(kaval): a mismatched daemon is killed and respawned.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. (It keys on ordered contract versions, never on builds, which are match-only.) This arm is only available to a drain-capable handshake.
2. Converge before you connect
Call converge() with the endpoint, your baked expectation, a probe that reads
the running daemon’s identity, the policy, and a build-drain fence:
// recycle-on-skew (kaval): a mismatched daemon is killed and respawned; a
// same-contract build change is reported to a human rather than auto-recycled.
const policy: ConvergencePolicy<"not-drainable"> = {
capability: "not-drainable",
onContractSkew: { kind: "recycle" },
onBuildMismatch: { kind: "nudge-human" },
};
const outcome = await converge({
endpoint,
baked, // expected build id + contract version
probe: () => probeIdentity(SOCKET_PATH), // identity over a version-agnostic channel
policy,
buildFence: createBuildDrainFence(), // once-per-boot drain fence
log: stderrLogger(),
});
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
existing boot methods. It returns a ConvergenceOutcome; wire it to your own
status surface and logs.
3. What the live recycle does underneath
When the decision is to recycle, the endpoint runs:
- read the gate (
gatePid+isHolderLive) — 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. restart(endpoint, steps) wraps this as
capture → drain → recycle → reattach, so a session can be captured and
re-adopted live across the kill.