Repository navigation
feat(client): explicit daemon lifecycle with ensureDaemon() and attach clients #3754
Description
Part of #3747.
Problem
Today any client call can manage the daemon:
- Start a detached daemon (
src/daemon-client/daemon-client-lifecycle.ts:156). - Replace a reachable daemon on a version or code mismatch (
src/daemon-client/daemon-launch-spec.ts:138), destroying other clients' sessions. - Force-retire a daemon after a request times out (
src/daemon-client/daemon-client-timeout.ts:46).
A library client can therefore start a daemon without the caller's plugins, or kill a daemon another process owns.
Required behavior
import { ensureDaemon, createAgentDeviceClient } from 'agent-device';
const connection = await ensureDaemon({
stateDir,
onStart?: (reason: 'missing' | 'incompatible', previous?: DaemonIdentity) => void,
});
createAgentDeviceClient({ stateDir, daemonLifecycle: 'attach' });
// daemonLifecycle: 'attach' | 'managed'; default 'managed' until 0.22attach
- Connects to the daemon registered in the state dir. It never starts, replaces or kills one, including after a timeout.
- A missing daemon fails with
details.reason: 'daemon-not-running'. - An incompatible one fails with
details.reason: 'daemon-incompatible'. Details include both identities, and the hint namesagent-device daemon stop.
ensureDaemon
- Starts a missing daemon.
- Replaces an incompatible daemon only through an authenticated quiesce request fenced to that daemon instance:
- the daemon closes admission atomically;
- it refuses with
daemon-busyif any request is in flight, or any session, repair session or live lease exists, and reopens admission; - otherwise it drains and exits.
- Daemons that predate the quiesce capability are never replaced. They get
daemon-incompatible. - Timeout force-retire belongs only to this path, after repeated failed liveness probes.
managed (default until 0.22) keeps today's behavior unchanged. The 0.21.x release stays additive.
Remote connections (daemonBaseUrl) never start or retire local daemons in any mode. daemonLifecycle is independent of daemonTransport, daemonServerMode and daemonBaseUrl (packages/contracts/src/client-connection.ts:34).
The CLI and MCP call ensureDaemon explicitly once their config is resolved. MCP creates a client per tool call (src/mcp/command-tools.ts:184). The CLI's switch from legacy replacement to idle-only replacement is an intentional behavior change, and the changelog records it.
Done when
- Tests cover:
- attach never starts, replaces or kills;
- attach after a timeout never kills;
ensureDaemonstarts a missing daemon;- an idle daemon is replaced;
- a busy daemon is refused (in-flight request, live lease);
- an old daemon without quiesce is refused;
- a remote connection never starts a local daemon.
- The client API docs and versioned help describe both modes.
- There's a changeset for
agent-device.
Activity
fix(daemon-client): wait for an acknowledged quiesce instead of killi…
fix(daemon-client): name the state dir in the not-running hint and re…
fix(daemon-client): document the drain timeout and tighten ensureDaem…