kolu
Docs

@kolu/surface-daemon-supervisor reference

The client half — the code that spawns, watches, recycles, and upgrades a daemon. It runs in the client process, never the daemon, and carries no @kolu/* app dependency.

The endpoint

createEndpoint(spec) is the state machine: connecting → connected | dead, then connected → degraded if the daemon dies mid-session. ensure() runs the always-recycle boot; current() is the live connection. It is generic over the client C and identity I.

const endpoint = createEndpoint<TopClient, TopIdentity>({
  hostId: "local",
  gatePath: GATE_PATH,
  socketPath: SOCKET_PATH,
  driver: survivableSpawnDriver({
    binPath: daemonEntry,
    args: [],
    env: { FLEET_TOP_GATE: GATE_PATH, FLEET_TOP_SOCKET: SOCKET_PATH },
    unitPrefix: "fleet-top",
  }),
  connect: () => connectTop(SOCKET_PATH), // dial + identity handshake
  log: stderrLogger(),
  onStatus: (hostId, status) =>
    process.stderr.write(`[supervisor] ${hostId}: ${status.state}\n`),
});

await endpoint.ensure(); // always-recycle boot = spawn → connect

// The live recycle: kill the daemon under a connected client and stand a fresh
// one up. Every step is required; the degenerate steps make no survival promise.
await restart(endpoint, {
  capture: async () => undefined,
  drain: async () => {},
  reattach: async () => {},
});
Export Role
createEndpoint(spec) the endpoint state machine (ensure(), current())
survivableSpawnDriver(cfg, deps?) the default DaemonDriver: systemd-run --user under a service, detached +unref otherwise
restart(endpoint, steps) the composed capture → drain → recycle → reattach sequence
waitForPidGone(pid, opts?) poll isHolderLive until the pid is reaped (ESRCH), so a respawn never races a live gate holder

The convergence kit

converge(args) is the live upgrade/recycle decision: it probes the running daemon’s identity, asks a pure decide, and enacts through the endpoint’s existing boot methods, returning a typed ConvergenceOutcome the caller wires to its own surfaces.

// 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(),
});
Export Role
converge(args) probe → decide → enact; returns a ConvergenceOutcome
decide(baked, running, policy, fenceSpent) the pure, zero-I/O decision fold — the policy table, unit-tested directly
ConvergencePolicy<Cap> · createBuildDrainFence() · outcomeAdopted(outcome) the declared policy, the once-per-boot drain fence, the uniform “was a survivor adopted?” reader
contractIsNewer · contractIsCompatible · buildsMatch re-exported from @kolu/surface-daemon (contract versions ordered; build ids match-only)