pi-subagents 0.58.0 → 0.60.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 (116) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +93 -5
  8. package/docs/workflows.md +94 -3
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +48 -243
  12. package/skills/council-mode/references/pass-contracts.md +150 -0
  13. package/skills/pi-subagents/SKILL.md +87 -37
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  15. package/skills/pi-subagents/references/execution-controls.md +52 -7
  16. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  18. package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
  19. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  20. package/src/agents/agent-management.ts +159 -42
  21. package/src/agents/agent-serializer.ts +4 -2
  22. package/src/agents/agents.ts +67 -26
  23. package/src/agents/runtime-agent-registry.ts +9 -16
  24. package/src/api/background-work.ts +5 -1
  25. package/src/api/delegation.ts +0 -7
  26. package/src/api/preflight.ts +6 -9
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +5 -3
  29. package/src/extension/index.ts +51 -23
  30. package/src/extension/public-execution.ts +16 -2
  31. package/src/extension/schemas.ts +36 -10
  32. package/src/extension/tool-description.ts +24 -5
  33. package/src/intercom/result-intercom.ts +2 -0
  34. package/src/profiles/profiles.ts +5 -6
  35. package/src/runs/background/active-async-capacity.ts +2 -2
  36. package/src/runs/background/async-execution.ts +52 -37
  37. package/src/runs/background/async-job-tracker.ts +17 -12
  38. package/src/runs/background/async-resume.ts +23 -25
  39. package/src/runs/background/async-status-snapshot.ts +23 -261
  40. package/src/runs/background/async-status.ts +110 -8
  41. package/src/runs/background/chain-append.ts +6 -3
  42. package/src/runs/background/chain-root-attachment.ts +60 -8
  43. package/src/runs/background/control-channel.ts +3 -2
  44. package/src/runs/background/fleet-view.ts +21 -11
  45. package/src/runs/background/notify.ts +158 -6
  46. package/src/runs/background/result-files.ts +2 -1
  47. package/src/runs/background/result-watcher.ts +2 -0
  48. package/src/runs/background/resume-guidance.ts +1 -1
  49. package/src/runs/background/retained-children.ts +1 -1
  50. package/src/runs/background/run-status.ts +30 -9
  51. package/src/runs/background/scheduled-runs.ts +86 -7
  52. package/src/runs/background/stale-run-reconciler.ts +10 -4
  53. package/src/runs/background/steering.ts +4 -14
  54. package/src/runs/background/subagent-runner.ts +351 -358
  55. package/src/runs/background/subagent-wait.ts +68 -12
  56. package/src/runs/background/terminal-run-index.ts +1 -1
  57. package/src/runs/background/wait-completions.ts +25 -1
  58. package/src/runs/background/wait-config.ts +23 -9
  59. package/src/runs/background/wait-tool.ts +9 -2
  60. package/src/runs/foreground/async-steering-action.ts +2 -2
  61. package/src/runs/foreground/execution.ts +125 -122
  62. package/src/runs/foreground/foreground-control.ts +3 -0
  63. package/src/runs/foreground/foreground-history.ts +1 -0
  64. package/src/runs/foreground/subagent-executor.ts +576 -240
  65. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  66. package/src/runs/shared/abort-recovery.ts +119 -0
  67. package/src/runs/shared/async-status-projection.ts +597 -0
  68. package/src/runs/shared/background-process-options.ts +9 -0
  69. package/src/runs/shared/child-identity.ts +19 -4
  70. package/src/runs/shared/child-launch-plan.ts +151 -0
  71. package/src/runs/shared/completion-evidence.ts +89 -0
  72. package/src/runs/shared/completion-guard.ts +1 -1
  73. package/src/runs/shared/dynamic-fanout.ts +2 -2
  74. package/src/runs/shared/host-step-status.ts +230 -0
  75. package/src/runs/shared/lane-metadata.ts +105 -0
  76. package/src/runs/shared/mcp-config-sources.ts +42 -6
  77. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  78. package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
  79. package/src/runs/shared/model-fallback.ts +9 -2
  80. package/src/runs/shared/mutation-evidence.ts +52 -3
  81. package/src/runs/shared/nested-events.ts +6 -2
  82. package/src/runs/shared/nested-render.ts +7 -3
  83. package/src/runs/shared/parallel-handoff.ts +419 -7
  84. package/src/runs/shared/parallel-utils.ts +7 -0
  85. package/src/runs/shared/pi-args.ts +67 -3
  86. package/src/runs/shared/single-output.ts +72 -22
  87. package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
  88. package/src/runs/shared/workflow-graph.ts +15 -0
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +5 -2
  94. package/src/shared/launch-contract.ts +1 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +232 -30
  97. package/src/shared/utils.ts +35 -55
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/tui/fleet-status.ts +134 -75
  104. package/src/tui/fleet.ts +11 -5
  105. package/src/tui/render-helpers.ts +31 -0
  106. package/src/tui/render.ts +895 -121
  107. package/src/watchdog/change-signature.ts +40 -1
  108. package/src/watchdog/turn-delta.ts +1 -1
  109. package/src/workflows/chat-progress.ts +6 -3
  110. package/src/workflows/host-command.ts +235 -0
  111. package/src/workflows/scripted-workflow.ts +503 -38
  112. package/src/workflows/workflow-child-summary.ts +9 -5
  113. package/src/workflows/workflow-preflight.ts +270 -0
  114. package/src/workflows/workflow-receipt.ts +43 -4
  115. package/src/workflows/workflow-settlement.ts +246 -0
  116. package/src/runs/shared/turn-budget.ts +0 -98
