pi-subagents 0.64.0 → 0.65.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 (89) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +2 -2
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +43 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/process-terminal.ts +1 -1
  34. package/src/runs/background/run-child-session.ts +613 -0
  35. package/src/runs/background/run-status.ts +0 -1
  36. package/src/runs/background/runner-aliases.ts +125 -0
  37. package/src/runs/background/runner-child-sessions.ts +31 -0
  38. package/src/runs/background/scheduled-runs.ts +18 -4
  39. package/src/runs/background/subagent-runner.ts +184 -901
  40. package/src/runs/foreground/async-steering-action.ts +1 -17
  41. package/src/runs/foreground/execution.ts +189 -377
  42. package/src/runs/foreground/foreground-control.ts +4 -0
  43. package/src/runs/foreground/subagent-executor.ts +100 -57
  44. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  45. package/src/runs/shared/abort-recovery.ts +3 -3
  46. package/src/runs/shared/capability-ceiling.ts +1 -2
  47. package/src/runs/shared/child-hooks.ts +25 -0
  48. package/src/runs/shared/child-identity.ts +13 -2
  49. package/src/runs/shared/child-launch.ts +314 -0
  50. package/src/runs/shared/child-lifecycle.ts +25 -0
  51. package/src/runs/shared/child-runtime-config.ts +126 -0
  52. package/src/runs/shared/child-session.ts +342 -0
  53. package/src/runs/shared/child-tool-plan.ts +530 -0
  54. package/src/runs/shared/claude-code-adapter.ts +5 -1
  55. package/src/runs/shared/completion-guard.ts +1 -1
  56. package/src/runs/shared/external-cli-preflight.ts +16 -0
  57. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  58. package/src/runs/shared/model-exclusions.ts +82 -14
  59. package/src/runs/shared/model-fallback.ts +47 -4
  60. package/src/runs/shared/nested-events.ts +29 -45
  61. package/src/runs/shared/nested-path.ts +0 -14
  62. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  63. package/src/runs/shared/parallel-utils.ts +0 -2
  64. package/src/runs/shared/permissions.ts +0 -13
  65. package/src/runs/shared/process-signal.ts +4 -1
  66. package/src/runs/shared/run-fanout-budget.ts +0 -13
  67. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  68. package/src/runs/shared/structured-output.ts +17 -4
  69. package/src/runs/shared/subagent-control.ts +2 -0
  70. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  71. package/src/runs/shared/tool-availability.ts +18 -62
  72. package/src/runs/shared/tool-budget.ts +0 -14
  73. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  74. package/src/runs/shared/worktree.ts +117 -30
  75. package/src/shared/child-session-name.ts +1 -1
  76. package/src/shared/jsonl-writer.ts +11 -0
  77. package/src/shared/thinking-ceiling.ts +0 -6
  78. package/src/shared/types.ts +55 -28
  79. package/src/shared/utils.ts +3 -4
  80. package/src/slash/slash-commands.ts +0 -6
  81. package/src/tui/fleet.ts +0 -1
  82. package/src/tui/render.ts +233 -31
  83. package/src/watchdog/child-status.ts +0 -1
  84. package/src/watchdog/register-child.ts +12 -12
  85. package/src/workflows/scripted-workflow.ts +48 -1
  86. package/src/runs/shared/child-protocol.ts +0 -415
  87. package/src/runs/shared/pi-args.ts +0 -1059
  88. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  89. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -14,13 +14,14 @@ import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts"
