pi-subagents 0.61.0 → 0.62.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 (45) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/docs/agents.md +6 -2
  3. package/docs/configuration.md +3 -3
  4. package/docs/extension-api.md +2 -2
  5. package/docs/observability.md +1 -1
  6. package/docs/tool-reference.md +2 -2
  7. package/install.mjs +0 -1
  8. package/package.json +1 -1
  9. package/skills/pi-subagents/references/execution-controls.md +3 -4
  10. package/src/agents/agent-management.ts +19 -0
  11. package/src/agents/agent-serializer.ts +3 -0
  12. package/src/agents/agents.ts +16 -2
  13. package/src/agents/runtime-agent-registry.ts +5 -1
  14. package/src/api/preflight.ts +4 -0
  15. package/src/extension/public-execution.ts +1 -0
  16. package/src/extension/schemas.ts +6 -2
  17. package/src/extension/tool-description.ts +1 -1
  18. package/src/runs/background/active-async-capacity.ts +26 -7
  19. package/src/runs/background/async-execution.ts +47 -16
  20. package/src/runs/background/async-resume.ts +3 -2
  21. package/src/runs/background/process-terminal.ts +16 -0
  22. package/src/runs/background/scheduled-runs.ts +63 -6
  23. package/src/runs/background/steering.ts +4 -1
  24. package/src/runs/background/subagent-runner.ts +14 -7
  25. package/src/runs/background/wait-tool.ts +1 -7
  26. package/src/runs/foreground/execution.ts +14 -7
  27. package/src/runs/foreground/subagent-executor.ts +3 -2
  28. package/src/runs/shared/acceptance.ts +70 -9
  29. package/src/runs/shared/capability-ceiling.ts +1 -0
  30. package/src/runs/shared/dynamic-fanout.ts +1 -1
  31. package/src/runs/shared/parallel-utils.ts +2 -6
  32. package/src/runs/shared/permissions.ts +1 -1
  33. package/src/runs/shared/pi-args.ts +24 -12
  34. package/src/runs/shared/pi-spawn.ts +69 -35
  35. package/src/runs/shared/structured-output.ts +33 -6
  36. package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  37. package/src/runs/shared/task-intent.ts +17 -6
  38. package/src/runs/shared/tool-timeout.ts +1 -1
  39. package/src/shared/atomic-json.ts +3 -1
  40. package/src/shared/fork-context.ts +0 -12
  41. package/src/shared/fork-session-cwd.ts +27 -0
  42. package/src/shared/launch-contract.ts +3 -0
  43. package/src/shared/types.ts +3 -1
  44. package/src/slash/slash-commands.ts +1 -1
  45. package/src/slash/subagents-admin.ts +2 -0
@@ -11,7 +11,7 @@ import { createRequire } from "node:module";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
13
13
  import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
14
- import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
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
17
  import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
@@ -19,7 +19,7 @@ import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeS
19
19
  import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
20
20
  import type { RunnerStep } from "../shared/parallel-utils.ts";
21
21
  import type { ContextMode } from "../shared/context-mode.ts";
