agent-yes 1.284.1 → 1.285.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.
Files changed (31) hide show
  1. package/dist/{SUPPORTED_CLIS-DZwCkmvh.js → SUPPORTED_CLIS-CXvLHLG2.js} +3 -3
  2. package/dist/{SUPPORTED_CLIS-B2Jis2RD.js → SUPPORTED_CLIS-tYZC_KWR.js} +2 -2
  3. package/dist/{agentShare-BSh14vJq.js → agentShare-0hRw0bQn.js} +2 -2
  4. package/dist/{callback-nuwvhn5x.js → callback-BN6cqDjb.js} +3 -3
  5. package/dist/{callback-Cyr2rDek.js → callback-DCaGBxD1.js} +2 -2
  6. package/dist/cli.js +5 -5
  7. package/dist/{cmdPs-tzFiiKC5.js → cmdPs-CUkarOK7.js} +123 -18
  8. package/dist/index.js +2 -2
  9. package/dist/{notifyDaemon-BBQKDl7Q.js → notifyDaemon-DNTRkaSU.js} +2 -2
  10. package/dist/{parentPingLoop-CVy-ueZc.js → parentPingLoop-CovRlBFM.js} +2 -2
  11. package/dist/{rustBinary-CRR9Sr3R.js → rustBinary-CkKsMycJ.js} +2 -2
  12. package/dist/{rustBinary-C9B5hRAU.js → rustBinary-ydpdBd3-.js} +2 -2
  13. package/dist/{schedule-CH_evU-N.js → schedule-CgzQNLfP.js} +4 -4
  14. package/dist/{serve-CC074vGJ.js → serve-nk0r7fxV.js} +17 -17
  15. package/dist/{setup-DDZMpGsh.js → setup-P7AMLtbV.js} +2 -2
  16. package/dist/{subcommands-D2iZ_g0A.js → subcommands-B_Lg_zDe.js} +15 -15
  17. package/dist/{subcommands-DafbEv0e.js → subcommands-SeVXiezY.js} +1 -1
  18. package/dist/{terminal-C-UzqGmj.js → terminal-DM_kIymf.js} +2 -2
  19. package/dist/trayApp-BIOUrF8R.js +5 -0
  20. package/dist/{trayApp-Dylg_sbJ.js → trayApp-CUxqXOmu.js} +2 -2
  21. package/dist/{ts-BmiGlG1v.js → ts-LgWg13cL.js} +3 -3
  22. package/dist/{versionChecker-D59qJF9N.js → versionChecker-CbqhEGUc.js} +2 -2
  23. package/dist/{widget-BENlkcvI.js → widget-DUkLAsSR.js} +3 -3
  24. package/dist/{ws-PKok408W.js → ws-BAy7xMDI.js} +2 -2
  25. package/dist/{ws-DvMM1i2j.js → ws-ChkhtUQm.js} +2 -2
  26. package/package.json +1 -1
  27. package/ts/cmdPs.spec.ts +126 -1
  28. package/ts/cmdPs.ts +148 -12
  29. package/ts/procStats.spec.ts +32 -6
  30. package/ts/procStats.ts +77 -5
  31. package/dist/trayApp-D5SzLS6p.js +0 -5
package/ts/cmdPs.ts CHANGED
@@ -21,6 +21,7 @@ import yargs from "yargs";
21
21
  import { buildAgentForest, flattenForest } from "./agentTree.ts";
