@walwal-harness/cli 7.1.43 → 7.1.44

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.
@@ -308,21 +308,67 @@ function collectPids(value: unknown, into: Set<number>): void {
308
308
  }
309
309
  }
310
310
 
311
- /** %cpu for a single pid via `ps -o %cpu= -p <pid>`; null if dead/guarded. */
312
- function cpuForPid(pid: number): number | null {
311
+ /** One `ps` snapshot: pid -> %cpu, and ppid -> [children]. */
312
+ function readProcTable(): { cpu: Map<number, number>; kids: Map<number, number[]> } {
313
+ const cpu = new Map<number, number>();
314
+ const kids = new Map<number, number[]>();
313
315
  try {
314
- const out = execFileSync("ps", ["-o", "%cpu=", "-p", String(pid)], {
316
+ const out = execFileSync("ps", ["-axo", "pid=,ppid=,%cpu="], {
315
317
  encoding: "utf8",
316
318
  timeout: 4000,
317
319
  stdio: ["ignore", "pipe", "ignore"],
318
320
  });
319
- const v = parseFloat(out.trim());
320
- return Number.isFinite(v) ? v : null;
321
+ for (const line of out.split("\n")) {
322
+ const m = line.trim().match(/^(\d+)\s+(\d+)\s+([\d.]+)$/);
323
+ if (!m) continue;
324
+ const pid = Number(m[1]);
325
+ const ppid = Number(m[2]);
326
+ const c = parseFloat(m[3]);
327
+ if (Number.isFinite(c)) cpu.set(pid, c);
328
+ const arr = kids.get(ppid);
329
+ if (arr) arr.push(pid);
330
+ else kids.set(ppid, [pid]);
331
+ }
321
332
  } catch {
322
- return null;
333
+ /* ps absent/guarded */
334
+ }
335
+ return { cpu, kids };
336
+ }
337
+
338
+ /** Collect a pid plus ALL its descendants into `into`. tmux/background workers
339
+ * run as CHILDREN of the recorded pane/shell pid, so sampling the root pid alone
340
+ * reads ~0% (the shell idles while the worker burns cpu in a child). */
341
+ function collectSubtree(
342
+ root: number,
343
+ table: { cpu: Map<number, number>; kids: Map<number, number[]> },
344
+ into: Set<number>
345
+ ): void {
346
+ const stack = [root];
347
+ while (stack.length) {
348
+ const pid = stack.pop() as number;
349
+ if (into.has(pid)) continue;
350
+ into.add(pid);
351
+ for (const k of table.kids.get(pid) ?? []) stack.push(k);
323
352
  }
324
353
  }
325
354
 
355
+ /** Sum %cpu of a set of pids from the table. */
356
+ function sumCpu(
357
+ pids: Set<number>,
358
+ table: { cpu: Map<number, number>; kids: Map<number, number[]> }
359
+ ): number | null {
360
+ let total = 0;
361
+ let any = false;
362
+ for (const pid of pids) {
363
+ const c = table.cpu.get(pid);
364
+ if (c != null) {
365
+ total += c;
366
+ any = true;
367
+ }
368
+ }
369
+ return any ? Math.round(total * 10) / 10 : null;
370
+ }
371
+
326
372
  /**
327
373
  * Discover tmux company panes: pane_pid keyed by session name, for sessions
328
374
  * matching `walwal_<projbasename>_*`. tmux may be absent → returns {}.
@@ -473,18 +519,14 @@ export async function sampleMetrics(harnessRoot: string): Promise<MetricsSample>
473
519
  const panes = tmuxCompanyPanes(projBase);
474
520
  for (const p of panes) pidSet.add(p.pid);
475
521
 
522
+ const procTable = readProcTable();
476
523
  let cpu: number | null = null;
477
524
  if (pidSet.size > 0) {
478
- let total = 0;
479
- let any = false;
480
- for (const pid of pidSet) {
481
- const c = cpuForPid(pid);
482
- if (c != null) {
483
- total += c;
484
- any = true;
485
- }
486
- }
487
- cpu = any ? Math.round(total * 10) / 10 : null;
525
+ // Sum each discovered pid's whole process subtree (deduped), so a busy
526
+ // worker running as a child of its tmux/background shell is counted.
527
+ const counted = new Set<number>();
528
+ for (const pid of pidSet) collectSubtree(pid, procTable, counted);
529
+ cpu = sumCpu(counted, procTable);
488
530
  }
489
531
 
490
532
  /* ---- 3. perAgent: attribute via tmux session name ---------------- */
@@ -495,7 +537,9 @@ export async function sampleMetrics(harnessRoot: string): Promise<MetricsSample>
495
537
  for (const p of panes) {
496
538
  // session name: walwal_<projBase>_<stamp> — stamp is the agent tag.
497
539
  const tag = p.session.slice(prefix.length) || p.session;
498
- const c = cpuForPid(p.pid);
540
+ const sub = new Set<number>();
541
+ collectSubtree(p.pid, procTable, sub);
542
+ const c = sumCpu(sub, procTable);
499
543
  const cur = acc[tag] ?? {};
500
544
  if (c != null) cur.cpu = Math.round(((cur.cpu ?? 0) + c) * 10) / 10;
501
545
  acc[tag] = cur;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.43",
3
+ "version": "7.1.44",
4
4
  "description": "Company-style AI agent harness for Claude and Codex. Installs commands, CXX agents, skills, HR-Resource hiring pool, and project-local .harness runtime state.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"