agent-yes 1.288.0 → 1.289.0

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 (56) hide show
  1. package/default.config.yaml +106 -0
  2. package/dist/{SUPPORTED_CLIS-BHowMgMY.js → SUPPORTED_CLIS-BlgVWZrM.js} +2 -2
  3. package/dist/{SUPPORTED_CLIS-ChhJyq4h.js → SUPPORTED_CLIS-C65PuIs6.js} +3 -3
  4. package/dist/{agentShare-T3RskvDT.js → agentShare-Cm15-dfZ.js} +3 -3
  5. package/dist/{callback-CuohbCJA.js → callback-Bbplrsbv.js} +3 -3
  6. package/dist/{callback-DeaETuHt.js → callback-Dwnkeqpz.js} +3 -3
  7. package/dist/cli.js +11 -11
  8. package/dist/cmdPs-BYBZQ0Vc.js +331 -0
  9. package/dist/{cwdConflictWarn-DIPD6HH5.js → cwdConflictWarn-BgXN56Jc.js} +1 -1
  10. package/dist/{cwdPassthroughHint-C0tfZlgZ.js → cwdPassthroughHint-CDEGVteh.js} +1 -1
  11. package/dist/{expose-Ds9aUFsq.js → expose-BlddYYx9.js} +1 -1
  12. package/dist/{forkNested-Dt5XP8ZH.js → forkNested-CJ7lQQCe.js} +1 -1
  13. package/dist/{hist-DSYaJEd3.js → hist-DZxq7NMA.js} +1 -1
  14. package/dist/index.js +2 -2
  15. package/dist/{initMsg-CeF4QNOM.js → initMsg-DoltWTZj.js} +1 -1
  16. package/dist/{initMsg-DejbYYjX.js → initMsg-axyQAPLK.js} +1 -1
  17. package/dist/{nodeRuntime-_zeWDISM.js → nodeRuntime-CipjIwfO.js} +1 -1
  18. package/dist/{notifyDaemon-BORqPeAl.js → notifyDaemon-Q3FF-WEZ.js} +47 -39
  19. package/dist/{openBrowser-BNP8xByu.js → openBrowser-THOzOGNm.js} +1 -1
  20. package/dist/{parentLink-Bv8ihRcu.js → parentLink-bpDGuQi6.js} +1 -1
  21. package/dist/{parentLink-HWunrH1s.js → parentLink-fdiuB13i.js} +1 -1
  22. package/dist/{parentPingLoop-COePRYr9.js → parentPingLoop-Dc7co5D7.js} +4 -4
  23. package/dist/{cmdPs-Bm4JG7hi.js → procStats-CyD8h5nx.js} +2 -329
  24. package/dist/{remotes-b6reVXzo.js → remotes-BjsglpjL.js} +2 -2
  25. package/dist/{remotes-BlHTR9ow.js → remotes-CAqtL_Ib.js} +1 -1
  26. package/dist/{rustBinary-BmXzqj5z.js → rustBinary-B57oRftk.js} +2 -2
  27. package/dist/{rustBinary-D12k7iOD.js → rustBinary-Dq0AytZ7.js} +2 -2
  28. package/dist/{schedule-DLoUmyJh.js → schedule-pim0Izhs.js} +6 -6
  29. package/dist/{serve-DaDlOtss.js → serve-CMsvjqPU.js} +174 -27
  30. package/dist/{setup-Cqy8nRrq.js → setup-BmxYINCx.js} +4 -4
  31. package/dist/{spawnGate-B2hA_2NQ.js → spawnGate-B26UnzqP.js} +2 -2
  32. package/dist/{spawnGate-FLwTAPW6.js → spawnGate-C8MSRt7X.js} +1 -1
  33. package/dist/{subcommands-DKTa3E6O.js → subcommands-CJBV06rg.js} +2 -2
  34. package/dist/{subcommands-BfiOyfz7.js → subcommands-CWDqhKQe.js} +21 -21
  35. package/dist/{systemPathLink-Dl2gJqeg.js → systemPathLink-BVdk48Vz.js} +1 -1
  36. package/dist/{terminal-C0tnGnrZ.js → terminal-CcmBzB3G.js} +2 -2
  37. package/dist/{todoCli-MWrejmdM.js → todoCli-azqbjBtz.js} +1 -1
  38. package/dist/{tray-BhPVpP_r.js → tray-CIt9mCKZ.js} +1 -1
  39. package/dist/trayApp-BovAZs7m.js +5 -0
  40. package/dist/{trayApp-DvL9qW6V.js → trayApp-DuZBkGl_.js} +2 -2
  41. package/dist/{ts-DJLp2_bv.js → ts-CSp4mJ7Z.js} +23 -9
  42. package/dist/{versionChecker-CkO7wYyV.js → versionChecker-CWCN4vwm.js} +2 -2
  43. package/dist/{webrtcRemote-H9Hok0_R.js → webrtcRemote-Bevc5ogS.js} +1 -1
  44. package/dist/{widget-CJqpScCG.js → widget-D2cCC8nz.js} +3 -3
  45. package/dist/{workspaceConfig-D6QndOCz.js → workspaceConfig-DTvPtr5j.js} +1 -1
  46. package/dist/{ws-DGENGNrr.js → ws-B5Qb2c-1.js} +3 -3
  47. package/dist/{ws-DzCKvzWD.js → ws-CyO9r4jx.js} +3 -3
  48. package/package.json +1 -1
  49. package/ts/agentNice.spec.ts +42 -2
  50. package/ts/index.ts +5 -4
  51. package/ts/notifyDaemon.spec.ts +16 -0
  52. package/ts/notifyDaemon.ts +22 -0
  53. package/ts/serve.ts +14 -0
  54. package/ts/serveSampler.spec.ts +390 -0
  55. package/ts/serveSampler.ts +218 -0
  56. package/dist/trayApp-BrAMlCY7.js +0 -5
