oira666_pi-subagent 0.2.26 → 0.2.27
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/package.json +1 -1
- package/runner.ts +77 -11
package/package.json
CHANGED
package/runner.ts
CHANGED
|
@@ -39,6 +39,13 @@ const SIGKILL_TIMEOUT_MS = 5000;
|
|
|
39
39
|
const HANG_GUARD_DELAY_MS = 5000;
|
|
40
40
|
const DEFAULT_STARTUP_TIMEOUT_MS = 120_000; // only for startup (before first assistant turn)
|
|
41
41
|
const SUBAGENT_STARTUP_TIMEOUT_ENV = "PI_SUBAGENT_STARTUP_TIMEOUT";
|
|
42
|
+
// A startup timeout is almost always a transient cold-start stall (slow cli /
|
|
43
|
+
// extension load, momentarily busy box) rather than a deterministic failure, so
|
|
44
|
+
// re-spawn a clean child a few times before surfacing the error. This does NOT
|
|
45
|
+
// change the per-attempt startup window.
|
|
46
|
+
const DEFAULT_STARTUP_RETRIES = 2;
|
|
47
|
+
const SUBAGENT_STARTUP_RETRIES_ENV = "PI_SUBAGENT_STARTUP_RETRIES";
|
|
48
|
+
const STARTUP_RETRY_BASE_BACKOFF_MS = 1_000;
|
|
42
49
|
const SUBAGENT_PI_COMMAND_ENV = "PI_SUBAGENT_PI_COMMAND";
|
|
43
50
|
const SUBAGENT_PI_ARGS_PREFIX_ENV = "PI_SUBAGENT_PI_ARGS_PREFIX";
|
|
44
51
|
|
|
@@ -109,15 +116,30 @@ function getCurrentPiCliScript(): string | null {
|
|
|
109
116
|
const script = process.argv[1];
|
|
110
117
|
if (!script) return null;
|
|
111
118
|
|
|
112
|
-
// When this extension is loaded by pi, process.argv[1] is the pi
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
// When this extension is loaded by pi, process.argv[1] is the pi entrypoint.
|
|
120
|
+
// Reusing it with process.execPath avoids relying on PATH while still running
|
|
121
|
+
// the exact same pi installation as the parent process.
|
|
122
|
+
//
|
|
123
|
+
// On Linux/macOS the launched entrypoint is usually the npm `bin` symlink
|
|
124
|
+
// (e.g. <prefix>/bin/pi) that points at .../pi-coding-agent/dist/cli.js, so
|
|
125
|
+
// argv[1] does NOT end in /dist/cli.js. Resolve symlinks before matching —
|
|
126
|
+
// otherwise this check fails and we fall back to scanning PATH, which can
|
|
127
|
+
// pick a different (e.g. much slower, cross-filesystem) pi install than the
|
|
128
|
+
// one actually running.
|
|
129
|
+
const candidates = [script];
|
|
130
|
+
try {
|
|
131
|
+
const real = fs.realpathSync(script);
|
|
132
|
+
if (real && real !== script) candidates.push(real);
|
|
133
|
+
} catch {
|
|
134
|
+
// argv[1] not statable; fall through with the raw value.
|
|
118
135
|
}
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
for (const candidate of candidates) {
|
|
137
|
+
const normalized = candidate.replace(/\\/g, "/");
|
|
138
|
+
if (normalized.includes("/pi-coding-agent/") && normalized.endsWith("/dist/cli.js")) {
|
|
139
|
+
return candidate;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
function findPiCliScriptOnPath(): string | null {
|
|
@@ -127,6 +149,22 @@ function findPiCliScriptOnPath(): string | null {
|
|
|
127
149
|
for (const shimName of process.platform === "win32" ? ["pi.cmd", "pi"] : ["pi"]) {
|
|
128
150
|
const shimPath = path.join(dir, shimName);
|
|
129
151
|
if (!fs.existsSync(shimPath)) continue;
|
|
152
|
+
|
|
153
|
+
// Common on Linux/macOS: the npm `bin/pi` entry is a symlink pointing
|
|
154
|
+
// straight at .../pi-coding-agent/dist/cli.js. Resolve it directly — its
|
|
155
|
+
// file *content* is JS (not a wrapper that names cli.js), so the text
|
|
156
|
+
// regex below would miss it and we'd skip this (often faster, same-
|
|
157
|
+
// filesystem) install in favour of a later PATH entry.
|
|
158
|
+
try {
|
|
159
|
+
const real = fs.realpathSync(shimPath);
|
|
160
|
+
const normalized = real.replace(/\\/g, "/");
|
|
161
|
+
if (normalized.includes("/pi-coding-agent/") && normalized.endsWith("/dist/cli.js")) {
|
|
162
|
+
return real;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// not a resolvable symlink; fall through to the wrapper-text scan.
|
|
166
|
+
}
|
|
167
|
+
|
|
130
168
|
let text = "";
|
|
131
169
|
try {
|
|
132
170
|
text = fs.readFileSync(shimPath, "utf8");
|
|
@@ -674,8 +712,17 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
674
712
|
fallbackModel,
|
|
675
713
|
);
|
|
676
714
|
let wasAborted = false;
|
|
677
|
-
|
|
678
|
-
|
|
715
|
+
const startupRetries = (() => {
|
|
716
|
+
const raw = process.env[SUBAGENT_STARTUP_RETRIES_ENV];
|
|
717
|
+
const parsed = parseNonNegativeInt(raw);
|
|
718
|
+
return parsed !== null ? parsed : DEFAULT_STARTUP_RETRIES;
|
|
719
|
+
})();
|
|
720
|
+
let startupTimedOut = false;
|
|
721
|
+
let exitCode = -1;
|
|
722
|
+
|
|
723
|
+
for (let attempt = 0; ; attempt++) {
|
|
724
|
+
startupTimedOut = false;
|
|
725
|
+
exitCode = await new Promise<number>((resolve) => {
|
|
679
726
|
const nextDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
680
727
|
const propagatedMaxDepth = Math.max(0, Math.floor(maxDepth));
|
|
681
728
|
const propagatedStack = [...parentAgentStack, agentName];
|
|
@@ -822,6 +869,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
822
869
|
if (startupTimeoutMs > 0) {
|
|
823
870
|
startupTimer = setTimeout(() => {
|
|
824
871
|
if (resolved || receivedFirstEvent) return;
|
|
872
|
+
startupTimedOut = true;
|
|
825
873
|
const message = `Subagent startup timeout: no JSON output after ${startupTimeoutMs}ms.`;
|
|
826
874
|
forcedExitCode = 1;
|
|
827
875
|
result.stopReason = "error";
|
|
@@ -876,7 +924,25 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
876
924
|
if (signal.aborted) kill();
|
|
877
925
|
else signal.addEventListener("abort", kill, { once: true });
|
|
878
926
|
}
|
|
879
|
-
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const noProgress = result.messages.length <= initialMessageCount;
|
|
930
|
+
const canRetry =
|
|
931
|
+
startupTimedOut && !wasAborted && noProgress && attempt < startupRetries;
|
|
932
|
+
if (!canRetry) break;
|
|
933
|
+
|
|
934
|
+
// Transient cold-start stall: clear the error markers the startup timer
|
|
935
|
+
// set on `result`, then re-spawn a clean child. The per-attempt startup
|
|
936
|
+
// window is unchanged; we just give the child another chance to boot.
|
|
937
|
+
result.exitCode = -1;
|
|
938
|
+
result.stopReason = undefined;
|
|
939
|
+
result.errorMessage = undefined;
|
|
940
|
+
result.stderr += `\n[pi-subagent] Startup timeout; retrying (attempt ${attempt + 2}/${startupRetries + 1}).`;
|
|
941
|
+
emitUpdate();
|
|
942
|
+
await new Promise<void>((resolve) =>
|
|
943
|
+
setTimeout(resolve, STARTUP_RETRY_BASE_BACKOFF_MS * (attempt + 1)),
|
|
944
|
+
);
|
|
945
|
+
}
|
|
880
946
|
|
|
881
947
|
result.exitCode = exitCode;
|
|
882
948
|
result.toolCalls = extractToolCalls(result.messages); // populate from parsed messages
|