@@ -48,7 +48,7 @@ import {
48
48
  encodeChildWatchdogConfig,
49
49
  type ChildWatchdogConfig,
50
50
  } from "../../watchdog/child-status.ts";
51
- import { WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
51
+ import { WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV, WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
52
52
  import {
53
53
  PI_CODING_AGENT_PACKAGE_ROOT_ENV,
54
54
  getAgentDir,
@@ -117,6 +117,7 @@ const FAST_MODE_ALLOWED_MODELS = new Set([
117
117
  "openai-codex/gpt-5.6-luna",
118
118
  "openai-codex/gpt-5.6-sol",
119
119
  ]);
120
+ const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
120
121
  export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
121
122
  export const SUBAGENT_ORCHESTRATOR_TARGET_ENV =
122
123
  "PI_SUBAGENT_ORCHESTRATOR_TARGET";
@@ -139,6 +140,7 @@ export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
139
140
  export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV =
140
141
  "PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
141
142
  export const SUBAGENT_PARENT_SESSION_ENV = "PI_SUBAGENT_PARENT_SESSION";
143
+ export const SUBAGENT_FORK_CACHE_KEY_ENV = "PI_SUBAGENT_FORK_CACHE_KEY";
142
144
  export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
143
145
  export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
144
146
  export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
@@ -146,8 +148,16 @@ export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
146
148
  export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
147
149
  export const SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_GLOBAL_CONTEXT";
148
150
 
151
+ export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
152
+ const parent = parentSessionId?.trim();
153
+ if (!parent) return undefined;
154
+ const digest = createHash("sha256").update(parent).digest("hex").slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH - "pi-fork:".length);
155
+ return `pi-fork:${digest}`;
156
+ }
157
+
149
158
  export interface BuildPiArgsInput {
150
159
  parentSessionId?: string;
160
+ forkCacheKey?: string;
151
161
  baseArgs: string[];
152
162
  task: string;
153
163
  sessionEnabled: boolean;
@@ -170,6 +180,11 @@ export interface BuildPiArgsInput {
170
180
  cwd?: string;
171
181
  promptFileStem?: string;
172
182
  intercomSessionName?: string;
183
+ /** Human-readable display name for the child session (agent + task excerpt,
184
+ * derived by the caller). Passed to the child as PI_SUBAGENT_SESSION_NAME;
185
+ * the prompt runtime applies it via pi.setSessionName unless an intercom
186
+ * target takes precedence. */
187
+ sessionName?: string;
173
188
  orchestratorIntercomTarget?: string;
174
189
  runId?: string;
175
190
  childAgentName?: string;
@@ -206,6 +221,8 @@ export interface BuildPiArgsInput {
206
221
  */
207
222
  taskDelivery?: SubagentTaskDelivery;
208
223
  waitToolEnabled?: boolean;
224
+ waitToolDefaultTimeoutMs?: number;
225
+ allowNestedSubagents?: boolean;
209
226
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
210
227
  thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
211
228
  extensionBindings?: ExtensionBindings;
@@ -283,6 +300,7 @@ function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fas
283
300
 
284
301
  export interface ResolvePiLaunchToolPlanInput {
285
302
  tools?: string[];
303
+ allowNestedSubagents?: boolean;
286
304
  extensions?: string[];
287
305
  subagentOnlyExtensions?: string[];
288
306
  mcpDirectTools?: string[];
@@ -481,7 +499,10 @@ export function resolvePiLaunchToolPlan(
481
499
  ? ["read", ...requestedBuiltinTools]
482
500
  : requestedBuiltinTools
483
501
  ).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
484
- const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
502
+ const fanoutAuthorized = declaredBuiltinTools.includes("subagent") || (
503
+ input.allowNestedSubagents === true &&
504
+ (!allowedToolSet || allowedToolSet.has("subagent"))
505
+ );
485
506
  const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
486
507
  ? []
487
508
  : (input.tools ?? []).filter(
@@ -496,8 +517,13 @@ export function resolvePiLaunchToolPlan(
496
517
  throw new Error(formatUnresolvedMcpDirectToolSelectors(mcpResolution.unresolvedSelectors));
497
518
  }
498
519
  const resolvedMcpSelections = mcpResolution.selections;
520
+ const resolvedMcpNames = new Set(resolvedMcpSelections.map((selection) => selection.name));
521
+ const legacyMcpNameCounts = countLegacyUnderscoreMcpToolNames(resolvedMcpSelections);
499
522
  const effectiveMcpSelections = resolvedMcpSelections.filter(
500
- (selection) => !allowedToolSet || allowedToolSet.has(selection.name),
523
+ (selection) =>
524
+ !allowedToolSet ||
525
+ allowedToolSet.has(selection.name) ||
526
+ isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
501
527
  );
502
528
  const effectiveMcpTools = effectiveMcpSelections.map(
503
529
  (selection) => selection.name,
@@ -652,6 +678,36 @@ export function resolvePiLaunchToolPlan(
652
678
  };
653
679
  }
654
680
 
681
+ // Capability ceilings persisted before #1685 may still name hyphenated MCP server prefixes with underscores.
682
+ function countLegacyUnderscoreMcpToolNames(selections: readonly ResolvedMcpDirectToolSelection[]): Map<string, number> {
683
+ const counts = new Map<string, number>();
684
+ for (const selection of selections) {
685
+ const legacyName = legacyUnderscoreMcpToolName(selection);
686
+ if (legacyName !== selection.name) counts.set(legacyName, (counts.get(legacyName) ?? 0) + 1);
687
+ }
688
+ return counts;
689
+ }
690
+
691
+ function isLegacyUnderscoreMcpToolAllowed(
692
+ selection: ResolvedMcpDirectToolSelection,
693
+ allowedToolSet: ReadonlySet<string>,
694
+ resolvedMcpNames: ReadonlySet<string>,
695
+ legacyMcpNameCounts: ReadonlyMap<string, number>,
696
+ ): boolean {
697
+ const legacyName = legacyUnderscoreMcpToolName(selection);
698
+ return legacyMcpNameCounts.get(legacyName) === 1 && !resolvedMcpNames.has(legacyName) && allowedToolSet.has(legacyName);
699
+ }
700
+
701
+ function legacyUnderscoreMcpToolName(selection: ResolvedMcpDirectToolSelection): string {
702
+ const slash = selection.selector.indexOf("/");
703
+ if (slash < 1) return selection.name;
704
+ const toolName = selection.selector.slice(slash + 1);
705
+ const suffix = `_${toolName}`;
706
+ if (!selection.name.endsWith(suffix)) return selection.name;
707
+ const prefix = selection.name.slice(0, -suffix.length);
708
+ return `${prefix.replace(/-/g, "_")}${suffix}`;
709
+ }
710
+
655
711
  /** Escape XML-significant characters in a string for safe attribute interpolation. */
656
712
  function escapeXmlAttr(value: string): string {
657
713
  return value
@@ -684,6 +740,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
684
740
 
685
741
  const toolPlan = resolvePiLaunchToolPlan({
686
742
  tools: input.tools,
743
+ allowNestedSubagents: input.allowNestedSubagents,
687
744
  extensions: input.extensions,
688
745
  subagentOnlyExtensions: input.subagentOnlyExtensions,
689
746
  mcpDirectTools: input.mcpDirectTools,
@@ -795,6 +852,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
795
852
  if (input.waitToolEnabled !== undefined) {
796
853
  env[WAIT_TOOL_ENABLED_ENV] = input.waitToolEnabled ? "true" : "false";
797
854
  }
855
+ if (input.waitToolDefaultTimeoutMs !== undefined) {
856
+ env[WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV] = String(input.waitToolDefaultTimeoutMs);
857
+ }
798
858
  const inheritedNestedRoute = Boolean(
799
859
  process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] &&
800
860
  process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] &&
@@ -878,6 +938,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
878
938
  if (input.intercomSessionName) {
879
939
  env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
880
940
  }
941
+ if (input.sessionName?.trim()) {
942
+ env.PI_SUBAGENT_SESSION_NAME = input.sessionName.trim();
943
+ }
881
944
  if (input.orchestratorIntercomTarget) {
882
945
  env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = input.orchestratorIntercomTarget;
883
946
  }
@@ -956,6 +1019,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
956
1019
 
957
1020
  env[SUBAGENT_PARENT_SESSION_ENV] =
958
1021
  input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
1022
+ env[SUBAGENT_FORK_CACHE_KEY_ENV] = input.forkCacheKey?.trim() || undefined;
959
1023
 
960
1024
  return {
961
1025
  args,
@@ -8,6 +8,17 @@ export interface SingleOutputSnapshot {
8
8
  exists: boolean;
9
9
  mtimeMs?: number;
10
10
  size?: number;
11
+ error?: string;
12
+ }
13
+
14
+ function missingFileStatError(error: unknown): boolean {
15
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
16
+ return code === "ENOENT" || code === "ENOTDIR";
17
+ }
18
+
19
+ function snapshotFromStatError(error: unknown): SingleOutputSnapshot {
20
+ if (missingFileStatError(error)) return { exists: false };
21
+ return { exists: false, error: error instanceof Error ? error.message : String(error) };
11
22
  }
12
23
 
13
24
  /**
@@ -161,23 +172,67 @@ export function validateFileOnlyOutputMode(outputMode: OutputMode | undefined, o
161
172
  return undefined;
162
173
  }
163
174
 
175
+ export function resolveSingleOutputClaimPath(outputPath: string): string {
176
+ let existing = path.resolve(outputPath);
177
+ const missingSegments: string[] = [];
178
+ while (!fs.existsSync(existing)) {
179
+ missingSegments.unshift(path.basename(existing));
180
+ const parent = path.dirname(existing);
181
+ if (parent === existing) break;
182
+ existing = parent;
183
+ }
184
+ return path.join(fs.realpathSync(existing), ...missingSegments);
185
+ }
186
+
187
+ function outputClaimError(outputPath: string, expectedClaimPath: string | undefined): string | undefined {
188
+ if (expectedClaimPath && resolveSingleOutputClaimPath(outputPath) !== expectedClaimPath) return "Output path changed after it was claimed.";
189
+ return undefined;
190
+ }
191
+
164
192
  export function captureSingleOutputSnapshot(outputPath: string | undefined): SingleOutputSnapshot | undefined {
165
193
  if (!outputPath) return undefined;
166
194
  try {
167
195
  const stat = fs.statSync(outputPath);
168
196
  return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
169
- } catch {
170
- // The snapshot is advisory; resolveSingleOutput reports concrete read/write failures.
171
- return { exists: false };
197
+ } catch (error) {
198
+ return snapshotFromStatError(error);
199
+ }
200
+ }
201
+
202
+ function inspectSingleOutputChange(
203
+ outputPath: string,
204
+ beforeRun: SingleOutputSnapshot | undefined,
205
+ ): { changed: boolean; error?: string } {
206
+ if (beforeRun?.error) return { changed: false, error: beforeRun.error };
207
+ try {
208
+ const stat = fs.statSync(outputPath);
209
+ return {
210
+ changed: !beforeRun?.exists || stat.mtimeMs !== beforeRun.mtimeMs || stat.size !== beforeRun.size,
211
+ };
212
+ } catch (error) {
213
+ if (missingFileStatError(error)) return { changed: false };
214
+ return { changed: false, error: error instanceof Error ? error.message : String(error) };
172
215
  }
173
216
  }
174
217
 
218
+ export function hasSingleOutputChangedSinceSnapshot(
219
+ outputPath: string | undefined,
220
+ beforeRun: SingleOutputSnapshot | undefined,
221
+ ): boolean | undefined {
222
+ if (!outputPath) return undefined;
223
+ const inspected = inspectSingleOutputChange(outputPath, beforeRun);
224
+ return inspected.error ? undefined : inspected.changed;
225
+ }
226
+
175
227
  function persistSingleOutput(
176
228
  outputPath: string | undefined,
177
229
  fullOutput: string,
178
- ): { savedPath?: string; error?: string } {
230
+ expectedClaimPath?: string,
231
+ ): { savedPath?: string; error?: string; fatalError?: boolean } {
179
232
  if (!outputPath) return {};
180
233
  try {
234
+ const claimError = outputClaimError(outputPath, expectedClaimPath);
235
+ if (claimError) return { error: claimError, fatalError: true };
181
236
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
182
237
  fs.writeFileSync(outputPath, fullOutput, "utf-8");
183
238
  return { savedPath: outputPath };
@@ -190,26 +245,21 @@ export function resolveSingleOutput(
190
245
  outputPath: string | undefined,
191
246
  fallbackOutput: string,
192
247
  beforeRun: SingleOutputSnapshot | undefined,
193
- ): { fullOutput: string; savedPath?: string; saveError?: string } {
248
+ expectedClaimPath?: string,
249
+ ): { fullOutput: string; savedPath?: string; saveError?: string; fatalError?: boolean } {
194
250
  if (!outputPath) return { fullOutput: fallbackOutput };
251
+ const claimError = outputClaimError(outputPath, expectedClaimPath);
252
+ if (claimError) return { fullOutput: fallbackOutput, saveError: claimError, fatalError: true };
195
253
 
196
- let changedSinceStart = false;
197
- try {
198
- const stat = fs.statSync(outputPath);
199
- changedSinceStart = !beforeRun?.exists
200
- || stat.mtimeMs !== beforeRun.mtimeMs
201
- || stat.size !== beforeRun.size;
202
- } catch (error) {
203
- const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
204
- if (code !== "ENOENT" && code !== "ENOTDIR") {
205
- return {
206
- fullOutput: fallbackOutput,
207
- saveError: `Failed to inspect output file: ${error instanceof Error ? error.message : String(error)}`,
208
- };
209
- }
254
+ const changedSinceStart = inspectSingleOutputChange(outputPath, beforeRun);
255
+ if (changedSinceStart.error) {
256
+ return {
257
+ fullOutput: fallbackOutput,
258
+ saveError: `Failed to inspect output file: ${changedSinceStart.error}`,
259
+ };
210
260
  }
211
261
 
212
- if (changedSinceStart) {
262
+ if (changedSinceStart.changed) {
213
263
  try {
214
264
  return { fullOutput: fs.readFileSync(outputPath, "utf-8"), savedPath: outputPath };
215
265
  } catch (error) {
@@ -220,9 +270,9 @@ export function resolveSingleOutput(
220
270
  }
221
271
  }
222
272
 
223
- const save = persistSingleOutput(outputPath, fallbackOutput);
273
+ const save = persistSingleOutput(outputPath, fallbackOutput, expectedClaimPath);
224
274
  if (save.savedPath) return { fullOutput: fallbackOutput, savedPath: save.savedPath };
225
- return { fullOutput: fallbackOutput, saveError: save.error };
275
+ return { fullOutput: fallbackOutput, saveError: save.error, ...(save.fatalError ? { fatalError: true } : {}) };
226
276
  }
227
277
 
228
278
  export function finalizeSingleOutput(params: {
@@ -1,12 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import type { BeforeProviderRequestEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { registerNativeSupervisorClient } from "../../intercom/native-supervisor-channel.ts";
6
6
  import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
7
7
  import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, PERMISSION_POLICY_ENV } from "./permissions.ts";
8
8
  import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
9
- import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
9
+ import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_FORK_CACHE_KEY_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
10
10
  import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
11
11
  import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
12
12
  import {
@@ -32,6 +32,9 @@ import { drainOutstandingWork } from "../background/auto-drain.ts";
32
32
  const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
33
33
  const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
34
34
  export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
35
+ /** Human-readable child display name (agent + task excerpt) set by the parent
36
+ * at launch; applied via pi.setSessionName when no intercom target exists. */
37
+ export const SUBAGENT_SESSION_NAME_ENV = "PI_SUBAGENT_SESSION_NAME";
35
38
  const STEERING_LEGACY_SETTLE_FALLBACK_MS = 1000;
36
39
  const STEERING_SAFETY_POLL_INTERVAL_MS = 5000;
37
40
 
@@ -288,10 +291,27 @@ const PORTABLE_TOOL_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
288
291
  const MAX_PORTABLE_TOOL_ID_LENGTH = 64;
289
292
  const COMPOSITE_TOOL_ID_APIS = new Set([
290
293
  "azure-openai-responses",
294
+ "cursor-native",
295
+ "openai-completions",
296
+ "openai-responses",
297
+ ]);
298
+ const PROMPT_CACHE_KEY_APIS = new Set([
299
+ "azure-openai-responses",
300
+ "openai-codex-responses",
291
301
  "openai-completions",
292
302
  "openai-responses",
293
303
  ]);
294
304
 
305
+ export function rewriteForkCacheProviderRequest(event: BeforeProviderRequestEvent, ctx?: Pick<ExtensionContext, "model">): unknown {
306
+ const forkCacheKey = process.env[SUBAGENT_FORK_CACHE_KEY_ENV]?.trim();
307
+ if (!forkCacheKey || !PROMPT_CACHE_KEY_APIS.has(ctx?.model?.api ?? "")) return undefined;
308
+ if (!event || typeof event !== "object") return undefined;
309
+ const payload = event.payload;
310
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
311
+ if (typeof (payload as { prompt_cache_key?: unknown }).prompt_cache_key !== "string") return undefined;
312
+ return { ...payload, prompt_cache_key: forkCacheKey };
313
+ }
314
+
295
315
  function portableToolId(id: string): string {
296
316
  if (PORTABLE_TOOL_ID_PATTERN.test(id) && id.length <= MAX_PORTABLE_TOOL_ID_LENGTH) return id;
297
317
  const encoded = `tool_${Buffer.from(id).toString("base64url") || "empty"}`;
@@ -671,7 +691,7 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
671
691
  registerPermissionGate(pi);
672
692
  registerToolBudget(pi, decodeToolBudgetEnv(process.env[TOOL_BUDGET_ENV], { allowZero: process.env[TOOL_BUDGET_ZERO_AUTH_ENV] === "1" }));
673
693
  registerChildWatchdog(pi);
674
- const waitToolEnabled = resolveWaitToolConfig().enabled;
694
+ const waitToolConfig = resolveWaitToolConfig();
675
695
  const waitState = {
676
696
  baseCwd: "",
677
697
  currentSessionId: null,
@@ -686,7 +706,7 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
686
706
  watcherRestartTimer: null,
687
707
  resultFileCoalescer: { schedule: () => false, clear: () => {} },
688
708
  } as unknown as SubagentState;
689
- if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, waitToolEnabled);
709
+ if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, waitToolConfig.enabled, undefined, waitToolConfig.defaultTimeoutMs);
690
710
  let nativeSupervisorClientRegistered = false;
691
711
  const registerNativeSupervisorClientOnce = (): void => {
692
712
  if (nativeSupervisorClientRegistered) return;
@@ -744,6 +764,8 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
744
764
  });
745
765
  }
746
766
 
767
+ onRuntimeEvent("before_provider_request", (event: unknown, ctx?: ExtensionContext) => rewriteForkCacheProviderRequest(event as BeforeProviderRequestEvent, ctx));
768
+
747
769
  onRuntimeEvent("context", (event: unknown, ctx?: ExtensionContext) => {
748
770
  if (!event || typeof event !== "object" || !("messages" in event) || !Array.isArray(event.messages)) return undefined;
749
771
  const messages = stripParentOnlySubagentMessages(event.messages, {
@@ -756,9 +778,14 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
756
778
  onRuntimeEvent("before_agent_start", async (event: unknown) => {
757
779
  if (!event || typeof event !== "object" || !("systemPrompt" in event) || typeof event.systemPrompt !== "string") return undefined;
758
780
  registerNativeSupervisorClientOnce();
781
+ // The intercom target is a routing address and always wins; the display
782
+ // name (agent + task excerpt, computed by the parent at launch) only
783
+ // applies when the bridge is not addressing this child.
759
784
  const intercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
760
- if (intercomSessionName && typeof pi.setSessionName === "function") {
761
- pi.setSessionName(intercomSessionName);
785
+ const displaySessionName = process.env[SUBAGENT_SESSION_NAME_ENV]?.trim();
786
+ const childSessionName = intercomSessionName || displaySessionName;
787
+ if (childSessionName && typeof pi.setSessionName === "function") {
788
+ pi.setSessionName(childSessionName);
762
789
  }
763
790
 
764
791
  const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV);
@@ -13,6 +13,21 @@ export interface WorkflowGraphBuildInput {
13
13
  dynamicGroupStatuses?: Record<number, { status: WorkflowNodeStatus; error?: string; acceptance?: SingleResult["acceptance"] }>;
14
14
  }
15
15
 
16
+ /** Return displayable workflow stages while hiding structural parallel groups and host monitors. */
17
+ export function workflowGraphStageNodes(graph: WorkflowGraphSnapshot | undefined): WorkflowGraphNode[] {
18
+ if (!graph?.nodes?.length) return [];
19
+ const stages: WorkflowGraphNode[] = [];
20
+ const visit = (node: WorkflowGraphNode): void => {
21
+ if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
22
+ for (const child of node.children ?? []) visit(child);
23
+ return;
24
+ }
25
+ if (node.kind !== "host-step") stages.push(node);
26
+ };
27
+ for (const node of graph.nodes) visit(node);
28
+ return stages;
29
+ }
30
+
16
31
  function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undefined {
17
32
  switch (status) {
18
33
  case "complete":