@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) |