agentflow-core 0.6.0 → 0.6.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/cli.cjs CHANGED
@@ -96,8 +96,8 @@ var init_loader = __esm({
96
96
  var import_path3 = require("path");
97
97
 
98
98
  // src/live.ts
99
- var import_node_fs = require("fs");
100
- var import_node_path = require("path");
99
+ var import_node_fs2 = require("fs");
100
+ var import_node_path2 = require("path");
101
101
 
102
102
  // src/graph-query.ts
103
103
  function getChildren(graph, nodeId) {
@@ -244,6 +244,264 @@ function getTraceTree(trace) {
244
244
 
245
245
  // src/live.ts
246
246
  init_loader();
247
+
248
+ // src/process-audit.ts
249
+ var import_node_child_process = require("child_process");
250
+ var import_node_fs = require("fs");
251
+ var import_node_path = require("path");
252
+ function isPidAlive(pid) {
253
+ try {
254
+ process.kill(pid, 0);
255
+ return true;
256
+ } catch {
257
+ return false;
258
+ }
259
+ }
260
+ function pidMatchesName(pid, name) {
261
+ try {
262
+ const cmdline = (0, import_node_fs.readFileSync)(`/proc/${pid}/cmdline`, "utf8");
263
+ return cmdline.includes(name);
264
+ } catch {
265
+ return false;
266
+ }
267
+ }
268
+ function readPidFile(path) {
269
+ try {
270
+ const pid = parseInt((0, import_node_fs.readFileSync)(path, "utf8").trim(), 10);
271
+ return isNaN(pid) ? null : pid;
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+ function auditPidFile(config) {
277
+ if (!config.pidFile) return null;
278
+ const pid = readPidFile(config.pidFile);
279
+ if (pid === null) {
280
+ return {
281
+ path: config.pidFile,
282
+ pid: null,
283
+ alive: false,
284
+ matchesProcess: false,
285
+ stale: !(0, import_node_fs.existsSync)(config.pidFile),
286
+ reason: (0, import_node_fs.existsSync)(config.pidFile) ? "PID file exists but content is invalid" : "No PID file found"
287
+ };
288
+ }
289
+ const alive = isPidAlive(pid);
290
+ const matchesProcess = alive ? pidMatchesName(pid, config.processName) : false;
291
+ const stale = !alive || alive && !matchesProcess;
292
+ let reason;
293
+ if (alive && matchesProcess) {
294
+ reason = `PID ${pid} alive and matches ${config.processName}`;
295
+ } else if (alive && !matchesProcess) {
296
+ reason = `PID ${pid} alive but is NOT ${config.processName} (PID reused by another process)`;
297
+ } else {
298
+ reason = `PID ${pid} no longer exists`;
299
+ }
300
+ return { path: config.pidFile, pid, alive, matchesProcess, stale, reason };
301
+ }
302
+ function auditSystemd(config) {
303
+ if (config.systemdUnit === null || config.systemdUnit === void 0) return null;
304
+ const unit = config.systemdUnit;
305
+ try {
306
+ const raw = (0, import_node_child_process.execSync)(
307
+ `systemctl --user show ${unit} --property=ActiveState,SubState,MainPID,NRestarts,Result --no-pager 2>/dev/null`,
308
+ { encoding: "utf8", timeout: 5e3 }
309
+ );
310
+ const props = {};
311
+ for (const line of raw.trim().split("\n")) {
312
+ const [k, ...v] = line.split("=");
313
+ if (k) props[k.trim()] = v.join("=").trim();
314
+ }
315
+ const activeState = props["ActiveState"] ?? "unknown";
316
+ const subState = props["SubState"] ?? "unknown";
317
+ const mainPid = parseInt(props["MainPID"] ?? "0", 10);
318
+ const restarts = parseInt(props["NRestarts"] ?? "0", 10);
319
+ const result = props["Result"] ?? "unknown";
320
+ return {
321
+ unit,
322
+ activeState,
323
+ subState,
324
+ mainPid,
325
+ restarts,
326
+ result,
327
+ crashLooping: activeState === "activating" && subState === "auto-restart",
328
+ failed: activeState === "failed"
329
+ };
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+ function auditWorkers(config) {
335
+ if (!config.workersFile || !(0, import_node_fs.existsSync)(config.workersFile)) return null;
336
+ try {
337
+ const data = JSON.parse((0, import_node_fs.readFileSync)(config.workersFile, "utf8"));
338
+ const orchPid = data.pid ?? null;
339
+ const orchAlive = orchPid ? isPidAlive(orchPid) : false;
340
+ const workers = [];
341
+ for (const [name, info] of Object.entries(data.tools ?? {})) {
342
+ const w = info;
343
+ const wPid = w.pid ?? null;
344
+ const wAlive = wPid ? isPidAlive(wPid) : false;
345
+ workers.push({
346
+ name,
347
+ pid: wPid,
348
+ declaredStatus: w.status ?? "unknown",
349
+ alive: wAlive,
350
+ stale: w.status === "running" && !wAlive
351
+ });
352
+ }
353
+ return {
354
+ orchestratorPid: orchPid,
355
+ orchestratorAlive: orchAlive,
356
+ startedAt: data.started_at ?? "",
357
+ workers
358
+ };
359
+ } catch {
360
+ return null;
361
+ }
362
+ }
363
+ function getOsProcesses(processName) {
364
+ try {
365
+ const raw = (0, import_node_child_process.execSync)(`ps aux`, { encoding: "utf8", timeout: 5e3 });
366
+ return raw.split("\n").filter((line) => line.includes(processName) && !line.includes("process-audit") && !line.includes("grep")).map((line) => {
367
+ const parts = line.trim().split(/\s+/);
368
+ return {
369
+ pid: parseInt(parts[1] ?? "0", 10),
370
+ cpu: parts[2] ?? "0",
371
+ mem: parts[3] ?? "0",
372
+ command: parts.slice(10).join(" ")
373
+ };
374
+ }).filter((p) => !isNaN(p.pid) && p.pid > 0);
375
+ } catch {
376
+ return [];
377
+ }
378
+ }
379
+ function discoverProcessConfig(dirs) {
380
+ let pidFile;
381
+ let workersFile;
382
+ let processName = "";
383
+ for (const dir of dirs) {
384
+ if (!(0, import_node_fs.existsSync)(dir)) continue;
385
+ let entries;
386
+ try {
387
+ entries = (0, import_node_fs.readdirSync)(dir);
388
+ } catch {
389
+ continue;
390
+ }
391
+ for (const f of entries) {
392
+ const fp = (0, import_node_path.join)(dir, f);
393
+ try {
394
+ if (!(0, import_node_fs.statSync)(fp).isFile()) continue;
395
+ } catch {
396
+ continue;
397
+ }
398
+ if (f.endsWith(".pid") && !pidFile) {
399
+ pidFile = fp;
400
+ if (!processName) {
401
+ processName = (0, import_node_path.basename)(f, ".pid");
402
+ }
403
+ }
404
+ if ((f === "workers.json" || f.endsWith("-workers.json")) && !workersFile) {
405
+ workersFile = fp;
406
+ if (!processName && f !== "workers.json") {
407
+ processName = (0, import_node_path.basename)(f, "-workers.json");
408
+ }
409
+ }
410
+ }
411
+ }
412
+ if (!processName && !pidFile && !workersFile) return null;
413
+ if (!processName) processName = "agent";
414
+ return { processName, pidFile, workersFile };
415
+ }
416
+ function auditProcesses(config) {
417
+ const pidFile = auditPidFile(config);
418
+ const systemd = auditSystemd(config);
419
+ const workers = auditWorkers(config);
420
+ const osProcesses = getOsProcesses(config.processName);
421
+ const knownPids = /* @__PURE__ */ new Set();
422
+ if (pidFile?.pid && !pidFile.stale) knownPids.add(pidFile.pid);
423
+ if (workers) {
424
+ if (workers.orchestratorPid) knownPids.add(workers.orchestratorPid);
425
+ for (const w of workers.workers) {
426
+ if (w.pid) knownPids.add(w.pid);
427
+ }
428
+ }
429
+ if (systemd?.mainPid) knownPids.add(systemd.mainPid);
430
+ const orphans = osProcesses.filter((p) => !knownPids.has(p.pid));
431
+ const problems = [];
432
+ if (pidFile?.stale) problems.push(`Stale PID file: ${pidFile.reason}`);
433
+ if (systemd?.crashLooping) problems.push("Systemd unit is crash-looping (auto-restart)");
434
+ if (systemd?.failed) problems.push("Systemd unit has failed");
435
+ if (systemd && systemd.restarts > 10) problems.push(`High systemd restart count: ${systemd.restarts}`);
436
+ if (pidFile?.pid && systemd?.mainPid && pidFile.pid !== systemd.mainPid) {
437
+ problems.push(`PID mismatch: file says ${pidFile.pid}, systemd says ${systemd.mainPid}`);
438
+ }
439
+ if (workers) {
440
+ for (const w of workers.workers) {
441
+ if (w.stale) problems.push(`Worker "${w.name}" (pid ${w.pid}) declares running but is dead`);
442
+ }
443
+ }
444
+ if (orphans.length > 0) problems.push(`${orphans.length} orphan process(es) not tracked by PID file or workers registry`);
445
+ return { pidFile, systemd, workers, osProcesses, orphans, problems };
446
+ }
447
+ function formatAuditReport(result) {
448
+ const lines = [];
449
+ lines.push("");
450
+ lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
451
+ lines.push("\u2551 \u{1F50D} P R O C E S S A U D I T \u2551");
452
+ lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
453
+ if (result.pidFile) {
454
+ const pf = result.pidFile;
455
+ const icon = pf.pid && pf.alive && pf.matchesProcess ? "\u2705" : pf.stale ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
456
+ lines.push(`
457
+ PID File: ${pf.path}`);
458
+ lines.push(` ${icon} ${pf.reason}`);
459
+ }
460
+ if (result.systemd) {
461
+ const sd = result.systemd;
462
+ const icon = sd.activeState === "active" ? "\u{1F7E2}" : sd.crashLooping ? "\u{1F7E1}" : sd.failed ? "\u{1F534}" : "\u26AA";
463
+ lines.push(`
464
+ Systemd: ${sd.unit}`);
465
+ lines.push(` ${icon} State: ${sd.activeState} (${sd.subState}) Result: ${sd.result}`);
466
+ lines.push(` Main PID: ${sd.mainPid || "none"} Restarts: ${sd.restarts}`);
467
+ }
468
+ if (result.workers) {
469
+ const w = result.workers;
470
+ lines.push(`
471
+ Workers (orchestrator pid ${w.orchestratorPid ?? "unknown"} ${w.orchestratorAlive ? "\u2705" : "\u274C"})`);
472
+ for (const worker of w.workers) {
473
+ const icon = worker.declaredStatus === "running" && worker.alive ? "\u{1F7E2}" : worker.stale ? "\u{1F534} STALE" : "\u26AA";
474
+ lines.push(` ${icon} ${worker.name.padEnd(14)} pid=${String(worker.pid ?? "-").padEnd(8)} status=${worker.declaredStatus}`);
475
+ }
476
+ }
477
+ if (result.osProcesses.length > 0) {
478
+ lines.push(`
479
+ OS Processes (${result.osProcesses.length} total)`);
480
+ for (const p of result.osProcesses) {
481
+ lines.push(` PID ${String(p.pid).padEnd(8)} CPU=${p.cpu.padEnd(6)} MEM=${p.mem.padEnd(6)} ${p.command.substring(0, 55)}`);
482
+ }
483
+ }
484
+ if (result.orphans.length > 0) {
485
+ lines.push(`
486
+ \u26A0\uFE0F ${result.orphans.length} ORPHAN PROCESS(ES):`);
487
+ for (const p of result.orphans) {
488
+ lines.push(` PID ${p.pid} \u2014 not tracked by PID file or workers registry`);
489
+ }
490
+ }
491
+ lines.push("");
492
+ if (result.problems.length === 0) {
493
+ lines.push(" \u2705 All checks passed \u2014 no process issues detected.");
494
+ } else {
495
+ lines.push(` \u26A0\uFE0F ${result.problems.length} issue(s):`);
496
+ for (const p of result.problems) {
497
+ lines.push(` \u2022 ${p}`);
498
+ }
499
+ }
500
+ lines.push("");
501
+ return lines.join("\n");
502
+ }
503
+
504
+ // src/live.ts
247
505
  var C = {
248
506
  reset: "\x1B[0m",
249
507
  bold: "\x1B[1m",
@@ -276,13 +534,13 @@ function parseArgs(argv) {
276
534
  config.recursive = true;
277
535
  i++;
278
536
  } else if (!arg.startsWith("-")) {
279
- config.dirs.push((0, import_node_path.resolve)(arg));
537
+ config.dirs.push((0, import_node_path2.resolve)(arg));
280
538
  i++;
281
539
  } else {
282
540
  i++;
283
541
  }
284
542
  }
285
- if (config.dirs.length === 0) config.dirs.push((0, import_node_path.resolve)("."));
543
+ if (config.dirs.length === 0) config.dirs.push((0, import_node_path2.resolve)("."));
286
544
  return config;
287
545
  }
288
546
  function printUsage() {
@@ -318,7 +576,7 @@ function scanFiles(dirs, recursive) {
318
576
  const seen = /* @__PURE__ */ new Set();
319
577
  function scanDir(d, topLevel) {
320
578
  try {
321
- const dirStat = (0, import_node_fs.statSync)(d);
579
+ const dirStat = (0, import_node_fs2.statSync)(d);
322
580
  const dirMtime = dirStat.mtime.getTime();
323
581
  const cachedMtime = dirMtimeCache.get(d);
324
582
  if (cachedMtime === dirMtime) {
@@ -334,13 +592,13 @@ function scanFiles(dirs, recursive) {
334
592
  }
335
593
  }
336
594
  const dirResults = [];
337
- for (const f of (0, import_node_fs.readdirSync)(d)) {
595
+ for (const f of (0, import_node_fs2.readdirSync)(d)) {
338
596
  if (f.startsWith(".")) continue;
339
- const fp = (0, import_node_path.join)(d, f);
597
+ const fp = (0, import_node_path2.join)(d, f);
340
598
  if (seen.has(fp)) continue;
341
599
  let stat;
342
600
  try {
343
- stat = (0, import_node_fs.statSync)(fp);
601
+ stat = (0, import_node_fs2.statSync)(fp);
344
602
  } catch {
345
603
  continue;
346
604
  }
@@ -372,13 +630,13 @@ function scanFiles(dirs, recursive) {
372
630
  }
373
631
  function safeReadJson(fp) {
374
632
  try {
375
- return JSON.parse((0, import_node_fs.readFileSync)(fp, "utf8"));
633
+ return JSON.parse((0, import_node_fs2.readFileSync)(fp, "utf8"));
376
634
  } catch {
377
635
  return null;
378
636
  }
379
637
  }
380
638
  function nameFromFile(filename) {
381
- return (0, import_node_path.basename)(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
639
+ return (0, import_node_path2.basename)(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
382
640
  }
383
641
  function normalizeStatus(val) {
384
642
  if (typeof val !== "string") return "unknown";
@@ -556,7 +814,7 @@ function processJsonFile(file) {
556
814
  }
557
815
  function processJsonlFile(file) {
558
816
  try {
559
- const content = (0, import_node_fs.readFileSync)(file.path, "utf8").trim();
817
+ const content = (0, import_node_fs2.readFileSync)(file.path, "utf8").trim();
560
818
  if (!content) return [];
561
819
  const lines = content.split("\n");
562
820
  const lineCount = lines.length;
@@ -708,6 +966,9 @@ var prevFileCount = 0;
708
966
  var newExecCount = 0;
709
967
  var sessionStart = Date.now();
710
968
  var firstRender = true;
969
+ var cachedAuditConfig = null;
970
+ var cachedAuditResult = null;
971
+ var lastAuditTime = 0;
711
972
  var fileCache = /* @__PURE__ */ new Map();
712
973
  function getRecordsCached(f) {
713
974
  const cached = fileCache.get(f.path);
@@ -827,6 +1088,22 @@ function render(config) {
827
1088
  const level = Math.round(v / maxBucket * 8);
828
1089
  return (failBuckets[i] > 0 ? C.red : C.green) + sparkChars[level] + C.reset;
829
1090
  }).join("");
1091
+ let auditResult = null;
1092
+ if (now - lastAuditTime > 1e4) {
1093
+ if (!cachedAuditConfig) {
1094
+ cachedAuditConfig = discoverProcessConfig(config.dirs);
1095
+ }
1096
+ if (cachedAuditConfig) {
1097
+ try {
1098
+ auditResult = auditProcesses(cachedAuditConfig);
1099
+ cachedAuditResult = auditResult;
1100
+ lastAuditTime = now;
1101
+ } catch {
1102
+ }
1103
+ }
1104
+ } else {
1105
+ auditResult = cachedAuditResult;
1106
+ }
830
1107
  const distributedTraces = [];
831
1108
  if (allTraces.length > 1) {
832
1109
  const traceGroups = groupByTraceId(allTraces);
@@ -907,6 +1184,41 @@ function render(config) {
907
1184
  );
908
1185
  writeLine(L, "");
909
1186
  writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
1187
+ if (auditResult) {
1188
+ const ar = auditResult;
1189
+ const healthy = ar.problems.length === 0;
1190
+ const healthIcon = healthy ? `${C.green}\u25CF${C.reset}` : `${C.red}\u25CF${C.reset}`;
1191
+ const healthLabel = healthy ? `${C.green}healthy${C.reset}` : `${C.red}${ar.problems.length} issue(s)${C.reset}`;
1192
+ const workerParts = [];
1193
+ if (ar.workers) {
1194
+ for (const w of ar.workers.workers) {
1195
+ const wIcon = w.declaredStatus === "running" && w.alive ? `${C.green}\u25CF${C.reset}` : w.stale ? `${C.red}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
1196
+ workerParts.push(`${wIcon} ${w.name}`);
1197
+ }
1198
+ }
1199
+ let sysdLabel = "";
1200
+ if (ar.systemd) {
1201
+ const si = ar.systemd.activeState === "active" ? `${C.green}\u25CF${C.reset}` : ar.systemd.crashLooping ? `${C.yellow}\u25CF${C.reset}` : ar.systemd.failed ? `${C.red}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
1202
+ sysdLabel = ` ${C.bold}Systemd${C.reset} ${si} ${ar.systemd.activeState}`;
1203
+ if (ar.systemd.restarts > 0) sysdLabel += ` ${C.dim}(${ar.systemd.restarts} restarts)${C.reset}`;
1204
+ }
1205
+ let pidLabel = "";
1206
+ if (ar.pidFile?.pid) {
1207
+ const pi = ar.pidFile.alive && ar.pidFile.matchesProcess ? `${C.green}\u25CF${C.reset}` : `${C.red}\u25CF${C.reset}`;
1208
+ pidLabel = ` ${C.bold}PID${C.reset} ${pi} ${ar.pidFile.pid}`;
1209
+ }
1210
+ writeLine(L, "");
1211
+ writeLine(L, ` ${C.bold}${C.under}Process Health${C.reset}`);
1212
+ writeLine(L, ` ${healthIcon} ${healthLabel}${pidLabel}${sysdLabel} ${C.bold}Procs${C.reset} ${C.dim}${ar.osProcesses.length}${C.reset} ${ar.orphans.length > 0 ? `${C.red}Orphans ${ar.orphans.length}${C.reset}` : `${C.dim}Orphans 0${C.reset}`}`);
1213
+ if (workerParts.length > 0) {
1214
+ writeLine(L, ` ${C.dim}Workers${C.reset} ${workerParts.join(" ")}`);
1215
+ }
1216
+ if (!healthy) {
1217
+ for (const p of ar.problems.slice(0, 3)) {
1218
+ writeLine(L, ` ${C.red}\u2022${C.reset} ${C.dim}${p}${C.reset}`);
1219
+ }
1220
+ }
1221
+ }
910
1222
  writeLine(L, "");
911
1223
  writeLine(
912
1224
  L,
@@ -1025,13 +1337,13 @@ function getDistDepth(dt, spanId, visited) {
1025
1337
  }
1026
1338
  function startLive(argv) {
1027
1339
  const config = parseArgs(argv);
1028
- const valid = config.dirs.filter((d) => (0, import_node_fs.existsSync)(d));
1340
+ const valid = config.dirs.filter((d) => (0, import_node_fs2.existsSync)(d));
1029
1341
  if (valid.length === 0) {
1030
1342
  console.error(`No valid directories found: ${config.dirs.join(", ")}`);
1031
1343
  console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
1032
1344
  process.exit(1);
1033
1345
  }
1034
- const invalid = config.dirs.filter((d) => !(0, import_node_fs.existsSync)(d));
1346
+ const invalid = config.dirs.filter((d) => !(0, import_node_fs2.existsSync)(d));
1035
1347
  if (invalid.length > 0) {
1036
1348
  console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
1037
1349
  }
@@ -1040,7 +1352,7 @@ function startLive(argv) {
1040
1352
  let debounce = null;
1041
1353
  for (const dir of config.dirs) {
1042
1354
  try {
1043
- (0, import_node_fs.watch)(dir, { recursive: config.recursive }, () => {
1355
+ (0, import_node_fs2.watch)(dir, { recursive: config.recursive }, () => {
1044
1356
  if (debounce) clearTimeout(debounce);
1045
1357
  debounce = setTimeout(() => render(config), 500);
1046
1358
  });
@@ -1054,262 +1366,6 @@ function startLive(argv) {
1054
1366
  });
1055
1367
  }
1056
1368
 
1057
- // src/process-audit.ts
1058
- var import_node_child_process = require("child_process");
1059
- var import_node_fs2 = require("fs");
1060
- var import_node_path2 = require("path");
1061
- function isPidAlive(pid) {
1062
- try {
1063
- process.kill(pid, 0);
1064
- return true;
1065
- } catch {
1066
- return false;
1067
- }
1068
- }
1069
- function pidMatchesName(pid, name) {
1070
- try {
1071
- const cmdline = (0, import_node_fs2.readFileSync)(`/proc/${pid}/cmdline`, "utf8");
1072
- return cmdline.includes(name);
1073
- } catch {
1074
- return false;
1075
- }
1076
- }
1077
- function readPidFile(path) {
1078
- try {
1079
- const pid = parseInt((0, import_node_fs2.readFileSync)(path, "utf8").trim(), 10);
1080
- return isNaN(pid) ? null : pid;
1081
- } catch {
1082
- return null;
1083
- }
1084
- }
1085
- function auditPidFile(config) {
1086
- if (!config.pidFile) return null;
1087
- const pid = readPidFile(config.pidFile);
1088
- if (pid === null) {
1089
- return {
1090
- path: config.pidFile,
1091
- pid: null,
1092
- alive: false,
1093
- matchesProcess: false,
1094
- stale: !(0, import_node_fs2.existsSync)(config.pidFile),
1095
- reason: (0, import_node_fs2.existsSync)(config.pidFile) ? "PID file exists but content is invalid" : "No PID file found"
1096
- };
1097
- }
1098
- const alive = isPidAlive(pid);
1099
- const matchesProcess = alive ? pidMatchesName(pid, config.processName) : false;
1100
- const stale = !alive || alive && !matchesProcess;
1101
- let reason;
1102
- if (alive && matchesProcess) {
1103
- reason = `PID ${pid} alive and matches ${config.processName}`;
1104
- } else if (alive && !matchesProcess) {
1105
- reason = `PID ${pid} alive but is NOT ${config.processName} (PID reused by another process)`;
1106
- } else {
1107
- reason = `PID ${pid} no longer exists`;
1108
- }
1109
- return { path: config.pidFile, pid, alive, matchesProcess, stale, reason };
1110
- }
1111
- function auditSystemd(config) {
1112
- if (config.systemdUnit === null || config.systemdUnit === void 0) return null;
1113
- const unit = config.systemdUnit;
1114
- try {
1115
- const raw = (0, import_node_child_process.execSync)(
1116
- `systemctl --user show ${unit} --property=ActiveState,SubState,MainPID,NRestarts,Result --no-pager 2>/dev/null`,
1117
- { encoding: "utf8", timeout: 5e3 }
1118
- );
1119
- const props = {};
1120
- for (const line of raw.trim().split("\n")) {
1121
- const [k, ...v] = line.split("=");
1122
- if (k) props[k.trim()] = v.join("=").trim();
1123
- }
1124
- const activeState = props["ActiveState"] ?? "unknown";
1125
- const subState = props["SubState"] ?? "unknown";
1126
- const mainPid = parseInt(props["MainPID"] ?? "0", 10);
1127
- const restarts = parseInt(props["NRestarts"] ?? "0", 10);
1128
- const result = props["Result"] ?? "unknown";
1129
- return {
1130
- unit,
1131
- activeState,
1132
- subState,
1133
- mainPid,
1134
- restarts,
1135
- result,
1136
- crashLooping: activeState === "activating" && subState === "auto-restart",
1137
- failed: activeState === "failed"
1138
- };
1139
- } catch {
1140
- return null;
1141
- }
1142
- }
1143
- function auditWorkers(config) {
1144
- if (!config.workersFile || !(0, import_node_fs2.existsSync)(config.workersFile)) return null;
1145
- try {
1146
- const data = JSON.parse((0, import_node_fs2.readFileSync)(config.workersFile, "utf8"));
1147
- const orchPid = data.pid ?? null;
1148
- const orchAlive = orchPid ? isPidAlive(orchPid) : false;
1149
- const workers = [];
1150
- for (const [name, info] of Object.entries(data.tools ?? {})) {
1151
- const w = info;
1152
- const wPid = w.pid ?? null;
1153
- const wAlive = wPid ? isPidAlive(wPid) : false;
1154
- workers.push({
1155
- name,
1156
- pid: wPid,
1157
- declaredStatus: w.status ?? "unknown",
1158
- alive: wAlive,
1159
- stale: w.status === "running" && !wAlive
1160
- });
1161
- }
1162
- return {
1163
- orchestratorPid: orchPid,
1164
- orchestratorAlive: orchAlive,
1165
- startedAt: data.started_at ?? "",
1166
- workers
1167
- };
1168
- } catch {
1169
- return null;
1170
- }
1171
- }
1172
- function getOsProcesses(processName) {
1173
- try {
1174
- const raw = (0, import_node_child_process.execSync)(`ps aux`, { encoding: "utf8", timeout: 5e3 });
1175
- return raw.split("\n").filter((line) => line.includes(processName) && !line.includes("process-audit") && !line.includes("grep")).map((line) => {
1176
- const parts = line.trim().split(/\s+/);
1177
- return {
1178
- pid: parseInt(parts[1] ?? "0", 10),
1179
- cpu: parts[2] ?? "0",
1180
- mem: parts[3] ?? "0",
1181
- command: parts.slice(10).join(" ")
1182
- };
1183
- }).filter((p) => !isNaN(p.pid) && p.pid > 0);
1184
- } catch {
1185
- return [];
1186
- }
1187
- }
1188
- function discoverProcessConfig(dirs) {
1189
- let pidFile;
1190
- let workersFile;
1191
- let processName = "";
1192
- for (const dir of dirs) {
1193
- if (!(0, import_node_fs2.existsSync)(dir)) continue;
1194
- let entries;
1195
- try {
1196
- entries = (0, import_node_fs2.readdirSync)(dir);
1197
- } catch {
1198
- continue;
1199
- }
1200
- for (const f of entries) {
1201
- const fp = (0, import_node_path2.join)(dir, f);
1202
- try {
1203
- if (!(0, import_node_fs2.statSync)(fp).isFile()) continue;
1204
- } catch {
1205
- continue;
1206
- }
1207
- if (f.endsWith(".pid") && !pidFile) {
1208
- pidFile = fp;
1209
- if (!processName) {
1210
- processName = (0, import_node_path2.basename)(f, ".pid");
1211
- }
1212
- }
1213
- if ((f === "workers.json" || f.endsWith("-workers.json")) && !workersFile) {
1214
- workersFile = fp;
1215
- if (!processName && f !== "workers.json") {
1216
- processName = (0, import_node_path2.basename)(f, "-workers.json");
1217
- }
1218
- }
1219
- }
1220
- }
1221
- if (!processName && !pidFile && !workersFile) return null;
1222
- if (!processName) processName = "agent";
1223
- return { processName, pidFile, workersFile };
1224
- }
1225
- function auditProcesses(config) {
1226
- const pidFile = auditPidFile(config);
1227
- const systemd = auditSystemd(config);
1228
- const workers = auditWorkers(config);
1229
- const osProcesses = getOsProcesses(config.processName);
1230
- const knownPids = /* @__PURE__ */ new Set();
1231
- if (pidFile?.pid && !pidFile.stale) knownPids.add(pidFile.pid);
1232
- if (workers) {
1233
- if (workers.orchestratorPid) knownPids.add(workers.orchestratorPid);
1234
- for (const w of workers.workers) {
1235
- if (w.pid) knownPids.add(w.pid);
1236
- }
1237
- }
1238
- if (systemd?.mainPid) knownPids.add(systemd.mainPid);
1239
- const orphans = osProcesses.filter((p) => !knownPids.has(p.pid));
1240
- const problems = [];
1241
- if (pidFile?.stale) problems.push(`Stale PID file: ${pidFile.reason}`);
1242
- if (systemd?.crashLooping) problems.push("Systemd unit is crash-looping (auto-restart)");
1243
- if (systemd?.failed) problems.push("Systemd unit has failed");
1244
- if (systemd && systemd.restarts > 10) problems.push(`High systemd restart count: ${systemd.restarts}`);
1245
- if (pidFile?.pid && systemd?.mainPid && pidFile.pid !== systemd.mainPid) {
1246
- problems.push(`PID mismatch: file says ${pidFile.pid}, systemd says ${systemd.mainPid}`);
1247
- }
1248
- if (workers) {
1249
- for (const w of workers.workers) {
1250
- if (w.stale) problems.push(`Worker "${w.name}" (pid ${w.pid}) declares running but is dead`);
1251
- }
1252
- }
1253
- if (orphans.length > 0) problems.push(`${orphans.length} orphan process(es) not tracked by PID file or workers registry`);
1254
- return { pidFile, systemd, workers, osProcesses, orphans, problems };
1255
- }
1256
- function formatAuditReport(result) {
1257
- const lines = [];
1258
- lines.push("");
1259
- lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
1260
- lines.push("\u2551 \u{1F50D} P R O C E S S A U D I T \u2551");
1261
- lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
1262
- if (result.pidFile) {
1263
- const pf = result.pidFile;
1264
- const icon = pf.pid && pf.alive && pf.matchesProcess ? "\u2705" : pf.stale ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
1265
- lines.push(`
1266
- PID File: ${pf.path}`);
1267
- lines.push(` ${icon} ${pf.reason}`);
1268
- }
1269
- if (result.systemd) {
1270
- const sd = result.systemd;
1271
- const icon = sd.activeState === "active" ? "\u{1F7E2}" : sd.crashLooping ? "\u{1F7E1}" : sd.failed ? "\u{1F534}" : "\u26AA";
1272
- lines.push(`
1273
- Systemd: ${sd.unit}`);
1274
- lines.push(` ${icon} State: ${sd.activeState} (${sd.subState}) Result: ${sd.result}`);
1275
- lines.push(` Main PID: ${sd.mainPid || "none"} Restarts: ${sd.restarts}`);
1276
- }
1277
- if (result.workers) {
1278
- const w = result.workers;
1279
- lines.push(`
1280
- Workers (orchestrator pid ${w.orchestratorPid ?? "unknown"} ${w.orchestratorAlive ? "\u2705" : "\u274C"})`);
1281
- for (const worker of w.workers) {
1282
- const icon = worker.declaredStatus === "running" && worker.alive ? "\u{1F7E2}" : worker.stale ? "\u{1F534} STALE" : "\u26AA";
1283
- lines.push(` ${icon} ${worker.name.padEnd(14)} pid=${String(worker.pid ?? "-").padEnd(8)} status=${worker.declaredStatus}`);
1284
- }
1285
- }
1286
- if (result.osProcesses.length > 0) {
1287
- lines.push(`
1288
- OS Processes (${result.osProcesses.length} total)`);
1289
- for (const p of result.osProcesses) {
1290
- lines.push(` PID ${String(p.pid).padEnd(8)} CPU=${p.cpu.padEnd(6)} MEM=${p.mem.padEnd(6)} ${p.command.substring(0, 55)}`);
1291
- }
1292
- }
1293
- if (result.orphans.length > 0) {
1294
- lines.push(`
1295
- \u26A0\uFE0F ${result.orphans.length} ORPHAN PROCESS(ES):`);
1296
- for (const p of result.orphans) {
1297
- lines.push(` PID ${p.pid} \u2014 not tracked by PID file or workers registry`);
1298
- }
1299
- }
1300
- lines.push("");
1301
- if (result.problems.length === 0) {
1302
- lines.push(" \u2705 All checks passed \u2014 no process issues detected.");
1303
- } else {
1304
- lines.push(` \u26A0\uFE0F ${result.problems.length} issue(s):`);
1305
- for (const p of result.problems) {
1306
- lines.push(` \u2022 ${p}`);
1307
- }
1308
- }
1309
- lines.push("");
1310
- return lines.join("\n");
1311
- }
1312
-
1313
1369
  // src/runner.ts
1314
1370
  var import_node_child_process2 = require("child_process");
1315
1371
  var import_node_fs3 = require("fs");