pi-subagents 0.23.0 → 0.23.1

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.
@@ -7,7 +7,7 @@ import type { Message } from "@mariozechner/pi-ai";
7
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
8
  import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
9
9
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
10
- import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput } from "../shared/single-output.ts";
10
+ import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
11
11
  import {
12
12
  type ActivityState,
13
13
  type ArtifactConfig,
@@ -100,6 +100,7 @@ interface StepResult {
100
100
  error?: string;
101
101
  success: boolean;
102
102
  skipped?: boolean;
103
+ sessionFile?: string;
103
104
  intercomTarget?: string;
104
105
  model?: string;
105
106
  attemptedModels?: string[];
@@ -575,6 +576,7 @@ async function runSingleStep(
575
576
  modelAttempts?: ModelAttempt[];
576
577
  artifactPaths?: ArtifactPaths;
577
578
  interrupted?: boolean;
579
+ sessionFile?: string;
578
580
  intercomTarget?: string;
579
581
  completionGuardTriggered?: boolean;
580
582
  }> {
@@ -582,7 +584,6 @@ async function runSingleStep(
582
584
  const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
583
585
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
584
586
  const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
585
- const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
586
587
 
587
588
  let artifactPaths: ArtifactPaths | undefined;
588
589
  if (ctx.artifactsDir && ctx.artifactConfig?.enabled !== false) {
@@ -604,10 +605,12 @@ async function runSingleStep(
604
605
  const attemptNotes: string[] = [];
605
606
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
606
607
  let finalResult: RunPiStreamingResult | undefined;
608
+ let finalOutputSnapshot: SingleOutputSnapshot | undefined;
607
609
  let completionGuardTriggeredFinal = false;
608
610
 
609
611
  for (let index = 0; index < candidates.length; index++) {
610
612
  const candidate = candidates[index];
613
+ const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
611
614
  const { args, env, tempDir } = buildPiArgs({
612
615
  baseArgs: ["--mode", "json", "-p"],
613
616
  task,
@@ -659,7 +662,9 @@ async function runSingleStep(
659
662
  ? 1
660
663
  : hiddenError?.hasError
661
664
  ? (hiddenError.exitCode ?? 1)
662
- : run.exitCode;
665
+ : run.error && run.exitCode === 0
666
+ ? 1
667
+ : run.exitCode;
663
668
  const error = completionGuardError
664
669
  ?? (hiddenError?.hasError
665
670
  ? hiddenError.details
@@ -676,6 +681,7 @@ async function runSingleStep(
676
681
  modelAttempts.push(attempt);
677
682
  if (candidate) attemptedModels.push(candidate);
678
683
  completionGuardTriggeredFinal = completionGuardTriggered;
684
+ finalOutputSnapshot = outputSnapshot;
679
685
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
680
686
  if (attempt.success || completionGuardTriggered) break;
681
687
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
@@ -684,7 +690,7 @@ async function runSingleStep(
684
690
 
685
691
  const rawOutput = finalResult?.finalOutput ?? "";
686
692
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
687
- ? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
693
+ ? resolveSingleOutput(step.outputPath, rawOutput, finalOutputSnapshot)
688
694
  : { fullOutput: rawOutput };
689
695
  const output = resolvedOutput.fullOutput;
690
696
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
@@ -731,6 +737,7 @@ async function runSingleStep(
731
737
  output: outputForSummary,
732
738
  exitCode: finalResult?.exitCode ?? 1,
733
739
  error: finalResult?.error,
740
+ sessionFile: step.sessionFile,
734
741
  intercomTarget: ctx.childIntercomTarget,
735
742
  model: finalResult?.model,
736
743
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
@@ -780,7 +787,7 @@ function markParallelGroupSetupFailure(input: {
780
787
  input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
781
788
  input.statusPayload.steps[flatTaskIndex].durationMs = 0;
782
789
  input.statusPayload.steps[flatTaskIndex].exitCode = 1;
783
- input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false });
790
+ input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, sessionFile: input.group.parallel[taskIndex].sessionFile });
784
791
  }
785
792
  input.statusPayload.currentStep = input.groupStartFlatIndex;
786
793
  input.statusPayload.lastUpdate = input.failedAt;
@@ -916,6 +923,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
916
923
  steps: flatSteps.map((step) => ({
917
924
  agent: step.agent,
918
925
  status: "pending",
926
+ ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
919
927
  skills: step.skills,
920
928
  model: step.model,
921
929
  attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
@@ -1409,6 +1417,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1409
1417
  error: pr.error,
1410
1418
  success: pr.exitCode === 0,
1411
1419
  skipped: pr.skipped,
1420
+ sessionFile: pr.sessionFile,
1412
1421
  intercomTarget: pr.intercomTarget,
1413
1422
  model: pr.model,
1414
1423
  attemptedModels: pr.attemptedModels,
@@ -1492,6 +1501,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1492
1501
  output: singleResult.output,
1493
1502
  error: singleResult.error,
1494
1503
  success: singleResult.exitCode === 0,
1504
+ sessionFile: singleResult.sessionFile,
1495
1505
  intercomTarget: singleResult.intercomTarget,
1496
1506
  model: singleResult.model,
1497
1507
  attemptedModels: singleResult.attemptedModels,
@@ -1580,9 +1590,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1580
1590
  }
1581
1591
  }
1582
1592
 
1593
+ const resultMode = config.resultMode ?? statusPayload.mode;
1583
1594
  const agentName = flatSteps.length === 1
1584
1595
  ? flatSteps[0].agent
1585
- : `chain:${flatSteps.map((s) => s.agent).join("->")}`;
1596
+ : resultMode === "parallel"
1597
+ ? `parallel:${flatSteps.map((s) => s.agent).join("+")}`
1598
+ : `chain:${flatSteps.map((s) => s.agent).join("->")}`;
1586
1599
  let sessionFile: string | undefined;
1587
1600
  let shareUrl: string | undefined;
1588
1601
  let gistUrl: string | undefined;
@@ -1667,7 +1680,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1667
1680
  writeAtomicJson(resultPath, {
1668
1681
  id,
1669
1682
  agent: agentName,
1670
- mode: config.resultMode ?? statusPayload.mode,
1683
+ mode: resultMode,
1671
1684
  success: !interrupted && results.every((r) => r.success),
1672
1685
  state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
1673
1686
  summary: interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
@@ -1677,6 +1690,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1677
1690
  error: r.error,
1678
1691
  success: r.success,
1679
1692
  skipped: r.skipped || undefined,
1693
+ sessionFile: r.sessionFile,
1680
1694
  intercomTarget: r.intercomTarget,
1681
1695
  model: r.model,
1682
1696
  attemptedModels: r.attemptedModels,
@@ -18,6 +18,7 @@ import {
18
18
  buildChainInstructions,
19
19
  writeInitialProgressFile,
20
20
  createParallelDirs,
21
+ suppressProgressForReadOnlyTask,
21
22
  aggregateParallelOutputs,
22
23
  isParallelStep,
23
24
  type StepOverrides,
@@ -178,8 +179,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
178
179
  } as SingleResult;
179
180
  }
180
181
 
181
- const behavior = input.parallelBehaviors[taskIndex]!;
182
182
  const taskTemplate = input.parallelTemplates[taskIndex] ?? "{previous}";
183
+ const behavior = suppressProgressForReadOnlyTask(input.parallelBehaviors[taskIndex]!, taskTemplate, input.originalTask);
183
184
  const templateHasPrevious = taskTemplate.includes("{previous}");
184
185
  const { prefix, suffix } = buildChainInstructions(
185
186
  behavior,
@@ -537,7 +538,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
537
538
 
538
539
  try {
539
540
  const agentNames = step.parallel.map((task) => task.agent);
540
- const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills);
541
+ const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
542
+ .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
541
543
  for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
542
544
  const behavior = parallelBehaviors[taskIndex]!;
543
545
  const outputPath = typeof behavior.output === "string"
@@ -616,6 +618,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
616
618
  }),
617
619
  };
618
620
  }
621
+ const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
622
+ const detached = detachedIndexInStep >= 0 ? parallelResults[detachedIndexInStep] : undefined;
623
+ if (detached) {
624
+ return {
625
+ content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
626
+ details: buildChainExecutionDetails({
627
+ results,
628
+ includeProgress,
629
+ allProgress,
630
+ allArtifactPaths,
631
+ artifactsDir,
632
+ chainAgents,
633
+ totalSteps,
634
+ currentStepIndex: stepIndex,
635
+ }),
636
+ };
637
+ }
619
638
 
620
639
  const failures = parallelResults
621
640
  .map((result, originalIndex) => ({ ...result, originalIndex }))
@@ -695,7 +714,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
695
714
  ? tuiOverride.skills
696
715
  : normalizeSkillInput(seqStep.skill),
697
716
  };
698
- const behavior = resolveStepBehavior(agentConfig, stepOverride, chainSkills);
717
+ const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfig, stepOverride, chainSkills), stepTemplate, originalTask);
699
718
 
700
719
  const isFirstProgress = behavior.progress && !progressCreated;
701
720
  if (isFirstProgress) {
@@ -822,24 +841,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
822
841
  if (r.progress) allProgress.push(r.progress);
823
842
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
824
843
 
825
- if (behavior.output && r.exitCode === 0) {
826
- try {
827
- const expectedPath = path.isAbsolute(behavior.output)
828
- ? behavior.output
829
- : path.join(chainDir, behavior.output);
830
- if (!fs.existsSync(expectedPath)) {
831
- const dirFiles = fs.readdirSync(chainDir);
832
- const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
833
- const warning = mdFiles.length > 0
834
- ? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
835
- : `Agent did not create expected output file: ${behavior.output}`;
836
- r.error = r.error ? `${r.error}\n${warning}` : warning;
837
- }
838
- } catch {
839
- // Ignore validation errors - this is just a diagnostic
840
- }
841
- }
842
-
843
844
  if (r.interrupted) {
844
845
  return {
845
846
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
@@ -855,6 +856,21 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
855
856
  }),
856
857
  };
857
858
  }
859
+ if (r.detached) {
860
+ return {
861
+ content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${r.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
862
+ details: buildChainExecutionDetails({
863
+ results,
864
+ includeProgress,
865
+ allProgress,
866
+ allArtifactPaths,
867
+ artifactsDir,
868
+ chainAgents,
869
+ totalSteps,
870
+ currentStepIndex: stepIndex,
871
+ }),
872
+ };
873
+ }
858
874
 
859
875
  if (r.exitCode !== 0) {
860
876
  const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
@@ -877,6 +893,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
877
893
  };
878
894
  }
879
895
 
896
+ if (behavior.output) {
897
+ try {
898
+ const expectedPath = path.isAbsolute(behavior.output)
899
+ ? behavior.output
900
+ : path.join(chainDir, behavior.output);
901
+ if (!fs.existsSync(expectedPath)) {
902
+ const dirFiles = fs.readdirSync(chainDir);
903
+ const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
904
+ const warning = mdFiles.length > 0
905
+ ? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
906
+ : `Agent did not create expected output file: ${behavior.output}`;
907
+ r.error = r.error ? `${r.error}\n${warning}` : warning;
908
+ }
909
+ } catch {
910
+ // Ignore validation errors; this diagnostic should not mask successful chain output.
911
+ }
912
+ }
913
+
880
914
  prev = getSingleResultOutput(r);
881
915
  }
882
916
  }
@@ -421,7 +421,7 @@ async function runSingleAttempt(
421
421
  const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
422
422
  ? evt.args as Record<string, unknown>
423
423
  : {};
424
- if (options.allowIntercomDetach && evt.toolName === "intercom") {
424
+ if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
425
425
  intercomStarted = true;
426
426
  }
427
427
  progress.toolCount++;
@@ -633,7 +633,10 @@ async function runSingleAttempt(
633
633
  return result;
634
634
  }
635
635
 
636
- if (exitCode === 0 && !result.error) {
636
+ if (result.error && result.exitCode === 0) {
637
+ result.exitCode = 1;
638
+ }
639
+ if (result.exitCode === 0 && !result.error) {
637
640
  const errInfo = detectSubagentError(result.messages);
638
641
  if (errInfo.hasError) {
639
642
  result.exitCode = errInfo.exitCode ?? 1;
@@ -746,7 +749,6 @@ export async function runSync(
746
749
 
747
750
  const shareEnabled = options.share === true;
748
751
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
749
- const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
750
752
  const skillNames = options.skills ?? agent.skills ?? [];
751
753
  const skillCwd = options.cwd ?? runtimeCwd;
752
754
  const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
@@ -797,6 +799,7 @@ export async function runSync(
797
799
  for (let i = 0; i < modelsToTry.length; i++) {
798
800
  const candidate = modelsToTry[i];
799
801
  if (candidate) attemptedModels.push(candidate);
802
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
800
803
  const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
801
804
  sessionEnabled,
802
805
  systemPrompt,
@@ -22,6 +22,8 @@ import {
22
22
  getStepAgents,
23
23
  isParallelStep,
24
24
  resolveStepBehavior,
25
+ suppressProgressForReadOnlyTask,
26
+ taskDisallowsFileUpdates,
25
27
  type ChainStep,
26
28
  type ResolvedStepBehavior,
27
29
  type SequentialStep,
@@ -206,6 +208,115 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
206
208
  return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
207
209
  }
208
210
 
211
+ function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; results: SingleResult[] }): void {
212
+ state.foregroundRuns ??= new Map();
213
+ state.foregroundRuns.set(input.runId, {
214
+ runId: input.runId,
215
+ mode: input.mode,
216
+ cwd: input.cwd,
217
+ updatedAt: Date.now(),
218
+ children: input.results.map((result, index) => ({
219
+ agent: result.agent,
220
+ index,
221
+ status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
222
+ ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
223
+ })),
224
+ });
225
+ while (state.foregroundRuns.size > 50) {
226
+ const oldest = [...state.foregroundRuns.values()].sort((left, right) => left.updatedAt - right.updatedAt)[0];
227
+ if (!oldest) break;
228
+ state.foregroundRuns.delete(oldest.runId);
229
+ }
230
+ }
231
+
232
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; intercomTarget: string; cwd: string; sessionFile: string } | undefined {
233
+ const requested = (params.id ?? params.runId)?.trim();
234
+ if (!requested || !state.foregroundRuns?.size) return undefined;
235
+ const direct = state.foregroundRuns.get(requested);
236
+ const matches = direct ? [direct] : [...state.foregroundRuns.values()].filter((run) => run.runId.startsWith(requested));
237
+ if (matches.length === 0) return undefined;
238
+ if (matches.length > 1) throw new Error(`Ambiguous foreground run id prefix '${requested}' matched: ${matches.map((run) => run.runId).join(", ")}. Provide a longer id.`);
239
+ const run = matches[0]!;
240
+ if (run.children.length > 1 && params.index === undefined) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Provide index to choose one.`);
241
+ const index = params.index ?? 0;
242
+ if (!Number.isInteger(index)) throw new Error(`Foreground run '${run.runId}' index must be an integer.`);
243
+ if (index < 0 || index >= run.children.length) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Index ${index} is out of range.`);
244
+ const child = run.children[index]!;
245
+ if (child.status === "detached") throw new Error(`Foreground run '${run.runId}' child ${index} is detached for intercom coordination and cannot be revived safely from the remembered foreground state. Reply to the supervisor request first; after the child exits, start a fresh follow-up if needed.`);
246
+ if (!child.sessionFile) throw new Error(`Foreground run '${run.runId}' child ${index} does not have a persisted session file to resume from.`);
247
+ if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
248
+ const sessionFile = path.resolve(child.sessionFile);
249
+ if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
250
+ return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, intercomTarget: resolveSubagentIntercomTarget(run.runId, child.agent, index), cwd: run.cwd, sessionFile };
251
+ }
252
+
253
+ type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
254
+ type ForegroundResumeSourceTarget = NonNullable<ReturnType<typeof resolveForegroundResumeTarget>> & { kind: "revive"; source: "foreground" };
255
+ type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget;
256
+
257
+ function isAsyncRunNotFound(error: unknown): boolean {
258
+ return error instanceof Error && error.message.startsWith("Async run not found.");
259
+ }
260
+
261
+ function isResumeAmbiguity(error: unknown): boolean {
262
+ return error instanceof Error && /Ambiguous .*run id prefix/.test(error.message);
263
+ }
264
+
265
+ function resumeTargetExact(target: { runId: string } | undefined, requested: string): boolean {
266
+ return target?.runId === requested;
267
+ }
268
+
269
+ function escapeRegExp(value: string): string {
270
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
271
+ }
272
+
273
+ function isExactResumeError(error: unknown, source: "async" | "foreground", requested: string): boolean {
274
+ if (!(error instanceof Error) || !requested) return false;
275
+ return new RegExp(`\\b${source} run '${escapeRegExp(requested)}'`, "i").test(error.message);
276
+ }
277
+
278
+ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState): ResumeSourceTarget {
279
+ const requested = (params.id ?? params.runId)?.trim() ?? "";
280
+ let foregroundTarget: ForegroundResumeSourceTarget | undefined;
281
+ let foregroundError: unknown;
282
+ let asyncTarget: AsyncResumeSourceTarget | undefined;
283
+ let asyncError: unknown;
284
+
285
+ try {
286
+ const target = resolveForegroundResumeTarget(params, state);
287
+ if (target) foregroundTarget = { kind: "revive", source: "foreground", ...target };
288
+ } catch (error) {
289
+ foregroundError = error;
290
+ }
291
+ try {
292
+ asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params) };
293
+ } catch (error) {
294
+ asyncError = error;
295
+ }
296
+
297
+ if (foregroundTarget && asyncTarget) {
298
+ const foregroundExact = resumeTargetExact(foregroundTarget, requested);
299
+ const asyncExact = resumeTargetExact(asyncTarget, requested);
300
+ if (foregroundExact && !asyncExact) return foregroundTarget;
301
+ if (asyncExact && !foregroundExact) return asyncTarget;
302
+ throw new Error(`Resume id '${requested}' is ambiguous between foreground run '${foregroundTarget.runId}' and async run '${asyncTarget.runId}'. Provide a full run id.`);
303
+ }
304
+ if (foregroundTarget) {
305
+ if (isExactResumeError(asyncError, "async", requested)) throw asyncError;
306
+ if (isResumeAmbiguity(asyncError) && !resumeTargetExact(foregroundTarget, requested)) throw asyncError;
307
+ return foregroundTarget;
308
+ }
309
+ if (asyncTarget) {
310
+ if (isExactResumeError(foregroundError, "foreground", requested)) throw foregroundError;
311
+ if (isResumeAmbiguity(foregroundError) && !resumeTargetExact(asyncTarget, requested)) throw foregroundError;
312
+ return asyncTarget;
313
+ }
314
+ if (foregroundError && !isAsyncRunNotFound(asyncError)) throw foregroundError;
315
+ if (foregroundError) throw foregroundError;
316
+ if (asyncError) throw asyncError;
317
+ throw new Error("Run not found. Provide id or runId.");
318
+ }
319
+
209
320
  function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
210
321
  if (runId) {
211
322
  const direct = state.asyncJobs.get(runId);
@@ -296,9 +407,9 @@ async function resumeAsyncRun(input: {
296
407
  };
297
408
  }
298
409
 
299
- let target: ReturnType<typeof resolveAsyncResumeTarget>;
410
+ let target: ResumeSourceTarget;
300
411
  try {
301
- target = resolveAsyncResumeTarget(input.params);
412
+ target = resolveResumeTarget(input.params, input.deps.state);
302
413
  } catch (error) {
303
414
  const message = error instanceof Error ? error.message : String(error);
304
415
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
@@ -352,7 +463,7 @@ async function resumeAsyncRun(input: {
352
463
  const agentConfig = agents.find((agent) => agent.name === target.agent);
353
464
  if (!agentConfig) {
354
465
  return {
355
- content: [{ type: "text", text: `Unknown agent for async resume: ${target.agent}` }],
466
+ content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
356
467
  isError: true,
357
468
  details: { mode: "management", results: [] },
358
469
  };
@@ -390,8 +501,9 @@ async function resumeAsyncRun(input: {
390
501
 
391
502
  const revivedId = result.details.asyncId ?? runId;
392
503
  const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
504
+ const sourceLabel = target.source === "foreground" ? "foreground" : "async";
393
505
  const lines = [
394
- `Revived async subagent from ${target.runId}.`,
506
+ `Revived ${sourceLabel} subagent from ${target.runId}.`,
395
507
  `Revived run: ${revivedId}`,
396
508
  `Agent: ${target.agent}`,
397
509
  `Session: ${target.sessionFile}`,
@@ -836,6 +948,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
836
948
  const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
837
949
  return executeAsyncChain(id, {
838
950
  chain,
951
+ task: params.task,
839
952
  agents,
840
953
  ctx: asyncCtx,
841
954
  availableModels,
@@ -972,6 +1085,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
972
1085
  const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
973
1086
  return executeAsyncChain(id, {
974
1087
  chain: asyncChain,
1088
+ task: params.task,
975
1089
  agents,
976
1090
  ctx: asyncCtx,
977
1091
  availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
@@ -992,8 +1106,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
992
1106
  });
993
1107
  }
994
1108
 
995
- const chainDetails = chainResult.details ? compactForegroundDetails(chainResult.details) : undefined;
996
- const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted)
1109
+ const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
1110
+ if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1111
+ const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
997
1112
  ? await maybeBuildForegroundIntercomReceipt({
998
1113
  pi: deps.pi,
999
1114
  intercomBridge: data.intercomBridge,
@@ -1010,7 +1125,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1010
1125
  };
1011
1126
  }
1012
1127
 
1013
- return chainResult;
1128
+ return chainDetails ? { ...chainResult, details: chainDetails } : chainResult;
1014
1129
  }
1015
1130
 
1016
1131
  interface ForegroundParallelRunInput {
@@ -1376,17 +1491,21 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1376
1491
  currentSessionId: deps.state.currentSessionId!,
1377
1492
  currentModelProvider: ctx.model?.provider,
1378
1493
  };
1379
- const parallelTasks = tasks.map((t, i) => ({
1380
- agent: t.agent,
1381
- task: params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!,
1382
- cwd: t.cwd,
1383
- ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
1384
- ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
1385
- ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
1386
- ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
1387
- ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
1388
- ...(behaviorOverrides[i]?.progress !== undefined ? { progress: behaviorOverrides[i]!.progress } : {}),
1389
- }));
1494
+ const parallelTasks = tasks.map((t, i) => {
1495
+ const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
1496
+ const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
1497
+ return {
1498
+ agent: t.agent,
1499
+ task: taskText,
1500
+ cwd: t.cwd,
1501
+ ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
1502
+ ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
1503
+ ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
1504
+ ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
1505
+ ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
1506
+ ...(progress !== undefined ? { progress } : {}),
1507
+ };
1508
+ });
1390
1509
  return executeAsyncChain(id, {
1391
1510
  chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
1392
1511
  resultMode: "parallel",
@@ -1411,7 +1530,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1411
1530
  }
1412
1531
  }
1413
1532
 
1414
- const behaviors = agentConfigs.map((config, index) => resolveStepBehavior(config, behaviorOverrides[index]!));
1533
+ const behaviors = agentConfigs.map((config, index) => suppressProgressForReadOnlyTask(resolveStepBehavior(config, behaviorOverrides[index]!), taskTexts[index]));
1415
1534
  const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
1416
1535
  const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
1417
1536
  const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
@@ -1495,16 +1614,26 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1495
1614
  const interrupted = results.find((result) => result.interrupted);
1496
1615
  const details = compactForegroundDetails({
1497
1616
  mode: "parallel",
1617
+ runId,
1498
1618
  results,
1499
1619
  progress: params.includeProgress ? allProgress : undefined,
1500
1620
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1501
1621
  });
1622
+ rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
1502
1623
  if (interrupted) {
1503
1624
  return {
1504
1625
  content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
1505
1626
  details,
1506
1627
  };
1507
1628
  }
1629
+ const detachedIndex = results.findIndex((result) => result.detached);
1630
+ const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
1631
+ if (detached) {
1632
+ return {
1633
+ content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
1634
+ details,
1635
+ };
1636
+ }
1508
1637
 
1509
1638
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
1510
1639
  pi: deps.pi,
@@ -1776,11 +1905,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1776
1905
  });
1777
1906
  const details = compactForegroundDetails({
1778
1907
  mode: "single",
1908
+ runId,
1779
1909
  results: [r],
1780
1910
  progress: params.includeProgress ? allProgress : undefined,
1781
1911
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1782
1912
  truncation: r.truncation,
1783
1913
  });
1914
+ rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
1784
1915
 
1785
1916
  if (!r.detached && !r.interrupted) {
1786
1917
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
@@ -1801,7 +1932,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1801
1932
 
1802
1933
  if (r.detached) {
1803
1934
  return {
1804
- content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}` }],
1935
+ content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
1805
1936
  details,
1806
1937
  };
1807
1938
  }
@@ -1842,6 +1973,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1842
1973
  ctx: ExtensionContext,
1843
1974
  ): Promise<AgentToolResult<Details>> => {
1844
1975
  deps.state.baseCwd = ctx.cwd;
1976
+ deps.state.foregroundRuns ??= new Map();
1845
1977
  deps.state.foregroundControls ??= new Map();
1846
1978
  deps.state.lastForegroundControlId ??= null;
1847
1979
  const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
@@ -69,6 +69,7 @@ export function captureSingleOutputSnapshot(outputPath: string | undefined): Sin
69
69
  const stat = fs.statSync(outputPath);
70
70
  return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
71
71
  } catch {
72
+ // The snapshot is advisory; resolveSingleOutput reports concrete read/write failures.
72
73
  return { exists: false };
73
74
  }
74
75
  }
@@ -94,18 +95,32 @@ export function resolveSingleOutput(
94
95
  ): { fullOutput: string; savedPath?: string; saveError?: string } {
95
96
  if (!outputPath) return { fullOutput: fallbackOutput };
96
97
 
98
+ let changedSinceStart = false;
97
99
  try {
98
100
  const stat = fs.statSync(outputPath);
99
- const changedSinceStart = !beforeRun?.exists
101
+ changedSinceStart = !beforeRun?.exists
100
102
  || stat.mtimeMs !== beforeRun.mtimeMs
101
103
  || stat.size !== beforeRun.size;
102
- if (changedSinceStart) {
104
+ } catch (error) {
105
+ const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
106
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
103
107
  return {
104
- fullOutput: fs.readFileSync(outputPath, "utf-8"),
105
- savedPath: outputPath,
108
+ fullOutput: fallbackOutput,
109
+ saveError: `Failed to inspect output file: ${error instanceof Error ? error.message : String(error)}`,
106
110
  };
107
111
  }
108
- } catch {}
112
+ }
113
+
114
+ if (changedSinceStart) {
115
+ try {
116
+ return { fullOutput: fs.readFileSync(outputPath, "utf-8"), savedPath: outputPath };
117
+ } catch (error) {
118
+ return {
119
+ fullOutput: fallbackOutput,
120
+ saveError: `Failed to read changed output file: ${error instanceof Error ? error.message : String(error)}`,
121
+ };
122
+ }
123
+ }
109
124
 
110
125
  const save = persistSingleOutput(outputPath, fallbackOutput);
111
126
  if (save.savedPath) return { fullOutput: fallbackOutput, savedPath: save.savedPath };
@@ -132,7 +147,7 @@ export function finalizeSingleOutput(params: {
132
147
  return { displayOutput, savedPath: params.savedPath, outputReference };
133
148
  }
134
149
  if (params.exitCode === 0 && params.saveError && params.outputPath) {
135
- displayOutput += `\n\nFailed to save output to: ${params.outputPath}\n${params.saveError}`;
150
+ displayOutput += `\n\nOutput file error: ${params.outputPath}\n${params.saveError}`;
136
151
  return { displayOutput, saveError: params.saveError };
137
152
  }
138
153
  return { displayOutput };