@@ -1,6 +1,6 @@
1
1
  import { n as __esmMin, r as __exportAll } from "./chunk-cZBX9HZv.js";
2
2
  import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
3
- import { i as getInstalledPackage } from "./versionChecker-CkO7wYyV.js";
3
+ import { i as getInstalledPackage } from "./versionChecker-CWCN4vwm.js";
4
4
  import { t as agentYesHome } from "./agentYesHome-BNtn19J4.js";
5
5
  import { u as removeControlCharacters } from "./todoAutomation-CbVVS4re.js";
6
6
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-BOIPD_Vs.js";
@@ -248,6 +248,20 @@ async function getSessionForCwd(cwd) {
248
248
  * Extract session ID from codex output
249
249
  * Session IDs are UUIDs in the format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
250
250
  */
251
+ /**
252
+ * True for the codex CLI and every codex-derived variant (`codex-ds`,
253
+ * `codex-ds-direct`, …) — they all run the same `codex` binary and print the
254
+ * same session-id banner, so the session capture/resume path must treat them
255
+ * alike. Keying those sites on an exact `=== "codex"` made `--continue` on a
256
+ * variant silently start a fresh session instead of resuming.
257
+ *
258
+ * The per-cwd session store is therefore shared across codex variants. That is
259
+ * fine: resuming re-applies the launching CLI's own `defaultArgs`, so a session
260
+ * resumed via `codex-ds` still talks to DeepSeek.
261
+ */
262
+ function isCodexFamily(cli) {
263
+ return cli === "codex" || cli.startsWith("codex-");
264
+ }
251
265
  function extractSessionId(output) {
252
266
  const match = output.match(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i);
253
267
  return match ? match[0] : null;
@@ -1599,10 +1613,10 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
1599
1613
  })();
1600
1614
  const originalPrompt = prompt;