14
14
  import { createAtomicJsonWriter, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
15
15
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
16
  import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
17
- import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
17
+ import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
18
18
  import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
19
19
  import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
20
20
  import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
21
21
  import type { RunnerStep } from "../shared/parallel-utils.ts";
22
22
  import type { ContextMode } from "../shared/context-mode.ts";
23
- import { resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
23
+ import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
24
+ import { JITI_ALIAS_ENV, resolveHostPeerAliases } from "./runner-aliases.ts";
24
25
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
25
26
  import { resolveNodeExecutable } from "../../shared/node-executable.ts";
26
27
  import { backgroundProcessOptions } from "../shared/background-process-options.ts";
@@ -31,7 +32,7 @@ import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin
31
32
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
32
33
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
33
34
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
34
- import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
35
+ import { assertThinkingWithinCeiling, intersectThinkingCeilings, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
35
36
  import { resolveExpectedWorktreeAgentCwd, resolveWorktreeProvider, shouldDeferWorktreeCwd, WORKTREE_AGENT_CWD_PLACEHOLDER } from "../shared/worktree.ts";
36
37
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
37
38
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
@@ -65,7 +66,10 @@ import {
65
66
  getAsyncConfigPath,
66
67
  resolveChildMaxSubagentDepth,
67
68
  } from "../../shared/types.ts";
68
- import { nestedResultsPath, nestedSummaryFromAsyncStatus, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
69
+ import { inheritedNestedParentAddressOf, inheritedNestedRouteOf, nestedResultsPath, nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
70
+ import type { ChildRuntimeConfig } from "../shared/child-runtime-config.ts";
71
+ import { childSessionFactoryModule } from "../shared/child-session.ts";
72
+ import { inheritedChildRuntime } from "../shared/child-launch.ts";
69
73
  import { resultFilePath } from "./result-files.ts";
70
74
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
71
75
  import { usageBudgetState } from "../shared/usage-budget.ts";
@@ -75,7 +79,7 @@ import { finalizeProcessTerminal, initializeProcessTerminal, readProcessTerminal
75
79
  import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
76
80
  import { statusStepDescription } from "./chain-append.ts";
77
81
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
78
- import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
82
+ import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
79
83
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
80
84
  import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
81
85
  import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
@@ -144,6 +148,8 @@ interface AsyncExecutionContext {
144
148
  modelScope?: ModelScopeConfig;
145
149
  /** Whether the parent session has an interactive UI. */
146
150
  interactive?: boolean;
151
+ /** The executor's own child runtime when the launch comes from an in-process child. */
152
+ childRuntime?: ChildRuntimeConfig;
147
153
  }
148
154
 
149
155
  export const DEFAULT_ASYNC_TIMEOUT_MS = 30 * 60 * 1000;
@@ -179,6 +185,7 @@ interface AsyncChainParams {
179
185
  worktreeSetupHook?: string;
180
186
  worktreeSetupHookTimeoutMs?: number;
181
187
  worktreeBaseDir?: string;
188
+ baseRef?: string;
182
189
  worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
183
190
  worktreeBranchPrefix?: string;
184
191
  controlConfig?: ResolvedControlConfig;
@@ -248,6 +255,7 @@ interface AsyncSingleParams {
248
255
  worktreeSetupHook?: string;
249
256
  worktreeSetupHookTimeoutMs?: number;
250
257
  worktreeBaseDir?: string;
258
+ baseRef?: string;
251
259
  worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
252
260
  worktreeBranchPrefix?: string;
253
261
  worktree?: boolean;
@@ -536,6 +544,13 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
536
544
  if (!jitiCliPath) {
537
545
  return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
538
546
  }
547
+ if (!piPackageRoot) {
548
+ return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}); a standalone pi binary has no package directory, so the async runner cannot create child sessions. Run this child in the foreground (async: false) or install pi from npm.` };
549
+ }
550
+ const hostPeerAliases = resolveHostPeerAliases(piPackageRoot);
551
+ if (hostPeerAliases.missing.length > 0) {
552
+ return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}, so the async runner cannot create child sessions. A standalone pi binary cannot run background children.` };
553
+ }
539
554
 
540
555
  fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
541
556
  const cfgPath = getAsyncConfigPath(suffix);
@@ -577,7 +592,8 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
577
592
  stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
578
593
  env: {
579
594
  ...omitExtensionBindingsEnv(process.env),
580
- ...(piPackageRoot ? { [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot } : {}),
595
+ [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot,
596
+ [JITI_ALIAS_ENV]: JSON.stringify(hostPeerAliases.aliases),
581
597
  },
582
598
  });
583
599
  closeFd(stdoutFd);
@@ -833,7 +849,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
833
849
  if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
834
850
  }
835
851
  try {
836
- assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling, decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])));
852
+ assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling, ctx.childRuntime?.capabilityCeiling));
837
853
  } catch (error) {
838
854
  throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
839
855
  }
@@ -913,7 +929,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
913
929
  const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
914
930
  params.thinkingCeiling,
915
931
  a.maxThinking,
916
- decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
932
+ ctx.childRuntime?.thinkingCeiling,
917
933
  );
