pi-subagents 0.48.0 → 0.49.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +47 -19
  2. package/docs/agents.md +1 -0
  3. package/docs/configuration.md +28 -5
  4. package/docs/missions.md +4 -2
  5. package/docs/observability.md +26 -2
  6. package/package.json +1 -1
  7. package/src/agents/agents.ts +27 -12
  8. package/src/api/preflight.ts +1 -1
  9. package/src/extension/config.ts +20 -0
  10. package/src/extension/index.ts +18 -8
  11. package/src/extension/public-execution.ts +30 -4
  12. package/src/extension/schemas.ts +8 -7
  13. package/src/extension/tool-description.ts +10 -8
  14. package/src/inspectors/herdr/actions.ts +11 -2
  15. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  16. package/src/intercom/intercom-bridge.ts +3 -1
  17. package/src/missions/store.ts +8 -3
  18. package/src/runs/background/active-async-capacity.ts +82 -25
  19. package/src/runs/background/async-execution.ts +24 -36
  20. package/src/runs/background/async-job-tracker.ts +4 -0
  21. package/src/runs/background/async-resume.ts +11 -1
  22. package/src/runs/background/fleet-view.ts +18 -5
  23. package/src/runs/background/resume-guidance.ts +27 -7
  24. package/src/runs/background/retained-children.ts +14 -6
  25. package/src/runs/background/run-status.ts +96 -4
  26. package/src/runs/background/subagent-runner.ts +4 -1
  27. package/src/runs/foreground/prompt-audit.ts +4 -3
  28. package/src/runs/foreground/subagent-executor.ts +219 -20
  29. package/src/runs/shared/completion-guard.ts +17 -1
  30. package/src/runs/shared/llm-intent-arbiter.ts +39 -23
  31. package/src/shared/agent-stream-options.ts +5 -0
  32. package/src/shared/artifacts.ts +2 -6
  33. package/src/shared/display-text.ts +50 -0
  34. package/src/shared/node-executable.ts +21 -0
  35. package/src/shared/types.ts +23 -4
  36. package/src/tui/fleet-transcript.ts +1 -48
  37. package/src/tui/fleet.ts +3 -1
  38. package/src/tui/render.ts +86 -39
  39. package/src/watchdog/permission-arbiter.ts +2 -1
  40. package/src/watchdog/review.ts +4 -3
  41. package/src/workflows/chat-progress.ts +8 -2
  42. package/src/workflows/scripted-workflow.ts +46 -7