22
22
  import {
23
23
  humanBytes,
24
+ type ProcRow,
24
25
  sampleTrees,
25
26
  snapshotProcs,
26
27
  systemStats,
@@ -85,8 +86,62 @@ export interface PsRow {
85
86
  status: string;
86
87
  cwd: string;
87
88
  repo: string;
89
+ /** This agent's OWN cost, exclusive of any nested agent (see sampleTrees). */
88
90
  stats: TreeStats;
89
91
  self: boolean;
92
+ /** `├─ `/`└─ ` rails from the agent forest. Empty outside --tree. */
93
+ prefix?: string;
94
+ /** Depth in the agent forest; 0 for roots. */
95
+ depth?: number;
96
+ /**
97
+ * This agent's cost PLUS every descendant agent's. Set only in --tree mode,
98
+ * and only for rows that actually have descendants — a leaf's subtree IS its
99
+ * own row, so repeating the numbers would be noise.
100
+ */
101
+ subtree?: TreeStats | null;
102
+ /**
103
+ * The OS processes this agent owns (wrapper, the CLI itself, its shells and
104
+ * pty hosts). Populated in --tree so the agent's number can be read as a sum
105
+ * of things you can actually point at.
106
+ */
107
+ procRows?: ProcRow[];
108
+ }
109
+
110
+ /**
111
+ * Roll each row up with its descendants, using the forest `depth` column: a
112
+ * row's descendants are the following rows with a strictly greater depth, up to
113
+ * the next row at or above its own level.
114
+ *
115
+ * Needed because sampleTrees attributes DEEPEST-FIRST and exclusively — a
116
+ * parent's own numbers deliberately omit its subagents, so without this a
117
+ * fan-out parent looks cheap while its real cost sits in rows below it.
118
+ *
119
+ * Returns null for a row with no descendants.
120
+ */
121
+ export function subtreeTotals(rows: Pick<PsRow, "depth" | "stats">[]): (TreeStats | null)[] {
122
+ return rows.map((r, i) => {
123
+ const depth = r.depth ?? 0;
124
+ // Guard the row's OWN stats the same way its descendants' are guarded
125
+ // below: the sampler can miss an agent that exited mid-window, and one
126
+ // absent entry must not take down the whole table.
127
+ const own = r.stats;
128
+ if (!own) return null;
129
+ let rss = own.rss;
130
+ let cpuPercent = own.cpuPercent;
131
+ let procs = own.procs;
132
+ let found = false;
133
+ for (let j = i + 1; j < rows.length; j++) {
134
+ const d = rows[j]?.depth ?? 0;
135
+ if (d <= depth) break; // left this row's subtree
136
+ const st = rows[j]?.stats;
137
+ if (!st) continue;
138
+ found = true;
139
+ rss += st.rss;
140
+ cpuPercent += st.cpuPercent;
141
+ procs += st.procs;
142
+ }
143
+ return found ? { pid: own.pid, rss, cpuPercent, procs } : null;
144
+ });
90
145
  }
91
146
 
92
147
  function pad(s: string, n: number): string {
@@ -96,15 +151,27 @@ function padStart(s: string, n: number): string {
96
151
  return s.length >= n ? s : " ".repeat(n - s.length) + s;
97
152
  }
98
153
 
99
- export function renderTable(rows: PsRow[], unattributed: TreeStats | null): string {
154
+ export function renderTable(
155
+ rows: PsRow[],
156
+ unattributed: TreeStats | null,
157
+ opts: { tree?: boolean } = {},
158
+ ): string {
159
+ const tree = opts.tree === true;
100
160
  const cells = rows.map((r) => ({
101
- pid: String(r.pid),
161
+ // In tree mode the rails live on the PID cell, so the hierarchy reads down
162
+ // the left edge exactly like `ay ls`.
163
+ pid: (tree ? (r.prefix ?? "") : "") + String(r.pid),
102
164
  cli: r.cli,
103
165
  status: r.status,
104
166
  cpu: r.stats.cpuPercent.toFixed(1),
105
167
  rss: humanBytes(r.stats.rss),
106
168
  procs: String(r.stats.procs),
107
169
  repo: r.repo,
170
+ cwd: shortenPath(r.cwd),
171
+ // Σ columns: this agent plus its descendants. Blank for leaves.
172
+ scpu: r.subtree ? r.subtree.cpuPercent.toFixed(1) : "",
173
+ srss: r.subtree ? humanBytes(r.subtree.rss) : "",
174
+ sprocs: r.subtree ? String(r.subtree.procs) : "",
108
175
  self: r.self,
109
176
  }));
110
177
  // The unmanaged row participates in the width pass — "unmanaged" is wider than
@@ -118,6 +185,10 @@ export function renderTable(rows: PsRow[], unattributed: TreeStats | null): stri
118
185
  rss: humanBytes(unattributed.rss),
119
186
  procs: String(unattributed.procs),
120
187
  repo: "-",
188
+ cwd: "-",
189
+ scpu: "",
190
+ srss: "",
191
+ sprocs: "",
121
192
  self: false,
122
193
  });
123
194
  }
@@ -129,6 +200,9 @@ export function renderTable(rows: PsRow[], unattributed: TreeStats | null): stri
129
200
  rss: Math.max(5, ...cells.map((c) => c.rss.length)),
130
201
  procs: Math.max(5, ...cells.map((c) => c.procs.length)),
131
202
  repo: Math.max(4, ...cells.map((c) => c.repo.length)),
203
+ scpu: Math.max(5, ...cells.map((c) => c.scpu.length)),
204
+ srss: Math.max(5, ...cells.map((c) => c.srss.length)),
205
+ sprocs: Math.max(6, ...cells.map((c) => c.sprocs.length)),
132
206
  };