22
- import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
22
+ import { resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
23
23
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
24
24
  import { resolveNodeExecutable } from "../../shared/node-executable.ts";
25
25
  import { backgroundProcessOptions } from "../shared/background-process-options.ts";
@@ -35,7 +35,7 @@ import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
35
35
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
36
36
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
37
37
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
38
- import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
38
+ import { resolveAcceptanceReportMode, resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
39
39
  import { createRunFanoutBudget, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
40
40
  import { validateImplementationToolContract } from "../shared/completion-guard.ts";
41
41
  import {
@@ -70,7 +70,7 @@ import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
70
70
  import { usageBudgetState } from "../shared/usage-budget.ts";
71
71
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
72
72
  import type { SessionLeaseRequest } from "../shared/session-lease.ts";
73
- import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
73
+ import { finalizeProcessTerminal, initializeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
74
74
  import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
75
75
  import { statusStepDescription } from "./chain-append.ts";
76
76
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
@@ -81,7 +81,7 @@ import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBin
81
81
  import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
82
82
 
83
83
  const require = createRequire(import.meta.url);
84
- const piPackageRoot = resolvePiPackageRoot();
84
+ const piPackageRoot = resolvePiPackageRoot() ?? resolveInstalledPiPackageRoot();
85
85
 
86
86
  function resolveJitiCliFromPackageJson(packageJsonPath: string): string | undefined {
87
87
  if (!fs.existsSync(packageJsonPath)) return undefined;
@@ -432,13 +432,15 @@ function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupS
432
432
  return { ok: false, error: `Timed out after ${timeoutMs}ms waiting for the async runner startup state '${expectedState}'.`, startupDidNotProceed: true };
433
433
  }
434
434
 
435
+ const writePrivateStartupControlJson = createAtomicJsonWriter({ mode: 0o600, ignoreCleanupErrorAfterSuccess: true });
436
+
435
437
  function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "proceed"; token: string }): void {
436
438
  // Delegate to the shared atomic JSON writer (temp file + rename, retrying
437
439
  // transient Windows EPERM/EBUSY/EACCES locks and cleaning up the temp file
438
440
  // on failure), so the startup handshake gets the same locking resilience as
439
441
  // every other async control/result file. This is exercised by
440
442
  // test/unit/atomic-json.test.ts.
441
- writePrivateAtomicJson(filePath, payload);
443
+ writePrivateStartupControlJson(filePath, payload);
442
444
  }
443
445
 
444
446
  function runnerIsAlive(pid: number): boolean {
@@ -520,7 +522,7 @@ export function emitProcessTerminalEvent(ctx: AsyncExecutionContext, proof: unkn
520
522
  }
521
523
  }
522
524
 
523
- function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, requestedCwd = cwd): SpawnRunnerResult {
525
+ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, onBeforeProceed?: (runnerProcessInstanceId: string) => void, requestedCwd = cwd): SpawnRunnerResult {
524
526
  const cwdError = preflightLaunchCwd(requestedCwd, cwd);
525
527
  if (cwdError) return { error: cwdError };
526
528
 
@@ -637,6 +639,24 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
637
639
  });
638
640
  } catch (error) {
639
641
  const message = `Failed to persist initial async status: ${error instanceof Error ? error.message : String(error)}`;
642
+ if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
643
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
644
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
645
+ }
646
+ try {
647
+ if (!launchAsyncDir) throw new Error("Async runner is missing its lifecycle directory.");
648
+ initializeProcessTerminal(launchAsyncDir, launchRunId, runnerProcessInstanceId);
649
+ } catch (error) {
650
+ const message = `Failed to establish async runner lifecycle sidecar: ${error instanceof Error ? error.message : String(error)}`;
651
+ if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
652
+ const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
653
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
654
+ }
655
+ try {
656
+ onBeforeProceed?.(runnerProcessInstanceId);
657
+ } catch (error) {
658
+ const message = `Failed to establish async runner capacity ownership: ${error instanceof Error ? error.message : String(error)}`;
659
+ if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
640
660
  const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
641
661
  return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
642
662
  }
@@ -905,6 +925,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
905
925
  const fast = s.fast ?? params.fast ?? a.fast;
906
926
  const toolPlan = resolvePiLaunchToolPlan({
907
927
  tools: a.tools,
928
+ excludeTools: a.excludeTools,
908
929
  allowNestedSubagents: a.allowNestedSubagents,
909
930
  extensions: a.extensions,
910
931
  subagentOnlyExtensions: a.subagentOnlyExtensions,
@@ -967,6 +988,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
967
988
  ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
968
989
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
969
990
  tools: a.tools,
991
+ excludeTools: a.excludeTools,
970
992
  allowNestedSubagents: a.allowNestedSubagents,
971
993
  extensions: a.extensions,
972
994
  subagentOnlyExtensions: a.subagentOnlyExtensions,
@@ -1002,7 +1024,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1002
1024
  acceptanceRole: a.acceptanceRole,
1003
1025
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
1004
1026
  ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
1005
- ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output"), { captureAcceptanceReport: s.acceptance !== false }) } : {}),
1027
+ ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(s.acceptance) }) } : {}),
1006
1028
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1007
1029
  ...(s.worktree ? { worktree: true } : {}),
1008
1030
  };
@@ -1171,7 +1193,7 @@ export function executeAsyncChain(
1171
1193
  chain: chain.map((step) => {
1172
1194
  if (isParallelStep(step)) return { parallel: step.parallel };
1173
1195
  if (isDynamicParallelStep(step)) return { acceptance: step.acceptance, parallel: step.parallel };
1174
- return { acceptance: step.acceptance };
1196
+ return { acceptance: step.acceptance, outputSchema: step.outputSchema };
1175
1197
  }),
1176
1198
  });
1177
1199
  if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
@@ -1332,6 +1354,7 @@ export function executeAsyncChain(
1332
1354
  },
1333
1355
  path.join(asyncDir, "status.json"),
1334
1356
  (proof) => emitProcessTerminalEvent(ctx, proof),
1357
+ (runnerProcessInstanceId) => params.activeAsyncCapacity?.markStarted(runnerProcessInstanceId),
1335
1358
  );