@@ -139,6 +139,7 @@ import {
139
139
  type Details,
140
140
  type ExtensionConfig,
141
141
  type ForegroundRunControl,
142
+ type IntercomBridgeConfig,
142
143
  type IntercomEventBus,
143
144
  type JsonSchemaObject,
144
145
  type MaxOutputConfig,
@@ -300,6 +301,8 @@ export interface SubagentParamsLike {
300
301
  concurrency?: number;
301
302
  worktree?: boolean;
302
303
  context?: "fresh" | "fork";
304
+ /** Per-run intercom bridge config. It replaces the global config for this launch only. */
305
+ intercomBridge?: IntercomBridgeConfig;
303
306
  async?: boolean;
304
307
  foregroundOnly?: boolean;
305
308
  timeoutMs?: number;
@@ -387,6 +390,8 @@ interface ExecutionContextData {
387
390
  signal: AbortSignal;
388
391
  onUpdate?: (r: AgentToolResult<Details>) => void;
389
392
  agents: AgentConfig[];
393
+ /** Discovered agent definitions before per-run bridge injection. */
394
+ recoveryAgents: AgentConfig[];
390
395
  runId: string;
391
396
  shareEnabled: boolean;
392
397
  sessionRoot: string;
@@ -1511,17 +1516,18 @@ async function resumeAsyncRun(input: {
1511
1516
  const discoveredAgents = discovered.agents;
1512
1517
  const modelScope = discovered.modelScope;
1513
1518
  const sessionName = resolveIntercomSessionTarget(input.deps.pi.getSessionName(), input.ctx.sessionManager.getSessionId());
1519
+ const recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined;
1514
1520
  const intercomBridge = resolveIntercomBridge({
1515
1521
  config: input.deps.config.intercomBridge,
1522
+ override: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
1516
1523
  context: input.params.context,
1517
1524
  orchestratorTarget: sessionName,
1518
1525
  });
1519
1526
  const agents = intercomBridge.active
1520
1527
  ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
1521
1528
  : discoveredAgents;
1522
- const recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined;
1523
- const discoveredAgentConfig = agents.find((agent) => agent.name === target.agent);
1524
- const agentConfig: AgentConfig | undefined = discoveredAgentConfig ?? (recoveryDescriptor ? {
1529
+ const discoveredAgentConfig = discoveredAgents.find((agent) => agent.name === target.agent);
1530
+ const baseAgentConfig: AgentConfig | undefined = discoveredAgentConfig ?? (recoveryDescriptor ? {
1525
1531
  name: recoveryDescriptor.agent,
1526
1532
  description: "Persisted async recovery contract",
1527
1533
  systemPrompt: "",
@@ -1531,7 +1537,7 @@ async function resumeAsyncRun(input: {
1531
1537
  source: "project",
1532
1538
  filePath: recoveryDescriptor.agentFilePath ?? path.join(getProjectSubagentsDir(recoveryDescriptor.cwd), "recovery-agent"),
1533
1539
  } : undefined);
1534
- if (!agentConfig) {
1540
+ if (!baseAgentConfig) {
1535
1541
  return {
1536
1542
  content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
1537
1543
  isError: true,
@@ -1672,7 +1678,8 @@ async function resumeAsyncRun(input: {
1672
1678
  if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "single", results: [], activeAsyncCapacity: error.snapshot } };
1673
1679
  return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "single", results: [] } };
1674
1680
  }
1675
- const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1681
+ const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(baseAgentConfig, recoveryDescriptor) : baseAgentConfig;
1682
+ const agentConfig = intercomBridge.active ? applyIntercomBridgeToAgent(recoveryAgentConfig, intercomBridge) : recoveryAgentConfig;
1676
1683
  const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1677
1684
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1678
1685
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
@@ -1682,7 +1689,8 @@ async function resumeAsyncRun(input: {
1682
1689
  agent: target.agent,
1683
1690
  task: buildRevivedAsyncTask(target as Parameters<typeof buildRevivedAsyncTask>[0], effectiveFollowUp),
1684
1691
  goal: effectiveFollowUp,
1685
- agentConfig: recoveryAgentConfig,
1692
+ agentConfig,
1693
+ recoveryAgentConfig,
1686
1694
  ctx: compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
1687
1695
  pi: input.deps.pi,
1688
1696
  cwd: input.requestCwd,
@@ -1717,6 +1725,7 @@ async function resumeAsyncRun(input: {
1717
1725
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1718
1726
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1719
1727
  controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
1728
+ intercomBridge: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
1720
1729
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1721
1730
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1722
1731
  availableModels,
@@ -1731,7 +1740,7 @@ async function resumeAsyncRun(input: {
1731
1740
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1732
1741
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1733
1742
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1734
- runFanoutBudget: recoveryDescriptor?.runFanoutBudget ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1743
+ runFanoutBudget: input.params.runFanoutBudget ?? recoveryDescriptor?.runFanoutBudget ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1735
1744
  parentWorkflowRunId: input.params.workflowParentRunId,
1736
1745
  workflowKey: input.params.workflowKey,
1737
1746
  activeAsyncCapacity,
@@ -2870,6 +2879,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2870
2879
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
2871
2880
  goal: params.task ?? "",
2872
2881
  agentConfig: a,
2882
+ recoveryAgentConfig: data.recoveryAgents.find((agent) => agent.name === params.agent),
2873
2883
  ctx: asyncCtx,
2874
2884
  availableModels,
2875
2885
  cwd: effectiveCwd,
@@ -2894,6 +2904,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2894
2904
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2895
2905
  worktreeBaseDir: deps.config.worktreeBaseDir,
2896
2906
  controlConfig,
2907
+ intercomBridge: params.intercomBridge,
2897
2908
  controlIntercomTarget,
2898
2909
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
2899
2910
  nestedRoute,
@@ -3239,10 +3250,128 @@ function buildParallelWorktreeTaskCwdError(
3239
3250
  return formatWorktreeTaskCwdConflict(conflict, sharedCwd);
3240
3251
  }
3241
3252
 
3242
- function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
3253
+ function resolveConfiguredSingleRunOutputBaseDir(deps: ExecutorDeps): string | undefined {
3243
3254
  return deps.config.singleRunOutputBaseDir
3244
3255
  ? path.resolve(deps.expandTilde(deps.config.singleRunOutputBaseDir))
3245
- : path.join(artifactsDir, "outputs", runId);
3256
+ : undefined;
3257
+ }
3258
+
3259
+ function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
3260
+ return resolveConfiguredSingleRunOutputBaseDir(deps) ?? path.join(artifactsDir, "outputs", runId);
3261
+ }
3262
+
3263
+ function resolveWorkflowAggregateOutputPath(
3264
+ output: string | boolean | undefined,
3265
+ ctxCwd: string,
3266
+ workflowCwd: string,
3267
+ outputBaseDir: string,
3268
+ configuredOutputBaseDir: string | undefined,
3269
+ ): string | undefined {
3270
+ if (typeof output === "string" && output && !path.isAbsolute(output) && !configuredOutputBaseDir) return path.resolve(workflowCwd, output);
3271
+ return resolveSingleOutputPath(output, ctxCwd, workflowCwd, outputBaseDir);
3272
+ }
3273
+
3274
+ function workflowChildDefaultOutput(aggregateOutputPath: string | undefined, artifactsDir: string, workflowRunId: string, workflowKey: string): string {
3275
+ if (aggregateOutputPath) {
3276
+ const parsed = path.parse(aggregateOutputPath);
3277
+ return path.join(parsed.dir, `${parsed.name}.${workflowKey}${parsed.ext || ".md"}`);
3278
+ }
3279
+ return path.join(artifactsDir, "outputs", workflowRunId, `${workflowKey}.md`);
3280
+ }
3281
+
3282
+ function writeWorkflowAggregateOutput(outputPath: string | undefined, text: string): string | undefined {
3283
+ if (!outputPath) return undefined;
3284
+ try {
3285
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
3286
+ fs.writeFileSync(outputPath, text, "utf-8");
3287
+ return undefined;
3288
+ } catch (error) {
3289
+ return `Output file error: ${outputPath}\n${error instanceof Error ? error.message : String(error)}`;
3290
+ }
3291
+ }
3292
+
3293
+ function appendWorkflowOutputWarning(text: string, warning: string | undefined): string {
3294
+ return warning ? `${text}\n\n${warning}` : text;
3295
+ }
3296
+
3297
+ function resolveWorkflowChildOutputPath(input: {
3298
+ ctxCwd: string;
3299
+ workflowCwd: string;
3300
+ artifactsDir: string;
3301
+ workflowRunId: string;
3302
+ aggregateOutputPath?: string;
3303
+ configuredOutputBaseDir?: string;
3304
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3305
+ workflowAgentScope?: unknown;
3306
+ key: string;
3307
+ params: Record<string, unknown>;
3308
+ }): string | undefined {
3309
+ const rawOutput = input.params.output;
3310
+ const hasExplicitOutput = typeof rawOutput === "string" || typeof rawOutput === "boolean";
3311
+ const childCwd = typeof input.params.cwd === "string" ? resolveChildCwd(input.workflowCwd, input.params.cwd) : input.workflowCwd;
3312
+ const agentScope = resolveExecutionAgentScope(input.params.agentScope ?? input.workflowAgentScope);
3313
+ const agents = input.discoverAgents(childCwd, agentScope).agents;
3314
+ const agent = typeof input.params.agent === "string" ? resolveAgentName(input.params.agent, agents).agent : undefined;
3315
+ const agentOutput = typeof agent?.output === "string" ? agent.output : undefined;
3316
+ const output = rawOutput === true || rawOutput === "true"
3317
+ ? agentOutput
3318
+ : hasExplicitOutput
3319
+ ? rawOutput
3320
+ : workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, input.key);
3321
+ return resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir);
3322
+ }
3323
+
3324
+ function workflowChildOutputClaims(input: {
3325
+ ctxCwd: string;
3326
+ workflowCwd: string;
3327
+ artifactsDir: string;
3328
+ workflowRunId: string;
3329
+ aggregateOutputPath?: string;
3330
+ configuredOutputBaseDir?: string;
3331
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3332
+ workflowAgentScope?: unknown;
3333
+ claimedOutputPaths: Map<string, string>;
3334
+ entries: Array<{ key: string; params: Record<string, unknown> }>;
3335
+ }): { error?: string; claims?: Map<string, string> } {
3336
+ const claims = new Map(input.claimedOutputPaths);
3337
+ for (const { key, params } of input.entries) {
3338
+ const resolved = resolveWorkflowChildOutputPath({ ...input, key, params });
3339
+ if (!resolved) continue;
3340
+ if (input.aggregateOutputPath && resolved === input.aggregateOutputPath) return { error: `Workflow child '${key}' output resolves to the workflow aggregate output path: ${resolved}. Use a distinct child output path.` };
3341
+ const previous = claims.get(resolved);
3342
+ if (previous) return { error: `Workflow children '${previous}' and '${key}' resolve output to the same path: ${resolved}. Use distinct child output paths.` };
3343
+ claims.set(resolved, key);
3344
+ }
3345
+ return { claims };
3346
+ }
3347
+
3348
+ function applyWorkflowChildOutputClaims(target: Map<string, string>, claims: Map<string, string>): void {
3349
+ target.clear();
3350
+ for (const [resolved, key] of claims) target.set(resolved, key);
3351
+ }
3352
+
3353
+ function prepareWorkflowChildLaunchParams(input: {
3354
+ workflowDefaults: SubagentParamsLike;
3355
+ childParams: Record<string, unknown>;
3356
+ parentWorkflowRunId: string;
3357
+ workflowKey: string;
3358
+ ctxCwd: string;
3359
+ workflowCwd: string;
3360
+ artifactsDir: string;
3361
+ aggregateOutputPath?: string;
3362
+ configuredOutputBaseDir?: string;
3363
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3364
+ workflowAgentScope?: unknown;
3365
+ options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor };
3366
+ }): SubagentParamsLike {
3367
+ let childParams = input.childParams;
3368
+ if (input.childParams.output === undefined && input.childParams.resume === undefined) {
3369
+ childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
3370
+ } else if (input.childParams.resume === undefined) {
3371
+ const resolvedOutput = resolveWorkflowChildOutputPath({ ctxCwd: input.ctxCwd, workflowCwd: input.workflowCwd, artifactsDir: input.artifactsDir, workflowRunId: input.parentWorkflowRunId, aggregateOutputPath: input.aggregateOutputPath, configuredOutputBaseDir: input.configuredOutputBaseDir, discoverAgents: input.discoverAgents, workflowAgentScope: input.workflowAgentScope, key: input.workflowKey, params: input.childParams });
3372
+ if (resolvedOutput) childParams = { ...input.childParams, output: resolvedOutput };
3373
+ }
3374
+ return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, input.options);
3246
3375
  }
3247
3376
 
3248
3377
  function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined {
@@ -4073,6 +4202,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4073
4202
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
4074
4203
  goal: task,
4075
4204
  agentConfig,
4205
+ recoveryAgentConfig: data.recoveryAgents.find((agent) => agent.name === params.agent),
4076
4206
  ctx: asyncCtx,
4077
4207
  availableModels,
4078
4208
  cwd: effectiveCwd,
@@ -4097,6 +4227,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4097
4227
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
4098
4228
  worktreeBaseDir: deps.config.worktreeBaseDir,
4099
4229
  controlConfig,
4230
+ intercomBridge: params.intercomBridge,
4100
4231
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
4101
4232
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
4102
4233
  nestedRoute: data.nestedRoute,
@@ -4469,16 +4600,19 @@ export function prepareWorkflowLaunchParams(
4469
4600
  const timeoutMs = childParams.timeoutMs ?? childParams.maxRuntimeMs ?? workflowDefaults.timeoutMs ?? workflowDefaults.maxRuntimeMs;
4470
4601
  const turnBudget = childParams.turnBudget ?? workflowDefaults.turnBudget;
4471
4602
  const toolBudget = childParams.toolBudget ?? workflowDefaults.toolBudget;
4603
+ const intercomBridge = childParams.intercomBridge ?? workflowDefaults.intercomBridge;
4472
4604
  return {
4473
4605
  action: "resume",
4474
4606
  id: childParams.resume.trim(),
4475
4607
  message: typeof childParams.task === "string" ? childParams.task.trim() : "",
4476
4608
  workflowParentRunId: parentWorkflowRunId,
4477
4609
  workflowKey,
4610
+ ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4478
4611
  ...(options.missionDetached ? { mission: false } : {}),
4479
4612
  ...(timeoutMs !== undefined ? { timeoutMs: timeoutMs as number } : {}),
4480
4613
  ...(turnBudget !== undefined ? { turnBudget: turnBudget as TurnBudgetConfig } : {}),
4481
4614
  ...(toolBudget !== undefined ? { toolBudget: toolBudget as ToolBudgetConfig } : {}),
4615
+ ...(intercomBridge !== undefined ? { intercomBridge: intercomBridge as IntercomBridgeConfig } : {}),
4482
4616
  };
4483
4617
  }
4484
4618
  const launchParams = {
@@ -4665,6 +4799,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4665
4799
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4666
4800
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4667
4801
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
4802
+ const workflowArtifactConfig: ArtifactConfig = omitUndefinedProperties({
4803
+ ...DEFAULT_ARTIFACT_CONFIG,
4804
+ enabled: requestParams.artifacts !== false,
4805
+ dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
4806
+ });
4807
+ const workflowArtifactsDir = getArtifactsDir(ctx.sessionManager.getSessionFile() ?? null, workflowCwd, workflowArtifactConfig.dir);
4808
+ if (workflowArtifactConfig.dir === "project" && !warnedArtifactPackageDirs.has(workflowCwd)) {
4809
+ warnedArtifactPackageDirs.add(workflowCwd);
4810
+ const warning = getProjectArtifactPackagingWarning(workflowCwd);
4811
+ if (warning) console.warn(`[pi-subagents] ${warning}`);
4812
+ }
4668
4813
  const workflowPrompts = { render: (ref: string, vars?: unknown) => renderWorkflowPrompt(ref, vars, workflowCwd) };
4669
4814
  const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd, workflowCwd, background: requestParams.async !== false });
4670
4815
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
@@ -4838,6 +4983,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4838
4983
  void Promise.resolve().then(async () => {
4839
4984
  const workflowResults: SingleResult[] = [];
4840
4985
  const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = workflowRequest;
4986
+ const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
4987
+ const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
4988
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId), configuredOutputBaseDir);
4989
+ const claimedOutputPaths = new Map<string, string>();
4841
4990
  const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
4842
4991
  let projectedTraceLength = 0;
4843
4992
  let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
@@ -4857,12 +5006,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4857
5006
  if (entry.operation !== "run") continue;
4858
5007
  const existing = workflowSteps.get(entry.key);
4859
5008
  if (entry.state === "reused" && existing) continue;
4860
- const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
5009
+ const mapped = entry.state === "started" || entry.state === "reused"
5010
+ ? "running"
5011
+ : entry.state === "completed"
5012
+ ? "completed"
5013
+ : entry.state === "stopped"
5014
+ ? "stopped"
5015
+ : "failed";
4861
5016
  if (existing) {
4862
5017
  existing.status = mapped;
4863
5018
  if (entry.agent) existing.agent = entry.agent;
4864
5019
  if (entry.runId) existing.runId = entry.runId;
4865
5020
  if (entry.state === "failed" && !entry.runId && existing.async === undefined) existing.async = false;
5021
+ if (entry.state === "stopped") existing.stopped = true;
5022
+ else delete existing.stopped;
4866
5023
  if (entry.error === undefined) delete existing.error;
4867
5024
  else existing.error = entry.error;
4868
5025
  if (entry.durationMs === undefined) delete existing.durationMs;
@@ -4877,6 +5034,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4877
5034
  startedAt: Date.now(),
4878
5035
  ...(entry.runId ? { runId: entry.runId } : {}),
4879
5036
  ...(entry.state === "failed" && !entry.runId ? { async: false } : {}),
5037
+ ...(entry.state === "stopped" ? { stopped: true } : {}),
4880
5038
  };
4881
5039
  status.steps?.push(step);
4882
5040
  workflowSteps.set(entry.key, step);
@@ -4897,7 +5055,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4897
5055
  ...(workflowState ? { state: workflowState } : {}),
4898
5056
  onTrace: updateTrace,
4899
5057
  admit: (calls) => {
5058
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
5059
+ if (outputClaims.error) throw new Error(outputClaims.error);
4900
5060
  status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5061
+ if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
4901
5062
  persist();
4902
5063
  },
4903
5064
  onEmit: (emits) => {
@@ -4921,7 +5082,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4921
5082
  });
4922
5083
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4923
5084
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4924
- { ...prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget }), runFanoutAdmitted: admission.admitted },
5085
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: workflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, options: { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget } }), runFanoutAdmitted: admission.admitted },
4925
5086
  missionBinding,
4926
5087
  deps.asyncByDefault,
4927
5088
  );
@@ -4939,7 +5100,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4939
5100
  return execute(randomUUID(), childRequest, workflowSignal, (update) => {
4940
5101
  const progress = update.details.progress?.[0];
4941
5102
  const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4942
- if (!progress || !step) return;
5103
+ if (!progress || !step || step.stopped) return;
4943
5104
  step.status = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4944
5105
  step.activityState = progress.activityState;
4945
5106
  step.lastActivityAt = progress.lastActivityAt;
@@ -4991,16 +5152,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4991
5152
  const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
4992
5153
  const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
4993
5154
  const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
5155
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary);
5156
+ const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
4994
5157
  const workflowUsage = sumResultsUsage(workflowResults);
4995
5158
  status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4996
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
5159
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4997
5160
  persist();
4998
5161
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4999
5162
  } catch (error) {
5000
5163
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
5001
5164
  const stopped = controller.signal.aborted;
5002
5165
  status = compactOptional<AsyncStatus>({ ...status, state: stopped ? "stopped" : "failed", stopped: stopped || undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
5003
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
5166
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, status.error ?? "Workflow failed.");
5167
+ const resultSummary = appendWorkflowOutputWarning(status.error ?? "Workflow failed.", outputWarning);
5168
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: resultSummary, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
5004
5169
  persist();
5005
5170
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
5006
5171
  } finally {
@@ -5015,6 +5180,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5015
5180
  }, workflowFanoutBudget));
5016
5181
  }
5017
5182
  const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = requestParams;
5183
+ const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
5184
+ const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
5185
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id), configuredOutputBaseDir);
5186
+ const claimedOutputPaths = new Map<string, string>();
5018
5187
  const workflowResults: SingleResult[] = [];
