agent-dag 1.35.43 → 1.36.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.
- package/dist/web/assets/index-CaAP5Ufx.css +1 -0
- package/dist/web/assets/index-DWHO_6dI.js +78 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/index.mjs +6 -1
- package/src/server/system-metrics.mjs +238 -0
- package/dist/web/assets/index-CbEDZOIk.js +0 -78
- package/dist/web/assets/index-rOAqrum-.css +0 -1
package/dist/web/index.html
CHANGED
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
|
|
41
41
|
})();
|
|
42
42
|
</script>
|
|
43
|
-
<script type="module" crossorigin src="/assets/index-
|
|
44
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
43
|
+
<script type="module" crossorigin src="/assets/index-DWHO_6dI.js"></script>
|
|
44
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CaAP5Ufx.css">
|
|
45
45
|
</head>
|
|
46
46
|
<body>
|
|
47
47
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.36.1",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch tool calls, token spend and every Claude Code subagent on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/server/index.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { CODEX_HOME, CODEX_SESSIONS_DIR, STOP, walkRolloutDays } from "./codex-d
|
|
|
14
14
|
import { PRODUCT } from "./brand.mjs";
|
|
15
15
|
import { invokedName, renameNotice } from "./invoked-as.mjs";
|
|
16
16
|
import { appendLogLine, codexCwdInWorkspace, writesCodexLog } from "./log-writer.mjs";
|
|
17
|
-
import { startSystemMetrics, systemSnapshot } from "./system-metrics.mjs";
|
|
17
|
+
import { readProcesses, startSystemMetrics, systemSnapshot } from "./system-metrics.mjs";
|
|
18
18
|
|
|
19
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const PKG_ROOT = resolve(__dirname, "..", "..");
|
|
@@ -3054,6 +3054,11 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
|
|
|
3054
3054
|
// Machine state, not session state: sampled on the server's own timer and
|
|
3055
3055
|
// deliberately kept out of the event stream. See src/server/system-metrics.mjs.
|
|
3056
3056
|
if (req.method === "GET" && url.pathname === "/api/system") return send(res, 200, systemSnapshot());
|
|
3057
|
+
// On demand only — the process list costs a subprocess on every platform,
|
|
3058
|
+
// so it is fetched while the detail panel is open and never on the timer.
|
|
3059
|
+
if (req.method === "GET" && url.pathname === "/api/system/processes") {
|
|
3060
|
+
return guard(readProcesses().then(procs => send(res, 200, { ok: true, procs })), res);
|
|
3061
|
+
}
|
|
3057
3062
|
if (req.method === "GET" && url.pathname === "/api/codex-quota") return guard(handleCodexQuota(req, res), res);
|
|
3058
3063
|
if (req.method === "GET" && url.pathname === "/api/ccusage") return guard(handleCcusage(req, res), res);
|
|
3059
3064
|
if (req.method === "GET" && url.pathname === "/api/claude-accounts") return guard(handleClaudeAccounts(req, res), res);
|
|
@@ -33,6 +33,9 @@ const HISTORY = 20;
|
|
|
33
33
|
let cpuTimer = null;
|
|
34
34
|
let memTimer = null;
|
|
35
35
|
let prevTicks = null;
|
|
36
|
+
let prevCoreTicks = null;
|
|
37
|
+
let cores = null;
|
|
38
|
+
let swap = null;
|
|
36
39
|
/** Newest last. Seeded empty; the first tick produces no percentage because a
|
|
37
40
|
* delta needs two readings. */
|
|
38
41
|
const cpuHistory = [];
|
|
@@ -52,6 +55,40 @@ function readTicks() {
|
|
|
52
55
|
return { idle, total };
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/** The same pair, per core, in `os.cpus()` order. */
|
|
59
|
+
function readCoreTicks() {
|
|
60
|
+
return os.cpus().map(cpu => {
|
|
61
|
+
let idle = 0;
|
|
62
|
+
let total = 0;
|
|
63
|
+
for (const [kind, ms] of Object.entries(cpu.times)) {
|
|
64
|
+
total += ms;
|
|
65
|
+
if (kind === "idle") idle += ms;
|
|
66
|
+
}
|
|
67
|
+
return { idle, total };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Busy percentage per core since the previous reading.
|
|
73
|
+
*
|
|
74
|
+
* The aggregate figure the topbar draws hides the shape of the load, and the
|
|
75
|
+
* shape is what tells a saturated machine from a machine running one hot
|
|
76
|
+
* single-threaded job. Same delta arithmetic as `cpuPercent`, one row per core,
|
|
77
|
+
* and the same refusal to invent a number before there are two readings.
|
|
78
|
+
*/
|
|
79
|
+
function corePercents() {
|
|
80
|
+
const now = readCoreTicks();
|
|
81
|
+
const prev = prevCoreTicks;
|
|
82
|
+
prevCoreTicks = now;
|
|
83
|
+
if (!prev || prev.length !== now.length) return null;
|
|
84
|
+
return now.map((c, i) => {
|
|
85
|
+
const dTotal = c.total - prev[i].total;
|
|
86
|
+
const dIdle = c.idle - prev[i].idle;
|
|
87
|
+
if (dTotal <= 0) return 0;
|
|
88
|
+
return Math.max(0, Math.min(100, Math.round((1 - dIdle / dTotal) * 1000) / 10));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
/**
|
|
56
93
|
* Busy percentage across all cores since the previous reading, 0-100.
|
|
57
94
|
*
|
|
@@ -160,6 +197,192 @@ async function readAvailable(platform = process.platform) {
|
|
|
160
197
|
return os.freemem();
|
|
161
198
|
}
|
|
162
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Swap out of macOS `sysctl -n vm.swapusage`, which prints
|
|
202
|
+
* `total = 14336.00M used = 12876.00M free = 1460.00M (encrypted)`.
|
|
203
|
+
*
|
|
204
|
+
* Swap is the reading a percentage cannot give you. A machine at "64% memory
|
|
205
|
+
* used" that is quietly paging 12 GB to disk is not the same machine as one at
|
|
206
|
+
* 64% with an empty swap file, and the difference is the one you can feel.
|
|
207
|
+
*/
|
|
208
|
+
export function swapFromSysctl(text) {
|
|
209
|
+
const unit = s => {
|
|
210
|
+
const m = /^([\d.]+)([KMG])?$/i.exec(s);
|
|
211
|
+
if (!m) return null;
|
|
212
|
+
const mult = { k: 1024, m: 1024 ** 2, g: 1024 ** 3 }[(m[2] || "M").toLowerCase()] ?? 1;
|
|
213
|
+
return Math.round(Number(m[1]) * mult);
|
|
214
|
+
};
|
|
215
|
+
const total = unit(/total\s*=\s*(\S+)/i.exec(String(text ?? ""))?.[1] ?? "");
|
|
216
|
+
const used = unit(/used\s*=\s*(\S+)/i.exec(String(text ?? ""))?.[1] ?? "");
|
|
217
|
+
if (total == null || used == null) return null;
|
|
218
|
+
return { total, used };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Swap out of `/proc/meminfo`, where it is two fields rather than one line. */
|
|
222
|
+
export function swapFromMeminfo(text) {
|
|
223
|
+
const s = String(text ?? "");
|
|
224
|
+
const total = /^SwapTotal:\s+(\d+)\s*kB/m.exec(s);
|
|
225
|
+
const free = /^SwapFree:\s+(\d+)\s*kB/m.exec(s);
|
|
226
|
+
if (!total || !free) return null;
|
|
227
|
+
const t = Number(total[1]) * 1024;
|
|
228
|
+
return { total: t, used: Math.max(0, t - Number(free[1]) * 1024) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Windows has no swap file in the Unix sense; the comparable pressure signal is
|
|
233
|
+
* commit charge, which `Win32_OperatingSystem` reports as total and free
|
|
234
|
+
* virtual memory in KB. Labelled "commit" in the UI rather than "swap", because
|
|
235
|
+
* calling it swap would be borrowing a word for a different mechanism.
|
|
236
|
+
*/
|
|
237
|
+
export function swapFromWmicJson(json) {
|
|
238
|
+
try {
|
|
239
|
+
const o = typeof json === "string" ? JSON.parse(json) : json;
|
|
240
|
+
const total = Number(o?.TotalVirtualMemorySize) * 1024;
|
|
241
|
+
const free = Number(o?.FreeVirtualMemory) * 1024;
|
|
242
|
+
if (!Number.isFinite(total) || !Number.isFinite(free) || total <= 0) return null;
|
|
243
|
+
return { total, used: Math.max(0, total - free) };
|
|
244
|
+
} catch { return null; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function readSwap(platform = process.platform) {
|
|
248
|
+
if (platform === "darwin") {
|
|
249
|
+
const out = await run("sysctl", ["-n", "vm.swapusage"]);
|
|
250
|
+
return out ? swapFromSysctl(out) : null;
|
|
251
|
+
}
|
|
252
|
+
if (platform === "linux") {
|
|
253
|
+
try { return swapFromMeminfo(await readFile("/proc/meminfo", "utf8")); }
|
|
254
|
+
catch { return null; }
|
|
255
|
+
}
|
|
256
|
+
if (platform === "win32") {
|
|
257
|
+
const out = await run("powershell.exe", [
|
|
258
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
259
|
+
"Get-CimInstance Win32_OperatingSystem | Select-Object TotalVirtualMemorySize,FreeVirtualMemory | ConvertTo-Json -Compress",
|
|
260
|
+
], 4_000);
|
|
261
|
+
return out ? swapFromWmicJson(out.trim()) : null;
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** How many rows the process list keeps. Enough to see what is eating the
|
|
267
|
+
* machine, few enough that the panel never becomes a scroll. */
|
|
268
|
+
const TOP_N = 8;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Rows out of `ps -Aceo pid,pcpu,pmem,comm -r`.
|
|
272
|
+
*
|
|
273
|
+
* `-c` gives the executable name without its full path and without the argv
|
|
274
|
+
* that would leak a prompt or a token into the UI; `-r` sorts by current CPU,
|
|
275
|
+
* which is the ordering that answers "what is eating this machine right now".
|
|
276
|
+
*/
|
|
277
|
+
export function parsePsProcesses(text, limit = TOP_N) {
|
|
278
|
+
const lines = String(text ?? "").trim().split("\n");
|
|
279
|
+
const out = [];
|
|
280
|
+
for (const line of lines.slice(1)) { // drop the header row
|
|
281
|
+
const m = /^\s*(\d+)\s+([\d.]+)\s+([\d.]+)\s+(.+?)\s*$/.exec(line);
|
|
282
|
+
if (!m) continue;
|
|
283
|
+
out.push({ pid: Number(m[1]), cpu: Number(m[2]), mem: Number(m[3]), name: m[4] });
|
|
284
|
+
if (out.length >= limit) break;
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Rows out of `Get-Process`.
|
|
291
|
+
*
|
|
292
|
+
* NOT `Win32_PerfFormattedData_PerfProc_Process`, which is what this used and
|
|
293
|
+
* which is not a class you may assume exists. It is published by perflib, and
|
|
294
|
+
* perflib is deregistered often enough to matter — a corporate image, a bad
|
|
295
|
+
* in-place upgrade, a half-run `lodctr`. On a machine reported from the field
|
|
296
|
+
* the class was simply absent (`Get-CimInstance: Invalid class`), and `typeperf`
|
|
297
|
+
* failed identically, which places the fault below WMI rather than in it. WMI
|
|
298
|
+
* was only mirroring what perflib had stopped publishing.
|
|
299
|
+
*
|
|
300
|
+
* `Get-Process` reads through NtQuerySystemInformation instead, so it depends on
|
|
301
|
+
* nothing that can be unregistered. The cost is that its `CPU` is total
|
|
302
|
+
* processor SECONDS since the process started, not a rate — so a percentage has
|
|
303
|
+
* to be derived from two readings, exactly the way the machine-wide figure is
|
|
304
|
+
* already derived from two tick samples. Reliability is worth one extra poll:
|
|
305
|
+
* an instant number from a class that may not exist is worth nothing at all.
|
|
306
|
+
*/
|
|
307
|
+
export function parseGetProcessJson(json, totalMem) {
|
|
308
|
+
let rows;
|
|
309
|
+
try { rows = typeof json === "string" ? JSON.parse(json) : json; }
|
|
310
|
+
catch { return []; }
|
|
311
|
+
if (!rows) return [];
|
|
312
|
+
if (!Array.isArray(rows)) rows = [rows];
|
|
313
|
+
return rows
|
|
314
|
+
.filter(r => r && r.ProcessName)
|
|
315
|
+
.map(r => ({
|
|
316
|
+
pid: Number(r.Id) || 0,
|
|
317
|
+
name: String(r.ProcessName),
|
|
318
|
+
// Null rather than 0 when the process denies the read: a system process
|
|
319
|
+
// we cannot query has an unknown CPU time, and calling that zero would
|
|
320
|
+
// rank it as idle.
|
|
321
|
+
cpuSec: typeof r.CPU === "number" ? r.CPU : null,
|
|
322
|
+
mem: totalMem > 0
|
|
323
|
+
? Math.round((Number(r.WorkingSetPrivate) || 0) / totalMem * 1000) / 10
|
|
324
|
+
: 0,
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Turn two `Get-Process` readings into a percentage per process.
|
|
330
|
+
*
|
|
331
|
+
* `prev` maps pid to the cpuSec of the previous reading. A pid absent from it —
|
|
332
|
+
* a process that started since — has no delta and reports null rather than a
|
|
333
|
+
* number invented from its whole lifetime, which would rank a freshly spawned
|
|
334
|
+
* compiler as though it had been burning a core since boot.
|
|
335
|
+
*
|
|
336
|
+
* Normalised by core count so a Windows row means the same thing as the Unix
|
|
337
|
+
* one: 100 is one machine, not one core.
|
|
338
|
+
*/
|
|
339
|
+
export function cpuFromDeltas(rows, prev, elapsedMs, cores, limit = TOP_N) {
|
|
340
|
+
const secs = elapsedMs / 1000;
|
|
341
|
+
const out = rows.map(r => {
|
|
342
|
+
const before = prev instanceof Map ? prev.get(r.pid) : undefined;
|
|
343
|
+
let cpu = null;
|
|
344
|
+
if (r.cpuSec != null && before != null && secs > 0) {
|
|
345
|
+
const d = r.cpuSec - before;
|
|
346
|
+
// A counter that went backwards means the pid was reused by a different
|
|
347
|
+
// process; report nothing rather than a negative or a wild number.
|
|
348
|
+
if (d >= 0) cpu = Math.max(0, Math.min(100, Math.round((d / secs / Math.max(1, cores)) * 1000) / 10));
|
|
349
|
+
}
|
|
350
|
+
return { pid: r.pid, cpu, mem: r.mem, name: r.name };
|
|
351
|
+
});
|
|
352
|
+
// Until the second reading lands there is no CPU to sort on, so the list is
|
|
353
|
+
// ordered by memory — which is a real answer to "what is this machine doing",
|
|
354
|
+
// not a placeholder.
|
|
355
|
+
const haveCpu = out.some(r => r.cpu != null);
|
|
356
|
+
out.sort(haveCpu
|
|
357
|
+
? (a, b) => (b.cpu ?? -1) - (a.cpu ?? -1)
|
|
358
|
+
: (a, b) => b.mem - a.mem);
|
|
359
|
+
return out.slice(0, limit);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The process list, on demand only — never on the ambient timer. */
|
|
363
|
+
/** Previous Windows reading, so the next one can be a rate. Cleared with the
|
|
364
|
+
* rest of the sampler state. */
|
|
365
|
+
let prevProcCpu = null;
|
|
366
|
+
let prevProcAt = 0;
|
|
367
|
+
|
|
368
|
+
export async function readProcesses(platform = process.platform) {
|
|
369
|
+
if (platform === "win32") {
|
|
370
|
+
const out = await run("powershell.exe", [
|
|
371
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
372
|
+
"Get-Process | Select-Object Id,ProcessName,CPU,@{n='WorkingSetPrivate';e={$_.PrivateMemorySize64}} | ConvertTo-Json -Compress",
|
|
373
|
+
], 6_000);
|
|
374
|
+
if (!out) return [];
|
|
375
|
+
const rows = parseGetProcessJson(out.trim(), os.totalmem());
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
const result = cpuFromDeltas(rows, prevProcCpu, now - prevProcAt, os.cpus().length);
|
|
378
|
+
prevProcCpu = new Map(rows.filter(r => r.cpuSec != null).map(r => [r.pid, r.cpuSec]));
|
|
379
|
+
prevProcAt = now;
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
const out = await run("ps", ["-Aceo", "pid,pcpu,pmem,comm", "-r"], 4_000);
|
|
383
|
+
return out ? parsePsProcesses(out) : [];
|
|
384
|
+
}
|
|
385
|
+
|
|
163
386
|
async function sampleMemory() {
|
|
164
387
|
if (memInFlight) return;
|
|
165
388
|
memInFlight = true;
|
|
@@ -171,13 +394,18 @@ async function sampleMemory() {
|
|
|
171
394
|
available,
|
|
172
395
|
usedPct: Math.max(0, Math.min(100, Math.round(((total - available) / total) * 1000) / 10)),
|
|
173
396
|
};
|
|
397
|
+
// Same 30s cadence as memory, and for the same reason: it moves in minutes
|
|
398
|
+
// and costs a subprocess on two of the three platforms.
|
|
399
|
+
swap = await readSwap();
|
|
174
400
|
} catch { /* keep the previous reading rather than blanking the meter */ }
|
|
175
401
|
finally { memInFlight = false; }
|
|
176
402
|
}
|
|
177
403
|
|
|
178
404
|
function sampleCpu() {
|
|
405
|
+
const per = corePercents();
|
|
179
406
|
const pct = cpuPercent();
|
|
180
407
|
if (pct == null) return;
|
|
408
|
+
cores = per;
|
|
181
409
|
cpuHistory.push(pct);
|
|
182
410
|
while (cpuHistory.length > HISTORY) cpuHistory.shift();
|
|
183
411
|
}
|
|
@@ -189,6 +417,7 @@ function sampleCpu() {
|
|
|
189
417
|
export function startSystemMetrics() {
|
|
190
418
|
if (cpuTimer) return;
|
|
191
419
|
prevTicks = readTicks(); // baseline, so the first tick has a delta
|
|
420
|
+
prevCoreTicks = readCoreTicks();
|
|
192
421
|
sampleMemory();
|
|
193
422
|
cpuTimer = setInterval(sampleCpu, CPU_INTERVAL_MS);
|
|
194
423
|
memTimer = setInterval(sampleMemory, MEM_INTERVAL_MS);
|
|
@@ -201,8 +430,13 @@ export function stopSystemMetrics() {
|
|
|
201
430
|
if (memTimer) clearInterval(memTimer);
|
|
202
431
|
cpuTimer = memTimer = null;
|
|
203
432
|
prevTicks = null;
|
|
433
|
+
prevCoreTicks = null;
|
|
204
434
|
cpuHistory.length = 0;
|
|
205
435
|
memory = null;
|
|
436
|
+
cores = null;
|
|
437
|
+
swap = null;
|
|
438
|
+
prevProcCpu = null;
|
|
439
|
+
prevProcAt = 0;
|
|
206
440
|
}
|
|
207
441
|
|
|
208
442
|
/**
|
|
@@ -223,6 +457,10 @@ export function systemSnapshot() {
|
|
|
223
457
|
cpuHistory: [...cpuHistory],
|
|
224
458
|
cores: os.cpus().length,
|
|
225
459
|
memory,
|
|
460
|
+
swap,
|
|
461
|
+
perCore: cores,
|
|
462
|
+
uptimeSec: Math.round(os.uptime()),
|
|
463
|
+
platform: process.platform,
|
|
226
464
|
loadavg: hasLoad ? load.map(n => Math.round(n * 100) / 100) : null,
|
|
227
465
|
intervalMs: CPU_INTERVAL_MS,
|
|
228
466
|
sampledAt: Date.now(),
|