@dougbots/avenor-pi 0.8.1 → 0.8.3
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 +1 -1
- package/dist/index.js +115 -25
- package/package.json +2 -2
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)
|
|
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.
|
|
3
|
+
"version": "0.8.3",
|
|
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.
|
|
38
|
+
"@dougbots/avenor-core": "^0.8.3"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@earendil-works/pi-coding-agent": "*",
|