agent-dag 1.35.43 → 1.36.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.
- package/dist/web/assets/index-CaAP5Ufx.css +1 -0
- package/dist/web/assets/index-OvdPf_n3.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 +187 -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-OvdPf_n3.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.0",
|
|
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,143 @@ 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 the Windows performance counter.
|
|
291
|
+
*
|
|
292
|
+
* `Get-Process` is the obvious call and the wrong one: its `CPU` property is
|
|
293
|
+
* total processor SECONDS consumed since the process started, so sorting by it
|
|
294
|
+
* ranks whatever has been running longest rather than whatever is busy now — a
|
|
295
|
+
* different question, and not the one the panel asks. The formatted performance
|
|
296
|
+
* counter publishes `PercentProcessorTime` directly, already a rate, so one
|
|
297
|
+
* query gives the same thing `ps -r` gives on Unix without a second sample.
|
|
298
|
+
*
|
|
299
|
+
* `_Total` and `Idle` are pseudo-processes in that class and are dropped.
|
|
300
|
+
*/
|
|
301
|
+
export function parseWindowsProcesses(json, totalMem, limit = TOP_N) {
|
|
302
|
+
let rows;
|
|
303
|
+
try { rows = typeof json === "string" ? JSON.parse(json) : json; }
|
|
304
|
+
catch { return []; }
|
|
305
|
+
if (!rows) return [];
|
|
306
|
+
if (!Array.isArray(rows)) rows = [rows];
|
|
307
|
+
const cores = os.cpus().length || 1;
|
|
308
|
+
return rows
|
|
309
|
+
.filter(r => r && r.Name && r.Name !== "_Total" && r.Name !== "Idle")
|
|
310
|
+
.map(r => ({
|
|
311
|
+
pid: Number(r.IDProcess) || 0,
|
|
312
|
+
// The counter is per-core-summed, exactly like macOS's 0-to-N00 scale, so
|
|
313
|
+
// it is normalised here to the 0-100 the Unix branch already reports.
|
|
314
|
+
cpu: Math.round((Number(r.PercentProcessorTime) || 0) / cores * 10) / 10,
|
|
315
|
+
mem: totalMem > 0
|
|
316
|
+
? Math.round((Number(r.WorkingSetPrivate) || 0) / totalMem * 1000) / 10
|
|
317
|
+
: 0,
|
|
318
|
+
name: String(r.Name),
|
|
319
|
+
}))
|
|
320
|
+
.sort((a, b) => b.cpu - a.cpu)
|
|
321
|
+
.slice(0, limit);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** The process list, on demand only — never on the ambient timer. */
|
|
325
|
+
export async function readProcesses(platform = process.platform) {
|
|
326
|
+
if (platform === "win32") {
|
|
327
|
+
const out = await run("powershell.exe", [
|
|
328
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
329
|
+
"Get-CimInstance Win32_PerfFormattedData_PerfProc_Process | Select-Object Name,IDProcess,PercentProcessorTime,WorkingSetPrivate | ConvertTo-Json -Compress",
|
|
330
|
+
], 6_000);
|
|
331
|
+
return out ? parseWindowsProcesses(out.trim(), os.totalmem()) : [];
|
|
332
|
+
}
|
|
333
|
+
const out = await run("ps", ["-Aceo", "pid,pcpu,pmem,comm", "-r"], 4_000);
|
|
334
|
+
return out ? parsePsProcesses(out) : [];
|
|
335
|
+
}
|
|
336
|
+
|
|
163
337
|
async function sampleMemory() {
|
|
164
338
|
if (memInFlight) return;
|
|
165
339
|
memInFlight = true;
|
|
@@ -171,13 +345,18 @@ async function sampleMemory() {
|
|
|
171
345
|
available,
|
|
172
346
|
usedPct: Math.max(0, Math.min(100, Math.round(((total - available) / total) * 1000) / 10)),
|
|
173
347
|
};
|
|
348
|
+
// Same 30s cadence as memory, and for the same reason: it moves in minutes
|
|
349
|
+
// and costs a subprocess on two of the three platforms.
|
|
350
|
+
swap = await readSwap();
|
|
174
351
|
} catch { /* keep the previous reading rather than blanking the meter */ }
|
|
175
352
|
finally { memInFlight = false; }
|
|
176
353
|
}
|
|
177
354
|
|
|
178
355
|
function sampleCpu() {
|
|
356
|
+
const per = corePercents();
|
|
179
357
|
const pct = cpuPercent();
|
|
180
358
|
if (pct == null) return;
|
|
359
|
+
cores = per;
|
|
181
360
|
cpuHistory.push(pct);
|
|
182
361
|
while (cpuHistory.length > HISTORY) cpuHistory.shift();
|
|
183
362
|
}
|
|
@@ -189,6 +368,7 @@ function sampleCpu() {
|
|
|
189
368
|
export function startSystemMetrics() {
|
|
190
369
|
if (cpuTimer) return;
|
|
191
370
|
prevTicks = readTicks(); // baseline, so the first tick has a delta
|
|
371
|
+
prevCoreTicks = readCoreTicks();
|
|
192
372
|
sampleMemory();
|
|
193
373
|
cpuTimer = setInterval(sampleCpu, CPU_INTERVAL_MS);
|
|
194
374
|
memTimer = setInterval(sampleMemory, MEM_INTERVAL_MS);
|
|
@@ -201,8 +381,11 @@ export function stopSystemMetrics() {
|
|
|
201
381
|
if (memTimer) clearInterval(memTimer);
|
|
202
382
|
cpuTimer = memTimer = null;
|
|
203
383
|
prevTicks = null;
|
|
384
|
+
prevCoreTicks = null;
|
|
204
385
|
cpuHistory.length = 0;
|
|
205
386
|
memory = null;
|
|
387
|
+
cores = null;
|
|
388
|
+
swap = null;
|
|
206
389
|
}
|
|
207
390
|
|
|
208
391
|
/**
|
|
@@ -223,6 +406,10 @@ export function systemSnapshot() {
|
|
|
223
406
|
cpuHistory: [...cpuHistory],
|
|
224
407
|
cores: os.cpus().length,
|
|
225
408
|
memory,
|
|
409
|
+
swap,
|
|
410
|
+
perCore: cores,
|
|
411
|
+
uptimeSec: Math.round(os.uptime()),
|
|
412
|
+
platform: process.platform,
|
|
226
413
|
loadavg: hasLoad ? load.map(n => Math.round(n * 100) / 100) : null,
|
|
227
414
|
intervalMs: CPU_INTERVAL_MS,
|
|
228
415
|
sampledAt: Date.now(),
|