agent-yes 1.247.0 → 1.247.1

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.
Files changed (31) hide show
  1. package/dist/{SUPPORTED_CLIS-B1DPQLRV.js → SUPPORTED_CLIS-C2ABCTI-.js} +3 -3
  2. package/dist/{SUPPORTED_CLIS-DUALQTec.js → SUPPORTED_CLIS-CUHk1XEQ.js} +2 -2
  3. package/dist/{agentShare-Clx5QcyE.js → agentShare-DNLrTMuA.js} +2 -2
  4. package/dist/{callback-BPAlieXe.js → callback-BCtFFO6C.js} +2 -2
  5. package/dist/{callback-DPd9PJQA.js → callback-D4dvKSSB.js} +3 -3
  6. package/dist/cli.js +5 -5
  7. package/dist/index.js +2 -2
  8. package/dist/{notifyDaemon-CAML5u0d.js → notifyDaemon-BkxrvCIr.js} +274 -27
  9. package/dist/{rustBinary-BU3E3uMi.js → rustBinary-Ai2OdeAn.js} +2 -2
  10. package/dist/{schedule-C8wzhq0y.js → schedule-CLxwtJLH.js} +4 -4
  11. package/dist/{serve-D_DP0VBy.js → serve-BBMEzRhm.js} +13 -13
  12. package/dist/{setup-DIa8Y-lx.js → setup-RZycHxht.js} +2 -2
  13. package/dist/{subcommands-DeHlaNOY.js → subcommands-BfAb-n_1.js} +106 -21
  14. package/dist/{subcommands-WJz-UHyC.js → subcommands-CJYXODZ1.js} +1 -1
  15. package/dist/{terminal-MnictO_E.js → terminal-BtDzyBzg.js} +2 -2
  16. package/dist/{ts-rCPxI3aR.js → ts-C6cxg8DC.js} +2 -2
  17. package/dist/{versionChecker-Du1qdZR9.js → versionChecker-Cc3tPea6.js} +2 -2
  18. package/dist/{widget-U6HkHQcz.js → widget-hr7bHKUW.js} +3 -3
  19. package/dist/{ws-1vGWz9c9.js → ws-BTGDj3Hj.js} +2 -2
  20. package/package.json +1 -1
  21. package/ts/notifyDaemon.spec.ts +28 -1
  22. package/ts/notifyDaemon.ts +73 -33
  23. package/ts/notifyInbox.spec.ts +84 -0
  24. package/ts/notifyInbox.ts +75 -0
  25. package/ts/notifyStore.spec.ts +67 -0
  26. package/ts/notifyStore.ts +60 -7
  27. package/ts/ownerHeartbeat.spec.ts +161 -0
  28. package/ts/ownerHeartbeat.ts +166 -0
  29. package/ts/procStartTime.spec.ts +119 -0
  30. package/ts/procStartTime.ts +122 -0
  31. package/ts/subcommands.ts +37 -6
@@ -1,11 +1,11 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import "./versionChecker-Du1qdZR9.js";
3
- import "./ts-rCPxI3aR.js";
2
+ import "./versionChecker-Cc3tPea6.js";
3
+ import "./ts-C6cxg8DC.js";
4
4
  import "./todoAutomation-Buo457VO.js";
5
5
  import "./JsonlStore-CPGIR0Wy.js";
6
6
  import "./globalPidIndex-D9DOSd-c.js";
7
7
  import "./pidStore-vnX0bgra.js";
8
8
  import "./messageLog-C-YIzhRj.js";
9
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DUALQTec.js";
9
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CUHk1XEQ.js";
10
10
 
11
11
  export { SUPPORTED_CLIS };
@@ -1,8 +1,8 @@
1
- import { t as CLIS_CONFIG } from "./ts-rCPxI3aR.js";
1
+ import { t as CLIS_CONFIG } from "./ts-C6cxg8DC.js";
2
2
 
3
3
  //#region ts/SUPPORTED_CLIS.ts
4
4
  const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
5
5
 
6
6
  //#endregion
7
7
  export { SUPPORTED_CLIS as t };
8
- //# sourceMappingURL=SUPPORTED_CLIS-DUALQTec.js.map
8
+ //# sourceMappingURL=SUPPORTED_CLIS-CUHk1XEQ.js.map
@@ -3,7 +3,7 @@ import "./globalPidIndex-D9DOSd-c.js";
3
3
  import "./messageLog-C-YIzhRj.js";
4
4
  import "./e2e-CEIdJGZ7.js";
5
5
  import "./configShared-IBbnn9b8.js";
6
- import { P as resolveOne } from "./subcommands-DeHlaNOY.js";
6
+ import { P as resolveOne } from "./subcommands-BfAb-n_1.js";
7
7
  import "./webrtcLink-BMYneHbu.js";
8
8
  import "./remotes-BxQnNxQv.js";
9
9
  import { a as startShare } from "./share-BGl3wutJ.js";
