@cohortapp/agent-sdk 2.8.0 → 2.8.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.
@@ -241,6 +241,47 @@ function humanActivity(intent, taskClass) {
241
241
  * uptimeDays?:number, spend24h?:number, device?:string, ram?:string, cores?:number
242
242
  * }>}
243
243
  */
244
+ /**
245
+ * Percentage of memory genuinely unavailable.
246
+ *
247
+ * On macOS this is `100 - (memory_pressure's System-wide memory free
248
+ * percentage)`. That is the same source `scripts/watchdog/memory-watchdog.sh`
249
+ * thresholds on, so the number in the Fleet view and the number the watchdog
250
+ * acts on can no longer disagree — they used to disagree by ~66 points, which
251
+ * is how a healthy machine came to show a standing CRITICAL.
252
+ *
253
+ * Anywhere else (or if memory_pressure yields nothing) falls back to
254
+ * `1 - free/total`, which on Linux does mean what it says.
255
+ *
256
+ * @param {object} o collector options; `o.execFile` is the test seam
257
+ * @param {object} osImpl
258
+ * @returns {Promise<number>}
259
+ */
260
+ async function collectMemUsedPct(o, osImpl) {
261
+ const fallback = () => {
262
+ try {
263
+ const total = Number(osImpl.totalmem());
264
+ const free = Number(osImpl.freemem());
265
+ if (Number.isFinite(total) && total > 0 && Number.isFinite(free)) {
266
+ return pct((1 - free / total) * 100);
267
+ }
268
+ } catch { /* fall through */ }
269
+ return 0;
270
+ };
271
+
272
+ const platform = o.platform || process.platform;
273
+ if (platform !== "darwin" && !o.execFile) return fallback();
274
+
275
+ const execFileImpl = o.execFile || execFile;
276
+ const out = await execFileSafe(execFileImpl, "/usr/bin/memory_pressure", [], 4000);
277
+ const m = /System-wide memory free percentage:\s*([\d.]+)%/.exec(out || "");
278
+ if (!m) return fallback();
279
+
280
+ const freePct = Number(m[1]);
281
+ if (!Number.isFinite(freePct) || freePct < 0 || freePct > 100) return fallback();
282
+ return pct(100 - freePct);
283
+ }
284
+
244
285
  export async function collectMachine(o = {}) {
245
286
  const osImpl = o.os || os;
246
287
  const machine = { loadavg: [0, 0, 0], memUsedPct: 0 };
@@ -253,14 +294,21 @@ export async function collectMachine(o = {}) {
253
294
  }
254
295
  } catch { /* keep [0,0,0] */ }
255
296
 
256
- // memUsedPct = 1 - free/total
257
- try {
258
- const total = Number(osImpl.totalmem());
259
- const free = Number(osImpl.freemem());
260
- if (Number.isFinite(total) && total > 0 && Number.isFinite(free)) {
261
- machine.memUsedPct = pct((1 - free / total) * 100);
262
- }
263
- } catch { /* keep 0 */ }
297
+ // memUsedPct — how much memory is genuinely UNAVAILABLE, not how much is
298
+ // merely occupied.
299
+ //
300
+ // This used to be `1 - os.freemem()/os.totalmem()`, which is not a pressure
301
+ // measure on macOS. os.freemem() counts only entirely-free pages, and macOS
302
+ // deliberately keeps almost all RAM occupied with file cache, purgeable and
303
+ // compressed pages that are reclaimable on demand. Measured on a healthy
304
+ // idle Mac mini M4 Pro at the same instant: os.freemem() → 99.2% "used",
305
+ // `memory_pressure` → 68% free. The old number sat permanently near 100 and
306
+ // fired CRITICAL alerts on machines with nothing wrong with them, while the
307
+ // memory watchdog — which reads memory_pressure — correctly stayed quiet.
308
+ //
309
+ // Prefer macOS's own answer; fall back to the free/total ratio only where
310
+ // memory_pressure does not exist (Linux, where the figure means what it says).
311
+ machine.memUsedPct = await collectMemUsedPct(o, osImpl);
264
312
 
265
313
  // cpuPct — coarse instantaneous estimate from loadavg/cpus when powermetrics
266
314
  // is unavailable. powermetrics (if it yields) overrides below.
@@ -173,7 +173,11 @@ test("detectClaudeAuth: scans recent log files under agentRoot", () => {
173
173
  });
174
174
 