133
207
  const line = (
134
208
  pid: string,
@@ -138,18 +212,63 @@ export function renderTable(rows: PsRow[], unattributed: TreeStats | null): stri
138
212
  rss: string,
139
213
  procs: string,
140
214
  repo: string,
215
+ cwd: string,
216
+ scpu = "",
217
+ srss = "",
218
+ sprocs = "",
141
219
  tail = "",
142
220
  ) =>
143
221
  `${pad(pid, w.pid)} ${pad(cli, w.cli)} ${pad(status, w.status)} ` +
144
222
  `${padStart(cpu, w.cpu)} ${padStart(rss, w.rss)} ${padStart(procs, w.procs)} ` +
145
- `${pad(repo, w.repo)}${tail}`;
223
+ // The Σ block only exists in tree mode, where a parent's own numbers exclude
224
+ // its subagents and are therefore misleading on their own.
225
+ (tree
226
+ ? `${padStart(scpu, w.scpu)} ${padStart(srss, w.srss)} ${padStart(sprocs, w.sprocs)} `
227
+ : "") +
228
+ // REPO stays as the at-a-glance identity; CWD follows with the full path
229
+ // (home collapsed to ~) because sibling worktrees of one repo share a REPO
230
+ // label and are otherwise indistinguishable here. NOT width-padded: it is
231
+ // the last column, and paths run long enough (120 chars on a real fleet)
232
+ // that padding to the widest would trail ~100 blanks on every other row.
233
+ `${pad(repo, w.repo)} ${cwd}${tail}`;
146
234
 
