@promptctl/cc-candybar 1.17.2 → 1.17.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/cc-candybar",
3
- "version": "1.17.2",
3
+ "version": "1.17.4",
4
4
  "description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -91,10 +91,10 @@
91
91
  "mobx": "^6.15.0"
92
92
  },
93
93
  "optionalDependencies": {
94
- "@promptctl/cc-candybar-darwin-arm64": "1.17.2",
95
- "@promptctl/cc-candybar-darwin-x64": "1.17.2",
96
- "@promptctl/cc-candybar-linux-x64": "1.17.2",
97
- "@promptctl/cc-candybar-linux-arm64": "1.17.2"
94
+ "@promptctl/cc-candybar-darwin-arm64": "1.17.4",
95
+ "@promptctl/cc-candybar-darwin-x64": "1.17.4",
96
+ "@promptctl/cc-candybar-linux-x64": "1.17.4",
97
+ "@promptctl/cc-candybar-linux-arm64": "1.17.4"
98
98
  },
99
99
  "pnpm": {
100
100
  "supportedArchitectures": {
@@ -1,8 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import net from "node:net";
3
+ import path from "node:path";
3
4
  import { launchDetachedSync } from "../proc/launch";
4
5
  import process from "node:process";
5
- import { socketPath, spawnLockPath, daemonDir } from "./paths";
6
+ import {
7
+ socketPath,
8
+ spawnLockPath,
9
+ spawnCooldownPath,
10
+ daemonDir,
11
+ } from "./paths";
6
12
 
7
13
  // [LAW:single-enforcer] One primitive per runtime that owns the entire
8
14
  // "obtain a daemon" verb. Every spawn site in the Node runtime now flows
@@ -133,6 +139,28 @@ async function spawnAndWaitForReady(
133
139
  outerDeadline: number,
134
140
  reasonSuffix: string,
135
141
  ): Promise<ObtainResult> {
142
+ const readyDeadline = Math.min(
143
+ Date.now() + spawnReadyTimeoutMs,
144
+ outerDeadline,
145
+ );
146
+
147
+ // [LAW:dataflow-not-control-flow] The spawn-rate bound is consulted as data,
148
+ // not a mode. If a spawn was attempted within SPAWN_COOLDOWN_MS, one is
149
+ // already in flight — do NOT add another Node process; wait for the in-flight
150
+ // boot. This keeps obtainDaemon under the same global rate cap as the kick
151
+ // path, so the rate bound holds for EVERY spawn site [LAW:one-source-of-truth].
152
+ if (!claimSpawnCooldown()) {
153
+ // [LAW:no-silent-failure] This failure's cause is the cooldown gate, not the
154
+ // lock path we arrived through — so it does NOT inherit reasonSuffix (which
155
+ // tags lock-held vs lock-fallback *spawn* provenance). We never spawned here.
156
+ return (await pollUntilReady(connectTimeoutMs, readyDeadline))
157
+ ? { kind: "attached" }
158
+ : {
159
+ kind: "failed",
160
+ reason: "spawn on cooldown; no daemon became ready during the wait",
161
+ };
162
+ }
163
+
136
164
  // [LAW:no-defensive-null-guards] obtainDaemon is typed Promise<ObtainResult>.
137
165
  // A synchronous throw from child_process.spawn (ENOENT, invalid options)
138
166
  // must become a typed failure, not a rejected promise.
@@ -151,20 +179,27 @@ async function spawnAndWaitForReady(
151
179
  reason: `spawn returned false${reasonSuffix}`,
152
180
  };
153
181
  }
154
- const readyDeadline = Math.min(
155
- Date.now() + spawnReadyTimeoutMs,
156
- outerDeadline,
157
- );
182
+ return (await pollUntilReady(connectTimeoutMs, readyDeadline))
183
+ ? { kind: "started" }
184
+ : {
185
+ kind: "failed",
186
+ reason: `daemon did not bind in time${reasonSuffix}`,
187
+ };
188
+ }
189
+
190
+ // Poll the socket until a daemon answers or the deadline elapses. Shared by the
191
+ // spawn path (→ "started") and the cooldown-blocked wait path (→ "attached"):
192
+ // both need "did a daemon come up in time", they differ only in how they label
193
+ // the outcome.
194
+ async function pollUntilReady(
195
+ connectTimeoutMs: number,
196
+ readyDeadline: number,
197
+ ): Promise<boolean> {
158
198
  while (Date.now() < readyDeadline) {
159
- if (await canConnect(socketPath(), connectTimeoutMs)) {
160
- return { kind: "started" };
161
- }
199
+ if (await canConnect(socketPath(), connectTimeoutMs)) return true;
162
200
  await sleep(20);
163
201
  }
164
- return {
165
- kind: "failed",
166
- reason: `daemon did not bind in time${reasonSuffix}`,
167
- };
202
+ return false;
168
203
  }
169
204
 
170
205
  // Synchronous fire-and-forget kick — used for "daemon-miss" recovery where
@@ -214,7 +249,7 @@ export function obtainDaemonKick(opts: { spawn?: () => boolean } = {}): void {
214
249
  process.stderr.write(
215
250
  `cc-candybar: spawn-lock held ${ageMs}ms (likely crashed holder) — spawning unlocked\n`,
216
251
  );
217
- safeSpawn(spawnFn);
252
+ cooldownGatedSpawn(spawnFn);
218
253
  }
219
254
  return;
220
255
  }