5019
5188
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
5020
5189
  const sendWorkflowProgress = () => {
@@ -5033,7 +5202,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5033
5202
  sendWorkflowProgress();
5034
5203
  },
5035
5204
  admit: (calls) => {
5205
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
5206
+ if (outputClaims.error) throw new Error(outputClaims.error);
5036
5207
  claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5208
+ if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
5037
5209
  },
5038
5210
  onEmit: (emits) => {
5039
5211
  liveWorkflow = { ...liveWorkflow, emits };
@@ -5054,7 +5226,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5054
5226
  });
5055
5227
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
5056
5228
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
5057
- { ...prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget }), runFanoutAdmitted: admission.admitted },
5229
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget } }), runFanoutAdmitted: admission.admitted },
5058
5230
  missionBinding,
5059
5231
  deps.asyncByDefault,
5060
5232
  );
@@ -5095,8 +5267,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5095
5267
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
5096
5268
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5097
5269
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5270
+ const workflowText = sections.join("\n\n");
5271
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
5272
+ const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5098
5273
  return attachWorkflowMission(withRunFanoutBudget({
5099
- content: [{ type: "text", text: sections.join("\n\n") }],
5274
+ content: [{ type: "text", text: displayText }],
5100
5275
  details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, chatProgress }),
5101
5276
  }, workflowFanoutBudget));
