@dougbots/avenor-pi 0.8.0 → 0.8.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.
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ interface RunStatusEntry {
15
15
  pid?: number;
16
16
  /** Backend type, used to determine whether to look for child supervisors. */
17
17
  backend?: string;
18
- /** Count of active (non-terminal) runs on this run's child supervisor, if discovered. */
18
+ /** Count of active (non-terminal) descendants, if discovered. */
19
19
  nestedCount?: number;
20
20
  }
21
21
  //#endregion
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Input, Key, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
2
  import { Type } from "typebox";
3
- import * as path from "node:path";
4
3
  import { Supervisor, answerPermissionTool, dial, eventsTool, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool } from "@dougbots/avenor-core";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
5
6
  //#region src/watch.ts
6
7
  const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
7
8
  const TRANSCRIPT_SCROLL_STEP = 4;
@@ -567,6 +568,118 @@ function decideCompletion(run) {
567
568
  return "send";
568
569
  }
569
570
  //#endregion
571
+ //#region src/nested-count.ts
572
+ const NESTED_DIAL_TIMEOUT_MS = 2e3;
573
+ const MAX_NESTED_DEPTH = 99;
574
+ const MAX_CONCURRENT_NESTED_DIALS = 16;
575
+ var AsyncLimiter = class {
576
+ limit;
577
+ active = 0;
578
+ queue = [];
579
+ constructor(limit) {
580
+ this.limit = limit;
581
+ }
582
+ async run(task) {
583
+ if (this.active >= this.limit) await new Promise((resolve) => this.queue.push(resolve));
584
+ this.active++;
585
+ try {
586
+ return await task();
587
+ } finally {
588
+ this.active--;
589
+ this.queue.shift()?.();
590
+ }
591
+ }
592
+ };
593
+ const nestedDialLimiter = new AsyncLimiter(MAX_CONCURRENT_NESTED_DIALS);
594
+ function isActiveRun(run) {
595
+ const status = String(run.status ?? "running").toLowerCase();
596
+ return status === "running" || status === "idle";
597
+ }
598
+ function childPiPid(run) {
599
+ if (String(run.backend ?? "").toLowerCase() !== "pi") return void 0;
600
+ if (typeof run.pid !== "number" || !Number.isInteger(run.pid) || run.pid <= 0) return void 0;
601
+ return run.pid;
602
+ }
603
+ function nestedSocketPath(pid) {
604
+ const requestedRoot = socketsRoot();
605
+ const requestedPath = path.join(requestedRoot, `avenor-mcp-${pid}.sock`);
606
+ if (!fs.existsSync(requestedRoot)) return requestedPath;
607
+ const root = fs.realpathSync(requestedRoot);
608
+ const parent = fs.realpathSync(path.dirname(requestedPath));
609
+ if (parent !== root) throw new Error("nested supervisor socket escaped sockets root");
610
+ return path.join(parent, path.basename(requestedPath));
611
+ }
612
+ /**
613
+ * Dial a nested supervisor with a bounded connection/RPC timeout.
614
+ * A client that resolves after the timeout loses the race and is closed.
615
+ */
616
+ async function dialWithTimeout(dial, socketPath, timeoutOverride) {
617
+ const effectiveTimeout = timeoutOverride ?? NESTED_DIAL_TIMEOUT_MS;
618
+ let timedOut = false;
619
+ let timeout;
620
+ const timeoutMarker = Symbol("dial timeout");
621
+ const connection = dial(socketPath, { callTimeoutMs: effectiveTimeout }).then((client) => {
622
+ if (timedOut) client.close();
623
+ return client;
624
+ });
625
+ try {
626
+ const result = await Promise.race([connection, new Promise((resolve) => {
627
+ timeout = setTimeout(() => resolve(timeoutMarker), effectiveTimeout);
628
+ })]);
629
+ if (result === timeoutMarker) {
630
+ timedOut = true;
631
+ throw new Error("dial timeout");
632
+ }
633
+ return result;
634
+ } finally {
635
+ if (timeout) clearTimeout(timeout);
636
+ }
637
+ }
638
+ async function countNestedRunsAtDepth(pid, dial, seen, depth) {
639
+ if (!Number.isInteger(pid) || pid <= 0 || seen.has(pid) || depth >= MAX_NESTED_DEPTH) return 0;
640
+ seen.add(pid);
641
+ let socketPath;
642
+ try {
643
+ socketPath = nestedSocketPath(pid);
644
+ } catch (error) {
645
+ console.warn("avenor nested count: invalid supervisor socket path", error);
646
+ return 0;
647
+ }
648
+ let activeRuns;
649
+ try {
650
+ activeRuns = await nestedDialLimiter.run(async () => {
651
+ const client = await dialWithTimeout(dial, socketPath);
652
+ try {
653
+ return (await client.list()).filter(isActiveRun);
654
+ } finally {
655
+ client.close();
656
+ }
657
+ });
658
+ } catch {
659
+ return 0;
660
+ }
661
+ let total = 0;
662
+ for (const run of activeRuns) {
663
+ const nestedPid = childPiPid(run);
664
+ total += 1;
665
+ if (nestedPid && depth + 1 < MAX_NESTED_DEPTH) total += await countNestedRunsAtDepth(nestedPid, dial, seen, depth + 1);
666
+ }
667
+ return total;
668
+ }
669
+ /**
670
+ * Count all active descendants of a spawned pi process. Each pi process owns
671
+ * its own supervisor, so walking the tree requires dialing the supervisor for
672
+ * every active pi run that exposes a child process PID.
673
+ *
674
+ * The depth guard prevents malformed acyclic data from recursing forever, and
675
+ * the dial limiter keeps a large fan-out from opening unbounded sockets at
676
+ * once. A traversal-wide PID set separately prevents cycles and duplicate
677
+ * subtree walks from stale cross-branch references.
678
+ */
679
+ async function countNestedRuns(pid, dial) {
680
+ return countNestedRunsAtDepth(pid, dial, /* @__PURE__ */ new Set(), 0);
681
+ }
682
+ //#endregion
570
683
  //#region src/agent-profile.ts