1336
1359
  } catch (error) {
1337
1360
  params.activeAsyncCapacity?.rollback();
@@ -1340,7 +1363,10 @@ export function executeAsyncChain(
1340
1363
  }
1341
1364
 
1342
1365
  if (spawnResult.error) {
1343
- if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
1366
+ if (spawnResult.startupDidNotProceed) {
1367
+ if (!spawnResult.runnerProcessInstanceId || params.activeAsyncCapacity?.rollbackBeforeRunnerProceed(spawnResult.runnerProcessInstanceId) !== true) params.activeAsyncCapacity?.rollback();
1368
+ }
1369
+ else if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) params.activeAsyncCapacity?.rollback();
1344
1370
  else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1345
1371
  return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${spawnResult.error}`);
1346
1372
  }
@@ -1348,8 +1374,6 @@ export function executeAsyncChain(
1348
1374
  params.activeAsyncCapacity?.rollback();
1349
1375
  return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': runner identity unavailable`);
1350
1376
  }
1351
- params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1352
-
1353
1377
  if (spawnResult.pid) {
1354
1378
  const eventFirstStep = eventChain[0];
1355
1379
  if (!eventFirstStep) {
@@ -1647,7 +1671,7 @@ export function executeAsyncSingle(
1647
1671
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1648
1672
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1649
1673
  const structuredOutput = params.structuredOutputSchema
1650
- ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"), { captureAcceptanceReport: params.acceptance !== false })
1674
+ ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(params.acceptance) })
1651
1675
  : undefined;
1652
1676
  let modelCandidates: string[] = [];
1653
1677
  if (!externalRunner) {
@@ -1667,6 +1691,7 @@ export function executeAsyncSingle(
1667
1691
  }
1668
1692
  const toolPlan = resolvePiLaunchToolPlan({
1669
1693
  tools: agentConfig.tools,
1694
+ excludeTools: agentConfig.excludeTools,
1670
1695
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1671
1696
  extensions: agentConfig.extensions,
1672
1697
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1713,6 +1738,7 @@ export function executeAsyncSingle(
1713
1738
  inheritSkills: agentConfig.inheritSkills,
1714
1739
  skills: resolvedSkills.map((skill) => skill.name),
1715
1740
  tools: toolPlan.effectiveToolAllowlist,
1741
+ ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1716
1742
  extensions: toolPlan.extensionArgs,
1717
1743
  mcpDirectTools: toolPlan.effectiveMcpTools,
1718
1744
  ...(outputPath ? { outputPath } : {}),
@@ -1751,6 +1777,7 @@ export function executeAsyncSingle(
1751
1777
  ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1752
1778
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1753
1779
  ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1780
+ ...(recoveryAgentConfig.excludeTools ? { excludeTools: [...recoveryAgentConfig.excludeTools] } : {}),
1754
1781
  ...(recoveryAgentConfig.allowNestedSubagents !== undefined ? { allowNestedSubagents: recoveryAgentConfig.allowNestedSubagents } : {}),
1755
1782
  ...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
1756
1783
  ...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
@@ -1818,6 +1845,7 @@ export function executeAsyncSingle(
1818
1845
  ...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
1819
1846
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1820
1847
  tools: agentConfig.tools,
1848
+ excludeTools: agentConfig.excludeTools,
1821
1849
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1822
1850
  extensions: agentConfig.extensions,
1823
1851
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1846,6 +1874,7 @@ export function executeAsyncSingle(
1846
1874
  ...(extensionBindings ? { extensionBindings } : {}),
1847
1875
  launchResolvedExtensions,
1848
1876
  effectiveAcceptance: resolvedAcceptance,
1877
+ acceptanceInput: params.acceptance,
1849
1878
  ...(structuredOutput ? { structuredOutput } : {}),
1850
1879
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1851
1880
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
@@ -1914,6 +1943,7 @@ export function executeAsyncSingle(
1914
1943
  },
1915
1944
  path.join(asyncDir, "status.json"),
1916
1945
  (proof) => emitProcessTerminalEvent(ctx, proof),
1946
+ (runnerProcessInstanceId) => params.activeAsyncCapacity?.markStarted(runnerProcessInstanceId),
1917
1947
  params.requestedCwd ?? runnerCwd,
1918
1948
  );
1919
1949
  } catch (error) {
@@ -1923,7 +1953,10 @@ export function executeAsyncSingle(
1923
1953
  }
1924
1954
 
1925
1955
  if (spawnResult.error) {
1926
- if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
1956
+ if (spawnResult.startupDidNotProceed) {
1957
+ if (!spawnResult.runnerProcessInstanceId || params.activeAsyncCapacity?.rollbackBeforeRunnerProceed(spawnResult.runnerProcessInstanceId) !== true) params.activeAsyncCapacity?.rollback();
1958
+ }
1959
+ else if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) params.activeAsyncCapacity?.rollback();
1927
1960
  else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1928
1961
  return formatAsyncStartError("single", `Failed to start async run '${id}': ${spawnResult.error}`);
1929
1962
  }
@@ -1931,8 +1964,6 @@ export function executeAsyncSingle(
1931
1964
  params.activeAsyncCapacity?.rollback();
1932
1965
  return formatAsyncStartError("single", `Failed to start async run '${id}': runner identity unavailable`);
1933
1966
  }
1934
- params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1935
-
1936
1967
  if (spawnResult.pid) {
1937
1968
  if (inheritedNestedRoute && nestedAddress) {
1938
1969
  const now = Date.now();
@@ -315,7 +315,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
315
315
  const parsed = value as Record<string, unknown>;
316
316
  const allowedFields = new Set([
317
317
  "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
318
- "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
318
+ "subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
319
319
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
320
320
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
321
321
  "launchResolvedExtensions", "runFanoutBudget", "lane",
@@ -356,7 +356,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
356
356
  else if (typeof parsed.inheritGlobalContext !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': inheritGlobalContext must be a boolean.`);
357
357
  if (parsed.allowNestedSubagents !== undefined && typeof parsed.allowNestedSubagents !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': allowNestedSubagents must be a boolean.`);
358
358
  if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
359
- for (const field of ["fallbackModels", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
359
+ for (const field of ["fallbackModels", "tools", "excludeTools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
360
360
  const item = parsed[field];
361
361
  if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
362
362
  }
@@ -590,6 +590,7 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
590
590
  thinking: descriptor.thinking,
591
591
  maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
592
592
  tools: descriptor.tools ? [...descriptor.tools] : undefined,
593
+ excludeTools: descriptor.excludeTools ? [...descriptor.excludeTools] : undefined,
593
594
  allowNestedSubagents: descriptor.allowNestedSubagents,
594
595
  extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
595
596
  subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
@@ -115,6 +115,22 @@ 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. */
119
+ export function initializeProcessTerminal(asyncDir: string, runId: string, runnerProcessInstanceId: string): void {
120
+ writeProcessTerminalCandidate(asyncDir, {
121
+ version: 1,
122
+ runId,
123
+ runnerProcessInstanceId,
124
+ writers: {},
125
+ });
126
+ writeAtomicJson(processTerminalPath(asyncDir), {
127
+ version: 1,
128
+ state: "pending",
129
+ runId,
130
+ runnerProcessInstanceId,
131
+ });
132
+ }
133
+
118
134
  export function markProcessTerminalCandidateLeaseRelease(asyncDir: string, token: string, acknowledged: boolean): void {
119
135
  const candidate = readProcessTerminalCandidate(asyncDir);
120
136
  if (!candidate || candidate.revivalLeaseToken !== token) return;
@@ -52,6 +52,8 @@ export interface ScheduleRecord {
52
52
  catchUp: "none" | "latest";
53
53
  timeoutMs?: number;
54
54
  paused: boolean;
55
+ sessionOnly?: boolean;
56
+ ownerSessionFile?: string;
55
57
  createdAt: string;
56
58
  updatedAt: string;
57
59
  activeRunId?: string;
@@ -72,6 +74,8 @@ export interface ScheduleRunRecord {
72
74
  error?: string;
73
75
  }
74
76
 
77
+ type PublicScheduleRecord = Omit<ScheduleRecord, "ownerSessionFile">;
78
+
75
79
  type ScheduledRunManagerDeps = {
76
80
  config: ExtensionConfig;
77
81
  launch(params: SubagentParamsLike, ctx: ExtensionContext, signal: AbortSignal): Promise<AgentToolResult<Details>>;
@@ -295,6 +299,8 @@ function parseSchedule(value: unknown, file: string): ScheduleRecord {
295
299
  } else if (record.trigger.kind === "interval") {
296
300
  if (typeof record.trigger.every !== "string" || typeof record.trigger.everyMs !== "number" || typeof record.trigger.anchorAt !== "string" || typeof record.trigger.nextRunAt !== "string") throw new Error(`Schedule record '${file}' has an invalid interval trigger.`);
297
301
  } else throw new Error(`Schedule record '${file}' has an unsupported trigger.`);
302
+ if (record.sessionOnly !== undefined && typeof record.sessionOnly !== "boolean") throw new Error(`Schedule record '${file}' has invalid sessionOnly.`);
303
+ if (record.sessionOnly === true && (typeof record.ownerSessionFile !== "string" || !record.ownerSessionFile.trim())) throw new Error(`Schedule record '${file}' is session-only but has no owner session file.`);
298
304
  return { ...record, target: parseScheduleTarget(record.target, file) } as ScheduleRecord;
299
305
  }
300
306
 
@@ -399,13 +405,19 @@ function duePlannedAt(schedule: ScheduleRecord, now: number): number | undefined
399
405
  }
400
406
 
401
407
  function textResult(text: string, schedules?: ScheduleRecord[], runs?: ScheduleRunRecord[], isError = false): AgentToolResult<Details> {
408
+ const publicSchedules = schedules?.map(publicScheduleRecord);
402
409
  return {
403
410
  content: [{ type: "text", text }],
404
411
  ...(isError ? { isError: true } : {}),
405
- details: { mode: "management", results: [], schedules: { ...(schedules ? { records: schedules } : {}), ...(runs ? { runs } : {}) } },
412
+ details: { mode: "management", results: [], schedules: { ...(publicSchedules ? { records: publicSchedules } : {}), ...(runs ? { runs } : {}) } },
406
413
  };
407
414
  }
408
415
 
416
+ function publicScheduleRecord(schedule: ScheduleRecord): PublicScheduleRecord {
417
+ const { ownerSessionFile: _ownerSessionFile, ...rest } = schedule;
418
+ return rest;
419
+ }
420
+
409
421
  function targetLabel(target: ScheduleTarget): string {
410
422
  const preview = previewSimpleWorkflowRun(target.workflowScript);
411
423
  return preview?.agent ? `workflowScript -> agent ${preview.agent}` : "workflowScript (dynamic)";
@@ -450,6 +462,32 @@ function snapshotContext(ctx: ExtensionContext, cwd: string): ExtensionContext {
450
462
  return { ...ctx, cwd, sessionManager };
451
463
  }
452
464
 
465
+ /**
466
+ * 规范化会话文件路径, 兼容 Windows 路径大小写差异.
467
+ *
468
+ * @param value 会话文件路径
469
+ * @returns 规范化后的路径, 空值时返回 undefined
470
+ */
471
+ function normalizedSessionFile(value: string | undefined): string | undefined {
472
+ if (!value || !value.trim()) return undefined;
473
+ const normalized = path.normalize(path.resolve(value));
474
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
475
+ }
476
+
477
+ /**
478
+ * 判断 Schedule 是否属于当前 Pi 会话.
479
+ *
480
+ * @param schedule Schedule 记录
481
+ * @param ctx 当前 Pi 会话上下文
482
+ * @returns 是否允许当前会话执行该 Schedule
483
+ */
484
+ function scheduleBelongsToSession(schedule: ScheduleRecord, ctx: ExtensionContext): boolean {
485
+ if (schedule.sessionOnly !== true) return true;
486
+ const ownerSessionFile = normalizedSessionFile(schedule.ownerSessionFile);
487
+ const currentSessionFile = normalizedSessionFile(ctx.sessionManager.getSessionFile());
488
+ return ownerSessionFile !== undefined && ownerSessionFile === currentSessionFile;
489
+ }
490
+
453
491
  export function listScheduledRunSummaries(cwd: string, root?: string): ScheduleRecord[] {
454
492
  return new ScheduleStore(scheduledRunStorePath(cwd, undefined, root), root === undefined ? path.resolve(cwd) : undefined).list();
455
493
  }
@@ -560,6 +598,10 @@ export class ScheduledRunManager {
560
598
  if (params.catchUp !== undefined && params.catchUp !== "none" && params.catchUp !== "latest") return textResult("catchUp must be 'none' or 'latest'.", undefined, undefined, true);
561
599
  if (params.missionId !== undefined || params.mission !== undefined || params.missionUpdate !== undefined || params.missionStatus !== undefined || params.missionScope !== undefined) return textResult("Mission attachment is deferred from this first schedule slice.", undefined, undefined, true);
562
600
  if (params.on !== undefined || params.timezone !== undefined || every === "day" || every === "week" || every === "month" || every === "year") return textResult("Calendar schedules are deferred from this first safe slice. Use a fixed interval such as every:'24h' or every:'7d'.", undefined, undefined, true);
601
+ const sessionOnly = params.sessionOnly === true;
602
+ if (sessionOnly && params.cwd !== undefined && !samePath(params.cwd, ctx.cwd)) return textResult("sessionOnly schedules cannot use an explicit cross-project cwd.", undefined, undefined, true);
603
+ const ownerSessionFile = sessionOnly ? ctx.sessionManager.getSessionFile() : undefined;
604
+ if (sessionOnly && !ownerSessionFile) return textResult("sessionOnly schedules require a persisted current session.", undefined, undefined, true);
563
605
  const sessionId = ctx.sessionManager.getSessionId() ?? "unknown";
564
606
  if (this.deps.resolveCapabilityCeiling?.(sessionId)) return textResult("Cannot persist a schedule while a capability ceiling is active.", undefined, undefined, true);
565
607
  const pendingCount = store.list().filter(hasPendingScheduleWork).length;
@@ -587,24 +629,25 @@ export class ScheduledRunManager {
587
629
  catchUp: params.catchUp ?? "latest",
588
630
  ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }),
589
631
  paused: false,
632
+ ...(sessionOnly ? { sessionOnly: true, ownerSessionFile: path.resolve(ownerSessionFile!) } : {}),
590
633
  createdAt: timestamp(now),
591
634
  updatedAt: timestamp(now),
592
635
  };
593
636
  store.write(schedule);
594
637
  store.appendEvent(schedule, "schedule.created");
595
638
  this.arm(schedule, store);
596
- return textResult(`Created schedule ${id}.\nName: ${schedule.name}\nTrigger: ${at ? `at ${at}` : `every ${every}`}\nNext: ${schedule.trigger.nextRunAt}\nTarget: ${targetLabel(schedule.target)}`, [schedule]);
639
+ return textResult(`Created schedule ${id}.\nName: ${schedule.name}\nTrigger: ${at ? `at ${at}` : `every ${every}`}\nSession only: ${schedule.sessionOnly === true ? "yes" : "no"}\nNext: ${schedule.trigger.nextRunAt}\nTarget: ${targetLabel(schedule.target)}`, [schedule]);
597
640
  }
598
641
 
599
642
  private list(): AgentToolResult<Details> {
600
643
  const schedules = this.requireStore().list().sort((a, b) => (a.trigger.nextRunAt ?? "").localeCompare(b.trigger.nextRunAt ?? ""));
601
644
  if (!schedules.length) return textResult("No project schedules.", []);
602
- return textResult([`Project schedules: ${schedules.length}`, ...schedules.map((item) => `- ${item.id} | ${item.paused ? "paused" : item.activeRunId ? "running" : "scheduled"} | ${item.trigger.nextRunAt ?? "no next run"} | ${item.name}`)].join("\n"), schedules);
645
+ return textResult([`Project schedules: ${schedules.length}`, ...schedules.map((item) => `- ${item.id} | ${item.paused ? "paused" : item.activeRunId ? "running" : "scheduled"} | ${item.trigger.nextRunAt ?? "no next run"} | ${item.sessionOnly === true ? "session-only" : "project"} | ${item.name}`)].join("\n"), schedules);
603
646
  }
604
647
 
605
648
  private show(params: SubagentParamsLike): AgentToolResult<Details> {
606
649
  const schedule = this.resolve(params);
607
- return textResult([`Schedule: ${schedule.id}`, `Name: ${schedule.name}`, `State: ${schedule.paused ? "paused" : schedule.activeRunId ? "running" : "scheduled"}`, `Target: ${targetLabel(schedule.target)}`, `CWD: ${shortenPath(schedule.cwd)}`, `Next: ${schedule.trigger.nextRunAt ?? "none"}`, `Catch up: ${schedule.catchUp}`, schedule.activeRunId ? `Active run: ${schedule.activeRunId}` : undefined].filter(Boolean).join("\n"), [schedule]);
650
+ return textResult([`Schedule: ${schedule.id}`, `Name: ${schedule.name}`, `State: ${schedule.paused ? "paused" : schedule.activeRunId ? "running" : "scheduled"}`, `Session only: ${schedule.sessionOnly === true ? "yes" : "no"}`, `Target: ${targetLabel(schedule.target)}`, `CWD: ${shortenPath(schedule.cwd)}`, `Next: ${schedule.trigger.nextRunAt ?? "none"}`, `Catch up: ${schedule.catchUp}`, schedule.activeRunId ? `Active run: ${schedule.activeRunId}` : undefined].filter(Boolean).join("\n"), [schedule]);
608
651
  }
609
652
 
610
653
  private history(params: SubagentParamsLike): AgentToolResult<Details> {
@@ -628,13 +671,18 @@ export class ScheduledRunManager {
628
671
  private async runManual(params: SubagentParamsLike): Promise<AgentToolResult<Details>> {
629
672
  const store = this.requireStore();
630
673
  const schedule = this.resolve(params);
674
+ const context = this.requireContext(store);
675
+ if (!scheduleBelongsToSession(schedule, context)) {
676
+ return textResult(`Skipped schedule ${schedule.id}: current session is not its owner.`, [schedule]);
677
+ }
631
678
  const run = await this.launch(store, schedule, this.now(), "manual", false);
632
679
  return textResult(`Manual schedule run ${run.id}: ${run.state}${run.asyncId ? ` (async ${run.asyncId})` : ""}.`, [store.get(schedule.id)], [run], run.state === "failed_launch");
633
680
  }
634
681
 
635
682
  private async runDue(): Promise<AgentToolResult<Details>> {
636
683
  const store = this.requireStore();
637
- const due = store.list().filter((schedule) => !schedule.paused && nextRunAt(schedule) !== undefined && nextRunAt(schedule)! <= this.now());
684
+ const context = this.requireContext(store);
685
+ const due = store.list().filter((schedule) => scheduleBelongsToSession(schedule, context) && !schedule.paused && nextRunAt(schedule) !== undefined && nextRunAt(schedule)! <= this.now());
638
686
  const runs: ScheduleRunRecord[] = [];
639
687
  for (const schedule of due) {
640
688
  const planned = duePlannedAt(schedule, this.now())!;
@@ -659,6 +707,7 @@ export class ScheduledRunManager {
659
707
  }
660
708
 
661
709
  private restoreOne(store: ScheduleStore, schedule: ScheduleRecord, notBefore?: number, rearm = true): void {
710
+ if (!scheduleBelongsToSession(schedule, this.requireContext(store))) return;
662
711
  if (schedule.activeRunId) {
663
712
  const run = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);
664
713
  if (run?.state === "running" && run.asyncId) this.observedAsyncIds.add(run.asyncId);
@@ -737,6 +786,7 @@ export class ScheduledRunManager {
737
786
  // is then nothing to run and nothing to re-arm.
738
787
  const schedule = store.find(id);
739
788
  if (!schedule) return;
789
+ if (!scheduleBelongsToSession(schedule, this.requireContext(store))) return;
740
790
  const planned = duePlannedAt(schedule, this.now());
741
791
  if (planned === undefined || schedule.paused) return;
742
792
  if (planned > this.now()) return this.arm(schedule, store);
@@ -857,13 +907,20 @@ export class ScheduledRunManager {
857
907
  private selectProject(cwd: string, ctx: ExtensionContext): void {
858
908
  const projectCwd = path.resolve(cwd);
859
909
  const root = scheduledRunStorePath(projectCwd, undefined, this.deps.storeRoot);
860
- if (path.resolve(ctx.cwd) === projectCwd) this.contexts.set(root, snapshotContext(ctx, projectCwd));
910
+ const isBoundContext = path.resolve(ctx.cwd) === projectCwd;
911
+ const previousContext = this.contexts.get(root);
912
+ const contextChanged = isBoundContext
913
+ && previousContext !== undefined
914
+ && normalizedSessionFile(previousContext.sessionManager.getSessionFile()) !== normalizedSessionFile(ctx.sessionManager.getSessionFile());
915
+ if (isBoundContext) this.contexts.set(root, snapshotContext(ctx, projectCwd));
861
916
  else if (!this.contexts.has(root)) throw new Error(`Cannot use project '${projectCwd}' until that project has been opened in this runtime.`);
862
917
  let store = this.stores.get(root);
863
918
  if (!store) {
864
919
  store = new ScheduleStore(root, this.deps.storeRoot === undefined ? projectCwd : undefined);
865
920
  this.stores.set(root, store);
866
921
  this.restore(store);
922
+ } else if (contextChanged) {
923
+ this.restore(store);
867
924
  }
868
925
  this.store = store;
869
926
  }
@@ -23,7 +23,10 @@ export function steeringMessagePreview(message: string): string {
23
23
  }
24
24
 
25
25
  export function steeringReceipt(message: string, receipt: string): string {
26
- return `${receipt} Message: ${JSON.stringify(steeringMessagePreview(message))}`;
26
+ const preview = steeringMessagePreview(message);
27
+ const longestFence = Math.max(2, ...[...preview.matchAll(/`{3,}/g)].map((match) => match[0]!.length));
28
+ const fence = "`".repeat(longestFence + 1);
29
+ return `${receipt}\n\nMessage sent:\n${fence}text\n${preview}\n${fence}`;
27
30
  }
28
31
 
29
32
  export function createSteeringStatus(): SteeringStatus {
@@ -77,9 +77,10 @@ import {
77
77
  } from "../shared/parallel-utils.ts";
78
78
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
79
79
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
80
+ import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
80
81
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
81
82
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
82
- import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
83
+ import { clearStructuredOutputCaptures, createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
83
84
  import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
84
85
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
85
86
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
@@ -130,7 +131,7 @@ import { assertThinkingWithinCeiling, decodeThinkingCeiling, SUBAGENT_THINKING_C
130
131
  import { launchBindingDigest } from "../../shared/launch-contract.ts";
131
132
  import { writeInitialProgressFile } from "../../shared/settings.ts";
132
133
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
133
- import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
134
+ import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
134
135
  import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
135
136
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
136
137
  import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
@@ -1397,7 +1398,7 @@ async function runSingleStepInner(
1397
1398
  }
1398
1399
 
1399
1400
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
1400
- ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
1401
+ ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(step.acceptanceInput) })
1401
1402
  : undefined);
1402
1403
  const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
1403
1404
  let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
@@ -1407,6 +1408,7 @@ async function runSingleStepInner(
1407
1408
  if (!step.runner) {
1408
1409
  resolvedTaskToolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
1409
1410
  tools: step.tools,
1411
+ excludeTools: step.excludeTools,
1410
1412
  allowNestedSubagents: step.allowNestedSubagents,
1411
1413
  extensions: step.extensions,
1412
1414
  subagentOnlyExtensions: step.subagentOnlyExtensions,
@@ -1627,6 +1629,9 @@ async function runSingleStepInner(
1627
1629
  const effectiveCwd = step.cwd ?? ctx.cwd;
1628
1630
  const cwdError = preflightLaunchCwd(step.requestedCwd ?? effectiveCwd, effectiveCwd);
1629
1631
  if (cwdError) return { agent: step.agent, output: cwdError, error: cwdError, exitCode: 1, context: step.context };
1632
+ if (step.context === "fork" && step.sessionFile && fs.existsSync(step.sessionFile)) {
1633
+ alignForkedSessionCwd(step.sessionFile, effectiveCwd);
1634
+ }
1630
1635
 
1631
1636
  const candidates = step.modelCandidates !== undefined
1632
1637
  ? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
@@ -1681,10 +1686,9 @@ async function runSingleStepInner(
1681
1686
  }));
1682
1687
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
1683
1688
  if (effectiveStructuredOutput) {
1684
- try {
1685
- if (fs.existsSync(effectiveStructuredOutput.outputPath)) fs.unlinkSync(effectiveStructuredOutput.outputPath);
1686
- } catch {
1687
- // Missing/stale structured-output files are handled after the child exits.
1689
+ const cleanupError = clearStructuredOutputCaptures(effectiveStructuredOutput);
1690
+ if (cleanupError) {
1691
+ return omitUndefinedProperties({ agent: step.agent, output: cleanupError, error: cleanupError, exitCode: 1, context: step.context });
1688
1692
  }
1689
1693
  }
1690
1694
  const watchdogConfig = resolveWatchdogConfig(step.cwd ?? ctx.cwd);
@@ -1712,6 +1716,7 @@ async function runSingleStepInner(
1712
1716
  inheritSkills: step.inheritSkills,
1713
1717
  requireReadTool: Boolean(step.skills?.length),
1714
1718
  tools: step.tools,
1719
+ excludeTools: step.excludeTools,
1715
1720
  allowNestedSubagents: step.allowNestedSubagents,
1716
1721
  extensions: step.extensions,
1717
1722
  subagentOnlyExtensions: step.subagentOnlyExtensions,
@@ -1761,6 +1766,7 @@ async function runSingleStepInner(
1761
1766
  if (step.definitionDigest) {
1762
1767
  const toolPlan = resolvedTaskToolPlan ?? resolvePiLaunchToolPlan(omitUndefinedProperties({
1763
1768
  tools: step.tools,
1769
+ excludeTools: step.excludeTools,
1764
1770
  allowNestedSubagents: step.allowNestedSubagents,
1765
1771
  extensions: step.extensions,
1766
1772
  subagentOnlyExtensions: step.subagentOnlyExtensions,
@@ -1793,6 +1799,7 @@ async function runSingleStepInner(
1793
1799
  inheritSkills: step.inheritSkills,
1794
1800
  skills: step.skills,
1795
1801
  tools: toolPlan.effectiveToolAllowlist,
1802
+ ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1796
1803
  extensions: toolPlan.extensionArgs,
1797
1804
  mcpDirectTools: toolPlan.effectiveMcpTools,
1798
1805
  ...(step.outputPath ? { outputPath: step.outputPath } : {}),
@@ -23,7 +23,7 @@ Ordinary async subagent runs already notify this session natively when they comp
23
23
  • { stopOnAttention: false } — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
24
24
  • { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running. Omitted values use waitTool.defaultTimeoutMs, then 30 minutes. Window expiry returns a non-error window_elapsed result with active work identities.
25
25
 
26
- Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`bg_wait\` (or the deprecated \`subagent_wait\` compatibility alias) in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: bg_wait and its subagent_wait compatibility alias are disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and return immediately without blocking."}`;
26
+ Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`bg_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: bg_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`;
27
27
  const execute: ToolDefinition<typeof SubagentWaitParams, Details>["execute"] = async (_id, params, signal, onUpdate, ctx) => finalizeToolResult(await waitForSubagents(params, signal, {
28
28
  state,
29
29
  events: pi.events,
@@ -40,10 +40,4 @@ Non-blocking subscriptions are visible in subagent status and differ from disabl
40
40
  execute,
41
41
  };
42
42
  pi.registerTool(primaryTool);
43
- pi.registerTool({
44
- ...primaryTool,
45
- name: "subagent_wait",
46
- label: "Subagent Wait (deprecated)",
47
- description: "Deprecated compatibility alias for `bg_wait`. Use `bg_wait` for background, provider, or detached work without a native completion notification. It has the same parameters and behavior.",
48
- });
49
43
  }