147
- const out: string[] = [line("PID", "CLI", "STATUS", "CPU%", "RSS", "PROCS", "REPO")];
148
- for (const c of cells) {
235
+ const out: string[] = [
236
+ line("PID", "CLI", "STATUS", "CPU%", "RSS", "PROCS", "REPO", "CWD", "ΣCPU%", "ΣRSS", "ΣPROCS"),
237
+ ];
238
+ cells.forEach((c, i) => {
149
239
  out.push(
150
- line(c.pid, c.cli, c.status, c.cpu, c.rss, c.procs, c.repo, c.self ? " ← this session" : ""),
240
+ line(
241
+ c.pid,
242
+ c.cli,
243
+ c.status,
244
+ c.cpu,
245
+ c.rss,
246
+ c.procs,
247
+ c.repo,
248
+ c.cwd,
249
+ c.scpu,
250
+ c.srss,
251
+ c.sprocs,
252
+ c.self ? " ← this session" : "",
253
+ ),
151
254
  );
152
- }
255
+ // The agent's own processes, one indent deeper than its rails. Rendered as
256
+ // a continuation of the row above rather than as table rows: they are a
257
+ // DIFFERENT kind of thing (an OS process, not an agent), and giving them
258
+ // the agent columns would invite reading a shell as a fifth agent.
259
+ const kids = rows[i]?.procRows;
260
+ if (!tree || !kids?.length) return;
261
+ const indent = " ".repeat((rows[i]?.prefix ?? "").length + 2);
262
+ const pw = Math.max(...kids.map((k) => String(k.pid).length));
263
+ const cw = Math.max(...kids.map((k) => k.comm.length));
264
+ for (const k of kids) {
265
+ out.push(
266
+ `${indent}· ${padStart(String(k.pid), pw)} ${pad(k.comm, cw)} ` +
267
+ `${padStart(k.cpuPercent.toFixed(1), 5)} ${padStart(humanBytes(k.rss), 7)}` +
268
+ (k.state === "Z" ? " zombie" : ""),
269
+ );
270
+ }
271
+ });
153
272
  return out.join("\n");
154
273
  }
155
274
 
