feat(client): daemonLifecycle attach mode and ensureDaemon()
Â
TH
1h ago+1708+1708+0123456789012345678901234567890123456789-77-77-01234567890123456789
Summary
Layer 2 of 3 for #3754 (umbrella #3747); based on #3764. The client half: who owns the daemon is now an explicit choice.
createAgentDeviceClient({ daemonLifecycle: 'attach' | 'managed' });managedis the default and behaves as before (additive in 0.21.x).attachconnects to the registered daemon and never starts, replaces or kills one, including after a timeout. A missing daemon fails withdetails.reason: 'daemon-not-running', an incompatible one with'daemon-incompatible'(both identities, hint namesagent-device daemon stop).ensureDaemon({ stateDir, onStart })starts a missing daemon and replaces an incompatible one only through the quiesce request from #3764. A busy daemon failsdaemon-busy, a daemon without the capability is never replaced, and a live daemon on another transport is quiesced, not killed. A daemon that times out on quiesce is force-retired only after repeated failed liveness probes.- A remote
daemonBaseUrlconnection never starts or retires a local daemon in either mode;ensureDaemonreads no remote env.
await ensureDaemon({ stateDir });
const client = createAgentDeviceClient({ stateDir, daemonLifecycle: 'attach' });
30 files incl. docs and changelog. The repo has no changeset tooling, so the CHANGELOG.md Unreleased entry is the changeset. Layer 3 adopts this in the CLI and MCP.
Validation
pnpm check:affected --run --head f21b0019dd: passed. New tests cover attach (missing, incompatible, unreachable, timeout), ensureDaemon (start, idle replace, busy, old daemon, transport, contention, unconfirmed retirement) and remote.
🤖 Generated with Claude Code