@cr1ms0n/pi-subagent 0.8.9 → 0.9.0

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.
@@ -94,51 +94,89 @@ export async function runTasks(
94
94
  const worktrees = options.worktrees ?? new WorktreeManager();
95
95
  const handles: Array<WorktreeHandle | undefined> = new Array(specs.length);
96
96
  const prepared: TaskSpec[] = [];
97
+ let setupTimedOut = false;
98
+ const progress: Partial<TaskResult>[] = [];
99
+ const checkpoint = (index: number, partial: Partial<TaskResult>) => {
100
+ progress[index] = { ...progress[index], ...partial };
101
+ options.onTaskProgress?.(index, partial);
102
+ };
97
103
 
98
104
  try {
99
105
  for (let index = 0; index < specs.length; index++) {
100
106
  if (options.signal?.aborted) throw new Error("Subagent run cancelled before worktree setup");
101
107
  const spec = { ...specs[index]! };
108
+ const setupController = new AbortController();
109
+ const setupSignal = options.signal ? AbortSignal.any([options.signal, setupController.signal]) : setupController.signal;
110
+ let setupTimer: NodeJS.Timeout | undefined;
111
+ if (spec.deadline !== undefined) {
112
+ const remaining = spec.deadline - Date.now();
113
+ if (remaining <= 0) {
114
+ setupTimedOut = true;
115
+ throw new Error("Subagent deadline expired before setup");
116
+ }
117
+ setupTimer = setTimeout(() => { setupTimedOut = true; setupController.abort(); }, remaining);
118
+ setupTimer.unref?.();
119
+ }
120
+ try {
102
121
  if (spec.isolation === "worktree") {
103
- const handle = await worktrees.create(spec.cwd || process.cwd(), spec.task.slice(0, 20), options.signal, {
122
+ const handle = await worktrees.create(spec.cwd || process.cwd(), spec.task.slice(0, 20), setupSignal, {
104
123
  includeWip: spec.includeWip === true,
105
124
  });
106
125
  handles[index] = handle;
107
126
  spec.cwd = handle.cwd;
108
127
  // Announce the worktree immediately so live runs can shield it from GC sweeps.
109
- options.onTaskProgress?.(index, {
128
+ checkpoint(index, {
110
129
  worktree: { cwd: handle.cwd, branch: handle.branch, baseCommit: handle.baseCommit, changed: false },
111
130
  });
112
131
  }
132
+ if (setupSignal.aborted) throw new Error("Subagent setup was cancelled or exceeded its task deadline");
113
133
  prepared.push(spec);
134
+ } finally { if (setupTimer) clearTimeout(setupTimer); }
114
135
  }
115
136
  } catch (error) {
116
- // Setup failure: remove only unchanged worktrees. Preserve modified ones.
117
- for (const handle of handles.filter((h): h is WorktreeHandle => !!h)) {
118
- await worktrees.finalize(handle).catch(() => {});
137
+ // Keep retained worktree pointers and report cleanup failures per task.
138
+ const setupErrors: Array<string | undefined> = [];
139
+ for (let index = 0; index < handles.length; index++) {
140
+ const handle = handles[index];
141
+ if (!handle) continue;
142
+ try {
143
+ const final = await worktrees.finalize(handle);
144
+ progress[index] = { ...progress[index], worktree: final.changed
145
+ ? { cwd: final.cwd, branch: final.branch, baseCommit: final.baseCommit, changed: true, diffSummary: final.diffSummary }
146
+ : undefined };
147
+ } catch (error) {
148
+ setupErrors[index] = `Worktree finalization failed: ${error instanceof Error ? error.message : String(error)}`;
149
+ // Its state is uncertain; preserve the handle for inspection/recovery.
150
+ progress[index] = { ...progress[index], worktree: { cwd: handle.cwd, branch: handle.branch, baseCommit: handle.baseCommit, changed: true } };
151
+ }
119
152
  }
120
- if (options.signal?.aborted) {
153
+ {
154
+ const setupState: RunState = setupTimedOut ? "timeout" : options.signal?.aborted ? "cancelled" : "failed";
121
155
  const results = specs.map<TaskResult>((spec, index) => ({
122
- label: `task-${index + 1}`,
156
+ ...progress[index],
157
+ index,
158
+ label: spec.label || `task-${index + 1}`,
123
159
  task: spec.task,
124
160
  model: spec.model,
125
- state: "cancelled",
161
+ routing: spec.routing,
162
+ state: setupState,
126
163
  exitCode: 1,
127
- messages: [],
128
- stderr: "",
129
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: 0, contextTokens: 0, turns: 0 },
130
- stopReason: "cancelled",
131
- errorMessage: error instanceof Error ? error.message : String(error),
164
+ messages: progress[index]?.messages ?? [],
165
+ stderr: progress[index]?.stderr ?? "",
166
+ usage: progress[index]?.usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: 0, contextTokens: 0, turns: 0 },
167
+ stopReason: setupTimedOut ? "timeout" : options.signal?.aborted ? "cancelled" : "setup_error",
168
+ timeoutPhase: setupTimedOut ? "starting" : undefined,
169
+ errorMessage: [error instanceof Error ? error.message : String(error), setupErrors[index]].filter(Boolean).join("; "),
132
170
  thinking: spec.thinking,
133
171
  profile: spec.profile,
172
+ backend: spec.backend ?? "pi",
134
173
  canWrite: spec.canWrite,
135
174
  outputFile: spec.output,
136
175
  outputMode: spec.outputMode,
137
176
  protocol: { headerSeen: false, assistantEndSeen: false, agentEndSeen: false, agentSettledSeen: false, validEvents: 0, parseErrors: 0 },
138
177
  }));
139
- return { mode: specs.length > 1 ? "parallel" : "single", results, state: "cancelled", summary: "Subagent run cancelled during setup" };
178
+ return { mode: specs.length > 1 ? "parallel" : "single", results, state: setupState, summary: summarize(results) };
140
179
  }
141
- throw error;
142
180
  }
143
181
 
144
182
  const runOne = async (index: number): Promise<TaskResult> => {
@@ -168,7 +206,7 @@ export async function runTasks(
168
206
  semaphore,
169
207
  options.getPiCommand ?? createGetPiCommand(),
170
208
  options.sessionDir,
171
- (partial) => options.onTaskProgress?.(index, {
209
+ (partial) => checkpoint(index, {
172
210
  ...partial,
173
211
  attempts: attempt > 1 ? attempt : undefined,
174
212
  usage: partial.usage && priorUsage ? addUsage(priorUsage, partial.usage) : partial.usage,
@@ -184,11 +222,11 @@ export async function runTasks(
184
222
  result = await runner.run(attemptSpec, options.signal);
185
223
  if (priorUsage) result.usage = addUsage(priorUsage, result.usage);
186
224
 
187
- const canRetry = attempt < maxAttempts && !options.signal?.aborted && isTransientFailure(result);
225
+ const canRetry = attempt < maxAttempts && !options.signal?.aborted && (spec.deadline === undefined || Date.now() < spec.deadline) && isTransientFailure(result);
188
226
  if (!canRetry) break;
189
227
  priorUsage = result.usage;
190
228
  const nextModel = fallbacks[attempt - 1];
191
- options.onTaskProgress?.(index, {
229
+ checkpoint(index, {
192
230
  state: "queued",
193
231
  model: nextModel ?? spec.model,
194
232
  attempts: attempt + 1,
@@ -203,6 +241,7 @@ export async function runTasks(
203
241
  result.errorMessage += ` (after ${attemptedModels.length} attempts: ${attemptedModels.join(" → ")})`;
204
242
  }
205
243
  }
244
+ result.routing = spec.routing;
206
245
  result.index = index;
207
246
  result.label = spec.label || `task-${index + 1}`;
208
247
  result.outputMode = spec.outputMode;
@@ -237,7 +276,24 @@ export async function runTasks(
237
276
  return result;
238
277
  };
239
278
 
240
- const results = await Promise.all(prepared.map((_, index) => runOne(index)));
279
+ // One unexpected task failure must not release run ownership while siblings still
280
+ // execute, or discard their results. Each task settles before the aggregate does.
281
+ const results = await Promise.all(prepared.map(async (spec, index): Promise<TaskResult> => {
282
+ try { return await runOne(index); }
283
+ catch (error) {
284
+ const partial = progress[index];
285
+ return {
286
+ ...partial, index, label: spec.label || `task-${index + 1}`, task: spec.task,
287
+ model: partial?.model ?? spec.model, routing: spec.routing, thinking: spec.thinking,
288
+ profile: spec.profile, backend: spec.backend ?? "pi", canWrite: spec.canWrite,
289
+ outputFile: spec.output, outputMode: spec.outputMode,
290
+ state: "failed", exitCode: 1, messages: partial?.messages ?? [], stderr: partial?.stderr ?? "",
291
+ usage: partial?.usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
292
+ stopReason: "error", errorMessage: error instanceof Error ? error.message : String(error),
293
+ protocol: partial?.protocol ?? { headerSeen: false, assistantEndSeen: false, agentEndSeen: false, agentSettledSeen: false, validEvents: 0, parseErrors: 0 },
294
+ };
295
+ }
296
+ }));
241
297
  return {
242
298
  mode: results.length > 1 ? "parallel" : "single",
243
299
  results,