kolu
Docs

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 as cross-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:

  1. read the gate under the pid-first law (readGateIdentity + injected readProcessIdentity, ±2 s start-time match; one-field legacy falls back to isHolderLive + socket accept) — is a live survivor holding it?
  2. kill it, then waitForPidGone — block until the pid is reaped, so the respawn never races a live gate holder;
  3. driver.spawn() — a survivable spawn (systemd-run --user, or detached +unref);
  4. waitForSocket, then connect() 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.