5102
5277
  } catch (error) {
@@ -5107,8 +5282,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5107
5282
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
5108
5283
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5109
5284
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5285
+ const workflowText = sections.join("\n\n");
5286
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
5287
+ const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5110
5288
  return attachWorkflowMission(withRunFanoutBudget({
5111
- content: [{ type: "text", text: sections.join("\n\n") }],
5289
+ content: [{ type: "text", text: displayText }],
5112
5290
  isError: true,
5113
5291
  details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console }, chatProgress }),
5114
5292
  }, workflowFanoutBudget));
@@ -5372,11 +5550,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5372
5550
  details: { mode: "management", results: [], spawnBudget, activeAsyncCapacity },
5373
5551
  };
5374
5552
  }
5375
- if (action === "status") {
5553
+ if (action === "status" || action === "debug.run") {
5376
5554
  if (!preserveActiveSession) deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
5377
5555
  const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
5378
5556
  const hasDirectoryTarget = Boolean(paramsWithResolvedCwd.dir);
5379
- const targetLabel = formatStatusTargetLabel(paramsWithResolvedCwd, targetRunId);
5557
+ const targetLabel = action === "debug.run" ? "Debug run" : formatStatusTargetLabel(paramsWithResolvedCwd, targetRunId);
5380
5558
  const withBudget = (result: AgentToolResult<Details>) => {
5381
5559
  const budgeted = withSpawnBudgetStatus(result, deps.state, deps.config, deps.state.currentSessionId);
5382
5560
  return {
@@ -5388,6 +5566,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5388
5566
  };
5389
5567
  const nestedScope = nestedResolutionScopeForExecutor(deps);
5390
5568
  const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
5569
+ if (action === "debug.run") {
5570
+ if (!targetRunId && !hasDirectoryTarget) {
5571
+ return withBudget({ content: [{ type: "text", text: "action='debug.run' requires id, runId, or dir." }], isError: true, details: { mode: "management", results: [] } });
5572
+ }
5573
+ if (paramsWithResolvedCwd.view) {
5574
+ return withBudget({ content: [{ type: "text", text: "action='debug.run' does not support status views." }], isError: true, details: { mode: "management", results: [] } });
5575
+ }
5576
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5577
+ }
5391
5578
  if (paramsWithResolvedCwd.view === "fleet") {
5392
5579
  return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5393
5580
  }
@@ -5777,6 +5964,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5777
5964
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
5778
5965
  const intercomBridge = resolveIntercomBridge({
5779
5966
  config: deps.config.intercomBridge,
5967
+ override: effectiveParams.intercomBridge,
5780
5968
  context: effectiveParams.context ?? (contextPolicy.usesFork ? "fork" : undefined),
5781
5969
  orchestratorTarget: sessionName,
5782
5970
  });
@@ -6039,6 +6227,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6039
6227
  signal,
6040
6228
  onUpdate: onUpdateWithContext,
6041
6229
  agents,
6230
+ recoveryAgents: discoveredAgents,
6042
6231
  runId,
6043
6232
  shareEnabled,
6044
6233
  sessionRoot,
@@ -6229,7 +6418,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6229
6418
  };
6230
6419
 
6231
6420
  let nestedForegroundStarted = false;
6421
+ let asyncLaunchFailed = false;
6232
6422
  try {
6423
+ if (effectiveAsync) {
6424
+ deps.state.liveAsyncSessionRoots ??= new Map();
6425
+ deps.state.liveAsyncSessionRoots.set(asyncRunId, sessionRoot);
6426
+ }
6233
6427
  if (workflowLaunchObserver) {
6234
6428
  const singleTask = hasTasks && effectiveParams.tasks?.length === 1 ? effectiveParams.tasks[0] : undefined;
6235
6429
  const launch = hasSingle
@@ -6243,7 +6437,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6243
6437
  }
6244
6438
  }
6245
6439
  const asyncResult = runAsyncPath(execData, deps);
6246
- if (asyncResult) return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));
6440
+ if (asyncResult) {
6441
+ asyncLaunchFailed = asyncResult.isError === true;
6442
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));
6443
+ }
6247
6444
  if (foregroundControl) {
6248
6445
  writeNestedForegroundEvent("subagent.nested.started");
6249
6446
  nestedForegroundStarted = true;
@@ -6265,10 +6462,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6265
6462
  return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
6266
6463
  }
