@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
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|