175
175
  test("collectMachine: derives memUsedPct + loadavg without powermetrics/df", async () => {
176
- const m = await collectMachine({ os: fakeOs({ totalmem: 10e9, freemem: 2e9, loadavg: [4, 2, 1], cpus: 4 }), powermetrics: false, disk: false });
176
+ // memUsedPct now prefers macOS's memory_pressure, so "no external tools" has
177
+ // to include that one too — otherwise this test reads the REAL machine and
178
+ // asserts against the fake os, which is how it started failing.
179
+ const noTools = (cmd, args, opts, cb) => { cb(new Error("ENOENT")); return { on() {} }; };
180
+ const m = await collectMachine({ os: fakeOs({ totalmem: 10e9, freemem: 2e9, loadavg: [4, 2, 1], cpus: 4 }), execFile: noTools, powermetrics: false, disk: false });
177
181
  assert.equal(m.memUsedPct, 80);
178
182
  assert.deepEqual(m.loadavg, [4, 2, 1]);
179
183
  assert.equal(m.cpuPct, 100); // load 4 / 4 cpus = 100%
@@ -662,3 +666,75 @@ test("collectStatus ships the FULL machine vocabulary the Fleet row reads", asyn
662
666
  rmSync(root, { recursive: true, force: true });
663
667
  }
664
668
  });
669
+
670
+ // ---------------------------------------------------------------------------
671
+ // memUsedPct — pressure, not occupancy
672
+ //
673
+ // This was `1 - os.freemem()/os.totalmem()`, which is not a pressure measure on
674
+ // macOS: os.freemem() counts only entirely-free pages, and macOS keeps nearly
675
+ // all RAM occupied with file cache, purgeable and compressed pages that are
676
+ // reclaimable on demand. Measured on a healthy idle Mac mini M4 Pro at one
677
+ // instant: os.freemem() → 99.2% "used", memory_pressure → 68% free. The seat
678
+ // showed a standing CRITICAL while the watchdog, reading memory_pressure,
679
+ // correctly did nothing.
680
+ // ---------------------------------------------------------------------------
681
+
682
+ test("memUsedPct comes from memory_pressure, not from free/total", async () => {
683
+ const seen = [];
684
+ const execFile = (cmd, args, opts, cb) => {
685
+ seen.push(cmd);
686
+ if (cmd === "/usr/bin/memory_pressure") {
687
+ cb(null, "System-wide memory free percentage: 68%\n");
688
+ } else {
689
+ cb(new Error("not stubbed"));
690
+ }
691
+ return { on() {} };
692
+ };
693
+ // An os whose free/total ratio would say 99% used — the old, wrong answer.
694
+ const osImpl = {
695
+ loadavg: () => [1, 1, 1],
696
+ totalmem: () => 24 * 1024 ** 3,
697
+ freemem: () => 0.2 * 1024 ** 3,
698
+ cpus: () => [],
699
+ uptime: () => 3600,
700
+ };
701
+
702
+ const m = await collectMachine({ execFile, os: osImpl, platform: "darwin" });
703
+
704
+ assert.equal(m.memUsedPct, 32, "100 - 68 free, i.e. what the watchdog thresholds on");
705
+ assert.notEqual(m.memUsedPct, 99, "the free/total ratio must not be the answer on darwin");
706
+ assert.ok(seen.includes("/usr/bin/memory_pressure"), "memory_pressure is consulted");
707
+ });
708
+
709
+ test("memUsedPct falls back to free/total when memory_pressure yields nothing", async () => {
710
+ // Linux, or a macOS where the binary is missing/erroring. There the ratio
711
+ // does mean what it says, so a fallback is right — silence is not.
712
+ const execFile = (cmd, args, opts, cb) => { cb(new Error("ENOENT")); return { on() {} }; };
713
+ const osImpl = {
714
+ loadavg: () => [0, 0, 0],
715
+ totalmem: () => 1000,
716
+ freemem: () => 250,
717
+ cpus: () => [],
718
+ uptime: () => 1,
719
+ };
720
+
721
+ const m = await collectMachine({ execFile, os: osImpl, platform: "darwin" });
722
+ assert.equal(m.memUsedPct, 75, "1 - 250/1000");
723
+ });
724
+
725
+ test("memUsedPct ignores an out-of-range memory_pressure reading", async () => {
726
+ const execFile = (cmd, args, opts, cb) => {
727
+ cb(null, "System-wide memory free percentage: 8000%\n");
728
+ return { on() {} };
729
+ };
730
+ const osImpl = {
731
+ loadavg: () => [0, 0, 0],
732
+ totalmem: () => 1000,
733
+ freemem: () => 400,
734
+ cpus: () => [],
735
+ uptime: () => 1,
736
+ };
737
+
738
+ const m = await collectMachine({ execFile, os: osImpl, platform: "darwin" });
739
+ assert.equal(m.memUsedPct, 60, "a nonsense percentage falls back rather than propagating");
740
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cohortapp/agent-sdk",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "Cohort Agent SDK \u2014 autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
5
5
  "type": "module",
6
6
  "bin": {