agent-dag 1.36.0 → 1.36.2

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.
@@ -40,7 +40,7 @@
40
40
  document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
41
41
  })();
42
42
  </script>
43
- <script type="module" crossorigin src="/assets/index-OvdPf_n3.js"></script>
43
+ <script type="module" crossorigin src="/assets/index-DMZuxcfY.js"></script>
44
44
  <link rel="stylesheet" crossorigin href="/assets/index-CaAP5Ufx.css">
45
45
  </head>
46
46
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.36.0",
3
+ "version": "1.36.2",
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": {
@@ -268,11 +268,53 @@ async function readSwap(platform = process.platform) {
268
268
  const TOP_N = 8;
269
269
 
270
270
  /**
271
- * Rows out of `ps -Aceo pid,pcpu,pmem,comm -r`.
271
+ * The `ps` argument list, which is not the same list on both Unixes.
272
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".
273
+ * `-r` was shipped for both and means two different things. On BSD it sorts the
274
+ * output by current CPU, which is the ordering the panel is built around. On
275
+ * Linux procps it is *"restrict the selection to only running processes"* a
276
+ * filter on state `R`, applied in PID order. A Linux deck therefore listed
277
+ * whatever happened to be on a CPU at the instant of the sample: usually one or
278
+ * two rows on an idle machine, and never the busiest ones, since a process
279
+ * pinning a core while blocked on I/O sits in `D` and one merely burning CPU
280
+ * over time is normally caught in `S`. Nothing errored and nothing was empty,
281
+ * which is why it survived two releases (#492).
282
+ *
283
+ * `--sort=-pcpu` is procps' own way to say what `-r` says on BSD. The column
284
+ * order is deliberately identical on both so one parser reads both, and `comm`
285
+ * stays last so a name containing a space survives intact.
286
+ *
287
+ * Keyed on linux rather than on darwin, because linux is the platform that is
288
+ * wrong: `-r` sorts on every BSD, while `--sort` is a procps long option that
289
+ * would make FreeBSD and OpenBSD exit non-zero. This way the only branch that
290
+ * changes is the one that was broken.
291
+ *
292
+ * Pure and exported for the same reason the parsers are: the command
293
+ * construction is the part that differs per platform, and a fixture cannot
294
+ * prove which flags were passed to produce it.
295
+ */
296
+ export function psArgs(platform = process.platform) {
297
+ // procps: an explicit CPU sort, and `comm` in `-o` is what keeps argv — and
298
+ // any prompt or token on it — out of the panel. Linux `comm` comes from
299
+ // /proc/<pid>/comm and is capped at 15 characters.
300
+ if (platform === "linux") return ["-eo", "pid,pcpu,pmem,comm", "--sort=-pcpu"];
301
+ // BSD/macOS: `-c` prints the accounting name rather than the argument vector,
302
+ // and `-r` sorts by current CPU.
303
+ return ["-Aceo", "pid,pcpu,pmem,comm", "-r"];
304
+ }
305
+
306
+ /**
307
+ * Rows out of `ps -o pid,pcpu,pmem,comm`, in that column order on both Unixes —
308
+ * see psArgs for how each platform is asked for it.
309
+ *
310
+ * `pcpu` is a percentage of ONE core on both, so a multi-threaded process runs
311
+ * past 100 and that is information rather than an error: 157 is one and a half
312
+ * cores. cpuFromDeltas puts the Windows column on this same scale.
313
+ *
314
+ * There is no row limit in the query because neither `ps` has one and `run`
315
+ * deliberately never inherits a shell, so there is no `| head` to pipe into.
316
+ * The loop below stops at `limit` instead, which costs one parse of a string
317
+ * we have already paid to read.
276
318
  */
277
319
  export function parsePsProcesses(text, limit = TOP_N) {
278
320
  const lines = String(text ?? "").trim().split("\n");
@@ -287,50 +329,110 @@ export function parsePsProcesses(text, limit = TOP_N) {
287
329
  }
288
330
 
289
331
  /**
290
- * Rows out of the Windows performance counter.
332
+ * Rows out of `Get-Process`.
291
333
  *
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.
334
+ * NOT `Win32_PerfFormattedData_PerfProc_Process`, which is what this used and
335
+ * which is not a class you may assume exists. It is published by perflib, and
336
+ * perflib is deregistered often enough to matter a corporate image, a bad
337
+ * in-place upgrade, a half-run `lodctr`. On a machine reported from the field
338
+ * the class was simply absent (`Get-CimInstance: Invalid class`), and `typeperf`
339
+ * failed identically, which places the fault below WMI rather than in it. WMI
340
+ * was only mirroring what perflib had stopped publishing.
298
341
  *
299
- * `_Total` and `Idle` are pseudo-processes in that class and are dropped.
342
+ * `Get-Process` reads through NtQuerySystemInformation instead, so it depends on
343
+ * nothing that can be unregistered. The cost is that its `CPU` is total
344
+ * processor SECONDS since the process started, not a rate — so a percentage has
345
+ * to be derived from two readings, exactly the way the machine-wide figure is
346
+ * already derived from two tick samples. Reliability is worth one extra poll:
347
+ * an instant number from a class that may not exist is worth nothing at all.
300
348
  */
301
- export function parseWindowsProcesses(json, totalMem, limit = TOP_N) {
349
+ export function parseGetProcessJson(json, totalMem) {
302
350
  let rows;
303
351
  try { rows = typeof json === "string" ? JSON.parse(json) : json; }
304
352
  catch { return []; }
305
353
  if (!rows) return [];
306
354
  if (!Array.isArray(rows)) rows = [rows];
307
- const cores = os.cpus().length || 1;
308
355
  return rows
309
- .filter(r => r && r.Name && r.Name !== "_Total" && r.Name !== "Idle")
356
+ .filter(r => r && r.ProcessName)
310
357
  .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,
358
+ pid: Number(r.Id) || 0,
359
+ name: String(r.ProcessName),
360
+ // Null rather than 0 when the process denies the read: a system process
361
+ // we cannot query has an unknown CPU time, and calling that zero would
362
+ // rank it as idle.
363
+ cpuSec: typeof r.CPU === "number" ? r.CPU : null,
315
364
  mem: totalMem > 0
316
365
  ? Math.round((Number(r.WorkingSetPrivate) || 0) / totalMem * 1000) / 10
317
366
  : 0,
318
- name: String(r.Name),
319
- }))
320
- .sort((a, b) => b.cpu - a.cpu)
321
- .slice(0, limit);
367
+ }));
368
+ }
369
+
370
+ /**
371
+ * Turn two `Get-Process` readings into a percentage per process.
372
+ *
373
+ * `prev` maps pid to the cpuSec of the previous reading. A pid absent from it —
374
+ * a process that started since — has no delta and reports null rather than a
375
+ * number invented from its whole lifetime, which would rank a freshly spawned
376
+ * compiler as though it had been burning a core since boot.
377
+ *
378
+ * Per core, NOT per machine, because that is what the column beside it means:
379
+ * `ps -o pcpu` is a percentage of one core on both Unixes and is reported
380
+ * unmodified, so a row reading 157 there is a process using one and a half
381
+ * cores. This used to divide by the core count and clamp to 100 on the reasoning
382
+ * that Unix reported 0-100 — it does not, and never did, so the normalisation
383
+ * corrected a scale that already matched and introduced the mismatch it was
384
+ * written to prevent: on a 12-core machine six busy cores read 600 on macOS and
385
+ * 50 on Windows (#493). One CPU-second burned per wall-second is 100 here, on
386
+ * every platform.
387
+ *
388
+ * Core count is deliberately not a parameter any more. The aggregate meter's
389
+ * 0-100 convention (see cpuPercent) is a different question with a different
390
+ * answer, and the only way this drifts back is if a core count is in reach.
391
+ */
392
+ export function cpuFromDeltas(rows, prev, elapsedMs, limit = TOP_N) {
393
+ const secs = elapsedMs / 1000;
394
+ const out = rows.map(r => {
395
+ const before = prev instanceof Map ? prev.get(r.pid) : undefined;
396
+ let cpu = null;
397
+ if (r.cpuSec != null && before != null && secs > 0) {
398
+ const d = r.cpuSec - before;
399
+ // A counter that went backwards means the pid was reused by a different
400
+ // process; report nothing rather than a negative or a wild number.
401
+ if (d >= 0) cpu = Math.round((d / secs) * 1000) / 10;
402
+ }
403
+ return { pid: r.pid, cpu, mem: r.mem, name: r.name };
404
+ });
405
+ // Until the second reading lands there is no CPU to sort on, so the list is
406
+ // ordered by memory — which is a real answer to "what is this machine doing",
407
+ // not a placeholder.
408
+ const haveCpu = out.some(r => r.cpu != null);
409
+ out.sort(haveCpu
410
+ ? (a, b) => (b.cpu ?? -1) - (a.cpu ?? -1)
411
+ : (a, b) => b.mem - a.mem);
412
+ return out.slice(0, limit);
322
413
  }