1601
1615
  if (prompt && inheritedParentPid) try {
1602
- const { resolveSpawner } = await import("./parentLink-HWunrH1s.js");
1616
+ const { resolveSpawner } = await import("./parentLink-fdiuB13i.js");
1603
1617
  const spawner = await resolveSpawner(inheritedParentPid);
1604
1618
  if (spawner) {
1605
- const { buildInitMsg } = await import("./initMsg-CeF4QNOM.js");
1619
+ const { buildInitMsg } = await import("./initMsg-DoltWTZj.js");
1606
1620
  const { formatIdentity } = await import("./identity-DFr3dfTO.js");
1607
1621
  const identity = formatIdentity({
1608
1622
  cwd: spawner.cwd,
@@ -1666,7 +1680,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
1666
1680
  prompt = prompt ? `[${hint}]\n\n${prompt}` : hint;
1667
1681
  }
1668
1682
  } catch {}
1669
- if (resume) if (cli === "codex" && resume) {
1683
+ if (resume) if (isCodexFamily(cli) && resume) {
1670
1684
  const storedSessionId = await getSessionForCwd(workingDir);
1671
1685
  if (storedSessionId) {
1672
1686
  cliArgs = [
@@ -1905,7 +1919,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
1905
1919
  logger.warn(`[pidStore] Failed to update status for PID ${exitedPid}:`, error);
1906
1920
  }
1907
1921
  let restoreArgs = conf.restoreArgs;
1908
- if (cli === "codex") {
1922
+ if (isCodexFamily(cli)) {
1909
1923
  const storedSessionId = await getSessionForCwd(workingDir);
1910
1924
  if (storedSessionId) {
1911
1925
  restoreArgs = ["resume", storedSessionId];
@@ -2057,7 +2071,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
2057
2071
  await exitAgent();
2058
2072
  break;
2059
2073
  }
2060
- if (cli === "codex") {
2074
+ if (isCodexFamily(cli)) {
2061
2075
  const sessionId = extractSessionId(line);
2062
2076
  if (sessionId) {
2063
2077
  logger.debug(`heartbeat|session|captured session ID: ${sessionId}`);
@@ -2071,7 +2085,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
2071
2085
  }, 100);
2072
2086
  const cleanupHeartbeat = () => clearInterval(heartbeatInterval);
2073
2087
  shell.onExit(cleanupHeartbeat);
2074
- const { startParentPingLoop } = await import("./parentPingLoop-COePRYr9.js");
2088
+ const { startParentPingLoop } = await import("./parentPingLoop-Dc7co5D7.js");
2075
2089
  const parentPingLoop = startParentPingLoop({
2076
2090
  parentPid,
2077
2091
  selfWrapperPid: process.pid,
@@ -2283,7 +2297,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
2283
2297
  ctx.isFatal = true;
2284
2298
  await exitAgent();
2285
2299
  }
2286
- if (cli === "codex") {
2300
+ if (isCodexFamily(cli)) {
2287
2301
  const sessionId = extractSessionId(line);
2288
2302
  if (sessionId) {
2289
2303
  logger.debug(`session|captured session ID: ${sessionId}`);
@@ -2334,4 +2348,4 @@ function sleep(ms) {
2334
2348
 
2335
2349
  //#endregion
2336
2350
  export { hasTodoStore as a, permissionBadge as c, answerAsk as i, AgentContext as l, agentYes as n, listAsks as o, config as r, listAsksForProject as s, CLIS_CONFIG as t, CLAUDE_SESSION_PIN_ENV as u };
2337
- //# sourceMappingURL=ts-DJLp2_bv.js.map
2351
+ //# sourceMappingURL=ts-CSp4mJ7Z.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "agent-yes";
10
- var version = "1.288.0";
10
+ var version = "1.289.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -215,4 +215,4 @@ async function displayVersion() {
215
215
 
216
216
  //#endregion
217
217
  export { versionString as a, getInstalledPackage as i, compareVersions as n, displayVersion as r, checkAndAutoUpdate as t };
218
- //# sourceMappingURL=versionChecker-CkO7wYyV.js.map
218
+ //# sourceMappingURL=versionChecker-CWCN4vwm.js.map
@@ -256,4 +256,4 @@ async function startWebrtcBridge(link) {
256
256
 
257
257
  //#endregion
258
258
  export { startWebrtcBridge };
259
- //# sourceMappingURL=webrtcRemote-H9Hok0_R.js.map
259
+ //# sourceMappingURL=webrtcRemote-Bevc5ogS.js.map
@@ -28,7 +28,7 @@ function parseFlags(args, known) {
28
28
  }
29
29
  /** Resolve the local daemon base URL + token (flags override discovery). */
30
30
  async function daemonTarget(flags) {
31
- const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-DaDlOtss.js");
31
+ const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-CMsvjqPU.js");
32
32
  const base = typeof flags.base === "string" ? flags.base : await resolveDaemonHttpBase();
33
33
  if (!base) throw new Error("no running ay serve daemon found — start `ay serve` or pass --base <url> (e.g. http://127.0.0.1:PORT)");
34
34
  const token = typeof flags.token === "string" ? flags.token : await loadTokenReadOnly() ?? "";
@@ -186,7 +186,7 @@ async function cmdMint(args) {
186
186
  const bad = caps.filter((c) => !KNOWN_CAPS.has(c));
187
187
  if (bad.length) throw new Error(`unknown cap(s): ${bad.join(", ")} (valid: ${[...KNOWN_CAPS].join(", ")})`);
188
188
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
189
- const { mintScopedTermToken } = await import("./serve-DaDlOtss.js");
189
+ const { mintScopedTermToken } = await import("./serve-CMsvjqPU.js");
190
190
  const r = await mintScopedTermToken(target, {
191
191
  ttlSec,
192
192
  caps
@@ -202,4 +202,4 @@ async function cmdMint(args) {
202
202
 
203
203
  //#endregion
204
204
  export { cmdMint, cmdWidget };
205
- //# sourceMappingURL=widget-CJqpScCG.js.map
205
+ //# sourceMappingURL=widget-D2cCC8nz.js.map
@@ -215,4 +215,4 @@ function allowsSkipPermissions() {
215
215
 
216
216
  //#endregion
217
217
  export { getProvisionRoot as a, getWorkspaceRoot as c, isProvisionAllowed as d, resolveSpawnCwd as f, getProvisionHook as i, hasProvisionHook as l, getMaxAgents as n, getSpawnHook as o, setWorkspaceRoot as p, getMinFreeMb as r, getSpawnWaitMs as s, allowsSkipPermissions as t, hasSpawnHook as u };
218
- //# sourceMappingURL=workspaceConfig-D6QndOCz.js.map
218
+ //# sourceMappingURL=workspaceConfig-DTvPtr5j.js.map
@@ -1,5 +1,5 @@
1
- import { D as listRecords } from "./subcommands-BfiOyfz7.js";
2
- import { a as getProvisionRoot } from "./workspaceConfig-D6QndOCz.js";
1
+ import { D as listRecords } from "./subcommands-CWDqhKQe.js";
2
+ import { a as getProvisionRoot } from "./workspaceConfig-DTvPtr5j.js";
3
3
  import { existsSync } from "fs";
4
4
  import { lstat, opendir } from "fs/promises";
5
5
  import path from "path";
@@ -424,4 +424,4 @@ async function cmdWs(args) {
424
424
 
425
425
  //#endregion
426
426
  export { isPathInside as a, walkWorkspaces as c, importProvisionModule as i, workspaceStatus as l, cmdWs as n, loadProvision as o, collectWorkspaces as r, resolveOperand as s, WS_JSON_SCHEMA as t };
427
- //# sourceMappingURL=ws-DGENGNrr.js.map
427
+ //# sourceMappingURL=ws-B5Qb2c-1.js.map
@@ -3,10 +3,10 @@ import "./globalPidIndex-DoaKEUkO.js";
3
3
  import "./messageLog-DdjBNqgt.js";
4
4
  import "./e2e-z1xpwSAg.js";
5
5
  import "./configShared-DKTgiz7K.js";
6
- import "./subcommands-BfiOyfz7.js";
6
+ import "./subcommands-CWDqhKQe.js";
7
7
  import "./notifyStore-2SWKGwZI.js";
8
8
  import "./webrtcLink-DLaJfTcB.js";
9
- import "./remotes-b6reVXzo.js";
10
- import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-DGENGNrr.js";
9
+ import "./remotes-BjsglpjL.js";
10
+ import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-B5Qb2c-1.js";
11
11
 
12
12
  export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, walkWorkspaces, workspaceStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.288.0",
3
+ "version": "1.289.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,5 +1,15 @@
1
- import { describe, expect, it } from "vitest";
2
- import { agentNiceValue } from "./agentNice.ts";
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ // applyAgentNice reaches setPriority through a dynamic `await import("os")`, so
4
+ // the module has to be mocked rather than spied — a namespace object's exports
5
+ // are frozen and a spy on them never takes effect.
6
+ const setPriority = vi.fn();
7
+ vi.mock("os", async (importActual) => {
8
+ const actual = await importActual<typeof import("os")>();
9
+ return { ...actual, default: { ...actual, setPriority }, setPriority };
10
+ });
11
+
12
+ import { agentNiceValue, applyAgentNice } from "./agentNice.ts";
3
13
 
4
14
  describe("agentNice.agentNiceValue", () => {
5
15
  it("defaults to 5 when unset or empty", () => {
@@ -22,3 +32,33 @@ describe("agentNice.agentNiceValue", () => {
22
32
  expect(agentNiceValue({ AGENT_YES_AGENT_NICE: "3.9" })).toBe(3); // truncates
23
33
  });
24
34
  });
35
+
36
+ describe("agentNice.applyAgentNice", () => {
37
+ afterEach(() => {
38
+ setPriority.mockReset();
39
+ });
40
+
41
+ it("deprioritizes the spawned pid to the configured nice", async () => {
42
+ await applyAgentNice(4242, { AGENT_YES_AGENT_NICE: "7" });
43
+
44
+ expect(setPriority).toHaveBeenCalledWith(4242, 7);
45
+ });
46
+
47
+ it("does nothing when nice is 0 (explicitly disabled)", async () => {
48
+ await applyAgentNice(4242, { AGENT_YES_AGENT_NICE: "0" });
49
+
50
+ // 0 means "leave the agent at the default priority" — calling setPriority(0)
51
+ // anyway would be a no-op syscall, but skipping it keeps the disabled path
52
+ // free of a platform call that can throw.
53
+ expect(setPriority).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it("swallows a setPriority failure — a scheduling hint must never break a spawn", async () => {
57
+ setPriority.mockImplementation(() => {
58
+ // What an unprivileged container or a pid that already exited looks like.
59
+ throw new Error("EPERM: operation not permitted");
60
+ });
61
+
62
+ await expect(applyAgentNice(4242, { AGENT_YES_AGENT_NICE: "5" })).resolves.toBeUndefined();
63
+ });
64
+ });
package/ts/index.ts CHANGED
@@ -10,6 +10,7 @@ import { agentYesHome } from "./agentYesHome.ts";
10
10
  import {
11
11
  extractSessionId,
12
12
  getSessionForCwd,
13
+ isCodexFamily,
13
14
  storeSessionForCwd,
14
15
  } from "./resume/codexSessionManager.ts";
15
16
  import pty, { ptyPackage } from "./pty.ts";
@@ -435,7 +436,7 @@ export default async function agentYes({
435
436
 
436
437
  // Handle --continue flag for codex session restoration
437
438
  if (resume) {
438
- if (cli === "codex" && resume) {
439
+ if (isCodexFamily(cli) && resume) {
439
440
  // Try to get stored session for this directory
440
441
  const storedSessionId = await getSessionForCwd(workingDir);
441
442
  if (storedSessionId) {
@@ -827,7 +828,7 @@ export default async function agentYes({
827
828
 
828
829
  // For codex, try to use stored session ID for this directory
829
830
  let restoreArgs = conf.restoreArgs;
830
- if (cli === "codex") {
831
+ if (isCodexFamily(cli)) {
831
832
  const storedSessionId = await getSessionForCwd(workingDir);
832
833
  if (storedSessionId) {
833
834
  // Use specific session ID instead of --last
@@ -1060,7 +1061,7 @@ export default async function agentYes({
1060
1061
  }
1061
1062
 
1062
1063
  // session ID capture for codex
1063
- if (cli === "codex") {
1064
+ if (isCodexFamily(cli)) {
1064
1065
  const sessionId = extractSessionId(line);
1065
1066
  if (sessionId) {
1066
1067
  logger.debug(`heartbeat|session|captured session ID: ${sessionId}`);
@@ -1447,7 +1448,7 @@ export default async function agentYes({
1447
1448
  }
1448
1449
 
1449
1450
  // session ID capture for codex
1450
- if (cli === "codex") {
1451
+ if (isCodexFamily(cli)) {
1451
1452
  const sessionId = extractSessionId(line);
1452
1453
  if (sessionId) {
1453
1454
  logger.debug(`session|captured session ID: ${sessionId}`);
@@ -72,6 +72,22 @@ describe("notifyd singleton lock", () => {
72
72
  expect(await acquireDaemonLock((pid) => pid === 424242)).toBe(true);
73
73
  });
74
74
 
75
+ it("GIVES UP (bounded, not a 15ms busy-wait) when the owner is OUR OWN pid, live and fresh", async () => {
76
+ // The pathological spin: pid reuse hands us the SAME pid a prior daemon held.
77
+ // With no os_start to prove the disagreement, the decline check skips
78
+ // (owner.pid === process.pid) AND the shared steal decision refuses (pid is
79
+ // "self" → not dead; ts fresh → not stale). Before the wait bound this looped
80
+ // at 15ms forever; now it must decline within the budget instead of spinning.
81
+ await mkdir(daemonLockDir(), { recursive: true });
82
+ await writeFile(
83
+ daemonLockOwnerPath(),
84
+ JSON.stringify({ pid: process.pid, started_at: 1, ts: Date.now() }),
85
+ );
86
+ const t0 = Date.now();
87
+ expect(await acquireDaemonLock(() => true, 80)).toBe(false); // bounded decline
88
+ expect(Date.now() - t0).toBeLessThan(2000);
89
+ });
90
+
75
91
  it("eventually steals a torn owner, but only AFTER the grace (no double daemon)", async () => {
76
92
  // A torn owner is also the mkdir→writeOwner window of a concurrent daemon
77
93
  // start, so it is respected within the grace (~1s) and stolen only past it —
@@ -258,6 +258,18 @@ export interface DaemonOptions {
258
258
  // the pid+started_at re-check before SIGTERM).
259
259
  const OWNER_TTL_MS = 30_000;
260
260
 
261
+ // Hard upper bound on how long a contender may wait for the singleton lock. The
262
+ // decline and steal predicates are SUPPOSED to be exhaustive, but they are not
263
+ // complements: a valid owner whose pid equals OUR OWN pid (pid reuse — a prior
264
+ // daemon died and the OS handed us the same pid, with no os_start recorded to
265
+ // prove the disagreement) is skipped by the decline check (`owner.pid !==
266
+ // process.pid`) yet also refused by the shared steal decision (its pid is "self",
267
+ // so not dead; its ts is fresh, so not stale). Without a bound that residue spins
268
+ // at 15ms forever, burning a whole core. Bounded well above the torn-owner grace
269
+ // (~1s) and well below the heartbeat TTL (30s), so a live holder is never waited
270
+ // out to staleness — we decline promptly instead and the next watch poll retries.
271
+ const LOCK_WAIT_BUDGET_MS = 5000;
272
+
261
273
  export interface DaemonIdentity {
262
274
  pid: number;
263
275
  started_at: number;
@@ -333,11 +345,21 @@ async function writeOwner(): Promise<boolean> {
333
345
  */
334
346
  export async function acquireDaemonLock(
335
347
  isAlive: (pid: number) => boolean = isPidAlive,
348
+ waitBudgetMs: number = LOCK_WAIT_BUDGET_MS,
336
349
  ): Promise<boolean> {
337
350
  // The lock dir lives under notify/ — ensure that exists first, else mkdir of
338
351
  // the lock throws ENOENT which must NOT be mistaken for "someone holds it".
339
352
  await mkdir(notifyDir(), { recursive: true }).catch(() => {});
353
+ const deadline = Date.now() + waitBudgetMs;
340
354
  for (;;) {
355
+ // Never busy-wait without bound. Every iteration must end in acquire,
356
+ // decline, or steal; if the identity is so split that NEITHER decline nor
357
+ // steal applies (see LOCK_WAIT_BUDGET_MS), give up and decline rather than
358
+ // spin at 15ms indefinitely — the next watch poll re-ensures the daemon.
359
+ if (Date.now() >= deadline) {
360
+ logger.warn("[notifyd] gave up acquiring the daemon lock — unverifiable live holder");
361
+ return false;
362
+ }
341
363
  try {
342
364
  await mkdir(daemonLockDir(), { recursive: false });
343
365
  daemonStartedAt = Date.now(); // fixed for this daemon's lifetime
package/ts/serve.ts CHANGED
@@ -58,6 +58,7 @@ import { isCallbackRevoked, loadCallbackSecretReadOnly } from "./callback.ts";
58
58
  import { CLAUDE_SESSION_PIN_ENV } from "./sessionEnv.ts";
59
59
  import { MAX_CALLBACK_MSG_BYTES, frameVisitorMessage, verifyCapability } from "./callbackCore.ts";
60
60
  import { isTerminalReply } from "./terminalReply.ts";
61
+ import * as serveSampler from "./serveSampler.ts";
61
62
  import { withIpcLock } from "./ipcLock.ts";
62
63
  import { removeControlCharacters } from "./removeControlCharacters.ts";
63
64
  import { parseStatusText } from "./statusText.ts";
@@ -2251,6 +2252,10 @@ export async function cmdServe(rest: string[]): Promise<number> {
2251
2252
  : await logBadges(r.log_file).then(async (b) =>
2252
2253
  (await isUserTyping(r.pid)) ? [...b, TYPING_BADGE.id] : b,
2253
2254
  ),
2255
+ // Per-agent resource window (CPU-seconds/RSS/procs) from the background
2256
+ // sampler. Null until the sampler's first pass lands; the console degrades
2257
+ // to a transparent backdrop.
2258
+ res: serveSampler.resField(r.pid),
2254
2259
  };
2255
2260
  };
2256
2261
 
@@ -3095,6 +3100,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
3095
3100
  loadavg: loadavg(),
3096
3101
  mem: { total: totalmem(), free: freemem() },
3097
3102
  uptime: uptime(),
3103
+ resources: serveSampler.resourcesField(),
3098
3104
  caps: {
3099
3105
  send: true, // the console can always drive agent stdin
3100
3106
  kill: true,
@@ -4982,6 +4988,14 @@ export async function cmdServe(rest: string[]): Promise<number> {
4982
4988
  }
4983
4989
  }
4984
4990
 
4991
+ // Start the per-agent resource sampler (left-panel heatmap data). Prime the
4992
+ // first bucket immediately so the first /api/ls already carries `res`. The
4993
+ // sampler reads the live wrapper pids through listRecords (same registry the
4994
+ // /api/ls handler consults), so both agree on the fleet.
4995
+ serveSampler.startSampler(async () =>
4996
+ (await listRecords(undefined, defaultOpts({ all: true }))).map((r) => r.pid),
4997
+ );
4998
+
4985
4999
  // server is null only when we degraded to WebRTC-only above (port wedged);
4986
5000
  // skip the HTTP connection banner since nothing is listening on the port.
4987
5001
  if (server) {