6267
6464
  } catch (error) {
6465
+ asyncLaunchFailed = effectiveAsync;
6268
6466
  const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
6269
6467
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
6270
6468
  return attachMission(errorResult);
6271
6469
  } finally {
6470
+ if (effectiveAsync && (asyncLaunchFailed || (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt))) deps.state.liveAsyncSessionRoots?.delete(asyncRunId);
6272
6471
  if (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
6273
6472
  if (foregroundControl) {
6274
6473
  settleForegroundSchedulingOwner(foregroundControl);
@@ -22,6 +22,10 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
22
22
  const CURSOR_FILE_MUTATION_THINKING =
23
23
  /(?:^|\n)\s*Cursor (?:edit|write)\s*:/i;
24
24
 
25
+ const IMPLEMENTATION_CHALLENGE_TASK_PATTERN = /^You are reviving a previous subagent conversation\.\n\nOriginal run: .+\nOriginal agent: .+(?:\nOriginal session file: .+)?\n\nUse the stored session context as background\. Answer the orchestrator's follow-up below\. Do not assume the original child process is still alive\.\n\nFollow-up:\nRun implementation challenge pass (?:one|two|\d+) and implement any better current-scope change\.$/;
26
+ const NO_BETTER_CHANGE_NEEDED_PATTERN = /^\s*no (?:better|further|additional) (?:current[- ]scope )?(?:code |source |file )?(?:change|changes|edit|edits|patch|patches) (?:is|are) needed[.!]?\s*$/i;
27
+ const NO_BETTER_CHANGE_QUALIFIER_PATTERN = /\b(?:do\s+not|don't|dont|not|never|cannot|can't|cant|unable|uncertain|unsure|unclear|maybe|might|may|\w+n['’]t)\b/i;
28
+
25
29
  interface CompletionMutationGuardInput {
26
30
  agent: string;
27
31
  task: string;
@@ -82,14 +86,26 @@ export function hasMutationToolCall(messages: Message[]): boolean {
82
86
  return false;
83
87
  }
84
88
 
89
+ function reportsNoBetterChallengeChange(messages: Message[]): boolean {
90
+ const report = messages
91
+ .filter((message) => message.role === "assistant")
92
+ .flatMap((message) => message.content)
93
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
94
+ .join("\n");
95
+ return NO_BETTER_CHANGE_NEEDED_PATTERN.test(report)
96
+ && !NO_BETTER_CHANGE_QUALIFIER_PATTERN.test(report);
97
+ }
98
+
85
99
  export function evaluateCompletionMutationGuard(input: CompletionMutationGuardInput): CompletionMutationGuardResult {
86
100
  const expectedMutation = hasMutationToolCapability(input.tools, input.mcpDirectTools)
87
101
  ? expectsImplementationMutation(input.agent, input.task)
88
102
  : false;
89
103
  const attemptedMutation = hasMutationToolCall(input.messages);
104
+ const noEditChallengeComplete = IMPLEMENTATION_CHALLENGE_TASK_PATTERN.test(input.task)
105
+ && reportsNoBetterChallengeChange(input.messages);
90
106
  return {
91
107
  expectedMutation,
92
108
  attemptedMutation,
93
- triggered: expectedMutation && !attemptedMutation,
109
+ triggered: expectedMutation && !attemptedMutation && !noEditChallengeComplete,
94
110
  };
95
111
  }