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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/runner.ts +77 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
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 CLI JS
113
- // entrypoint. Reusing it with process.execPath avoids relying on PATH while
114
- // still running the exact same pi installation as the parent process.
115
- const normalized = script.replace(/\\/g, "/");
116
- if (!normalized.includes("/pi-coding-agent/") || !normalized.endsWith("/dist/cli.js")) {
117
- return null;
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
- return script;
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
- const exitCode = await new Promise<number>((resolve) => {
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