918
934
  if (!externalRunner) {
919
935
  try {
@@ -957,7 +973,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
957
973
  model,
958
974
  modelCandidates,
959
975
  capabilityCeiling: params.capabilityCeiling,
960
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
976
+ inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
961
977
  agentName: a.name,
962
978
  permissionRules,
963
979
  runtimeSnapshotHost: ctx.pi,
@@ -1002,8 +1018,6 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1002
1018
  thinking: resolveEffectiveThinking(model, effectiveThinking),
1003
1019
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1004
1020
  launchResolvedExtensions,
1005
- ...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
1006
- ...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
1007
1021
  modelCandidates: externalRunner ? undefined : modelCandidates,
1008
1022
  ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
1009
1023
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
@@ -1207,6 +1221,7 @@ export function executeAsyncChain(
1207
1221
  worktreeSetupHook,
1208
1222
  worktreeSetupHookTimeoutMs,
1209
1223
  worktreeBaseDir,
1224
+ baseRef,
1210
1225
  worktreeProvider,
1211
1226
  worktreeBranchPrefix,
1212
1227
  controlConfig,
@@ -1224,8 +1239,8 @@ export function executeAsyncChain(
1224
1239
  });
1225
1240
  if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
1226
1241
  const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
1227
- const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
1228
- const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
1242
+ const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
1243
+ const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
1229
1244
  const asyncDir = inheritedNestedRoute
1230
1245
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
1231
1246
  : path.join(DIRS.async, id);
@@ -1338,10 +1353,12 @@ export function executeAsyncChain(
1338
1353
  completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
1339
1354
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1340
1355
  piPackageRoot,
1341
- piArgv1: process.argv[1],
1356
+ childSessionFactoryModule: childSessionFactoryModule(),
1357
+ inheritedChildRuntime: inheritedChildRuntime(ctx.childRuntime),
1342
1358
  worktreeSetupHook,
1343
1359
  worktreeSetupHookTimeoutMs,
1344
1360
  worktreeBaseDir,
1361
+ baseRef,
1345
1362
  worktreeProvider,
1346
1363
  worktreeBranchPrefix,
1347
1364
  controlConfig,
@@ -1454,7 +1471,7 @@ export function executeAsyncChain(
1454
1471
  path: nestedAddress.path,
1455
1472
  asyncDir,
1456
1473
  pid: spawnResult.pid,
1457
- ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
1474
+ ownerIntercomTarget: ctx.childRuntime?.intercomSessionName,
1458
1475
  leafIntercomTarget: childIntercomTargets?.[0],
1459
1476
  intercomTarget: childIntercomTargets?.[0],
1460
1477
  ownerState: "live",
@@ -1541,6 +1558,7 @@ export function executeAsyncSingle(
1541
1558
  worktreeSetupHook,
1542
1559
  worktreeSetupHookTimeoutMs,
1543
1560
  worktreeBaseDir,
1561
+ baseRef,
1544
1562
  worktreeProvider,
1545
1563
  worktreeBranchPrefix,
1546
1564
  controlConfig,
@@ -1581,7 +1599,7 @@ export function executeAsyncSingle(
1581
1599
  if (extensionBindings !== undefined) unsupported.push("extension bindings");
1582
1600
  if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
1583
1601
  }
1584
- const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
1602
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), ctx.childRuntime?.capabilityCeiling);
1585
1603
  try {
1586
1604
  assertAgentAllowedByCapabilityCeiling(agentConfig.name, capabilityCeiling);
1587
1605
  } catch (error) {
@@ -1624,8 +1642,8 @@ export function executeAsyncSingle(
1624
1642
  }
1625
1643
  systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: runnerCwd, agentName: agentConfig.name });
1626
1644
 
1627
- const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
1628
- const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
1645
+ const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
1646
+ const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
1629
1647
  const asyncDir = inheritedNestedRoute
1630
1648
  ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
1631
1649
  : path.join(DIRS.async, id);
@@ -1690,7 +1708,7 @@ export function executeAsyncSingle(
1690
1708
  const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
1691
1709
  params.thinkingCeiling,
1692
1710
  agentConfig.maxThinking,
1693
- decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
1711
+ ctx.childRuntime?.thinkingCeiling,
1694
1712
  );
1695
1713
  if (!externalRunner) {
1696
1714
  try {
@@ -1750,7 +1768,7 @@ export function executeAsyncSingle(
1750
1768
  model,
1751
1769
  modelCandidates,
1752
1770
  capabilityCeiling,
1753
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1771
+ inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
1754
1772
  agentName: agentConfig.name,
1755
1773
  permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
1756
1774
  runtimeSnapshotHost: ctx.pi,
@@ -1847,6 +1865,7 @@ export function executeAsyncSingle(
1847
1865
  ...(controlConfig ? { controlConfig } : {}),
1848
1866
  ...(params.context ? { context: params.context } : {}),
1849
1867
  ...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
1868
+ ...(params.baseRef !== undefined ? { baseRef: params.baseRef } : {}),
1850
1869
  ...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
1851
1870
  ...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
1852
1871
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, recoveryAgentConfig.maxSubagentDepth),
@@ -1897,8 +1916,6 @@ export function executeAsyncSingle(
1897
1916
  extensions: agentConfig.extensions,
1898
1917
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
1899
1918
  mcpDirectTools: agentConfig.mcpDirectTools,
1900
- ...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
1901
- ...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
1902
1919
  mutationTools: agentConfig.mutationTools,
1903
1920
  completionGuard: agentConfig.completionGuard,
1904
1921
  systemPrompt,
@@ -1944,10 +1961,12 @@ export function executeAsyncSingle(
1944
1961
  completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
1945
1962
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1946
1963
  piPackageRoot,
1947
- piArgv1: process.argv[1],
1964
+ childSessionFactoryModule: childSessionFactoryModule(),
1965
+ inheritedChildRuntime: inheritedChildRuntime(ctx.childRuntime),
1948
1966
  worktreeSetupHook,
1949
1967
  worktreeSetupHookTimeoutMs,
1950
1968
  worktreeBaseDir,
1969
+ baseRef,
1951
1970
  worktreeProvider,
1952
1971
  worktreeBranchPrefix,
1953
1972
  controlConfig,
@@ -2030,7 +2049,7 @@ export function executeAsyncSingle(
2030
2049
  path: nestedAddress.path,
2031
2050
  asyncDir,
2032
2051
  pid: spawnResult.pid,
2033
- ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
2052
+ ownerIntercomTarget: ctx.childRuntime?.intercomSessionName,
2034
2053
  leafIntercomTarget: childIntercomTarget?.(agent, 0),
2035
2054
  intercomTarget: childIntercomTarget?.(agent, 0),
2036
2055
  ownerState: "live",
@@ -39,6 +39,8 @@ interface AsyncJobTrackerOptions {
39
39
  watch?: typeof fs.watch;
40
40
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
41
41
  now?: () => number;
42
+ /** Resolve native supervisor requests without scanning supervisor mailboxes. */
43
+ supervisorRequestState?: (event: ControlEvent) => "pending" | "resolved" | "unknown";
42
44
  }
43
45
 
44
46
  const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
@@ -293,6 +295,15 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
293
295
  if ((parsed as { type?: unknown }).type !== "subagent.control") return;
294
296
  const record = parsed as { event?: ControlEvent; channels?: string[]; childIntercomTarget?: string; noticeText?: string; intercom?: { to?: string; message?: string } };
295
297
  if (!record.event || !Array.isArray(record.channels)) return;
298
+ if (record.event.type === "needs_attention" && record.event.reason === "supervisor_request" && options.supervisorRequestState) {
299
+ let requestState: "pending" | "resolved" | "unknown" = "unknown";
300
+ try {
301
+ requestState = options.supervisorRequestState(record.event);
302
+ } catch (error) {
303
+ console.error(`Failed to resolve supervisor request state for async control event in '${job.asyncDir}':`, error);
304
+ }
305
+ if (requestState === "resolved") return;
306
+ }
296
307
  const payload = {
297
308
  event: record.event,
298
309
  source: "async" as const,
@@ -12,6 +12,7 @@ import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
12
12
  import { resultFilePath, resultPayloadPathForIndexedRun } from "./result-files.ts";
13
13
  import { canScanAsyncRunPrefix, MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH } from "./run-id-query.ts";
14
14
  import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/parallel-handoff.ts";
15
+ import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
15
16
  import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
16
17
  import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
17
18
 
@@ -320,7 +321,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
320
321
  "subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
321
322
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
322
323
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
323
- "launchResolvedExtensions", "runFanoutBudget", "lane",
324
+ "launchResolvedExtensions", "runFanoutBudget", "lane", "baseRef",
324
325
  "extensionBindings",
325
326
  ]);
326
327
  for (const field of Object.keys(parsed)) {
@@ -366,6 +367,14 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
366
367
  for (const field of ["launchContractDigest", "sessionFile", "model", "modelProvider", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
367
368
  if (parsed[field] !== undefined && (typeof parsed[field] !== "string" || !(parsed[field] as string).trim())) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
368
369
  }
370
+ if (parsed.baseRef !== undefined) {
371
+ if (typeof parsed.baseRef !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': baseRef must be a string.`);
372
+ try {
373
+ parsed.baseRef = normalizeWorktreeBaseRef(parsed.baseRef);
374
+ } catch (error) {
375
+ throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
376
+ }
377
+ }
369
378
  if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
370
379
  if (parsed.structuredOutputSchema !== undefined && (!parsed.structuredOutputSchema || typeof parsed.structuredOutputSchema !== "object" || Array.isArray(parsed.structuredOutputSchema))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': structuredOutputSchema must be an object.`);
371
380
  if (parsed.memory !== undefined) {
@@ -623,7 +632,7 @@ export function buildRevivedAsyncTask(target: AsyncResumeTarget, message: string
623
632
  `Original agent: ${target.agent}`,
624
633
  target.sessionFile ? `Original session file: ${target.sessionFile}` : undefined,
625
634
  "",
626
- "Use the stored session context as background. Answer the orchestrator's follow-up below. Do not assume the original child process is still alive.",
635
+ "Use the stored session context as background. Answer the orchestrator's follow-up below. Do not assume the original child session is still running.",
627
636
  "",
628
637
  "Follow-up:",
629
638
  message,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Cross-OS control channel for async subagent runs.
3
3
  *
4
- * Background runs use child processes. Unix detaches them from the parent process.
4
+ * Background runs use a detached runner process. Unix detaches it from the parent process.
5
5
  * The original control path delivered an interrupt with
6
6
  * `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot
7
7
  * deliver those signals cross-process via `process.kill` and throws `ENOSYS`,
@@ -74,33 +74,10 @@ export interface SteerRequest {
74
74
  source?: string;
75
75
  }
76
76
 
77
- export interface SteerCapability {
78
- type: "steer-capability";
79
- protocolVersion: 1;
80
- index: number;
81
- pid: number;
82
- readyAt: number;
83
- supported: boolean;
84
- }
85
-
86
- export interface SteerAck {
87
- type: "steer-ack";
88
- protocolVersion: 1;
89
- requestId: string;
90
- index: number;
91
- ts: number;
92
- state: "delivered" | "queued" | "failed";
93
- deliveryStatus?: SteerDeliveryStatus;
94
- message: string;
95
- }
96
-
97
77
  const STEER_REQUESTS_DIR = "steer-requests";
98
78
  const STOP_REQUESTS_DIR = "stop-requests";
99
79
  const REVIVAL_BRIEFS_DIR = "revival-briefs";
100
80
  export const MAX_STEER_QUEUE_SIZE = 20;
101
- const STEER_TARGETS_DIR = "steer-targets";
102
- const STEER_CAPABILITIES_DIR = "steer-capabilities";
103
- const STEER_ACKS_DIR = "steer-acks";
104
81
  const STEER_INBOX_CLOSED_FILE = "steer-inbox-closed.json";
105
82
  const MAX_STEER_MESSAGE_BYTES = 128 * 1024;
106
83
  const MAX_STEER_REQUEST_ID_LENGTH = 256;
@@ -143,35 +120,6 @@ export function closeSteerInbox(asyncDir: string, state: string, write: (filePat
143
120
  write(steerInboxClosedPath(asyncDir), { version: 1, closedAt: Date.now(), state });
144
121
  }
145
122
 
146
- /** Per-child inbox consumed by the child prompt runtime inside the Pi process. */
147
- export function stepSteerInboxDir(asyncDir: string, index: number): string {
148
- assertChildIndex(index);
149
- return path.join(controlInboxDir(asyncDir), STEER_TARGETS_DIR, String(index));
150
- }
151
-
152
- export function steerCapabilitiesDir(asyncDir: string): string {
153
- return path.join(controlInboxDir(asyncDir), STEER_CAPABILITIES_DIR);
154
- }
155
-
156
- export function steerCapabilityPath(asyncDir: string, index: number): string {
157
- assertChildIndex(index);
158
- return path.join(steerCapabilitiesDir(asyncDir), `${index}.json`);
159
- }
160
-
161
- export function steerAcksDir(asyncDir: string, index: number): string {
162
- assertChildIndex(index);
163
- return path.join(controlInboxDir(asyncDir), STEER_ACKS_DIR, String(index));
164
- }
165
-
166
- function steerAckFileName(requestId: string): string {
167
- return `${Buffer.from(requestId).toString("base64url")}.json`;
168
- }
169
-
170
- export function steerAckPathFromDir(dir: string, requestId: string): string {
171
- if (!/^[^\s]+$/.test(requestId) || requestId.length > 256) throw new Error("steer acknowledgment requestId is invalid.");
172
- return path.join(dir, steerAckFileName(requestId));
173
- }
174
-
175
123
  function assertChildIndex(index: number): void {
176
124
  if (!Number.isInteger(index) || index < 0 || index > 1_000_000) throw new Error("child index must be a non-negative integer.");
177
125
  }
@@ -229,45 +177,6 @@ export function writeSteerRequestToExistingDir(dir: string, request: SteerReques
229
177
  return requestPath;
230
178
  }
231
179
 
232
- export function writeSteerCapabilityAt(filePath: string, capability: Omit<SteerCapability, "type" | "protocolVersion">): string {
233
- assertChildIndex(capability.index);
234
- if (!Number.isInteger(capability.pid) || capability.pid <= 0) throw new Error("steer capability pid must be a positive integer.");
235
- if (!Number.isFinite(capability.readyAt) || capability.readyAt <= 0) throw new Error("steer capability readyAt must be a finite timestamp.");
236
- const record: SteerCapability = { type: "steer-capability", protocolVersion: 1, ...capability };
237
- writeAtomicJson(filePath, record);
238
- return filePath;
239
- }
240
-
241
- export function writeSteerCapability(asyncDir: string, capability: Omit<SteerCapability, "type" | "protocolVersion">): string {
242
- return writeSteerCapabilityAt(steerCapabilityPath(asyncDir, capability.index), capability);
243
- }
244
-
245
- function steerAckWritePath(filePath: string, ack: Omit<SteerAck, "type" | "protocolVersion">): string {
246
- const parsed = path.parse(filePath);
247
- const stateOrder = ack.state === "queued" ? "0" : ack.state === "delivered" ? "1" : "2";
248
- const timestamp = String(Math.trunc(ack.ts)).padStart(13, "0");
249
- for (let suffix = 0; suffix < 1_000; suffix += 1) {
250
- const candidate = path.join(parsed.dir, `${parsed.name}-${timestamp}-${stateOrder}-${ack.state}${suffix === 0 ? "" : `-${suffix}`}${parsed.ext}`);
251
- if (!fs.existsSync(candidate)) return candidate;
252
- }
253
- throw new Error("steer acknowledgment queue is full.");
254
- }
255
-
256
- export function writeSteerAckAt(filePath: string, ack: Omit<SteerAck, "type" | "protocolVersion">): string {
257
- assertChildIndex(ack.index);
258
- if (!/^[^\s]+$/.test(ack.requestId) || ack.requestId.length > 256) throw new Error("steer acknowledgment requestId is invalid.");
259
- if (!Number.isFinite(ack.ts) || ack.ts <= 0) throw new Error("steer acknowledgment ts must be a finite timestamp.");
260
- if (!ack.message.trim() || ack.message.length > 1000) throw new Error("steer acknowledgment message is invalid.");
261
- const record: SteerAck = { type: "steer-ack", protocolVersion: 1, ...ack, message: ack.message.trim() };
262
- const ackPath = steerAckWritePath(filePath, ack);
263
- writeAtomicJson(ackPath, record);
264
- return ackPath;
265
- }
266
-
267
- export function writeSteerAck(asyncDir: string, ack: Omit<SteerAck, "type" | "protocolVersion">): string {
268
- return writeSteerAckAt(path.join(steerAcksDir(asyncDir, ack.index), steerAckFileName(ack.requestId)), ack);
269
- }
270
-
271
180
  /**
272
181
  * Parent side: drop a portable interrupt request the runner's inbox watcher will
273
182
  * pick up regardless of OS. Written atomically (temp + rename), dir auto-created.
@@ -350,97 +259,6 @@ export function requestAsyncSteer(
350
259
  return requestPath;
351
260
  }
352
261
 
353
- export function enqueueStepSteer(asyncDir: string, index: number, request: SteerRequest): string {
354
- assertChildIndex(index);
355
- const { targetIndexes: _targetIndexes, ...singleTargetRequest } = request;
356
- return writeSteerRequestToDir(stepSteerInboxDir(asyncDir, index), { ...singleTargetRequest, targetIndex: index, type: "steer" });
357
- }
358
-
359
- function parseSteerCapability(raw: unknown): SteerCapability | undefined {
360
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
361
- const input = raw as Partial<SteerCapability>;
362
- if (input.type !== "steer-capability" || input.protocolVersion !== 1) return undefined;
363
- const { index, pid, readyAt, supported } = input;
364
- if (typeof index !== "number" || !Number.isInteger(index) || index < 0 || index > 1_000_000) return undefined;
365
- if (typeof pid !== "number" || typeof readyAt !== "number" || !Number.isInteger(pid) || pid <= 0 || !Number.isFinite(readyAt) || readyAt <= 0 || typeof supported !== "boolean") return undefined;
366
- return { type: "steer-capability", protocolVersion: 1, index, pid, readyAt, supported };
367
- }
368
-
369
- function parseSteerAck(raw: unknown): SteerAck | undefined {
370
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
371
- const input = raw as Partial<SteerAck>;
372
- if (input.type !== "steer-ack" || input.protocolVersion !== 1 || typeof input.requestId !== "string" || !/^[^\s]+$/.test(input.requestId) || input.requestId.length > 256) return undefined;
373
- const { index, ts, state, message } = input;
374
- if (typeof index !== "number" || typeof ts !== "number" || !Number.isInteger(index) || index < 0 || index > 1_000_000 || !Number.isFinite(ts) || ts <= 0) return undefined;
375
- if (state !== "delivered" && state !== "queued" && state !== "failed") return undefined;
376
- if (input.deliveryStatus !== undefined && input.deliveryStatus !== "delivered" && input.deliveryStatus !== "queued") return undefined;
377
- if (typeof message !== "string" || !message.trim() || message.length > 1000) return undefined;
378
- return { type: "steer-ack", protocolVersion: 1, requestId: input.requestId, index, ts, state, ...(input.deliveryStatus ? { deliveryStatus: input.deliveryStatus } : {}), message: message.trim() };
379
- }
380
-
381
- export function readSteerCapability(asyncDir: string, index: number): SteerCapability | undefined {
382
- try {
383
- return parseSteerCapability(JSON.parse(fs.readFileSync(steerCapabilityPath(asyncDir, index), "utf-8")));
384
- } catch {
385
- return undefined;
386
- }
387
- }
388
-
389
- export function consumeSteerCapabilities(asyncDir: string, fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync"> = fs): SteerCapability[] {
390
- const dir = steerCapabilitiesDir(asyncDir);
391
- if (!fsImpl.existsSync(dir)) return [];
392
- const capabilities: SteerCapability[] = [];
393
- for (const entry of fsImpl.readdirSync(dir).filter((name) => /^\d+\.json$/.test(name)).sort()) {
394
- try {
395
- const capability = parseSteerCapability(JSON.parse(fsImpl.readFileSync(path.join(dir, entry), "utf-8")));
396
- if (capability) capabilities.push(capability);
397
- } catch {
398
- // A partially written or malformed capability is ignored until a valid one arrives.
399
- }
400
- }
401
- return capabilities;
402
- }
403
-
404
- export function consumeSteerAckFromDir(
405
- dir: string,
406
- requestId: string,
407
- fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync" | "rmSync"> = fs,
408
- ): SteerAck | undefined {
409
- if (!fsImpl.existsSync(dir)) return undefined;
410
- let entries: string[];
411
- try { entries = fsImpl.readdirSync(dir).filter((name) => name.endsWith(".json")).sort(); } catch { return undefined; }
412
- for (const entry of entries) {
413
- const target = path.join(dir, entry);
414
- let ack: SteerAck | undefined;
415
- try { ack = parseSteerAck(JSON.parse(fsImpl.readFileSync(target, "utf-8"))); } catch { ack = undefined; }
416
- if (ack?.requestId !== requestId) continue;
417
- try { fsImpl.rmSync(target, { force: true }); } catch { return undefined; }
418
- return ack;
419
- }
420
- return undefined;
421
- }
422
-
423
- export function consumeSteerAcks(asyncDir: string, fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync" | "rmSync"> = fs): SteerAck[] {
424
- const root = path.join(controlInboxDir(asyncDir), STEER_ACKS_DIR);
425
- if (!fsImpl.existsSync(root)) return [];
426
- const acks: SteerAck[] = [];
427
- let indexNames: string[];
428
- try { indexNames = fsImpl.readdirSync(root).filter((name) => /^\d+$/.test(name)); } catch { return []; }
429
- for (const indexName of indexNames) {
430
- const dir = path.join(root, indexName);
431
- let entries: string[];
432
- try { entries = fsImpl.readdirSync(dir).filter((name) => name.endsWith(".json")).sort(); } catch { continue; }
433
- for (const entry of entries) {
434
- const target = path.join(dir, entry);
435
- let ack: SteerAck | undefined;
436
- try { ack = parseSteerAck(JSON.parse(fsImpl.readFileSync(target, "utf-8"))); } catch { ack = undefined; }
437
- try { fsImpl.rmSync(target, { force: true }); } catch { continue; }
438
- if (ack) acks.push(ack);
439
- }
440
- }
441
- return acks;
442
- }
443
-
444
262
  function parseSteerRequest(raw: unknown): SteerRequest | undefined {
445
263
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
446
264
  const input = raw as Partial<SteerRequest>;
@@ -665,8 +483,6 @@ export function watchAsyncControlInbox(
665
483
  onTimeout?: () => void;
666
484
  onStop?: (request: StopRequest) => void;
667
485
  onSteer?: (request: SteerRequest) => void;
668
- onSteerCapability?: (capability: SteerCapability) => void;
669
- onSteerAck?: (ack: SteerAck) => void;
670
486
  pollIntervalMs?: number;
671
487
  safetyPollIntervalMs?: number;
672
488
  platform?: NodeJS.Platform;
@@ -691,8 +507,6 @@ export function watchAsyncControlInbox(
691
507
  if (consumeTimeoutRequest(asyncDir, fsImpl)) opts.onTimeout?.();
692
508
  if (consumeInterruptRequest(asyncDir, fsImpl)) opts.onInterrupt();
693
509
  for (const request of consumeSteerRequests(asyncDir, fsImpl)) opts.onSteer?.(request);
694
- for (const capability of consumeSteerCapabilities(asyncDir, fsImpl)) opts.onSteerCapability?.(capability);
695
- for (const ack of consumeSteerAcks(asyncDir, fsImpl)) opts.onSteerAck?.(ack);
696
510
  } catch {
697
511
  // Never let inbox errors crash the runner.
698
512
  }
@@ -718,32 +532,16 @@ export function watchAsyncControlInbox(
718
532
  const watchDir = (target: string, create = false): void => {
719
533
  if (disposed || watchedDirs.has(target)) return;
720
534
  if (create) fsImpl.mkdirSync(target, { recursive: true });
721
- const watcher = fsImpl.watch(resolveWatchPath(target, fsImpl.realpathSync.native), () => {
722
- watchExistingSteerAckDirs();
723
- check();
724
- });
535
+ const watcher = fsImpl.watch(resolveWatchPath(target, fsImpl.realpathSync.native), () => check());
725
536
  watcher.on?.("error", startPolling);
726
537
  watchers.push(watcher);
727
538
  watchedDirs.add(target);
728
539
  };
729
- const watchExistingSteerAckDirs = (): void => {
730
- const ackRoot = path.join(dir, STEER_ACKS_DIR);
731
- let entries: string[];
732
- try {
733
- entries = fsImpl.readdirSync(ackRoot).filter((name) => /^\d+$/.test(name));
734
- } catch {
735
- return;
736
- }
737
- for (const entry of entries) watchDir(path.join(ackRoot, entry));
738
- };
739
540
  try {
740
541
  if (shouldUseNativeFsWatch("runner-control-inbox", opts.platform)) {
741
542
  watchDir(dir);
742
543
  watchDir(stopRequestsDir(asyncDir), true);
743
544
  watchDir(steerRequestsDir(asyncDir), true);
744
- watchDir(steerCapabilitiesDir(asyncDir), true);
745
- watchDir(path.join(dir, STEER_ACKS_DIR), true);
746
- watchExistingSteerAckDirs();
747
545
  startSafetyPolling();
748
546
  } else {
749
547
  startPolling();
@@ -115,7 +115,7 @@ export function writeProcessTerminalCandidate(asyncDir: string, candidate: Proce
115
115
  writePrivateAtomicJson(processTerminalCandidatePath(asyncDir), candidate);
116
116
  }
117
117
 
118
- /** Establish ownership before authorizing a runner to launch any child process. */
118
+ /** Establish ownership before authorizing a runner to start any child session. */
119
119
  export function initializeProcessTerminal(asyncDir: string, runId: string, runnerProcessInstanceId: string): void {
120
120
  writeProcessTerminalCandidate(asyncDir, {
121
121
  version: 1,