@@ -229,4 +229,4 @@ function transformEvent(rawEvent, agentId, forwarded) {
229
229
 
230
230
  //#endregion
231
231
  export { createScopedShare, listShares, revokeAllShares, revokeShare };
232
- //# sourceMappingURL=agentShare-Clx5QcyE.js.map
232
+ //# sourceMappingURL=agentShare-DNLrTMuA.js.map
@@ -3,10 +3,10 @@ import "./globalPidIndex-D9DOSd-c.js";
3
3
  import "./messageLog-C-YIzhRj.js";
4
4
  import "./e2e-CEIdJGZ7.js";
5
5
  import "./configShared-IBbnn9b8.js";
6
- import "./subcommands-DeHlaNOY.js";
6
+ import "./subcommands-BfAb-n_1.js";
7
7
  import "./webrtcLink-BMYneHbu.js";
8
8
  import "./remotes-BxQnNxQv.js";
9
9
  import "./callbackCore-Dd28_cBM.js";
10
- import { i as loadOrCreateCallbackSecret, n as isCallbackRevoked, r as loadCallbackSecretReadOnly, t as cmdCallback } from "./callback-DPd9PJQA.js";
10
+ import { i as loadOrCreateCallbackSecret, n as isCallbackRevoked, r as loadCallbackSecretReadOnly, t as cmdCallback } from "./callback-D4dvKSSB.js";
11
11
 
12
12
  export { cmdCallback };
@@ -1,5 +1,5 @@
1
1
  import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
2
- import { P as resolveOne } from "./subcommands-DeHlaNOY.js";
2
+ import { P as resolveOne } from "./subcommands-BfAb-n_1.js";
3
3
  import { a as mintCapability, o as parseExpires } from "./callbackCore-Dd28_cBM.js";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { chmodSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -72,7 +72,7 @@ async function resolveBase(flag) {
72
72
  if (flag) return flag;
73
73
  if (process.env.AGENT_YES_CALLBACK_BASE) return process.env.AGENT_YES_CALLBACK_BASE;
74
74
  try {
75
- const { resolveLocalServeUrl } = await import("./serve-D_DP0VBy.js");
75
+ const { resolveLocalServeUrl } = await import("./serve-BBMEzRhm.js");
76
76
  const url = await resolveLocalServeUrl();
77
77
  if (url) return url;
78
78
  } catch {}
@@ -187,4 +187,4 @@ async function cmdCallback(rest) {
187
187
 
188
188
  //#endregion
189
189
  export { loadOrCreateCallbackSecret as i, isCallbackRevoked as n, loadCallbackSecretReadOnly as r, cmdCallback as t };
190
- //# sourceMappingURL=callback-DPd9PJQA.js.map
190
+ //# sourceMappingURL=callback-D4dvKSSB.js.map
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
3
3
  import { n as logger } from "./logger-CDIsZ-Pp.js";
4
- import { i as versionString, n as displayVersion, t as checkAndAutoUpdate } from "./versionChecker-Du1qdZR9.js";
5
- import { n as getRustBinary } from "./rustBinary-BU3E3uMi.js";
4
+ import { i as versionString, n as displayVersion, t as checkAndAutoUpdate } from "./versionChecker-Cc3tPea6.js";
5
+ import { n as getRustBinary } from "./rustBinary-Ai2OdeAn.js";
6
6
  import { argv } from "process";
7
7
  import { spawn } from "child_process";
8
8
  import ms from "ms";
@@ -285,7 +285,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
285
285
  const rawArg = process.argv[2];
286
286
  const managerCommands = !invokedCliName(process.argv);
287
287
  const isHelpFlag = rawArg === "-h" || rawArg === "--help";
288
- const { isSubcommand, runSubcommand, cmdHelp, isUnknownManagerToken } = await import("./subcommands-WJz-UHyC.js");
288
+ const { isSubcommand, runSubcommand, cmdHelp, isUnknownManagerToken } = await import("./subcommands-CJYXODZ1.js");
289
289
  if (isHelpFlag && process.argv.length === 3) {
290
290
  await cmdHelp(managerCommands);
291
291
  process.exit(0);
@@ -295,7 +295,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
295
295
  process.exit(code ?? 0);
296
296
  }
297
297
  {
298
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-B1DPQLRV.js");
298
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-C2ABCTI-.js");
299
299
  if (isUnknownManagerToken(rawArg, managerCommands, SUPPORTED_CLIS)) {
300
300
  process.stderr.write(`ay: unknown subcommand or CLI '${rawArg}'.\n See 'ay help' for subcommands. To run an agent, name a CLI:\n 'ay <cli> …' (e.g. 'ay claude …') or 'ay --cli <cli> …'.\n`);
301
301
  process.exit(1);
@@ -346,7 +346,7 @@ if (config.useRust) {
346
346
  }
347
347
  }
348
348
  if (rustBinary) {
349
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-B1DPQLRV.js");
349
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-C2ABCTI-.js");
350
350
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
351
351
  if (config.verbose) {
352
352
  console.log(`[rust] Using binary: ${rustBinary}`);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import "./versionChecker-Du1qdZR9.js";
3
- import { a as hasTodoStore, i as answerAsk, l as AgentContext, n as agentYes, o as listAsks, r as config, s as listAsksForProject, t as CLIS_CONFIG } from "./ts-rCPxI3aR.js";
2
+ import "./versionChecker-Cc3tPea6.js";
3
+ import { a as hasTodoStore, i as answerAsk, l as AgentContext, n as agentYes, o as listAsks, r as config, s as listAsksForProject, t as CLIS_CONFIG } from "./ts-C6cxg8DC.js";
4
4
  import { _ as isKnownKind, a as renderTree, b as statesOf, c as monitorHint, d as openStore, f as DONE_STATE, g as initialState, h as canTransition, i as renderDigest, l as CycleError, m as ORPHANED_STATE, o as unblockedTasks, p as LIFECYCLES, r as openBlockers, s as describeBlock, t as reconcileTodos, u as TodoStore, v as nextStates, x as removeControlCharacters, y as requiredGate } from "./todoAutomation-Buo457VO.js";
5
5
  import "./JsonlStore-CPGIR0Wy.js";
6
6
  import "./globalPidIndex-D9DOSd-c.js";
@@ -3,12 +3,14 @@ import "./globalPidIndex-D9DOSd-c.js";
3
3
  import "./messageLog-C-YIzhRj.js";
4
4
  import "./e2e-CEIdJGZ7.js";
5
5
  import "./configShared-IBbnn9b8.js";
6
- import { $ as shouldStealLock, C as listRecords, J as hostId, K as appendEvent, Q as readInbox, X as listInboxParents, Z as liveWatchers, _ as isPidAlive, c as deriveLiveState, et as daemonLockDir, j as renderLogTailLines, nt as notifyDir, q as gcInboxes, tt as daemonLockOwnerPath } from "./subcommands-DeHlaNOY.js";
6
+ import { $ as shouldStealLock, C as listRecords, J as hostId, K as appendEvent, Q as readInbox, X as listInboxParents, Z as liveWatchers, _ as isPidAlive, c as deriveLiveState, et as daemonLockDir, j as renderLogTailLines, nt as notifyDir, q as gcInboxes, tt as daemonLockOwnerPath } from "./subcommands-BfAb-n_1.js";
7
7
  import "./webrtcLink-BMYneHbu.js";
8
8
  import "./remotes-BxQnNxQv.js";
9
9
  import { mkdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
10
10
  import { randomUUID } from "node:crypto";
11
- import { execFile } from "node:child_process";
11
+ import { execFile, execFileSync } from "node:child_process";
12
+ import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
13
+ import { Worker } from "node:worker_threads";
12
14
 
13
15
  //#region ts/notifyRouter.ts
14
16
  const DEFAULT_IDLE_CONFIRM_MS = 3e4;
@@ -146,6 +148,243 @@ function syntheticExited(cs, pid) {
146
148
  };
147
149
  }
148
150
 
151
+ //#endregion
152
+ //#region ts/procStartTime.ts
153
+ /**
154
+ * Best-effort OS process start time, as an OPAQUE token.
155
+ *
156
+ * Why: `ay notifyd`'s singleton lock records `{pid, started_at}` where
157
+ * `started_at` is OUR OWN wall-clock stamp from when the daemon acquired the
158
+ * lock. That proves nothing about the pid the OS currently has: a pid recycled
159
+ * onto an unrelated live process inside the owner TTL would pass `isPidAlive`
160
+ * and be trusted as "the daemon is running" — so `ensureDaemon` would decline to
161
+ * start a real one. Cross-checking the OS's own notion of when that pid started
162
+ * closes the window: the recycled process has a different start time.
163
+ *
164
+ * The token is deliberately opaque (a raw platform string, compared only for
165
+ * equality) — no epoch math, no clock-tick/boot-time arithmetic, nothing to get
166
+ * subtly wrong across platforms. It is only ever compared against a token this
167
+ * same code produced for the same pid.
168
+ *
169
+ * BEST-EFFORT by contract: every reader returns `null` when it cannot answer
170
+ * (unsupported platform, missing /proc, `ps` unavailable or slow). Callers MUST
171
+ * treat null as "no opinion" and fall back to the existing checks — this
172
+ * tightens an edge, it must never introduce a new way to fail.
173
+ *
174
+ * Issue #169 item 1.
175
+ */
176
+ /**
177
+ * Extract field 22 (`starttime`, in clock ticks since boot) from a Linux
178
+ * `/proc/<pid>/stat` line.
179
+ *
180
+ * The parse must start AFTER the last `)`: field 2 is the executable name in
181
+ * parentheses and may itself contain spaces and parens (`(my prog (v2))`), so
182
+ * naive whitespace splitting mis-indexes every later field. After the last `)`
183
+ * the remaining fields begin at field 3, hence `starttime` is index 19.
184
+ */
185
+ function parseLinuxStartTime(stat) {
186
+ const close = stat.lastIndexOf(")");
187
+ if (close < 0) return null;
188
+ const starttime = stat.slice(close + 1).trim().split(/\s+/)[19];
189
+ return starttime && /^\d+$/.test(starttime) ? starttime : null;
190
+ }
191
+ function defaultProcStat(pid) {
192
+ try {
193
+ return readFileSync(`/proc/${pid}/stat`, "utf8");
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+ function defaultPsLstart(pid) {
199
+ try {
200
+ return execFileSync("ps", [
201
+ "-o",
202
+ "lstart=",
203
+ "-p",
204
+ String(pid)
205
+ ], {
206
+ encoding: "utf8",
207
+ timeout: 500,
208
+ stdio: [
209
+ "ignore",
210
+ "pipe",
211
+ "ignore"
212
+ ]
213
+ });
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+ /** Normalize a raw reader result: trim, and treat an empty answer as "unknown". */
219
+ function normalize(raw) {
220
+ if (raw === null) return null;
221
+ const trimmed = raw.trim();
222
+ return trimmed === "" ? null : trimmed;
223
+ }
224
+ /**
225
+ * An opaque token identifying WHEN `pid` started, or null when unknowable.
226
+ *
227
+ * Windows deliberately returns null: the only readily available reader is a
228
+ * PowerShell `Get-Process` spawn (hundreds of ms), far too heavy for a path
229
+ * that runs every couple of seconds under `ay notify watch`.
230
+ */
231
+ function osProcessStartToken(pid, deps) {
232
+ if (!Number.isFinite(pid) || pid <= 0) return null;
233
+ const platform = deps?.platform ?? process.platform;
234
+ if (platform === "linux") {
235
+ const stat = (deps?.readProcStat ?? defaultProcStat)(pid);
236
+ return stat ? parseLinuxStartTime(stat) : null;
237
+ }
238
+ if (platform === "darwin" || platform === "freebsd" || platform === "openbsd") return normalize((deps?.readPsLstart ?? defaultPsLstart)(pid));
239
+ return null;
240
+ }
241
+ /**
242
+ * Does a RECORDED start token prove the pid has been recycled since?
243
+ *
244
+ * True only on positive disagreement — both sides present and different. A
245
+ * missing record (an owner written by an older build, or on a platform with no
246
+ * reader) or an unreadable current value yields false, i.e. "no opinion, keep
247
+ * trusting the other checks". Fail-open is correct here precisely because this
248
+ * is an ADDITIONAL guard layered on pid-liveness + heartbeat freshness.
249
+ */
250
+ function osStartTokenMismatch(recorded, current) {
251
+ if (typeof recorded !== "string" || recorded === "") return false;
252
+ if (current === null) return false;
253
+ return current !== recorded;
254
+ }
255
+
256
+ //#endregion
257
+ //#region ts/ownerHeartbeat.ts
258
+ /**
259
+ * The `ay notifyd` singleton lock's owner heartbeat, run on its OWN thread.
260
+ *
261
+ * Why a thread: the daemon proves it is alive by refreshing `ts` in the lock's
262
+ * `owner.json`; another `ay notify watch` steals the lock once that stamp is
263
+ * older than `OWNER_TTL`. On a single-threaded JS timer, a SYNCHRONOUS block in
264
+ * the daemon longer than the TTL would stall the heartbeat, let a second daemon
265
+ * take the lock, and leave two daemons writing the same inboxes. The loop is all
266
+ * `await`s and the TTL sits far above any realistic block, so that was always
267
+ * theoretical — but a single-threaded process can only bound it, never eliminate
268
+ * it. A worker thread does: its timer keeps firing no matter what the main
269
+ * thread is doing.
270
+ *
271
+ * The worker owns the whole beat (read token → verify → atomic rewrite), so a
272
+ * blocked main thread contributes nothing to keeping the lock alive. The owner
273
+ * payload is fixed for the daemon's lifetime — only `ts` moves — so there is no
274
+ * state to synchronize back.
275
+ *
276
+ * FENCING is preserved exactly as on the main thread: the beat refreshes only
277
+ * while `owner.json` still carries OUR token. Any other value — a different
278
+ * token (superseded) or none (another daemon's mkdir→write window) — means we no
279
+ * longer own the lock, so it stops rather than clobber an owner that isn't ours.
280
+ *
281
+ * Deliberately one-way: the beat never signals the daemon. Deciding to shut down
282
+ * stays with the daemon loop, which already re-reads the owner token every tick
283
+ * and exits when it isn't ours. Adding a second, cross-thread path to the same
284
+ * decision would only create a way for the two to disagree.
285
+ *
286
+ * The worker source is passed inline (`eval: true`) rather than shipped as a
287
+ * separate file: it needs no bundler entry, no dist path resolution, and works
288
+ * identically under both `bun` and `node` (verified on both).
289
+ *
290
+ * Falls back to a main-thread timer if a Worker can't be created for any reason,
291
+ * so this can only ever be as good as the previous behaviour — never worse.
292
+ *
293
+ * Issue #169 item 5.
294
+ */
295
+ const WORKER_SRC = `
296
+ const { workerData } = require("node:worker_threads");
297
+ const fs = require("node:fs");
298
+ const { ownerPath, token, payload, intervalMs } = workerData;
299
+
300
+ function currentToken() {
301
+ try {
302
+ return JSON.parse(fs.readFileSync(ownerPath, "utf8")).token ?? null;
303
+ } catch {
304
+ return null;
305
+ }
306
+ }
307
+
308
+ function beat() {
309
+ if (currentToken() !== token) return false;
310
+ // Atomic (write temp + rename), so a concurrent reader never sees a torn owner
311
+ // and a null read reliably means "no owner file" rather than "write in flight".
312
+ const tmp = ownerPath + "." + token + ".beat.tmp";
313
+ try {
314
+ fs.writeFileSync(tmp, JSON.stringify(Object.assign({}, payload, { ts: Date.now() })));
315
+ fs.renameSync(tmp, ownerPath);
316
+ } catch {
317
+ try { fs.unlinkSync(tmp); } catch {}
318
+ }
319
+ return true;
320
+ }
321
+
322
+ const timer = setInterval(() => {
323
+ if (!beat()) clearInterval(timer);
324
+ }, intervalMs);
325
+ `;
326
+ /**
327
+ * Main-thread fallback with identical semantics, used when a Worker can't be
328
+ * created. Exported so the specs can exercise it directly on every platform —
329
+ * it is the path that runs on any runtime where worker threads are unavailable.
330
+ */
331
+ function startInlineHeartbeat(opts) {
332
+ const timer = setInterval(() => {
333
+ let cur = null;
334
+ try {
335
+ cur = JSON.parse(readFileSync(opts.ownerPath, "utf8")).token ?? null;
336
+ } catch {
337
+ cur = null;
338
+ }
339
+ if (cur !== opts.token) {
340
+ clearInterval(timer);
341
+ return;
342
+ }
343
+ const tmp = `${opts.ownerPath}.${opts.token}.beat.tmp`;
344
+ try {
345
+ writeFileSync(tmp, JSON.stringify({
346
+ ...opts.payload,
347
+ ts: Date.now()
348
+ }));
349
+ renameSync(tmp, opts.ownerPath);
350
+ } catch {
351
+ try {
352
+ unlinkSync(tmp);
353
+ } catch {}
354
+ }
355
+ }, opts.intervalMs);
356
+ if (typeof timer.unref === "function") timer.unref();
357
+ return {
358
+ isolated: false,
359
+ stop: async () => {
360
+ clearInterval(timer);
361
+ }
362
+ };
363
+ }
364
+ function startOwnerHeartbeat(opts) {
365
+ try {
366
+ const worker = new Worker(WORKER_SRC, {
367
+ eval: true,
368
+ workerData: {
369
+ ownerPath: opts.ownerPath,
370
+ token: opts.token,
371
+ payload: opts.payload,
372
+ intervalMs: opts.intervalMs
373
+ }
374
+ });
375
+ worker.on("error", () => {});
376
+ if (typeof worker.unref === "function") worker.unref();
377
+ return {
378
+ isolated: true,
379
+ stop: async () => {
380
+ await worker.terminate().catch(() => {});
381
+ }
382
+ };
383
+ } catch {
384
+ return startInlineHeartbeat(opts);
385
+ }
386
+ }
387
+
149
388
  //#endregion
150
389
  //#region ts/notifyDaemon.ts
151
390
  /**
@@ -291,6 +530,7 @@ async function liveChildrenSnapshot() {
291
530
  }
292
531
  const OWNER_TTL_MS = 3e4;
293
532
  let daemonStartedAt = 0;
533
+ let daemonOsStart = null;
294
534
  let daemonToken = "";
295
535
  /** The token currently written in the daemon lock owner file, or null. */
296
536
  async function readDaemonOwnerToken() {
@@ -312,7 +552,8 @@ async function writeOwner() {
312
552
  pid: process.pid,
313
553
  started_at: daemonStartedAt,
314
554
  ts: Date.now(),
315
- token: daemonToken
555
+ token: daemonToken,
556
+ os_start: daemonOsStart
316
557
  }));
317
558
  await rename(tmp, daemonLockOwnerPath());
318
559
  return true;
@@ -335,6 +576,7 @@ async function acquireDaemonLock(isAlive = isPidAlive) {
335
576
  await mkdir(daemonLockDir(), { recursive: false });
336
577
  daemonStartedAt = Date.now();
337
578
  daemonToken = randomUUID();
579
+ daemonOsStart = osProcessStartToken(process.pid);
338
580
  if (!await writeOwner()) {
339
581
  await rm(daemonLockDir(), {
340
582
  recursive: true,
@@ -352,10 +594,12 @@ async function acquireDaemonLock(isAlive = isPidAlive) {
352
594
  try {
353
595
  owner = JSON.parse(raw);
354
596
  } catch {}
355
- if (owner && typeof owner.pid === "number" && owner.pid > 0 && owner.pid !== process.pid && typeof owner.ts === "number" && now - owner.ts <= OWNER_TTL_MS && isAlive(owner.pid)) return false;
356
- if (shouldStealLock(raw, now, {
597
+ const recycled = owner && typeof owner.pid === "number" && owner.pid > 0 ? osStartTokenMismatch(owner.os_start, osProcessStartToken(owner.pid)) : false;
598
+ if (owner && typeof owner.pid === "number" && owner.pid > 0 && owner.pid !== process.pid && typeof owner.ts === "number" && now - owner.ts <= OWNER_TTL_MS && isAlive(owner.pid) && !recycled) return false;
599
+ const lockAgeMs = now - await stat(daemonLockDir()).then((s) => s.mtimeMs).catch(() => now);
600
+ if (recycled || shouldStealLock(raw, now, {
357
601
  staleMs: OWNER_TTL_MS,
358
- lockAgeMs: now - await stat(daemonLockDir()).then((s) => s.mtimeMs).catch(() => now),
602
+ lockAgeMs,
359
603
  selfPid: process.pid,
360
604
  isAlive
361
605
  })) {
@@ -386,19 +630,20 @@ async function runDaemon(opts = {}) {
386
630
  return 0;
387
631
  }
388
632
  let running = true;
389
- const ownerBeat = setInterval(() => {
390
- (async () => {
391
- if (await readDaemonOwnerToken() !== daemonToken) {
392
- clearInterval(ownerBeat);
393
- return;
394
- }
395
- await writeOwner();
396
- })();
397
- }, Math.max(1e3, Math.floor(OWNER_TTL_MS / 3)));
398
- if (typeof ownerBeat.unref === "function") ownerBeat.unref();
633
+ const ownerBeat = startOwnerHeartbeat({
634
+ ownerPath: daemonLockOwnerPath(),
635
+ token: daemonToken,
636
+ payload: {
637
+ pid: process.pid,
638
+ started_at: daemonStartedAt,
639
+ token: daemonToken,
640
+ os_start: daemonOsStart
641
+ },
642
+ intervalMs: Math.max(1e3, Math.floor(OWNER_TTL_MS / 3))
643
+ });
399
644
  const cleanup = async () => {
400
645
  running = false;
401
- clearInterval(ownerBeat);
646
+ await ownerBeat.stop();
402
647
  if (await readDaemonOwnerToken() === daemonToken) await rm(daemonLockDir(), {
403
648
  recursive: true,
404
649
  force: true
@@ -512,23 +757,25 @@ async function gcTick(host) {
512
757
  * kill an unrelated process. `stop` re-reads and re-checks this identity right
513
758
  * before signalling, so it can't SIGTERM a pid recycled between status and stop.
514
759
  *
515
- * Residual edge (documented, not yet closed): a pid recycled onto an UNRELATED
516
- * live process WITHIN the tight OWNER_TTL window (a few ticks) could be briefly
517
- * trusted, since we compare our recorded `started_at` against the owner file, not
518
- * against the OS's actual process start time (which is non-portable to read:
519
- * `/proc/<pid>/stat` on Linux vs `proc_pidinfo`/`ps -o lstart` on macOS). The
520
- * heartbeat freshness + tight TTL keep the window to seconds; a real OS
521
- * start-time cross-check is deferred to a future issue.
760
+ * The pid-recycled-inside-the-TTL window is closed by an OS start-time
761
+ * cross-check (issue #169 item 1): the owner file records the OS's own start
762
+ * time for the daemon pid at acquire, and every read re-reads it and rejects a
763
+ * disagreement an unrelated process that inherited the pid has a different
764
+ * start time and is never reported as "the daemon". The reader is best-effort
765
+ * per platform (`/proc/<pid>/stat` on Linux, `ps -o lstart` on macOS/BSD, none
766
+ * on Windows); where it can't answer, behaviour is exactly as before the
767
+ * heartbeat freshness + tight TTL still bound the window to seconds.
522
768
  */
523
769
  async function daemonIdentity(now = Date.now()) {
524
770
  const raw = await readFile(daemonLockOwnerPath(), "utf8").catch(() => "");
525
771
  try {
526
772
  const o = JSON.parse(raw);
527
- if (typeof o?.pid === "number" && o.pid > 0 && typeof o.started_at === "number" && o.started_at > 0 && typeof o.ts === "number" && now - o.ts <= OWNER_TTL_MS && isPidAlive(o.pid)) return {
773
+ if (typeof o?.pid === "number" && o.pid > 0 && typeof o.started_at === "number" && o.started_at > 0 && typeof o.ts === "number" && now - o.ts <= OWNER_TTL_MS && isPidAlive(o.pid) && !osStartTokenMismatch(o.os_start, osProcessStartToken(o.pid))) return {
528
774
  pid: o.pid,
529
775
  started_at: o.started_at,
530
776
  ts: o.ts,
531
- token: typeof o.token === "string" ? o.token : null
777
+ token: typeof o.token === "string" ? o.token : null,
778
+ os_start: typeof o.os_start === "string" ? o.os_start : null
532
779
  };
533
780
  } catch {}
534
781
  return null;
@@ -589,4 +836,4 @@ async function ensureDaemon() {
589
836
 
590
837
  //#endregion
591
838
  export { daemonStatus, ensureDaemon, requestDaemonStop, runDaemon };
592
- //# sourceMappingURL=notifyDaemon-CAML5u0d.js.map
839
+ //# sourceMappingURL=notifyDaemon-BkxrvCIr.js.map
@@ -1,4 +1,4 @@
1
- import { r as getInstalledPackage } from "./versionChecker-Du1qdZR9.js";
1
+ import { r as getInstalledPackage } from "./versionChecker-Cc3tPea6.js";
2
2
  import { execFileSync } from "child_process";
3
3
  import { existsSync, mkdirSync, unlinkSync } from "fs";
4
4
  import { chmod, copyFile } from "fs/promises";
@@ -225,4 +225,4 @@ async function getRustBinary(options = {}) {
225
225
 
226
226
  //#endregion
227
227
  export { getRustBinary as n, findSpawnHiddenLauncher as t };
228
- //# sourceMappingURL=rustBinary-BU3E3uMi.js.map
228
+ //# sourceMappingURL=rustBinary-Ai2OdeAn.js.map
@@ -1,12 +1,12 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import "./versionChecker-Du1qdZR9.js";
3
- import "./ts-rCPxI3aR.js";
2
+ import "./versionChecker-Cc3tPea6.js";
3
+ import "./ts-C6cxg8DC.js";
4
4
  import "./todoAutomation-Buo457VO.js";
5
5
  import "./JsonlStore-CPGIR0Wy.js";
6
6
  import "./globalPidIndex-D9DOSd-c.js";
7
7
  import "./pidStore-vnX0bgra.js";
8
8
  import "./messageLog-C-YIzhRj.js";
9
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DUALQTec.js";
9
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CUHk1XEQ.js";
10
10
  import { f as resolveSpawnCwd } from "./workspaceConfig-CpilQzMY.js";
11
11
  import { n as liveEnv, t as ensureNodeRuntime } from "./nodeRuntime-BtL6Oo1o.js";
12
12
  import { createHash } from "node:crypto";
@@ -153,4 +153,4 @@ async function cmdSchedule(rest) {
153
153
 
154
154
  //#endregion
155
155
  export { cmdSchedule };
156
- //# sourceMappingURL=schedule-C8wzhq0y.js.map
156
+ //# sourceMappingURL=schedule-CLxwtJLH.js.map
@@ -1,7 +1,7 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import { r as getInstalledPackage } from "./versionChecker-Du1qdZR9.js";
3
- import { t as findSpawnHiddenLauncher } from "./rustBinary-BU3E3uMi.js";
4
- import { c as permissionBadge, i as answerAsk, o as listAsks, u as CLAUDE_SESSION_PIN_ENV } from "./ts-rCPxI3aR.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-Cc3tPea6.js";
3
+ import { t as findSpawnHiddenLauncher } from "./rustBinary-Ai2OdeAn.js";
4
+ import { c as permissionBadge, i as answerAsk, o as listAsks, u as CLAUDE_SESSION_PIN_ENV } from "./ts-C6cxg8DC.js";
5
5
  import { t as agentYesHome$1 } from "./agentYesHome-CtmsY6I-.js";
6
6
  import { x as removeControlCharacters } from "./todoAutomation-Buo457VO.js";
7
7
  import "./JsonlStore-CPGIR0Wy.js";
@@ -11,12 +11,12 @@ import { r as recordInbox } from "./messageLog-C-YIzhRj.js";
11
11
  import { t as pgidForWrapper } from "./reaper-DP49VCKx.js";
12
12
  import "./e2e-CEIdJGZ7.js";
13
13
  import "./configShared-IBbnn9b8.js";
14
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DUALQTec.js";
15
- import { A as recentReadEdges, C as listRecords, D as readNotes, G as writeToIpc, M as renderRawLog, N as renderRawLogLines, O as readPtysize, P as resolveOne, Y as isTransientLockMkdirError, f as extractNeedsInput, j as renderLogTailLines, k as recentMessageEdges, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, rt as TYPING_BADGE, u as extractBadges, x as isUserTyping, z as snapshotStatus } from "./subcommands-DeHlaNOY.js";
14
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CUHk1XEQ.js";
15
+ import { A as recentReadEdges, C as listRecords, D as readNotes, G as writeToIpc, M as renderRawLog, N as renderRawLogLines, O as readPtysize, P as resolveOne, Y as isTransientLockMkdirError, f as extractNeedsInput, j as renderLogTailLines, k as recentMessageEdges, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, rt as TYPING_BADGE, u as extractBadges, x as isUserTyping, z as snapshotStatus } from "./subcommands-BfAb-n_1.js";
16
16
  import "./webrtcLink-BMYneHbu.js";
17
17
  import "./remotes-BxQnNxQv.js";
18
18
  import { i as frameVisitorMessage, s as verifyCapability, t as MAX_CALLBACK_MSG_BYTES } from "./callbackCore-Dd28_cBM.js";
19
- import { n as isCallbackRevoked, r as loadCallbackSecretReadOnly } from "./callback-DPd9PJQA.js";
19
+ import { n as isCallbackRevoked, r as loadCallbackSecretReadOnly } from "./callback-D4dvKSSB.js";
20
20
  import { a as getProvisionRoot, d as isProvisionAllowed, f as resolveSpawnCwd, i as getProvisionHook, l as hasProvisionHook, o as getSpawnHook, t as allowsSkipPermissions, u as hasSpawnHook } from "./workspaceConfig-CpilQzMY.js";
21
21
  import { n as liveEnv, t as ensureNodeRuntime } from "./nodeRuntime-BtL6Oo1o.js";
22
22
  import { r as spawnRejectionReason } from "./spawnGate-BFJPdHCQ.js";
@@ -2306,7 +2306,7 @@ Options:
2306
2306
  }
2307
2307
  });
2308
2308
  if (req.method === "GET" && p === "/api/ws") {
2309
- const ws = await import("./ws-1vGWz9c9.js");
2309
+ const ws = await import("./ws-BTGDj3Hj.js");
2310
2310
  try {
2311
2311
  await ws.loadProvision();
2312
2312
  } catch (e) {
@@ -2326,7 +2326,7 @@ Options:
2326
2326
  if (req.method === "GET" && p === "/api/ws/status") {
2327
2327
  const dirRaw = url.searchParams.get("path");
2328
2328
  if (!dirRaw) return new Response("missing ?path=<workspace dir>", { status: 400 });
2329
- const ws = await import("./ws-1vGWz9c9.js");
2329
+ const ws = await import("./ws-BTGDj3Hj.js");
2330
2330
  let prov;
2331
2331
  try {
2332
2332
  prov = await ws.loadProvision();
@@ -3079,7 +3079,7 @@ Options:
3079
3079
  const perm = body.perm ?? "r";
3080
3080
  if (perm !== "r" && perm !== "rw") return new Response(`invalid perm ${perm} (want r or rw)`, { status: 400 });
3081
3081
  try {
3082
- const { createScopedShare } = await import("./agentShare-Clx5QcyE.js");
3082
+ const { createScopedShare } = await import("./agentShare-DNLrTMuA.js");
3083
3083
  const share = await createScopedShare({
3084
3084
  agent: body.agent,
3085
3085
  perm,
@@ -3094,12 +3094,12 @@ Options:
3094
3094
  }
3095
3095
  }
3096
3096
  if (req.method === "GET" && p === "/api/shares") {
3097
- const { listShares } = await import("./agentShare-Clx5QcyE.js");
3097
+ const { listShares } = await import("./agentShare-DNLrTMuA.js");
3098
3098
  return Response.json(listShares());
3099
3099
  }
3100
3100
  const revokeM = /^\/api\/share\/([^/]+)$/.exec(p);
3101
3101
  if (req.method === "DELETE" && revokeM) {
3102
- const { revokeShare } = await import("./agentShare-Clx5QcyE.js");
3102
+ const { revokeShare } = await import("./agentShare-DNLrTMuA.js");
3103
3103
  const ok = revokeShare(decodeURIComponent(revokeM[1]));
3104
3104
  return new Response(ok ? "revoked" : "no such share", { status: ok ? 200 : 404 });
3105
3105
  }
@@ -3401,7 +3401,7 @@ Options:
3401
3401
  const shutdown = (resolve) => {
3402
3402
  if (heartbeat) clearInterval(heartbeat);
3403
3403
  closeShare?.();
3404
- import("./agentShare-Clx5QcyE.js").then((m) => m.revokeAllShares()).catch(() => {});
3404
+ import("./agentShare-DNLrTMuA.js").then((m) => m.revokeAllShares()).catch(() => {});
3405
3405
  server?.stop();
3406
3406
  Promise.resolve(releaseHostLock?.()).catch(() => {}).then(resolve);
3407
3407
  };
@@ -3414,4 +3414,4 @@ Options:
3414
3414
 
3415
3415
  //#endregion
3416
3416
  export { cmdServe, loadTokenReadOnly, mintScopedTermToken, resolveDaemonHttpBase, resolveLocalServeUrl };
3417
- //# sourceMappingURL=serve-D_DP0VBy.js.map
3417
+ //# sourceMappingURL=serve-BBMEzRhm.js.map
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
32
32
  if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
33
33
  if (noShare) return 0;
34
34
  process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
35
- const { cmdServe } = await import("./serve-D_DP0VBy.js");
35
+ const { cmdServe } = await import("./serve-BBMEzRhm.js");
36
36
  return cmdServe([
37
37
  "install",
38
38
  "--share",
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
42
42
 
43
43
  //#endregion
44
44
  export { cmdSetup };
45
- //# sourceMappingURL=setup-DIa8Y-lx.js.map
45
+ //# sourceMappingURL=setup-RZycHxht.js.map