323
414
 
324
415
  /** The process list, on demand only — never on the ambient timer. */
416
+ /** Previous Windows reading, so the next one can be a rate. Cleared with the
417
+ * rest of the sampler state. */
418
+ let prevProcCpu = null;
419
+ let prevProcAt = 0;
420
+
325
421
  export async function readProcesses(platform = process.platform) {
326
422
  if (platform === "win32") {
327
423
  const out = await run("powershell.exe", [
328
424
  "-NoProfile", "-NonInteractive", "-Command",
329
- "Get-CimInstance Win32_PerfFormattedData_PerfProc_Process | Select-Object Name,IDProcess,PercentProcessorTime,WorkingSetPrivate | ConvertTo-Json -Compress",
425
+ "Get-Process | Select-Object Id,ProcessName,CPU,@{n='WorkingSetPrivate';e={$_.PrivateMemorySize64}} | ConvertTo-Json -Compress",
330
426
  ], 6_000);
331
- return out ? parseWindowsProcesses(out.trim(), os.totalmem()) : [];
427
+ if (!out) return [];
428
+ const rows = parseGetProcessJson(out.trim(), os.totalmem());
429
+ const now = Date.now();
430
+ const result = cpuFromDeltas(rows, prevProcCpu, now - prevProcAt);
431
+ prevProcCpu = new Map(rows.filter(r => r.cpuSec != null).map(r => [r.pid, r.cpuSec]));
432
+ prevProcAt = now;
433
+ return result;
332
434
  }
333
- const out = await run("ps", ["-Aceo", "pid,pcpu,pmem,comm", "-r"], 4_000);
435
+ const out = await run("ps", psArgs(platform), 4_000);
334
436
  return out ? parsePsProcesses(out) : [];
335
437
  }
336
438
 
@@ -386,6 +488,8 @@ export function stopSystemMetrics() {
386
488
  memory = null;
387
489
  cores = null;
388
490
  swap = null;
491
+ prevProcCpu = null;
492
+ prevProcAt = 0;
389
493
  }
390
494
 
391
495
  /**