pi-subagents 0.49.0 → 0.51.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 (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. package/src/runs/foreground/chain-execution.ts +0 -1565
@@ -99,7 +99,9 @@ function missionStateAction(location: MissionStoreLocation, record: MissionRecor
99
99
  function retainedResumeTarget(record: MissionRecord, retainedChildren: RetainedChild[]): RetainedChild | undefined {
100
100
  const latestRun = record.runs.at(-1);
101
101
  if (!latestRun) return undefined;
102
- return retainedChildren.find((child) => child.runId === latestRun.runId || child.parentRunId === latestRun.runId);
102
+ return retainedChildren.find((child) =>
103
+ child.resumability.state === "resumable" && (child.runId === latestRun.runId || child.parentRunId === latestRun.runId)
104
+ );
103
105
  }
104
106
 
105
107
  function nextReadyAction(location: MissionStoreLocation, record: MissionRecord, retainedChildren: RetainedChild[]): string {
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { createHash } from "node:crypto";
3
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
5
  import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
5
6
  import { PROMPT_REDACTED } from "../shared/utils.ts";
@@ -294,13 +295,17 @@ export function syncMissionFromAsyncCompletion(value: unknown): MissionRecord |
294
295
  current = readMission(binding.location, binding.missionId);
295
296
  } catch (error) {
296
297
  if (!(error instanceof MissionNotFoundError)) throw error;
298
+ const reason = "mission-record-missing";
299
+ const markerId = createHash("sha256").update(JSON.stringify([runId, binding.missionId, reason])).digest("hex");
300
+ const markerPath = path.join(event.asyncDir, `.mission-sync-skipped-${markerId}.json`);
297
301
  try {
302
+ fs.writeFileSync(markerPath, `${JSON.stringify({ runId, missionId: binding.missionId, reason })}\n`, { encoding: "utf-8", mode: 0o600, flag: "wx" });
298
303
  fs.appendFileSync(path.join(event.asyncDir, "events.jsonl"), `${JSON.stringify({
299
304
  type: "subagent.mission.sync.skipped",
300
305
  ts: Date.now(),
301
306
  runId,
302
307
  missionId: binding.missionId,
303
- reason: "mission-record-missing",
308
+ reason,
304
309
  missionPath: missionRecordPath(binding.location, binding.missionId),
305
310
  })}\n`, "utf-8");
306
311
  } catch {
@@ -77,11 +77,6 @@ function nonNegativeTokenCount(value: unknown, label: string): number {
77
77
  return value as number;
78
78
  }
79
79
 
80
- function parseStoredGoal(value: unknown, label: string): { goal?: MissionGoal; legacyObjective?: string } {
81
- if (typeof value === "string") return { legacyObjective: requiredString(value, label).trim() };
82
- return { goal: parseGoal(value, label) };
83
- }
84
-
85
80
  function parseGoal(value: unknown, label: string): MissionGoal {
86
81
  const input = asObject(value, label);
87
82
  if (input.status !== "active" && input.status !== "paused" && input.status !== "budget-exhausted") throw new Error(`${label}.status is invalid`);
@@ -223,18 +218,18 @@ export function parseMissionRecord(value: unknown, source = "mission record"): M
223
218
  const decisions = input.decisions as unknown[];
224
219
  const artifacts = input.artifacts as unknown[];
225
220
  const receipts = (input.receipts ?? []) as unknown[];
226
- const parsedGoal = input.goal !== undefined ? parseStoredGoal(input.goal, `${source}.goal`) : {};
221
+ const goal = input.goal !== undefined ? parseGoal(input.goal, `${source}.goal`) : undefined;
227
222
  const budget = input.budget !== undefined ? parseBudget(input.budget, `${source}.budget`) : undefined;
228
223
  const usage = input.usage !== undefined ? parseUsage(input.usage, `${source}.usage`) : undefined;
229
- const objective = optionalString(input.objective, `${source}.objective`)?.trim() ?? parsedGoal.legacyObjective;
224
+ const objective = optionalString(input.objective, `${source}.objective`)?.trim();
230
225
  if (!objective) throw new Error(`${source}.objective must be a non-empty string`);
231
- if (parsedGoal.goal && !budget) throw new Error(`${source}.budget is required for a goal mission`);
226
+ if (goal && !budget) throw new Error(`${source}.budget is required for a goal mission`);
232
227
  return {
233
228
  schemaVersion: 1,
234
229
  id: validateMissionId(input.id, `${source}.id`),
235
230
  title: requiredString(input.title, `${source}.title`),
236
231
  objective,
237
- ...(parsedGoal.goal ? { goal: parsedGoal.goal } : {}),
232
+ ...(goal ? { goal } : {}),
238
233
  ...(budget ? { budget } : {}),
239
234
  ...(usage ? { usage } : {}),
240
235
  status: missionStatus(input.status, `${source}.status`),
@@ -3,6 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { BUILTIN_AGENT_NAMES } from "../agents/agents.ts";
6
+ import { getPiSpawnCommand } from "../runs/shared/pi-spawn.ts";
6
7
  import { findModelInfo, getSupportedThinkingLevels, splitKnownThinkingSuffix, toModelInfo } from "../shared/model-info.ts";
7
8
  import { getAgentDir } from "../shared/utils.ts";
8
9
 
@@ -340,7 +341,8 @@ async function probeModel(
340
341
  if (typeof pi.exec !== "function") {
341
342
  return { status: "skipped", message: "pi.exec is unavailable in this runtime." };
342
343
  }
343
- const result = await pi.exec("pi", ["-p", "--model", fullId, "--no-tools", 'Reply with exactly "OK".'], {
344
+ const spawnSpec = getPiSpawnCommand(["-p", "--model", fullId, "--no-tools", 'Reply with exactly "OK".']);
345
+ const result = await pi.exec(spawnSpec.command, spawnSpec.args, {
344
346
  cwd: os.tmpdir(),
345
347
  timeout: 45_000,
346
348
  } as Record<string, unknown>);
@@ -1,37 +1,118 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AsyncStatus } from "../../shared/types.ts";
4
+ import { readStatus } from "../../shared/utils.ts";
5
+ import { encodeIndexSegment } from "./index-segment.ts";
6
+ import { updateTerminalRunIndex } from "./terminal-run-index.ts";
7
+ import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
4
8
 
5
9
  export const ACTIVE_RUN_INDEX_DIR = ".active-runs";
10
+ export const DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS = 24 * 60 * 60 * 1000;
11
+
12
+ const TOOL_CALL_INDEX_DIR = "tool-calls";
6
13
 
7
14
  function indexDir(asyncDirRoot: string): string {
8
15
  return path.join(asyncDirRoot, ACTIVE_RUN_INDEX_DIR);
9
16
  }
10
17
 
18
+ function toolCallIndexDir(asyncDirRoot: string, toolCallId: string): string {
19
+ return path.join(indexDir(asyncDirRoot), TOOL_CALL_INDEX_DIR, encodeIndexSegment(toolCallId));
20
+ }
21
+
22
+ function toolCallIndexPath(asyncDir: string, toolCallId: string): string {
23
+ return path.join(toolCallIndexDir(path.dirname(asyncDir), toolCallId), path.basename(asyncDir));
24
+ }
25
+
11
26
  function markerPath(asyncDir: string): string {
12
27
  return path.join(indexDir(path.dirname(asyncDir)), path.basename(asyncDir));
13
28
  }
14
29
 
30
+ function removeEmptyAncestors(start: string, stop: string): void {
31
+ let current = start;
32
+ while (current !== stop && current.startsWith(`${stop}${path.sep}`)) {
33
+ try {
34
+ fs.rmdirSync(current);
35
+ } catch {
36
+ return;
37
+ }
38
+ current = path.dirname(current);
39
+ }
40
+ }
41
+
15
42
  export function isActiveAsyncState(state: AsyncStatus["state"]): boolean {
16
43
  return state === "queued" || state === "running";
17
44
  }
18
45
 
46
+ function releaseToolCallAliases(asyncDir: string): void {
47
+ const root = path.join(indexDir(path.dirname(asyncDir)), TOOL_CALL_INDEX_DIR);
48
+ let entries: fs.Dirent[];
49
+ try {
50
+ entries = fs.readdirSync(root, { withFileTypes: true });
51
+ } catch (error) {
52
+ const code = (error as NodeJS.ErrnoException).code;
53
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
54
+ console.error(`Failed to inspect async active-run tool-call index root '${root}':`, error);
55
+ }
56
+ return;
57
+ }
58
+ for (const entry of entries) {
59
+ if (!entry.isDirectory()) continue;
60
+ const aliasMarker = path.join(root, entry.name, path.basename(asyncDir));
61
+ try {
62
+ fs.rmSync(aliasMarker, { force: true });
63
+ removeEmptyAncestors(path.dirname(aliasMarker), root);
64
+ } catch {
65
+ // Alias cleanup must not affect the authoritative active-run marker.
66
+ }
67
+ }
68
+ }
69
+
19
70
  export function releaseActiveRunIndex(asyncDir: string): void {
20
71
  try {
21
72
  fs.rmSync(markerPath(asyncDir));
22
73
  } catch (error) {
23
74
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
24
75
  }
76
+ releaseToolCallAliases(asyncDir);
25
77
  }
26
78
 
27
- export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"]): void {
79
+ export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string, options: { retryCapacityErrors?: boolean } = {}): void {
28
80
  const marker = markerPath(asyncDir);
29
81
  if (isActiveAsyncState(state)) {
30
82
  fs.mkdirSync(path.dirname(marker), { recursive: true });
31
83
  fs.writeFileSync(marker, "", { flag: "a" });
84
+ if (toolCallId) {
85
+ try {
86
+ const toolCallMarker = toolCallIndexPath(asyncDir, toolCallId);
87
+ fs.mkdirSync(path.dirname(toolCallMarker), { recursive: true });
88
+ fs.writeFileSync(toolCallMarker, "", { flag: "a" });
89
+ } catch (error) {
90
+ // Tool-call aliases are optional. The authoritative active-run marker
91
+ // must keep the launch discoverable even when alias indexing fails.
92
+ console.error(`Failed to write async active-run tool-call index for '${asyncDir}':`, error);
93
+ }
94
+ }
32
95
  return;
33
96
  }
34
97
  releaseActiveRunIndex(asyncDir);
98
+ const status = readStatus(asyncDir);
99
+ if (status && status.state === state) {
100
+ try {
101
+ updateTerminalRunIndex(asyncDir, status);
102
+ } catch (error) {
103
+ if (options.retryCapacityErrors && isStorageCapacityError(error)) throw error;
104
+ console.error(`Failed to write async terminal-run index for '${asyncDir}':`, error);
105
+ }
106
+ }
107
+ }
108
+
109
+ export function activeRunMarkerAgeMs(asyncDir: string, now = Date.now()): number | undefined {
110
+ try {
111
+ return Math.max(0, now - fs.statSync(markerPath(asyncDir)).mtimeMs);
112
+ } catch (error) {
113
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
114
+ throw error;
115
+ }
35
116
  }
36
117
 
37
118
  export function readActiveRunIndex(asyncDirRoot: string): string[] | undefined {
@@ -44,3 +125,15 @@ export function readActiveRunIndex(asyncDirRoot: string): string[] | undefined {
44
125
  throw error;
45
126
  }
46
127
  }
128
+
129
+ export function readActiveRunToolCallIndex(asyncDirRoot: string, toolCallId: string): string[] {
130
+ try {
131
+ return fs.readdirSync(toolCallIndexDir(asyncDirRoot, toolCallId), { withFileTypes: true })
132
+ .filter((entry) => entry.isFile())
133
+ .map((entry) => entry.name);
134
+ } catch (error) {
135
+ const code = (error as NodeJS.ErrnoException).code;
136
+ if (code === "ENOENT" || code === "ENOTDIR") return [];
137
+ throw error;
138
+ }
139
+ }
@@ -13,9 +13,10 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import type { AgentConfig } from "../../agents/agents.ts";
14
14
  import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
15
15
  import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
16
+ import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
17
  import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
17
18
  import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
18
- import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isParallelStep, resolveChainPath, resolveExistingReadPaths, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
19
+ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveChainPath, resolveExistingReadPaths, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
19
20
  import type { RunnerStep } from "../shared/parallel-utils.ts";
20
21
  import type { ContextMode } from "../shared/context-mode.ts";
21
22
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
@@ -24,6 +25,7 @@ import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } f
24
25
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
25
26
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
26
27
  import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
28
+ import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
27
29
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
28
30
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
29
31
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
@@ -58,6 +60,7 @@ import {
58
60
  resolveChildMaxSubagentDepth,
59
61
  } from "../../shared/types.ts";
60
62
  import { nestedResultsPath, nestedSummaryFromAsyncStatus, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
63
+ import { resultFilePath } from "./result-files.ts";
61
64
  import { appendTurnBudgetSystemPrompt, initialTurnBudgetState } from "../shared/turn-budget.ts";
62
65
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
63
66
  import { usageBudgetState } from "../shared/usage-budget.ts";
@@ -123,6 +126,7 @@ interface AsyncExecutionContext {
123
126
  pi: ExtensionAPI;
124
127
  cwd: string;
125
128
  currentSessionId: string;
129
+ completionOwnerId?: string;
126
130
  /** Parent session id used by permission-system ask forwarding. */
127
131
  parentSessionId?: string;
128
132
  permissions?: PermissionConfig;
@@ -142,7 +146,7 @@ interface AsyncChainParams {
142
146
  /** Raw caller-facing goal used only by the started event. */
143
147
  goal?: string;
144
148
  attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
145
- resultMode?: Exclude<SubagentRunMode, "single">;
149
+ resultMode?: SubagentRunMode;
146
150
  agents: AgentConfig[];
147
151
  ctx: AsyncExecutionContext;
148
152
  availableModels?: AvailableModelInfo[];
@@ -174,6 +178,12 @@ interface AsyncChainParams {
174
178
  toolBudget?: ResolvedToolBudget;
175
179
  usageBudget?: UsageBudgetConfig;
176
180
  configToolBudget?: ResolvedToolBudget;
181
+ /** Optional per-call hard toolTimeoutMs override (highest precedence). */
182
+ callToolTimeoutMs?: number;
183
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
184
+ configToolTimeoutMs?: number;
185
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
186
+ toolTimeoutMsEnv?: string | undefined;
177
187
  /** Global cap on simultaneously-running subagent tasks within the async run. */
178
188
  globalConcurrencyLimit?: number;
179
189
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
@@ -217,6 +227,7 @@ interface AsyncSingleParams {
217
227
  worktreeSetupHook?: string;
218
228
  worktreeSetupHookTimeoutMs?: number;
219
229
  worktreeBaseDir?: string;
230
+ worktree?: boolean;
220
231
  controlConfig?: ResolvedControlConfig;
221
232
  intercomBridge?: IntercomBridgeConfig;
222
233
  controlIntercomTarget?: string;
@@ -225,10 +236,16 @@ interface AsyncSingleParams {
225
236
  acceptance?: AcceptanceInput;
226
237
  timeoutMs?: number;
227
238
  absoluteDeadlineAt?: number;
239
+ /** Optional per-call hard toolTimeoutMs override (highest precedence). */
240
+ toolTimeoutMs?: number;
228
241
  turnBudget?: { maxTurns: number; graceTurns?: number };
229
242
  toolBudget?: ResolvedToolBudget | ToolBudgetConfig;
230
243
  usageBudget?: UsageBudgetConfig;
231
244
  configToolBudget?: ResolvedToolBudget;
245
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
246
+ configToolTimeoutMs?: number;
247
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
248
+ toolTimeoutMsEnv?: string | undefined;
232
249
  allowZeroToolBudget?: boolean;
233
250
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
234
251
  runFanoutBudget?: RunFanoutBudgetDescriptor;
@@ -267,6 +284,12 @@ export interface AsyncRunnerStepBuildParams {
267
284
  validateOutputBindings?: boolean;
268
285
  toolBudget?: ResolvedToolBudget;
269
286
  configToolBudget?: ResolvedToolBudget;
287
+ /** Optional per-call hard toolTimeoutMs override from the subagent invocation. */
288
+ callToolTimeoutMs?: number;
289
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
290
+ configToolTimeoutMs?: number;
291
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
292
+ toolTimeoutMsEnv?: string | undefined;
270
293
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
271
294
  }
272
295
 
@@ -408,7 +431,7 @@ function terminateRunnerBeforeProceed(pid: number): boolean {
408
431
  return !runnerIsAlive(pid);
409
432
  }
410
433
 
411
- function persistPreProceedStartupFailure(asyncDir: string, runId: string, runnerProcessInstanceId: string, sessionId: string | undefined, message: string): void {
434
+ function persistPreProceedStartupFailure(asyncDir: string, runId: string, runnerProcessInstanceId: string, sessionId: string | undefined, completionOwnerId: string | undefined, message: string): void {
412
435
  const now = Date.now();
413
436
  try {
414
437
  const statusPath = path.join(asyncDir, "status.json");
@@ -420,6 +443,7 @@ function persistPreProceedStartupFailure(asyncDir: string, runId: string, runner
420
443
  ...status,
421
444
  runId,
422
445
  ...(sessionId ? { sessionId } : {}),
446
+ ...(completionOwnerId ? { completionOwnerId } : {}),
423
447
  state: "failed",
424
448
  lastUpdate: now,
425
449
  error: message,
@@ -471,10 +495,11 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
471
495
  fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
472
496
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
473
497
  const nodeCommand = resolveNodeExecutable();
474
- const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; revivalLease?: unknown };
498
+ const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
475
499
  const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
476
500
  const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
477
501
  const launchSessionId = typeof launchForStartup.sessionId === "string" ? launchForStartup.sessionId : undefined;
502
+ const launchCompletionOwnerId = typeof launchForStartup.completionOwnerId === "string" ? launchForStartup.completionOwnerId : undefined;
478
503
  const startupPath = typeof launchForStartup.revivalLease === "object" && launchAsyncDir
479
504
  ? path.join(launchAsyncDir, "runner-startup.json")
480
505
  : undefined;
@@ -564,7 +589,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
564
589
  proc.unref();
565
590
  if (startupPath && startupAckPath && startupProceedPath) {
566
591
  const persistStartupFailure = (message: string) => {
567
- if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, message);
592
+ if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
568
593
  };
569
594
  const ready = waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS);
570
595
  if (ready.ok === false) {
@@ -649,13 +674,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
649
674
  : chain;
650
675
  const firstStep = chain[0];
651
676
  const originalTask = params.task ?? (firstStep
652
- ? (isCheckpointStep(firstStep)
653
- ? undefined
654
- : isParallelStep(firstStep)
655
- ? firstStep.parallel[0]?.task
656
- : isDynamicParallelStep(firstStep)
657
- ? firstStep.parallel.task
658
- : (firstStep as SequentialStep).task)
677
+ ? (isParallelStep(firstStep)
678
+ ? firstStep.parallel[0]?.task
679
+ : isDynamicParallelStep(firstStep)
680
+ ? firstStep.parallel.task
681
+ : (firstStep as SequentialStep).task)
659
682
  : undefined);
660
683
  try {
661
684
  if (params.validateOutputBindings !== false) {
@@ -668,13 +691,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
668
691
  const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
669
692
 
670
693
  for (const s of chain) {
671
- const stepAgents = isCheckpointStep(s)
672
- ? []
673
- : isParallelStep(s)
674
- ? s.parallel.map((t) => t.agent)
675
- : isDynamicParallelStep(s)
676
- ? [s.parallel.agent]
677
- : [(s as SequentialStep).agent];
694
+ const stepAgents = isParallelStep(s)
695
+ ? s.parallel.map((t) => t.agent)
696
+ : isDynamicParallelStep(s)
697
+ ? [s.parallel.agent]
698
+ : [(s as SequentialStep).agent];
678
699
  for (const agentName of stepAgents) {
679
700
  if (!agents.find((x) => x.name === agentName)) {
680
701
  return { error: `Unknown agent: ${agentName}` };
@@ -696,7 +717,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
696
717
  };
697
718
  const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }, runFanoutPath?: string) => {
698
719
  const a = agents.find((x) => x.name === s.agent)!;
699
- const externalRunner = a.runner?.type === "external-cli";
720
+ const externalRunner = a.runner?.type === "external-cli" || a.runner?.type === "external-job";
721
+ const externalRunnerType = a.runner?.type;
700
722
  if (externalRunner) {
701
723
  const unsupported: string[] = [];
702
724
  if (s.model !== undefined) unsupported.push("model override");
@@ -704,7 +726,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
704
726
  if (s.acceptance !== undefined || params.agentContract !== undefined || s.agentContract !== undefined) unsupported.push("acceptance/agent contract");
705
727
  if (s.toolBudget !== undefined || params.toolBudget !== undefined || a.toolBudget !== undefined || params.configToolBudget !== undefined) unsupported.push("tool budget");
706
728
  if (params.contextForAgent?.(s.agent) === "fork") unsupported.push("fork context");
707
- if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='external-cli' and does not support: ${unsupported.join(", ")}.`);
729
+ if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
708
730
  }
709
731
  try {
710
732
  assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling, decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])));
@@ -714,6 +736,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
714
736
  const toolBudgetInput = s.toolBudget ?? params.toolBudget ?? a.toolBudget ?? params.configToolBudget;
715
737
  const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, s.toolBudget ? "toolBudget" : a.toolBudget ? "agent.toolBudget" : "config.toolBudget");
716
738
  if (resolvedToolBudget.error) throw new AsyncStartValidationError(resolvedToolBudget.error);
739
+ const resolvedToolTimeout = resolveToolTimeoutMs({
740
+ callValue: params.callToolTimeoutMs,
741
+ agentValue: a.defaultToolTimeoutMs,
742
+ configValue: params.configToolTimeoutMs,
743
+ envValue: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
744
+ });
745
+ if (resolvedToolTimeout.error) throw new AsyncStartValidationError(resolvedToolTimeout.error);
717
746
  const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
718
747
  const instructionCwd = behaviorCwd ?? stepCwd;
719
748
  const readExistenceCwd = behaviorCwd ? stepCwd : instructionCwd;
@@ -788,7 +817,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
788
817
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
789
818
  const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
790
819
  if (externalRunner && permissionRules) {
791
- throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='external-cli', which cannot enforce native Pi child permission rules.`);
820
+ throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
792
821
  }
793
822
  return {
794
823
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
@@ -827,6 +856,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
827
856
  sessionFile,
828
857
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
829
858
  timeoutMs: a.defaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS,
859
+ toolTimeoutMs: resolvedToolTimeout.toolTimeoutMs,
830
860
  waitToolEnabled: params.waitToolEnabled,
831
861
  effectiveAcceptance: resolveEffectiveAcceptance({
832
862
  explicit: s.acceptance,
@@ -844,6 +874,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
844
874
  ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
845
875
  ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
846
876
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
877
+ ...(s.worktree ? { worktree: true } : {}),
847
878
  };
848
879
  };
849
880
 
@@ -862,9 +893,6 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
862
893
 
863
894
  try {
864
895
  const builtSteps = chain.map((s, stepIndex) => {
865
- if (isCheckpointStep(s)) {
866
- return { checkpoint: s.checkpoint, ...(s.message ? { message: s.message } : {}), phase: s.phase, label: s.label };
867
- }
868
896
  if (isParallelStep(s)) {
869
897
  const parallelBehaviors = s.parallel.map((task) => {
870
898
  const agent = agents.find((candidate) => candidate.name === task.agent)!;
@@ -930,8 +958,17 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
930
958
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
931
959
  };
932
960
  }
961
+ const sequential = s as SequentialStep;
962
+ let behaviorCwd: string | undefined;
963
+ if (sequential.worktree) {
964
+ try {
965
+ behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, 0, worktreeBaseDir);
966
+ } catch {
967
+ behaviorCwd = undefined;
968
+ }
969
+ }
933
970
  const staticStep = nextFlatStep();
934
- return buildSeqStep(s as SequentialStep, staticStep.sessionFile, undefined, false, undefined, staticStep.index, undefined, `chain[${stepIndex}]`);
971
+ return buildSeqStep(sequential, staticStep.sessionFile, behaviorCwd, false, undefined, staticStep.index, undefined, `chain[${stepIndex}]`);
935
972
  });
936
973
  const steps = params.attachRoot
937
974
  ? [{
@@ -1000,7 +1037,6 @@ export function executeAsyncChain(
1000
1037
  const resultMode = params.resultMode ?? "chain";
1001
1038
  const acceptanceErrors = validateExecutionAcceptance({
1002
1039
  chain: chain.map((step) => {
1003
- if (isCheckpointStep(step)) return {};
1004
1040
  if (isParallelStep(step)) return { parallel: step.parallel };
1005
1041
  if (isDynamicParallelStep(step)) return { acceptance: step.acceptance, parallel: step.parallel };
1006
1042
  return { acceptance: step.acceptance };
@@ -1050,6 +1086,9 @@ export function executeAsyncChain(
1050
1086
  asyncDir,
1051
1087
  toolBudget: params.toolBudget,
1052
1088
  configToolBudget: params.configToolBudget,
1089
+ callToolTimeoutMs: params.callToolTimeoutMs,
1090
+ configToolTimeoutMs: params.configToolTimeoutMs,
1091
+ toolTimeoutMsEnv: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
1053
1092
  capabilityCeiling,
1054
1093
  });
1055
1094
  if ("error" in built) {
@@ -1086,7 +1125,7 @@ export function executeAsyncChain(
1086
1125
  {
1087
1126
  id,
1088
1127
  steps,
1089
- resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1128
+ resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
1090
1129
  cwd: runnerCwd,
1091
1130
  placeholder: "{previous}",
1092
1131
  maxOutput,
@@ -1096,6 +1135,7 @@ export function executeAsyncChain(
1096
1135
  sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
1097
1136
  asyncDir,
1098
1137
  sessionId: ctx.currentSessionId,
1138
+ completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
1099
1139
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1100
1140
  piPackageRoot,
1101
1141
  piArgv1: process.argv[1],
@@ -1222,6 +1262,7 @@ export function executeAsyncChain(
1222
1262
  id,
1223
1263
  pid: spawnResult.pid,
1224
1264
  sessionId: ctx.currentSessionId,
1265
+ completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
1225
1266
  mode: resultMode,
1226
1267
  agent: firstAgents[0],
1227
1268
  agents: flatAgents,
@@ -1292,7 +1333,8 @@ export function executeAsyncSingle(
1292
1333
  const task = params.task ?? "";
1293
1334
  const acceptanceErrors = validateAcceptanceInput(params.acceptance);
1294
1335
  if (acceptanceErrors.length > 0) return formatAsyncStartError("single", acceptanceErrors.join(" "));
1295
- const externalRunner = agentConfig.runner?.type === "external-cli";
1336
+ const externalRunner = agentConfig.runner?.type === "external-cli" || agentConfig.runner?.type === "external-job";
1337
+ const externalRunnerType = agentConfig.runner?.type;
1296
1338
  const permissionRules = resolvePermissionRules(ctx.permissions, agentConfig.permissions);
1297
1339
  if (externalRunner) {
1298
1340
  const unsupported: string[] = [];
@@ -1304,7 +1346,7 @@ export function executeAsyncSingle(
1304
1346
  if (params.context === "fork") unsupported.push("fork context");
1305
1347
  if ((params.skills?.length ?? 0) > 0) unsupported.push("skills");
1306
1348
  if (permissionRules) unsupported.push("native Pi child permissions");
1307
- if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='external-cli' and does not support: ${unsupported.join(", ")}.`);
1349
+ if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
1308
1350
  }
1309
1351
  const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
1310
1352
  try {
@@ -1313,6 +1355,10 @@ export function executeAsyncSingle(
1313
1355
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1314
1356
  }
1315
1357
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
1358
+ const instructionCwd = params.worktree === true
1359
+ ? resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s0`, 0, worktreeBaseDir)
1360
+ : runnerCwd;
1361
+ const readExistenceCwd = params.worktree === true ? runnerCwd : instructionCwd;
1316
1362
  const skillNames = params.skills ?? agentConfig.skills ?? [];
1317
1363
  const availableModels = params.availableModels;
1318
1364
  const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(
@@ -1354,7 +1400,7 @@ export function executeAsyncSingle(
1354
1400
  }
1355
1401
 
1356
1402
  const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
1357
- const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
1403
+ const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
1358
1404
  systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
1359
1405
  const outputMode = params.outputMode ?? "inline";
1360
1406
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
@@ -1363,7 +1409,7 @@ export function executeAsyncSingle(
1363
1409
  // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
1364
1410
  // absolute paths pass through; relative paths resolve against the child cwd.
1365
1411
  const reads = params.reads !== undefined ? params.reads : agentConfig.defaultReads ?? false;
1366
- const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, runnerCwd) : [];
1412
+ const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, readExistenceCwd) : [];
1367
1413
  const readsInstruction = readPaths.length > 0
1368
1414
  ? `[Read from: ${readPaths.join(", ")}]\n\n`
1369
1415
  : "";
@@ -1384,17 +1430,27 @@ export function executeAsyncSingle(
1384
1430
  ? deadlineAt - Date.now()
1385
1431
  : params.timeoutMs;
1386
1432
  if (timeoutMs !== undefined && timeoutMs <= 0) return formatAsyncStartError("single", "The source run's absolute deadline expired before recovery could launch.");
1433
+ const resolvedToolTimeout = resolveToolTimeoutMs({
1434
+ callValue: params.toolTimeoutMs,
1435
+ agentValue: agentConfig.defaultToolTimeoutMs,
1436
+ configValue: params.configToolTimeoutMs,
1437
+ envValue: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
1438
+ });
1439
+ if (resolvedToolTimeout.error) return formatAsyncStartError("single", resolvedToolTimeout.error);
1440
+ const toolTimeoutMs = resolvedToolTimeout.toolTimeoutMs;
1387
1441
  const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
1388
1442
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1389
1443
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1390
1444
  const structuredOutput = params.structuredOutputSchema
1391
1445
  ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
1392
1446
  : undefined;
1393
- const modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope })
1394
- .flatMap((candidate) => {
1395
- const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1396
- return resolved ? [resolved] : [];
1397
- });
1447
+ const modelCandidates = externalRunner
1448
+ ? []
1449
+ : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope })
1450
+ .flatMap((candidate) => {
1451
+ const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1452
+ return resolved ? [resolved] : [];
1453
+ });
1398
1454
  const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1399
1455
  const toolPlan = resolvePiLaunchToolPlan({
1400
1456
  tools: agentConfig.tools,
@@ -1529,11 +1585,12 @@ export function executeAsyncSingle(
1529
1585
  ...(structuredOutput ? { structuredOutput } : {}),
1530
1586
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1531
1587
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1588
+ ...(params.worktree === true ? { worktree: true } : {}),
1532
1589
  },
1533
1590
  ],
1534
1591
  resultPath: params.parentWorkflowRunId !== undefined && params.revivalLease !== undefined
1535
1592
  ? workflowAwaitedAsyncResultPath(asyncDir)
1536
- : inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1593
+ : inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
1537
1594
  cwd: runnerCwd,
1538
1595
  placeholder: "{previous}",
1539
1596
  maxOutput,
@@ -1543,6 +1600,7 @@ export function executeAsyncSingle(
1543
1600
  sessionDir: resolvedSessionDir,
1544
1601
  asyncDir,
1545
1602
  sessionId: ctx.currentSessionId,
1603
+ completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
1546
1604
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1547
1605
  piPackageRoot,
1548
1606
  piArgv1: process.argv[1],
@@ -1552,6 +1610,7 @@ export function executeAsyncSingle(
1552
1610
  controlConfig,
1553
1611
  timeoutMs,
1554
1612
  deadlineAt,
1613
+ toolTimeoutMs,
1555
1614
  turnBudget: params.turnBudget,
1556
1615
  toolBudget: params.toolBudget,
1557
1616
  usageBudget: params.usageBudget,
@@ -1635,6 +1694,7 @@ export function executeAsyncSingle(
1635
1694
  id,
1636
1695
  pid: spawnResult.pid,
1637
1696
  sessionId: ctx.currentSessionId,
1697
+ completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
1638
1698
  mode: "single",
1639
1699
  agent,
1640
1700
  task: task?.trim() ? PROMPT_REDACTED : undefined,