571
684
  const PROFILE_ENTRY_TYPE = "pi-agents:profile";
572
685
  /**
@@ -758,29 +871,6 @@ function createExtension(deps = defaultDeps) {
758
871
  return;
759
872
  }
760
873
  }
761
- /**
762
- * Count active (non-terminal) runs on a child supervisor discovered via
763
- * the spawned pi process's PID. The child pi process creates its own
764
- * avenor supervisor at `avenor-mcp-<pid>.sock`; we dial it briefly to
765
- * list runs and count those still active. Returns 0 on any failure
766
- * (socket not yet ready, supervisor idle-shut-down, etc.).
767
- */
768
- async function countNestedRuns(pid) {
769
- if (!pid) return 0;
770
- const socketPath = path.join(socketsRoot(), `avenor-mcp-${pid}.sock`);
771
- let client;
772
- try {
773
- client = await Promise.race([deps.dial(socketPath, { callTimeoutMs: 2e3 }), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("dial timeout")), 2e3))]);
774
- return (await client.list()).filter((r) => {
775
- const status = String(r.status ?? "running").toLowerCase();
776
- return status === "running" || status === "idle";
777
- }).length;
778
- } catch {
779
- return 0;
780
- } finally {
781
- client?.close();
782
- }
783
- }
784
874
  async function pollRuns() {
785
875
  const liveMap = /* @__PURE__ */ new Map();
786
876
  try {
@@ -861,7 +951,7 @@ function createExtension(deps = defaultDeps) {
861
951
  });
862
952
  const nestedPids = entries.filter((e) => e.backend === "pi" && e.pid && !isTerminalStatus(e.status)).map((e) => e.pid);
863
953
  if (nestedPids.length > 0) {
864
- const nestedCounts = await Promise.all(nestedPids.map((pid) => countNestedRuns(pid)));
954
+ const nestedCounts = await Promise.all(nestedPids.map((pid) => countNestedRuns(pid, deps.dial)));
865
955
  let idx = 0;
866
956
  for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status)) entry.nestedCount = nestedCounts[idx++] ?? 0;
867
957
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-pi",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -35,7 +35,7 @@
35
35
  "dev": "tsdown --watch"
36
36
  },
37
37
  "dependencies": {
38
- "@dougbots/avenor-core": "^0.8.0"
38
+ "@dougbots/avenor-core": "^0.8.2"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": "*",