@cello-protocol/daemon 0.0.50 → 0.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,10 @@ import { join } from "node:path";
14
14
  import { startDaemon } from "../daemon.js";
15
15
  import { createDirectoryEndpointResolver } from "../directory-bootstrap.js";
16
16
  import { buildManifestDeps } from "../manifest-deps.js";
17
+ // EXIT_ALREADY_RUNNING is distinct from 1 (generic startup failure) so a caller can tell "lost the
18
+ // race" from "broken" — connectOrStart relies on exactly that distinction, so the constant is shared
19
+ // rather than written down twice.
20
+ import { DaemonAlreadyRunningError, EXIT_ALREADY_RUNNING } from "../singleton-lock.js";
17
21
  const MAX_CONNECTIONS = 16;
18
22
  // Composition root: stdout JSON logger
19
23
  const logger = {
@@ -111,6 +115,15 @@ async function main() {
111
115
  });
112
116
  }
113
117
  main().catch((err) => {
118
+ // DOD-SINGLE-DAEMON-1 (AC2): losing the singleton race is not a crash — it is the system working.
119
+ // Say so in one plain line an operator can act on, name the pid that holds the lock, and exit
120
+ // non-zero without a stack trace. Two daemons is the silent, wrong outcome; this is the loud, right
121
+ // one.
122
+ if (err instanceof DaemonAlreadyRunningError) {
123
+ logger.info("daemon.start.refused", { reason: "already_running", holderPid: err.holderPid });
124
+ process.stderr.write(`${err.message}\n`);
125
+ process.exit(EXIT_ALREADY_RUNNING);
126
+ }
114
127
  logger.error("daemon.startup.failed", {
115
128
  error: err instanceof Error ? err.message : String(err),
116
129
  });
@@ -1 +1 @@
1
- {"version":3,"file":"cello-daemon.js","sourceRoot":"","sources":["../../src/bin/cello-daemon.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cello-daemon.js","sourceRoot":"","sources":["../../src/bin/cello-daemon.ts"],"names":[],"mappings":"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"}
@@ -3,17 +3,17 @@
3
3
  *
4
4
  * Pseudocode:
5
5
  * 1. connectOrStart(celloDir, logger):
6
- * a. Read lock file at ~/.cello/daemon.lock
7
- * b. If lock exists:
8
- * - Check if PID is alive (signal 0)
9
- * - If alive, attempt to connect to socket
10
- * - If connection succeeds, return existing daemon client
11
- * - If connection fails (socket gone), remove stale lock and start fresh
12
- * c. If lock doesn't exist or is stale:
13
- * - Remove stale lock (log daemon_lock_stale_removed)
14
- * - Spawn daemon as detached child process
15
- * - Wait for daemon.started event (read from child stdout)
16
- * - Return client connected to new daemon
6
+ * a. Try to connect to the daemon socket. If it answers, we are done.
7
+ * b. Otherwise ask the KERNEL whether a daemon holds the singleton lock (DOD-SINGLE-DAEMON-1):
8
+ * - HELD → a daemon is alive but not answering (mid-startup, or its socket was destroyed).
9
+ * Retry the connection, then fail loudly. NEVER spawn a second daemon beside it,
10
+ * and never delete its lock.
11
+ * - FREE → no daemon is running, whatever daemon.lock claims. Clear the stale lock, spawn,
12
+ * and wait for the socket to accept connections.
13
+ *
14
+ * The lock FILE is not consulted to make this decision — only the OS lock is. A pid in a JSON file
15
+ * can be dead, or REUSED by an unrelated process (in which case `isProcessAlive` says "alive"
16
+ * forever, and a daemon that trusted it would never start again).
17
17
  */
18
18
  import { type IpcClient } from "./ipc-client.js";
19
19
  import type { Logger } from "./types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"connect-or-start.d.ts","sourceRoot":"","sources":["../src/connect-or-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,CAAC,CAiCxB"}
1
+ {"version":3,"file":"connect-or-start.d.ts","sourceRoot":"","sources":["../src/connect-or-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,CAAC,CAqDxB"}
@@ -3,58 +3,105 @@
3
3
  *
4
4
  * Pseudocode:
5
5
  * 1. connectOrStart(celloDir, logger):
6
- * a. Read lock file at ~/.cello/daemon.lock
7
- * b. If lock exists:
8
- * - Check if PID is alive (signal 0)
9
- * - If alive, attempt to connect to socket
10
- * - If connection succeeds, return existing daemon client
11
- * - If connection fails (socket gone), remove stale lock and start fresh
12
- * c. If lock doesn't exist or is stale:
13
- * - Remove stale lock (log daemon_lock_stale_removed)
14
- * - Spawn daemon as detached child process
15
- * - Wait for daemon.started event (read from child stdout)
16
- * - Return client connected to new daemon
6
+ * a. Try to connect to the daemon socket. If it answers, we are done.
7
+ * b. Otherwise ask the KERNEL whether a daemon holds the singleton lock (DOD-SINGLE-DAEMON-1):
8
+ * - HELD → a daemon is alive but not answering (mid-startup, or its socket was destroyed).
9
+ * Retry the connection, then fail loudly. NEVER spawn a second daemon beside it,
10
+ * and never delete its lock.
11
+ * - FREE → no daemon is running, whatever daemon.lock claims. Clear the stale lock, spawn,
12
+ * and wait for the socket to accept connections.
13
+ *
14
+ * The lock FILE is not consulted to make this decision — only the OS lock is. A pid in a JSON file
15
+ * can be dead, or REUSED by an unrelated process (in which case `isProcessAlive` says "alive"
16
+ * forever, and a daemon that trusted it would never start again).
17
17
  */
18
18
  import { spawn } from "node:child_process";
19
19
  import { openSync } from "node:fs";
20
- import { readFile } from "node:fs/promises";
20
+ import { mkdir, readFile } from "node:fs/promises";
21
21
  import { join } from "node:path";
22
- import { readLock, removeLock, isProcessAlive } from "./lock-file.js";
22
+ import { readLock, removeLock } from "./lock-file.js";
23
+ import { probeSingletonLock, SINGLETON_LOCK_FILENAME, EXIT_ALREADY_RUNNING } from "./singleton-lock.js";
23
24
  import { connectToDaemon } from "./ipc-client.js";
24
25
  export async function connectOrStart(celloDir, logger, daemonBin) {
26
+ // A CELLO_DIR that does not exist yet (first ever run) would make the singleton probe below fail to
27
+ // open its lock file, and "I could not ask the kernel" would be reported as "I don't know whether a
28
+ // daemon is running" — a blind spot invented out of an empty directory. Create it first; the daemon
29
+ // does the same on startup, so this changes nothing except the quality of the answer.
30
+ await mkdir(celloDir, { recursive: true });
25
31
  const lockFilePath = join(celloDir, "daemon.lock");
26
- // Read existing lock
27
32
  const lock = await readLock(lockFilePath);
33
+ // The socket path is deterministic; the lock's copy is metadata, and it may be stale.
34
+ const socketPath = lock?.socketPath ?? join(celloDir, "daemon.sock");
35
+ // 1. If a daemon is answering, we are done. This is the overwhelmingly common path.
36
+ try {
37
+ return { client: await connectToDaemon(socketPath), alreadyRunning: true };
38
+ }
39
+ catch {
40
+ // Not answering. WHY it is not answering is the whole question — see below.
41
+ }
42
+ // 2. Ask the KERNEL whether a daemon is alive, not `daemon.lock`.
43
+ //
44
+ // DOD-SINGLE-DAEMON-1 (AC3): the old code asked `isProcessAlive(lock.pid)` and, when the socket
45
+ // did not answer, called the lock "stale", deleted it, and spawned a second daemon onto the live
46
+ // one's database. Both of its beliefs were unreliable: a pid can be REUSED (so a dead daemon's lock
47
+ // looks alive forever), and a live daemon can legitimately not answer yet (it is mid-startup, or an
48
+ // exiting orphan deleted its socket). The lock file cannot tell these apart. The OS lock can.
49
+ const probe = probeSingletonLock(celloDir, logger);
50
+ if (probe === "held") {
51
+ // A daemon is holding the lock right now. Spawning beside it is never correct. It may simply be
52
+ // mid-startup, so give it a moment to come up.
53
+ const client = await connectWithRetry(celloDir, socketPath, logger, lock?.pid ?? null);
54
+ return { client, alreadyRunning: true };
55
+ }
56
+ if (probe === "unknown") {
57
+ // We could not ask the kernel. That is NOT permission to spawn: "I don't know whether a daemon is
58
+ // running" must never be resolved by starting a second one. Fail loud with the real reason.
59
+ throw new Error("Could not determine whether a daemon is already running (the singleton lock could not be " +
60
+ `checked at ${celloDir}). Refusing to start a second daemon on a guess. See the ` +
61
+ "daemon.singleton.probe.failed log line for the underlying error.");
62
+ }
63
+ // 3. The lock is genuinely free, so no daemon is running — whatever `daemon.lock` claims. Only now
64
+ // is spawning correct.
28
65
  if (lock) {
29
- if (isProcessAlive(lock.pid)) {
30
- // Try connecting to the existing daemon
31
- try {
32
- const client = await connectToDaemon(lock.socketPath);
33
- return { client, alreadyRunning: true };
34
- }
35
- catch (err) {
36
- logger.info("daemon.lock.stale", {
37
- pid: lock.pid,
38
- reason: "socket_unreachable",
39
- error: err instanceof Error ? err.message : String(err),
40
- });
41
- await removeLock(lockFilePath, logger);
42
- }
43
- }
44
- else {
45
- // PID is dead — stale lock
46
- logger.info("daemon.lock.stale", {
47
- pid: lock.pid,
48
- reason: "process_dead",
49
- });
50
- await removeLock(lockFilePath, logger);
51
- }
66
+ logger.info("daemon.lock.stale", { pid: lock.pid, reason: "no_singleton_holder" });
67
+ await removeLock(lockFilePath, logger);
52
68
  }
53
- // Start a new daemon
54
69
  const client = await spawnDaemon(celloDir, daemonBin, lockFilePath, logger);
55
70
  return { client, alreadyRunning: false };
56
71
  }
57
- async function spawnDaemon(celloDir, daemonBin, _lockFilePath, _logger) {
72
+ /**
73
+ * A daemon holds the singleton lock but is not answering. Wait for it, then fail LOUDLY.
74
+ *
75
+ * The loud failure is the point (SI). The alternative — the old behaviour — was to quietly start a
76
+ * second daemon, which is the very thing that gives one machine two writers on one hash chain. An
77
+ * operator who is told "a daemon is running but not answering" can act; an operator silently given a
78
+ * second daemon cannot even see the problem.
79
+ */
80
+ async function connectWithRetry(celloDir, socketPath, logger, holderPid) {
81
+ const deadline = Date.now() + 10_000;
82
+ let lastError;
83
+ while (Date.now() < deadline) {
84
+ try {
85
+ return await connectToDaemon(socketPath);
86
+ }
87
+ catch (err) {
88
+ lastError = err;
89
+ await new Promise((r) => setTimeout(r, 150));
90
+ }
91
+ }
92
+ logger.error("daemon.unreachable", { holderPid, socketPath });
93
+ // The pid comes from daemon.lock — the very file this unit exists because it cannot be trusted. It
94
+ // may be stale, or name a process that merely inherited the number. So it is offered as a hint, and
95
+ // the operator is pointed at the authoritative answer: whoever holds the lock file open IS the
96
+ // daemon. Telling someone to `kill` an unverified pid is how you get them to kill something else.
97
+ const hint = holderPid === null ? "" : ` (daemon.lock names pid ${holderPid}, which may be stale)`;
98
+ throw new Error(`A daemon is already running${hint} and holds the singleton lock, but is not answering on ` +
99
+ `${socketPath} (${lastError instanceof Error ? lastError.message : String(lastError)}). ` +
100
+ "Refusing to start a second daemon beside it — two daemons on one database is the failure this " +
101
+ `prevents. To find the real holder: \`lsof ${join(celloDir, SINGLETON_LOCK_FILENAME)}\`, then stop ` +
102
+ "that process and try again.");
103
+ }
104
+ async function spawnDaemon(celloDir, daemonBin, lockFilePath, logger) {
58
105
  const socketPath = join(celloDir, "daemon.sock");
59
106
  const logPath = join(celloDir, "daemon.log");
60
107
  // The daemon's stdout/stderr go to a LOG FILE, never a pipe to this process.
@@ -91,6 +138,17 @@ async function spawnDaemon(celloDir, daemonBin, _lockFilePath, _logger) {
91
138
  // Not accepting yet (ENOENT/ECONNREFUSED). If the child has already exited, it
92
139
  // died during startup — surface the log tail rather than spinning to the deadline.
93
140
  if (exitCode !== undefined) {
141
+ // ...unless it exited because it LOST THE SINGLETON RACE. Two callers can legitimately probe
142
+ // "free" at the same instant (the probe is not a reservation) and both spawn; the kernel then
143
+ // picks exactly one winner and the other exits EXIT_ALREADY_RUNNING. That is the design
144
+ // working, not a failure: a healthy daemon is coming up right now. Reporting "Daemon exited
145
+ // (code 3) during startup" to an operator whose daemon is fine would be this unit's own lie.
146
+ // The distinct exit code exists precisely so we can tell these apart — so use it.
147
+ if (exitCode === EXIT_ALREADY_RUNNING) {
148
+ const winner = await readLock(lockFilePath);
149
+ logger.info("daemon.spawn.lost_race", { holderPid: winner?.pid ?? null });
150
+ return await connectWithRetry(celloDir, socketPath, logger, winner?.pid ?? null);
151
+ }
94
152
  throw new Error(`Daemon exited (code ${exitCode}) during startup.\n${await daemonLogTail(logPath)}`);
95
153
  }
96
154
  await new Promise((r) => setTimeout(r, 150));
@@ -1 +1 @@
1
- {"version":3,"file":"connect-or-start.js","sourceRoot":"","sources":["../src/connect-or-start.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connect-or-start.js","sourceRoot":"","sources":["../src/connect-or-start.ts"],"names":[],"mappings":"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"}
package/dist/daemon.d.ts CHANGED
@@ -62,5 +62,16 @@ export interface DaemonHandle {
62
62
  getAutoNatService(): IAutoNatService;
63
63
  }
64
64
  export declare const INBOUND_SESSION_TTL_MS: number;
65
+ /**
66
+ * DOD-SINGLE-DAEMON-1: take the singleton lock, and make sure it is released if startup fails
67
+ * ANYWHERE after that point — not just on the two failure paths that happened to think of it.
68
+ *
69
+ * The real binary exits on a startup failure and the kernel reclaims the lock regardless, so this is
70
+ * not a production-safety hole. It is a DIAGNOSTIC one, and a nasty kind: an in-process caller (every
71
+ * vitest daemon, any embedder) whose startup throws — a corrupt database, a bad key, an EACCES on the
72
+ * lock file — would leak the lock and then be told, on the next attempt in that process, "another
73
+ * daemon is already running". The true cause is replaced by a lie, and the directory is wedged for the
74
+ * life of the process.
75
+ */
65
76
  export declare function startDaemon(config: DaemonConfig): Promise<DaemonHandle>;
66
77
  //# sourceMappingURL=daemon.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAuB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA6kM7E"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAMpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAuB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAS7E"}
package/dist/daemon.js CHANGED
@@ -29,7 +29,8 @@ import { mkdir } from "node:fs/promises";
29
29
  import { randomUUID, createHash } from "node:crypto";
30
30
  import { dirname, join } from "node:path";
31
31
  import { loadAgents } from "./agent-loader.js";
32
- import { acquireLock, removeLock } from "./lock-file.js";
32
+ import { acquireLock, removeLockIfOwned } from "./lock-file.js";
33
+ import { acquireSingletonLock } from "./singleton-lock.js";
33
34
  import { createIpcServer } from "./ipc-server.js";
34
35
  import { renderForSurface } from "./vocabulary.js";
35
36
  import { SessionNodeManager } from "./session-node-manager.js";
@@ -184,7 +185,29 @@ class ProductionSessionNodeFactory {
184
185
  // M8C-TTL-1: receiver-side session-request TTL. CORE ships the DoD's own 24h default;
185
186
  // per-agent configurability is PARKED on M9-CFG-001 (D17 — same pattern as D14/D15/D16).
186
187
  export const INBOUND_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
188
+ /**
189
+ * DOD-SINGLE-DAEMON-1: take the singleton lock, and make sure it is released if startup fails
190
+ * ANYWHERE after that point — not just on the two failure paths that happened to think of it.
191
+ *
192
+ * The real binary exits on a startup failure and the kernel reclaims the lock regardless, so this is
193
+ * not a production-safety hole. It is a DIAGNOSTIC one, and a nasty kind: an in-process caller (every
194
+ * vitest daemon, any embedder) whose startup throws — a corrupt database, a bad key, an EACCES on the
195
+ * lock file — would leak the lock and then be told, on the next attempt in that process, "another
196
+ * daemon is already running". The true cause is replaced by a lie, and the directory is wedged for the
197
+ * life of the process.
198
+ */
187
199
  export async function startDaemon(config) {
200
+ await mkdir(config.celloDir, { recursive: true });
201
+ const singletonLock = acquireSingletonLock(config.celloDir, config.logger);
202
+ try {
203
+ return await startDaemonHoldingLock(config, singletonLock);
204
+ }
205
+ catch (err) {
206
+ singletonLock.release();
207
+ throw err;
208
+ }
209
+ }
210
+ async function startDaemonHoldingLock(config, singletonLock) {
188
211
  const { celloDir, socketPath, lockFilePath, maxConnections, version, logger, manifestProvider, manifestRootKeys, manifestThreshold, manifestVersionStore: injectedManifestVersionStore, manifestPollScheduler, directoryHttpUrl, signalingConnect, challengeVerifier, directoryEndpointResolver, sessionNodeFactory, sessionNegotiator, getRelayCircuitAddress, telegramBotClient: injectedTelegramBotClient, } = config;
189
212
  // CELLO-M7-TRANSPORT-001: composition-root selection of the transport selector.
190
213
  // Driven by CELLO_ENV; fails fast at startup (here, not at first session) when a
@@ -212,6 +235,14 @@ export async function startDaemon(config) {
212
235
  // encrypted DB (AC-008), not a manifest-version.json file. ──
213
236
  await mkdir(celloDir, { recursive: true });
214
237
  await mkdir(dirname(socketPath), { recursive: true });
238
+ // DOD-SINGLE-DAEMON-1: the caller already took the kernel's exclusive lock (see startDaemon) —
239
+ // BEFORE this function touches anything that assumes a single writer. A daemon that loses that race
240
+ // never reaches here at all: it never opens the database, never registers an agent, never connects
241
+ // to the directory. Two daemons sharing an identity is how a hash chain gets two leaves at the same
242
+ // index, and the seal then attests to the damage.
243
+ //
244
+ // AC4: the advisory JSON keeps its metadata role (it is how the NEXT process learns our pid), but
245
+ // the OS lock is what decides whether a daemon may run. This file never gates startup.
215
246
  await acquireLock(lockFilePath, { pid: process.pid, socketPath, version });
216
247
  // M9-CORE-001: one security-gateway client, shared by both seams — the outbound screen in
217
248
  // cello_send and the inbound screen inside SessionNodeManager. Absent config falls back to a
@@ -342,7 +373,9 @@ export async function startDaemon(config) {
342
373
  sessionNodeManager.getDb().close();
343
374
  }
344
375
  catch { /* ignore */ }
345
- await removeLock(lockFilePath, logger).catch(() => { });
376
+ await removeLockIfOwned(lockFilePath, process.pid, logger).catch(() => { });
377
+ // The singleton lock is released by startDaemon's catch — every throw out of this function goes
378
+ // through it, so no failure path can leak the lock by forgetting.
346
379
  throw new Error("Manifest verification failed. The daemon cannot start with an unverified manifest when manifestProvider is configured. " +
347
380
  "Check the logs for the specific failure reason (manifest_signature_invalid, manifest_expired, or manifest_version_rollback).");
348
381
  }
@@ -5879,7 +5912,8 @@ export async function startDaemon(config) {
5879
5912
  await ipcServer.start();
5880
5913
  }
5881
5914
  catch (err) {
5882
- await removeLock(lockFilePath, logger);
5915
+ // As above: startDaemon's catch releases the singleton lock for us.
5916
+ await removeLockIfOwned(lockFilePath, process.pid, logger);
5883
5917
  throw err;
5884
5918
  }
5885
5919
  // MCP-002: Instantiate NotificationDispatcher (wired to IPC server)
@@ -5960,14 +5994,30 @@ export async function startDaemon(config) {
5960
5994
  manifestPollScheduler.cancel();
5961
5995
  }
5962
5996
  logger.info("daemon.stopped", { pid: process.pid, reason });
5963
- // CONN-001 (code-review LOW): stopAllSignaling() stops the shared manager AND every per-agent
5964
- // manager (best-effort). The previously-separate per-agent stop loop here was redundant and
5965
- // unguarded (would abort the rest of shutdown if stop() ever threw on a second call) — removed.
5966
- await stopAllSignaling();
5967
- // Gracefully mark active sessions interrupted (AC-009) before stopping IPC
5968
- await sessionNodeManager.gracefulShutdown();
5969
- await ipcServer.stop();
5970
- await removeLock(lockFilePath, logger);
5997
+ try {
5998
+ // CONN-001 (code-review LOW): stopAllSignaling() stops the shared manager AND every per-agent
5999
+ // manager (best-effort). The previously-separate per-agent stop loop here was redundant and
6000
+ // unguarded (would abort the rest of shutdown if stop() ever threw on a second call) — removed.
6001
+ await stopAllSignaling();
6002
+ // Gracefully mark active sessions interrupted (AC-009) before stopping IPC
6003
+ await sessionNodeManager.gracefulShutdown();
6004
+ await ipcServer.stop();
6005
+ }
6006
+ finally {
6007
+ // DOD-SINGLE-DAEMON-1: in a `finally`, because a throw anywhere above must not leave the lock
6008
+ // held. In the real binary the process exits and the kernel reclaims it — but an in-process
6009
+ // caller (vitest, an embedder) whose shutdown throws would otherwise find every subsequent
6010
+ // startDaemon in that process reporting "another daemon is already running", with the real
6011
+ // cause thrown away. That is the same leak F2 fixed on the startup path.
6012
+ //
6013
+ // DOD-DAEMON-CLEANUP-1 (AC1): the lock file goes only if it is still OURS. Another daemon may
6014
+ // have taken it over while we ran, and deleting a live daemon's lock is what makes `cello
6015
+ // logout` say "No daemon running" and the next `cello login` spawn a third one.
6016
+ await removeLockIfOwned(lockFilePath, process.pid, logger).catch(() => { });
6017
+ // Released LAST: while we hold it, no successor daemon can start, which is what lets
6018
+ // ipcServer.stop()'s socket re-check above be race-free.
6019
+ singletonLock.release();
6020
+ }
5971
6021
  }
5972
6022
  function getSessionNodeManager() {
5973
6023
  return sessionNodeManager;