@@ -176,7 +295,8 @@ export async function cmdPs(rest: string[]): Promise<number> {
176
295
  .option("tree", {
177
296
  type: "boolean",
178
297
  default: false,
179
- description: "Keep ay ls's parent>child forest order instead of sorting",
298
+ description:
299
+ "Forest order with ├─ rails, plus Σ columns rolling each agent up with its subagents",
180
300
  })
181
301
  .option("interval", {
182
302
  type: "number",
@@ -187,6 +307,7 @@ export async function cmdPs(rest: string[]): Promise<number> {
187
307
  .option("help", { alias: "h", type: "boolean", default: false, description: "Show this help" })
188
308
  .example("ay ps", "biggest agents first, with box vitals")
189
309
  .example("ay ps --sort cpu", "who is actually burning CPU right now")
310
+ .example("ay ps --tree", "parent>child forest; Σ columns include each agent's subagents")
190
311
  .example("ay ps --json", "machine-readable rollup")
191
312
  .help(false)
192
313
  .version(false)
@@ -216,9 +337,10 @@ export async function cmdPs(rest: string[]): Promise<number> {
216
337
  // first-come, so sampling in that order would let a parent absorb a nested
217
338
  // agent's whole subtree and render the child as a bogus 0-proc row. Reverse to
218
339
  // claim deepest-first, then display in forest order.
219
- const ordered = flattenForest(buildAgentForest(records)).map((r) => r.record);
340
+ const flat = flattenForest(buildAgentForest(records));
341
+ const ordered = flat.map((r) => r.record);
220
342
  const windowMs = Math.max(100, (Number.isFinite(argv.interval) ? argv.interval : 1) * 1000);
221
- const { trees, unattributed } = await sampleTrees(
343
+ const { trees, unattributed, members } = await sampleTrees(
222
344
  [...ordered].reverse().map((r) => r.pid),
223
345
  { windowMs },
224
346
  );
@@ -227,7 +349,7 @@ export async function cmdPs(rest: string[]): Promise<number> {
227
349
  await Promise.all(ordered.map(async (r) => [r.pid, (await deriveLiveState(r)).state] as const)),
228
350
  );
229
351
  const selfPid = process.pid;
230
- const rows: PsRow[] = ordered.map((r) => ({
352
+ const rows: PsRow[] = ordered.map((r, i) => ({
231
353
  pid: r.pid,
232
354
  cli: r.cli,
233
355
  status: states.get(r.pid) ?? r.status,
@@ -237,8 +359,22 @@ export async function cmdPs(rest: string[]): Promise<number> {
237
359
  // Mark the tree we are standing in — anything that later grows a kill
238
360
  // affordance must not let you shoot the session issuing the command.
239
361
  self: trees.get(r.pid) !== undefined && isSelfTree(r.pid, selfPid),
362
+ prefix: flat[i]?.prefix ?? "",
363
+ depth: flat[i]?.depth ?? 0,
240
364
  }));
241
365
 
366
+ // Only meaningful in forest order — sorting by rss would scatter descendants
367
+ // away from their parent and make a rollup describe the wrong span of rows.
368
+ if (argv.tree) {
369
+ const totals = subtreeTotals(rows);
370
+ rows.forEach((r, i) => {
371
+ r.subtree = totals[i] ?? null;
372
+ // Heaviest first: the reason to expand an agent is to find what inside it
373
+ // is costing something, and that is almost never the wrapper.
374
+ r.procRows = [...(members?.get(r.pid) ?? [])].sort((x, y) => y.rss - x.rss);
375
+ });
376
+ }
377
+
242
378
  if (!argv.tree) {
243
379
  const key = String(argv.sort);
244
380
  rows.sort((a, b) => {
@@ -281,7 +417,7 @@ export async function cmdPs(rest: string[]): Promise<number> {
281
417
 
282
418
  const header = formatSystemLine(sys);
283
419
  if (header) process.stdout.write(` ${header}\n\n`);
284
- process.stdout.write(renderTable(rows, unattributed) + "\n");
420
+ process.stdout.write(renderTable(rows, unattributed, { tree: argv.tree === true }) + "\n");
285
421
  return 0;
286
422
  }
287
423
 
@@ -53,6 +53,7 @@ describe("parseProcStat", () => {
53
53
  expect(s).toEqual({
54
54
  pid: 42,
55
55
  ppid: 7,
56
+ comm: "claude", // statLine's default
56
57
  rss: 10 * 4096,
57
58
  cpuSeconds: 2, // (150 + 50) ticks / 100 USER_HZ
58
59
  state: "S",
@@ -97,12 +98,37 @@ describe("parsePsTime", () => {
97
98
  });
98
99
  });
99
100
 
101
+ describe("comm extraction", () => {
102
+ it("takes the comm from a stat line even when it holds spaces and parens", () => {
103
+ // The killer case: a comm containing ')' is why the field parse anchors on
104
+ // the LAST ')' — and the comm itself must span the FIRST '(' to that same
105
+ // anchor, or a name like "Web Content (1)" gets truncated.
106
+ const s = parseProcStat(9, statLine({ pid: 9, ppid: 1, comm: "Web Content (1)" }));
107
+ expect(s?.comm).toBe("Web Content (1)");
108
+ expect(s?.ppid).toBe(1); // fields after the comm still line up
109
+ });
110
+
111
+ it("keeps a plain comm intact", () => {
112
+ expect(parseProcStat(9, statLine({ pid: 9, ppid: 1, comm: "bash" }))?.comm).toBe("bash");
113
+ });
114
+ });
115
+
100
116
  describe("parsePsTable", () => {
117
+ it("rejoins a comm that contains spaces, since it is the LAST column", () => {
118
+ const procs = parsePsTable(" 10 1 2048 00:10 S /usr/bin/Google Chrome Helper\n");
119
+ expect(procs.get(10)?.comm).toBe("/usr/bin/Google Chrome Helper");
120
+ // The columns before it must be unaffected by those spaces.
121
+ expect(procs.get(10)?.rss).toBe(2048 * 1024);
122
+ expect(procs.get(10)?.state).toBe("S");
123
+ });
124
+
101
125
  it("normalizes ps KiB into bytes so both backends agree", () => {
102
126
  const procs = parsePsTable(" 10 1 2048 00:10 S\n 11 10 512 00:00 Z\n");
103
127
  expect(procs.get(10)).toEqual({
104
128
  pid: 10,
105
129
  ppid: 1,
130
+ // This fixture predates the comm column; an absent one must read as "".
131
+ comm: "",
106
132
  rss: 2048 * 1024,
107
133
  cpuSeconds: 10,
108
134
  state: "S",
@@ -129,7 +155,7 @@ describe("descendantsOf", () => {
129
155
  ] as [number, number][]
130
156
  ).map(([pid, ppid]) => [
131
157
  pid,
132
- { pid, ppid, rss: 0, cpuSeconds: 0, state: "S", startToken: `t${pid}` },
158
+ { pid, ppid, comm: "node", rss: 0, cpuSeconds: 0, state: "S", startToken: `t${pid}` },
133
159
  ]),
134
160
  );
135
161
  const kids = buildChildIndex(procs);
@@ -144,8 +170,8 @@ describe("descendantsOf", () => {
144
170
 
145
171
  it("terminates on a parent cycle instead of blowing the stack", () => {
146
172
  const cyclic = new Map<number, ProcSample>([
147
- [1, { pid: 1, ppid: 2, rss: 0, cpuSeconds: 0, state: "S", startToken: "t1" }],
148
- [2, { pid: 2, ppid: 1, rss: 0, cpuSeconds: 0, state: "S", startToken: "t2" }],
173
+ [1, { pid: 1, ppid: 2, comm: "node", rss: 0, cpuSeconds: 0, state: "S", startToken: "t1" }],
174
+ [2, { pid: 2, ppid: 1, comm: "node", rss: 0, cpuSeconds: 0, state: "S", startToken: "t2" }],
149
175
  ]);
150
176
  expect(descendantsOf(1, buildChildIndex(cyclic)).size).toBe(2);
151
177
  });
@@ -319,9 +345,9 @@ describe("parseMeminfo", () => {
319
345
  describe("systemStats", () => {
320
346
  it("reads load/mem and counts zombies from the snapshot", async () => {
321
347
  const procs = new Map<number, ProcSample>([
322
- [1, { pid: 1, ppid: 0, rss: 0, cpuSeconds: 0, state: "S", startToken: "t1" }],
323
- [2, { pid: 2, ppid: 1, rss: 0, cpuSeconds: 0, state: "Z", startToken: "t2" }],
324
- [3, { pid: 3, ppid: 1, rss: 0, cpuSeconds: 0, state: "Z", startToken: "t3" }],
348
+ [1, { pid: 1, ppid: 0, comm: "node", rss: 0, cpuSeconds: 0, state: "S", startToken: "t1" }],
349
+ [2, { pid: 2, ppid: 1, comm: "node", rss: 0, cpuSeconds: 0, state: "Z", startToken: "t2" }],
350
+ [3, { pid: 3, ppid: 1, comm: "node", rss: 0, cpuSeconds: 0, state: "Z", startToken: "t3" }],
325
351
  ]);
326
352
  const sys = await systemStats(procs, {
327
353
  readSys: async (name) =>
package/ts/procStats.ts CHANGED
@@ -34,6 +34,8 @@ const execFileAsync = promisify(execFile);
34
34
  export interface ProcSample {
35
35
  pid: number;
36
36
  ppid: number;
37
+ /** Executable name (`bash`, `node`). May be a full path from the ps fallback. */
38
+ comm: string;
37
39
  /** Resident set size in bytes. */
38
40
  rss: number;
39
41
  /** Cumulative CPU (user+sys) in seconds since process start. */
@@ -113,6 +115,10 @@ export function parseProcStat(
113
115
  ): ProcSample | null {
114
116
  const close = stat.lastIndexOf(")");
115
117
  if (close < 0) return null;
118
+ // comm is field 2, wrapped in parens and free to contain spaces AND parens —
119
+ // hence first '(' to LAST ')', the same anchor the field parse below uses.
120
+ const open = stat.indexOf("(");
121
+ const comm = open >= 0 && open < close ? stat.slice(open + 1, close) : "";
116
122
  const f = stat
117
123
  .slice(close + 1)
118
124
  .trim()
@@ -128,6 +134,7 @@ export function parseProcStat(
128
134
  return {
129
135
  pid,
130
136
  ppid,
137
+ comm,
131
138
  rss: Number.isFinite(rssPages) ? rssPages * pageSizeBytes : 0,
132
139
  cpuSeconds: (utime + stime) / USER_HZ,
133
140
  state,
@@ -144,7 +151,7 @@ export interface ProcReaders {
144
151
  readStat?: (pid: number) => Promise<string | null>;
145
152
  /** Whole-file read of /proc/loadavg, /proc/meminfo, … or null. */
146
153
  readSys?: (name: string) => Promise<string | null>;
147
- /** `ps -eo pid=,ppid=,rss=,time=,state=` output, for the non-Linux fallback. */
154
+ /** `ps -eo pid=,ppid=,rss=,time=,state=,comm=` output, for the non-Linux fallback. */
148
155
  readPsTable?: () => Promise<string | null>;
149
156
  }
150
157
 
@@ -183,7 +190,7 @@ export async function defaultReadSys(name: string): Promise<string | null> {
183
190
 
184
191
  export async function defaultReadPsTable(): Promise<string | null> {
185
192
  try {
186
- const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,rss=,time=,state="], {
193
+ const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,rss=,time=,state=,comm="], {
187
194
  encoding: "utf8",
188
195
  timeout: 3000,
189
196
  maxBuffer: 16 * 1024 * 1024,
@@ -208,13 +215,20 @@ export function parsePsTime(raw: string): number {
208
215
  return seconds;
209
216
  }
210
217
 
211
- /** Parse the `ps -eo pid=,ppid=,rss=,time=,state=` fallback table. */
218
+ /**
219
+ * Parse the `ps -eo pid=,ppid=,rss=,time=,state=,comm=` fallback table.
220
+ *
221
+ * comm is requested LAST on purpose: it can contain spaces (`Google Chrome
222
+ * Helper`), so anywhere else it would shift every column after it. Being last,
223
+ * everything from field 6 on is simply rejoined.
224
+ */
212
225
  export function parsePsTable(out: string): Map<number, ProcSample> {
213
226
  const procs = new Map<number, ProcSample>();
214
227
  for (const line of out.split("\n")) {
215
228
  const m = line.trim().split(/\s+/);
216
229
  if (m.length < 5) continue;
217
230
  const [pidS, ppidS, rssS, timeS, state] = m;
231
+ const comm = m.slice(5).join(" ");
218
232
  const pid = Number(pidS);
219
233
  const ppid = Number(ppidS);
220
234
  const rssKib = Number(rssS);
@@ -222,6 +236,7 @@ export function parsePsTable(out: string): Map<number, ProcSample> {
222
236
  procs.set(pid, {
223
237
  pid,
224
238
  ppid,
239
+ comm,
225
240
  // ps reports RSS in KiB; /proc gives bytes. Normalize to bytes so both
226
241
  // backends feed identical numbers into the rollup.
227
242
  rss: Number.isFinite(rssKib) ? rssKib * 1024 : 0,
@@ -303,6 +318,52 @@ export interface TreeStats {
303
318
  procs: number;
304
319
  }
305
320
 
321
+ /** One OS process inside an agent's tree, as rendered by `ay ps --tree`. */
322
+ export interface ProcRow {
323
+ pid: number;
324
+ ppid: number;
325
+ /** Executable basename — the full path from `ps -o comm=` is noise in a table. */
326
+ comm: string;
327
+ rss: number;
328
+ /** Percent of ONE core over the sample window. */
329
+ cpuPercent: number;
330
+ state: string;
331
+ }
332
+
333
+ /**
334
+ * Per-process detail for one claimed member set, same window and pid-reuse
335
+ * rules as `rollup` (see there for why a newcomer or a reused pid contributes
336
+ * no CPU delta).
337
+ */
338
+ function memberRows(
339
+ members: Set<number>,
340
+ first: Map<number, ProcSample>,
341
+ second: Map<number, ProcSample>,
342
+ elapsedSeconds: number,
343
+ ): ProcRow[] {
344
+ const out: ProcRow[] = [];
345
+ for (const pid of members) {
346
+ const now = second.get(pid);
347
+ if (!now) continue;
348
+ const before = first.get(pid);
349
+ const reused =
350
+ before !== undefined &&
351
+ before.startToken !== "" &&
352
+ now.startToken !== "" &&
353
+ before.startToken !== now.startToken;
354
+ const delta = before && !reused ? Math.max(0, now.cpuSeconds - before.cpuSeconds) : 0;
355
+ out.push({
356
+ pid,
357
+ ppid: now.ppid,
358
+ comm: (now.comm || "").split(/[\\/]+/).filter(Boolean).pop() ?? "",
359
+ rss: now.rss,
360
+ cpuPercent: elapsedSeconds > 0 ? (delta / elapsedSeconds) * 100 : 0,
361
+ state: now.state,
362
+ });
363
+ }
364
+ return out;
365
+ }
366
+
306
367
  function rollup(
307
368
  root: number,
308
369
  members: Set<number>,
@@ -360,7 +421,12 @@ function rollup(
360
421
  export async function sampleTrees(
361
422
  roots: number[],
362
423
  opts: { windowMs?: number; deps?: ProcReaders } = {},
363
- ): Promise<{ trees: Map<number, TreeStats>; unattributed: TreeStats }> {
424
+ ): Promise<{
425
+ trees: Map<number, TreeStats>;
426
+ unattributed: TreeStats;
427
+ /** root pid → the OS processes claimed by that root, for --tree. */
428
+ members: Map<number, ProcRow[]>;
429
+ }> {
364
430
  const windowMs = Math.max(100, opts.windowMs ?? 1000);
365
431
  // Measure the ACTUAL elapsed time, not the requested window: a loaded box (the
366
432
  // exact case this command is for) can oversleep by hundreds of ms, and
@@ -385,10 +451,12 @@ export async function sampleTrees(
385
451
  const kids = buildChildIndex(second);
386
452
  const claimed = new Set<number>();
387
453
  const trees = new Map<number, TreeStats>();
454
+ const memberRowsByRoot = new Map<number, ProcRow[]>();
388
455
  for (const root of roots) {
389
456
  const members = descendantsOf(root, kids, claimed);
390
457
  for (const pid of members) claimed.add(pid);
391
458
  trees.set(root, rollup(root, members, first, second, elapsedSeconds));
459
+ memberRowsByRoot.set(root, memberRows(members, first, second, elapsedSeconds));
392
460
  }
393
461
 
394
462
  // Everything ay does NOT manage, as one row. This is the answer to "is there
@@ -396,7 +464,11 @@ export async function sampleTrees(
396
464
  // box from one quietly hoarding orphans.
397
465
  const rest = new Set<number>();
398
466
  for (const pid of second.keys()) if (!claimed.has(pid)) rest.add(pid);
399
- return { trees, unattributed: rollup(0, rest, first, second, elapsedSeconds) };
467
+ return {
468
+ trees,
469
+ unattributed: rollup(0, rest, first, second, elapsedSeconds),
470
+ members: memberRowsByRoot,
471
+ };
400
472
  }
401
473
 
402
474
  /** Whole-box vitals for the header line. */
@@ -1,5 +0,0 @@
1
- import "./versionChecker-D59qJF9N.js";
2
- import "./rustBinary-C9B5hRAU.js";
3
- import { a as trayHiddenMarker, i as launchTray, n as hasDesktop, r as isTrayHidden, t as cmdTray } from "./trayApp-Dylg_sbJ.js";
4
-
5
- export { cmdTray };