@@ -222,16 +257,26 @@ export function obtainDaemonKick(opts: { spawn?: () => boolean } = {}): void {
222
257
  process.stderr.write(
223
258
  `cc-candybar: spawn-lock unavailable (${lock.reason}) — spawning unlocked\n`,
224
259
  );
225
- safeSpawn(spawnFn);
260
+ cooldownGatedSpawn(spawnFn);
226
261
  return;
227
262
  }
228
263
  try {
229
- safeSpawn(spawnFn);
264
+ cooldownGatedSpawn(spawnFn);
230
265
  } finally {
231
266
  releaseSpawnLock();
232
267
  }
233
268
  }
234
269
 
270
+ // [LAW:single-enforcer] Every kick spawn site routes through here, so the
271
+ // spawn-rate bound is applied at exactly one boundary — mirror of Rust's
272
+ // spawn_daemon_rate_limited. On cooldown we do nothing: a spawn was attempted
273
+ // within SPAWN_COOLDOWN_MS and is likely still booting; the kick is
274
+ // fire-and-forget, so "already in flight" is a complete answer.
275
+ function cooldownGatedSpawn(spawnFn: () => boolean): void {
276
+ if (!claimSpawnCooldown()) return;
277
+ safeSpawn(spawnFn);
278
+ }
279
+
235
280
  function spawnLockAgeMs(): number | null {
236
281
  try {
237
282
  const st = fs.statSync(spawnLockPath());
@@ -260,6 +305,93 @@ function safeSpawn(spawnFn: () => boolean): void {
260
305
  }
261
306
  }
262
307
 
308
+ // ─── Spawn cooldown (shared spawn-RATE bound) ────────────────────────────────
309
+ //
310
+ // [LAW:one-source-of-truth] spawn.lock dedups spawns at one INSTANT; the
311
+ // cooldown bounds them over TIME. Without it, the Rust kick — which releases
312
+ // spawn.lock milliseconds after forking, before the 0.5-3s Node boot window —
313
+ // re-spawns on every render tick during an outage (spawn rate ≈ tick rate:
314
+ // dozens/sec, process-table exhaustion). One file's mtime records the last spawn
315
+ // ATTEMPT; both runtimes consult it. The constant and filename are mirrored TS↔
316
+ // Rust (scripts/check-protocol.mjs). Worst case with the bound: ~20 spawns/min
317
+ // globally, each of which exits cleanly via the sibling socket-lease defenses.
318
+ // Exported for the boundary unit tests (test/daemon-acquire.test.ts), which pin
319
+ // the window arithmetic against the exact constant the same way the Rust unit
320
+ // tests do — so a TS↔Rust decision divergence at a boundary is caught even
321
+ // though check-protocol only diffs the constant's value.
322
+ export const SPAWN_COOLDOWN_MS = 3_000;
323
+
324
+ // [LAW:effects-at-boundaries] The window arithmetic — the subtle part: a
325
+ // future-mtime garbage record (beyond the stale-lock window) must not pin the
326
+ // cooldown forever, while a small negative age is just ms-truncation of
327
+ // Date.now() against the higher-precision fs mtime and still counts as a
328
+ // just-recorded attempt — is a pure function of the record's age, extracted from
329
+ // the fs read so it is unit-tested without touching the filesystem. Mirrors the
330
+ // Rust cooldown_decision. `null` age (missing/unreadable file) allows (first
331
+ // spawn); the mtime IS the timestamp, so "unparseable timestamp" is
332
+ // unrepresentable by construction.
333
+ export type CooldownDecision =
334
+ | { kind: "allow" }
335
+ | { kind: "allow-future-garbage"; futureMs: number }
336
+ | { kind: "deny" };
337
+
338
+ export function cooldownDecision(ageMs: number | null): CooldownDecision {
339
+ if (ageMs === null) return { kind: "allow" };
340
+ if (ageMs < -STALE_LOCK_MS)
341
+ return { kind: "allow-future-garbage", futureMs: -ageMs };
342
+ if (ageMs < SPAWN_COOLDOWN_MS) return { kind: "deny" };
343
+ return { kind: "allow" };
344
+ }
345
+
346
+ // [LAW:single-enforcer] The sole authority on daemon-spawn RATE. Returns true —
347
+ // and RECORDS the attempt (updating spawn.cooldown's mtime to now) — when a
348
+ // spawn is permitted; false when an attempt was recorded within
349
+ // SPAWN_COOLDOWN_MS. Recording-on-grant (BEFORE the caller spawns) is
350
+ // load-bearing: a spawn that then throws or returns false still counts against
351
+ // the rate, so a broken binary is not retried in a tight loop. A future-mtime
352
+ // garbage record warns loudly and falls toward ALLOWING the spawn
353
+ // [LAW:no-silent-failure].
354
+ function claimSpawnCooldown(): boolean {
355
+ const path = spawnCooldownPath();
356
+ const decision = cooldownDecision(cooldownAgeMs(path));
357
+ if (decision.kind === "deny") return false;
358
+ if (decision.kind === "allow-future-garbage") {
359
+ process.stderr.write(
360
+ `cc-candybar: spawn.cooldown mtime is ${decision.futureMs}ms in the future — ignoring and spawning\n`,
361
+ );
362
+ }
363
+ recordSpawnAttempt(path);
364
+ return true;
365
+ }
366
+
367
+ function cooldownAgeMs(path: string): number | null {
368
+ try {
369
+ return Date.now() - fs.statSync(path).mtimeMs;
370
+ } catch {
371
+ return null;
372
+ }
373
+ }
374
+
375
+ function recordSpawnAttempt(filePath: string): void {
376
+ // [LAW:composability] Self-sufficient — ensure the state dir exists rather
377
+ // than leaning on an ambient ensureStateDir() precondition, so any spawn site
378
+ // routing through claimSpawnCooldown records correctly (mirrors Rust's
379
+ // record_spawn_attempt). Content is human-diagnostic only; the mtime is the
380
+ // authority. A write failure means no cooldown recorded — worst case one extra
381
+ // spawn, which bind() arbitrates — but surface it loudly rather than silently
382
+ // un-bound the rate [LAW:no-silent-failure].
383
+ try {
384
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
385
+ fs.writeFileSync(filePath, `${process.pid} ${Date.now()}\n`, {
386
+ mode: 0o600,
387
+ });
388
+ } catch (e) {
389
+ process.stderr.write(
390
+ `cc-candybar: could not record spawn.cooldown: ${(e as Error).message}\n`,
391
+ );
392
+ }
393
+ }
394
+
263
395
  // ─── Spawn-lock (Node side) ──────────────────────────────────────────────────
264
396
  //
265
397
  // O_EXLOCK / fcntl(F_SETLK) aren't reliably exposed across Node platforms, so
@@ -274,7 +406,9 @@ function safeSpawn(spawnFn: () => boolean): void {
274
406
  // lock is a thundering-herd optimization, so a missed dedup just means one
275
407
  // extra Node process eats a bind() race and exits.
276
408
 
277
- const STALE_LOCK_MS = 10_000;
409
+ // Exported for the cooldownDecision boundary tests. Mirrored TS↔Rust (diffed by
410
+ // check-protocol) since both runtimes apply it to the same spawn.cooldown file.
411
+ export const STALE_LOCK_MS = 10_000;
278
412
 
279
413
  let heldLock: { fd: number; path: string } | null = null;
280
414
 
@@ -146,6 +146,17 @@ export function spawnLockPath(): string {
146
146
  return path.join(stateDir(), "spawn.lock");
147
147
  }
148
148
 
149
+ // [LAW:one-source-of-truth] The spawn-RATE bound (as distinct from spawn.lock's
150
+ // instantaneous dedup) is anchored to one file's mtime beside spawn.lock: the
151
+ // time of the last daemon-spawn ATTEMPT. Both runtimes gate on the SAME file, so
152
+ // the filename is mirrored TS↔Rust (rust-client/src/main.rs SPAWN_COOLDOWN_FILE)
153
+ // and diffed by scripts/check-protocol.mjs — a drift would silently split the
154
+ // rate bound in two.
155
+ const SPAWN_COOLDOWN_FILE = "spawn.cooldown";
156
+ export function spawnCooldownPath(): string {
157
+ return path.join(stateDir(), SPAWN_COOLDOWN_FILE);
158
+ }
159
+
149
160
  export function logPath(): string {
150
161
  return path.join(stateDir(), "daemon.log");
151
162
  }
@@ -0,0 +1,135 @@
1
+ import { launchSync, type LaunchOpts, type LaunchResult } from "../proc/launch";
2
+
3
+ // ─── Process start-time fingerprint ──────────────────────────────────────────
4
+ //
5
+ // [FRAMING:representation] A bare pid is an under-constrained identity: the
6
+ // kernel recycles pids, so "a process with pid 989 exists" does NOT prove "989
7
+ // is the same process that wrote this lease". A crashed daemon whose pid the OS
8
+ // later hands to an unrelated long-lived process reads `alive` on every
9
+ // subsequent start, so no daemon ever comes up (brandon-daemon-lifecycle-2b3.4
10
+ // RESIDUAL 1 — the inverse of the socket-theft storm). The kernel ALSO stamps
11
+ // each process with a start-time it never rewinds within that pid's life;
12
+ // (pid, start-time) is the pair that actually IS a process identity, so a
13
+ // recycled pid is provably a DIFFERENT process (different start-time).
14
+ //
15
+ // [LAW:one-source-of-truth] The authority is the kernel's process identity, not
16
+ // a bare pid. `ps -o lstart=` exposes the start-time on every Unix we ship to
17
+ // (darwin + linux). The reported token is treated as OPAQUE — compared by string
18
+ // equality, never parsed — so there is no date-format representation to drift
19
+ // [FRAMING:representation]: a producer/consumer parse mismatch is unrepresentable
20
+ // when neither side parses. That opaque-equality invariant holds ONLY if the
21
+ // GENERATOR is deterministic. `ps -o lstart=` is strftime-formatted (locale) via
22
+ // `localtime()` (timezone), so the SAME process renders different tokens across a
23
+ // locale change OR a timezone change (TZ env, DST, tzdata update). We pin BOTH on
24
+ // the `ps` subprocess — `LC_ALL=C` (dominates any ambient LC_TIME/LC_ALL) and
25
+ // `TZ=UTC` — so the token is a locale- and timezone-invariant UTC rendering of
26
+ // the start instant, and equality is sound.
27
+
28
+ // A read of a pid's kernel start-time. Only TWO outcomes, because nothing
29
+ // derivable from a `ps` exit code can SOUNDLY prove a process is dead — a
30
+ // non-zero exit means "no start-time to report", which conflates a genuinely
31
+ // absent pid with an access failure (hardened `/proc`/`hidepid`, a
32
+ // permission-denied read of a live process). A `gone` state produced from that
33
+ // would be an over-claim ([LAW:types-are-the-program]) that could false-dead a
34
+ // live owner and reclaim its socket. So:
35
+ // start — the pid is live and this is its start-time token (the ONLY
36
+ // thing `ps` can assert soundly: it printed a row).
37
+ // unavailable — `ps` reported no start-time (absent OR unreadable OR errored).
38
+ // Callers defer to kill(pid,0), the sound liveness test, which
39
+ // correctly reclaims a truly-dead pid and spares a live one.
40
+ // The fingerprint's unique contribution is the `start`+mismatch case (a recycled
41
+ // pid whose live start-time differs from the lease); everything else falls back
42
+ // to kill, no worse than before the fingerprint existed.
43
+ export type StartTimeRead =
44
+ | { kind: "start"; token: string }
45
+ | { kind: "unavailable"; detail: string };
46
+
47
+ // The subprocess boundary, injected so the parse/branch logic is exercised by
48
+ // input enumeration with a fake launcher while the real path runs the real `ps`.
49
+ export type Launcher = (opts: LaunchOpts) => LaunchResult;
50
+
51
+ // [LAW:effects-at-boundaries][LAW:single-enforcer] The sole effect (spawning
52
+ // `ps`) goes through the one subprocess enforcer, `launchSync`. The ONLY sound
53
+ // signal `ps` gives is a printed start-time row (a live pid we could read);
54
+ // [LAW:no-silent-failure] every other outcome — absent pid, access failure,
55
+ // spawn-error, timeout — is `unavailable`, deferring the alive/dead call to
56
+ // kill(pid,0) rather than risk declaring a live owner dead from a `ps` exit
57
+ // code that cannot distinguish "gone" from "cannot read".
58
+ export function readStartTime(
59
+ pid: number,
60
+ launch: Launcher = launchSync,
61
+ ): StartTimeRead {
62
+ const res = launch({
63
+ bin: "ps",
64
+ args: ["-o", "lstart=", "-p", String(pid)],
65
+ category: "process-fingerprint",
66
+ timeoutMs: 2000,
67
+ // [FRAMING:representation] Pin BOTH locale and timezone so the token is
68
+ // deterministic across daemons — the writer and reader must render the same
69
+ // process identically for opaque string equality to be sound. LC_ALL=C fixes
70
+ // the strftime format; TZ=UTC fixes localtime(), so DST / TZ / tzdata changes
71
+ // can't make one live process render two different start-time strings.
72
+ env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
73
+ });
74
+ if (res.ok) {
75
+ const token = res.stdout.trim();
76
+ if (token.length > 0) return { kind: "start", token };
77
+ // Exit 0 with empty output is anomalous (a live pid always lists) — don't
78
+ // mint an empty-string fingerprint; defer to kill.
79
+ return { kind: "unavailable", detail: "ps produced no start-time" };
80
+ }
81
+ const detail =
82
+ res.reason === "spawn-error"
83
+ ? (res.error ?? "ps spawn failed")
84
+ : `ps ${res.reason} (exit ${res.exitCode})`;
85
+ return { kind: "unavailable", detail };
86
+ }
87
+
88
+ export interface LivenessDeps {
89
+ readStartTime: (pid: number) => StartTimeRead;
90
+ pidAlive: (pid: number) => boolean;
91
+ }
92
+
93
+ // [LAW:dataflow-not-control-flow] The liveness verdict is a pure fold over the
94
+ // start-time read + the lease's recorded token. Full input space:
95
+ // read=start + token matches lease → true (same process still alive)
96
+ // read=start + token differs → false (a DIFFERENT process holds the
97
+ // pid — a recycle, or a restarted daemon)
98
+ // read=unavailable → kill(pid,0) fallback (the sound
99
+ // alive/dead test: a truly-dead pid →
100
+ // false → reclaim; a live pid ps couldn't
101
+ // read → true → spared)
102
+ // lease token = null (unfingerprinted at write, e.g. a host without `ps`) →
103
+ // kill(pid,0) fallback for the same reason
104
+ //
105
+ // [LAW:no-silent-failure] Only the `start`+mismatch case comes from the
106
+ // fingerprint; every ambiguous `ps` outcome defers to kill, so the DANGEROUS
107
+ // direction (declaring a live daemon dead → stealing its socket, the storm this
108
+ // epic fights) never happens merely because `ps` could not answer. The only
109
+ // thing forfeited when `ps` cannot answer is detecting a recycled pid.
110
+ export function sameLiveProcess(
111
+ pid: number,
112
+ leaseToken: string | null,
113
+ deps: LivenessDeps,
114
+ ): boolean {
115
+ if (leaseToken === null) return deps.pidAlive(pid);
116
+ const read = deps.readStartTime(pid);
117
+ switch (read.kind) {
118
+ case "start":
119
+ return read.token === leaseToken;
120
+ case "unavailable":
121
+ return deps.pidAlive(pid);
122
+ }
123
+ }
124
+
125
+ // Read THIS process's own start-time to stamp into its lease. `unavailable`
126
+ // (no `ps`) collapses to `null` — the lease records "unfingerprinted", and every
127
+ // reader of it falls back to kill(pid,0) via sameLiveProcess. A dead result is
128
+ // impossible for our own live pid; if it somehow occurs we also record null.
129
+ export function readOwnStartTime(
130
+ pid: number,
131
+ read: (pid: number) => StartTimeRead = readStartTime,
132
+ ): string | null {
133
+ const r = read(pid);
134
+ return r.kind === "start" ? r.token : null;
135
+ }
@@ -22,6 +22,11 @@ import {
22
22
  readSocketIdentity,
23
23
  type SocketIdentity,
24
24
  } from "./socket-ownership";
25
+ import {
26
+ readStartTime,
27
+ readOwnStartTime,
28
+ sameLiveProcess,
29
+ } from "./process-fingerprint";
25
30
  import { dlog, closeLog } from "./log";
26
31
  import {
27
32
  PROTOCOL_VERSION,
@@ -127,8 +132,16 @@ const BIN_CHECK_INTERVAL_MS = 60 * 1000;
127
132
  // connection. Any uncaught error exits non-zero; the next client obtains a
128
133
  // fresh daemon via obtainDaemonKick() (fire-and-forget caller) or
129
134
  // obtainDaemon() (caller waits for readiness) in src/daemon/acquire.ts.
135
+ // [LAW:one-source-of-truth] Our own kernel start-time, read once at startup and
136
+ // stamped into our lease so a future daemon's arbitration can prove whether our
137
+ // pid still names THIS process or a recycled ghost (process-fingerprint.ts).
138
+ // null when this host cannot fingerprint (no `ps`) — readers then fall back to
139
+ // kill(pid,0), no worse than before the fingerprint existed.
140
+ let myStartTime: string | null = null;
141
+
130
142
  export function runDaemon(): void {
131
143
  fs.mkdirSync(daemonDir(), { recursive: true });
144
+ myStartTime = readOwnStartTime(process.pid);
132
145
  // [LAW:single-enforcer] Verify the socket parent is uid==me + mode 0700 +
133
146
  // not a symlink before we bind. Without this check, a same-host attacker
134
147
  // could pre-create the predictable `/tmp/cc-candybar-<uid>` directory and
@@ -236,7 +249,11 @@ function handleAddressInUse(server: net.Server, sockPath: string): void {
236
249
  // [LAW:one-source-of-truth] Derive the lease from the SAME sockPath threaded
237
250
  // through unlink + rebind below, not the re-derived global — one identity
238
251
  // source for the whole arbitration.
239
- const decision = arbitrateSocket(readLease(leasePathFor(sockPath)), pidAlive);
252
+ const decision = arbitrateSocket(
253
+ readLease(leasePathFor(sockPath)),
254
+ (pid, startTime) =>
255
+ sameLiveProcess(pid, startTime, { readStartTime, pidAlive }),
256
+ );
240
257
  if (decision.kind === "attach-and-exit") {
241
258
  dlog("info", `EADDRINUSE: ${decision.reason} — exiting`);
242
259
  process.exit(0);
@@ -280,20 +297,19 @@ function onListening(sockPath: string): void {
280
297
  // the FD yields no path-comparable identity. stat(path), taken as close to the
281
298
  // bind as possible, is the only path-comparable truth available.
282
299
  //
283
- // [FRAMING:representation] Be honest about the ONE window this capture cannot
284
- // close. It catches an absent/unreadable path (below): we could not form a
285
- // fingerprint, so we have no proof of ownership and exit toward the SAFE
286
- // direction (a false-displaced daemon just lets whoever holds the path serve
287
- // no orphan) WITHOUT writing a lease that would stomp a thief's. It does NOT
288
- // catch a path that is present but already holds a THIEF's socket — if a second
289
- // daemon completed its full EADDRINUSE read-lease unlink rebind cycle in
290
- // the single event-loop tick between bind() and this 'listening' callback, we
291
- // fingerprint the thief's identity and the self-check reads `owned` forever
292
- // (the orphan this module exists to kill). That race is irreducible at this
293
- // layer (no race-free handle on our own socket's path-identity exists) and
294
- // vanishingly narrow; fully closing it needs the lock-based liveness the epic
295
- // deferred (a flock/start-time fingerprint) the same residual sibling .1's
296
- // arbitrateSocket documents for its false-alive direction.
300
+ // [FRAMING:representation] This inode capture still cannot close the capture
301
+ // race on its own: if a second daemon completed its full EADDRINUSE →
302
+ // read-lease unlink rebind cycle in the single event-loop tick between
303
+ // bind() and this 'listening' callback, we stat the path and capture the
304
+ // THIEF's inode as "ours". No race-free handle on our own socket's path
305
+ // identity exists (an AF_UNIX listener's fstat is a different namespace's
306
+ // inode), so the captured inode cannot be made trustworthy. But that is no
307
+ // longer the immortal orphan it was (brandon-daemon-lifecycle-2b3.4 RESIDUAL
308
+ // 2): the ownership self-check now ALSO requires the lease to still name us,
309
+ // and a real thief writes its own pid into the lease so a displaced daemon
310
+ // drains within a bounded number of intervals instead of reading `owned`
311
+ // forever (see checkOwnership). The absent/unreadable case below still exits
312
+ // toward the SAFE direction without writing a lease that would stomp a thief's.
297
313
  const boundRead = readSocketIdentity(sockPath);
298
314
  if (boundRead.kind !== "present") {
299
315
  dlog(
@@ -330,15 +346,20 @@ function onListening(sockPath: string): void {
330
346
  // --- socket-ownership self-check ---
331
347
  //
332
348
  // [LAW:single-enforcer] The sole enforcer of "serving implies owning the socket
333
- // path over time" (brandon-daemon-lifecycle-2b3.2). Periodically re-stats the
334
- // path; if its kernel identity no longer matches what we bound, we were
335
- // displaced (its socket unlinked + rebound by a reclaimer) and drain through the
336
- // SAME shutdown funnel as signals, the RSS backstop, and the watchdog — no
337
- // parallel exit path.
349
+ // path over time" (brandon-daemon-lifecycle-2b3.2). Each interval it re-reads
350
+ // BOTH representations a displacer can touch the path's kernel identity (still
351
+ // the inode we bound?) and the lease (still names our pid?) and drains through
352
+ // the SAME shutdown funnel as signals, the RSS backstop, and the watchdog on
353
+ // either mismatch. The lease arm closes the capture race (RESIDUAL 2): a thief
354
+ // that stole our socket inside the bind→listening tick, which the inode arm
355
+ // cannot see because we captured the thief's inode, is caught the moment the
356
+ // thief writes its own pid into the lease. No parallel exit path.
338
357
  function armOwnershipWatch(sockPath: string, bound: SocketIdentity): void {
339
358
  makeOwnershipWatch({
340
359
  bound,
360
+ myPid: process.pid,
341
361
  readIdentity: () => readSocketIdentity(sockPath),
362
+ readLease: () => readLease(leasePathFor(sockPath)),
342
363
  shutdown: (code) => shutdown(code),
343
364
  log: dlog,
344
365
  }).arm();
@@ -418,7 +439,7 @@ function writeLeaseFile(sockPath: string): void {
418
439
  pid: process.pid,
419
440
  version: PROTOCOL_VERSION,
420
441
  binPath: process.argv[1],
421
- startedAt: new Date().toISOString(),
442
+ startTime: myStartTime,
422
443
  });
423
444
  if (reason !== null) dlog("warn", `lease write failed: ${reason}`);
424
445
  }