pi-subagents 0.60.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 (76) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/docs/agents.md +7 -3
  3. package/docs/configuration.md +9 -5
  4. package/docs/extension-api.md +14 -7
  5. package/docs/models.md +1 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +13 -4
  8. package/docs/workflows.md +14 -13
  9. package/install.mjs +1 -1
  10. package/package.json +1 -1
  11. package/skills/pi-subagents/SKILL.md +6 -4
  12. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +41 -11
  14. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  15. package/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  16. package/skills/pi-subagents/references/review-and-validation.md +1 -1
  17. package/src/agents/agent-management.ts +121 -63
  18. package/src/agents/agent-serializer.ts +3 -0
  19. package/src/agents/agents.ts +543 -223
  20. package/src/agents/runtime-agent-registry.ts +5 -1
  21. package/src/api/background-work.ts +7 -2
  22. package/src/api/external-runs.ts +67 -4
  23. package/src/api/preflight.ts +17 -8
  24. package/src/api/shared-types.ts +1 -0
  25. package/src/extension/index.ts +7 -4
  26. package/src/extension/public-execution.ts +48 -4
  27. package/src/extension/rpc.ts +62 -4
  28. package/src/extension/schemas.ts +17 -9
  29. package/src/extension/tool-description.ts +15 -19
  30. package/src/runs/background/active-async-capacity.ts +26 -7
  31. package/src/runs/background/async-execution.ts +98 -51
  32. package/src/runs/background/async-job-tracker.ts +62 -3
  33. package/src/runs/background/async-resume.ts +6 -3
  34. package/src/runs/background/async-status.ts +59 -9
  35. package/src/runs/background/auto-drain.ts +1 -1
  36. package/src/runs/background/fleet-view.ts +1 -1
  37. package/src/runs/background/process-terminal.ts +16 -0
  38. package/src/runs/background/result-watcher.ts +1 -1
  39. package/src/runs/background/resume-guidance.ts +1 -1
  40. package/src/runs/background/run-status.ts +2 -2
  41. package/src/runs/background/scheduled-runs.ts +63 -6
  42. package/src/runs/background/steering.ts +4 -1
  43. package/src/runs/background/subagent-runner.ts +15 -9
  44. package/src/runs/background/subagent-wait.ts +20 -21
  45. package/src/runs/background/wait-completions.ts +1 -1
  46. package/src/runs/background/wait-tool.ts +18 -18
  47. package/src/runs/foreground/execution.ts +75 -9
  48. package/src/runs/foreground/subagent-executor.ts +181 -60
  49. package/src/runs/shared/acceptance.ts +113 -27
  50. package/src/runs/shared/capability-ceiling.ts +1 -0
  51. package/src/runs/shared/dynamic-fanout.ts +1 -1
  52. package/src/runs/shared/host-step-status.ts +1 -0
  53. package/src/runs/shared/model-fallback.ts +61 -17
  54. package/src/runs/shared/parallel-utils.ts +2 -6
  55. package/src/runs/shared/permissions.ts +1 -1
  56. package/src/runs/shared/pi-args.ts +24 -12
  57. package/src/runs/shared/pi-spawn.ts +69 -35
  58. package/src/runs/shared/structured-output.ts +33 -6
  59. package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  60. package/src/runs/shared/task-intent.ts +17 -6
  61. package/src/runs/shared/tool-timeout.ts +1 -1
  62. package/src/runs/shared/workflow-graph.ts +3 -2
  63. package/src/shared/atomic-json.ts +3 -1
  64. package/src/shared/fork-context.ts +0 -12
  65. package/src/shared/fork-session-cwd.ts +27 -0
  66. package/src/shared/launch-contract.ts +3 -0
  67. package/src/shared/types.ts +43 -4
  68. package/src/shared/workflow-child-permit.ts +91 -0
  69. package/src/slash/prompt-template-bridge.ts +37 -1
  70. package/src/slash/slash-commands.ts +18 -26
  71. package/src/slash/subagents-admin.ts +2 -0
  72. package/src/tui/render.ts +95 -52
  73. package/src/workflows/scripted-workflow.ts +153 -4
  74. package/src/workflows/workflow-child-summary.ts +1 -1
  75. package/src/workflows/workflow-receipt.ts +41 -4
  76. package/src/workflows/workflow-resources.ts +150 -0
@@ -10,33 +10,25 @@ const WORKFLOW_RESUME_KEY_GUIDANCE = "Each workflow key identifies one result la
10
10
  const WORKFLOW_OUTPUT_BINDING_GUIDANCE = "For durable workflow child files, set output on runs.run/runs.all; task filename prose is not an output declaration, and return the child's outputReference, outputPathMapping, or artifactPaths instead of inventing a literal path.";
11
11
  const WORKFLOW_LANES_GUIDANCE = "For bounded parallel sequential chains, use runs.lanes([{key,stages:[{key,agent,task},{key,resume:'previous',task},...]}]); first stages run together, later stages sequence per lane, and the bounded board reports lane-local failures. Only an explicit structuredOutput.verdict === 'blocked' blocks a successful stage; reviewer prose is not parsed.";
12
12
  const WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE = "workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains instead.";
13
- const WORKFLOW_HOST_GUIDANCE = "For one non-interactive operator-owned command, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). v1 supports only command steps; output is bounded and command failure fails the workflow.";
14
- const AGENT_CAPABILITY_GUIDANCE = "For capability selection, use { action: \"list\", capabilities: true } for compact prompt-free rows.";
13
+ const WORKFLOW_RESOURCE_GUIDANCE = "For permission/policy-extension interoperability, use an extension-owned named resource such as {workflow:'review',args:{task:'...'}} or {workflow:'run-ci',args:{command:'npm test'}}. The host resolves the script and authority internally so policy can distinguish it from raw workflowScript/workflowScriptPath; args are bounded plain data, and do not combine workflow with agent, task, workflowScript, or workflowScriptPath.";
14
+ const WORKFLOW_HOST_GUIDANCE = "For permission-sensitive host calls, use an extension-owned resource such as {workflow:'run-ci',args:{command:'npm test'}}; raw workflowScript/workflowScriptPath have unknown resource provenance and cannot use runs.host. In a resource that grants it, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). v1 supports only command steps; output is bounded and command failure fails the workflow.";
15
15
 
16
- export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, or workflowScriptPath to load a script from the request cwd. The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
16
+ export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
17
17
 
18
18
  export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
19
19
 
20
20
  export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
21
- `Use subagent only when delegation is needed. Before executing, call { action: "list" } and run only executable, non-disabled agents. ${AGENT_CAPABILITY_GUIDANCE}`,
22
- "Omit action for execution. Use { agent, task? } only for one child; use workflowScript for multi-step or parallel work.",
23
- "workflowScript means exactly one top-level subagent tool call with async:true. Inside it, use runs.run/runs.all to launch children; do not make another top-level subagent call for those children.",
24
- WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE,
25
- WORKFLOW_LANES_GUIDANCE,
26
- WORKFLOW_HOST_GUIDANCE,
27
- WORKFLOW_RESUME_KEY_GUIDANCE,
28
- WORKFLOW_OUTPUT_BINDING_GUIDANCE,
29
- "For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all.",
30
- "Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
31
- "To pass an explicit model to a child, first call { action: \"models\" } and copy an exact provider/id (e.g. provider/model-id); bare ids resolve only when unique in the registry, and agent names (gpt-pro, advisor) are not model ids. Set per-run thinking with a suffix on the model string (e.g. provider/model-id:high; off/minimal/low/medium/high/xhigh/max); the suffix wins over the agent's thinking default. The thinking field only applies to action='watchdog.configure' and is ignored on dispatch.",
32
- EXTERNAL_CLI_RUNNER_GUIDANCE,
33
- "Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
21
+ 'Use subagent only when delegation is needed. Before execution, call { action: "list" } and run only executable, non-disabled agents.',
22
+ 'Omit action for execution; use { agent, task? } for one child. For multi-step or parallel work, make exactly one top-level { workflowScript, async: true } call and launch children only inside it. Use action only for management/control.',
23
+ "workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions, or explicit Promise chains.",
24
+ "Inside workflowScript, use runs.run/runs.all and await their results. runs.all returns an ordered array, not a key map; stored runs.run promises must later be observed with direct await, Promise.race, or Promise.all.",
25
+ 'Keep one writer per cwd/worktree; isolate concurrent writers. For durable files, set output on runs.run/runs.all and return the child\'s outputReference, outputPathMapping, or artifactPaths. For advanced workflows, read the bundled pi-subagents skill or call { action: "guide", topic: "workflows" }.',
34
26
  ];
35
27
 
36
28
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
37
29
  • Use { action: "list" } before execution and only run executable/non-disabled agents.
38
30
  • Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
39
- • Async/background runs are the normal default unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Use async:false only when the parent must block until completion. Async mode still shows progress. Final reviews and gate checks stay async; needing a result is not a blocking reason. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
31
+ • Async/background runs are the normal default unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Use async:false only when the parent must block until completion. Async mode still shows progress. Final reviews and gate checks stay async; needing a result is not a blocking reason. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Ordinary async subagents notify this session natively, so return control and do not call bg_wait merely to get a completion wake. Do not sleep or poll status just to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
40
32
  • ${WORKFLOW_RESUME_KEY_GUIDANCE}
41
33
  • ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
42
34
  • ${WORKFLOW_HOST_GUIDANCE}
@@ -47,6 +39,8 @@ export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
47
39
 
48
40
  export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for inline orchestration or { workflowScriptPath } to load it from the request cwd. The script inputs are mutually exclusive. Omit action for execution. Use action only for management/control actions.
49
41
 
42
+ ${WORKFLOW_RESOURCE_GUIDANCE}
43
+
50
44
  EXECUTION:
51
45
  • ${EXTERNAL_CLI_RUNNER_GUIDANCE}
52
46
  • Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
@@ -63,12 +57,14 @@ EXECUTION:
63
57
  MANAGEMENT / CONTROL (use action; omit execution fields):
64
58
  • validate checks workflowScript or workflowScriptPath syntax and statically decidable structure without launching children. list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, worktree.cleanup (plan-only), lane.status, lane.recordMerge, lane.recordSupersession, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
65
59
  • status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
66
- • Create durable project schedules with { action:"schedule.create", id?, name?, at:"+10m" | ISO, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
60
+ • Create durable project schedules with { action:"schedule.create", id?, name?, sessionOnly?:true, at:"+10m" | ISO, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. With sessionOnly:true, the schedule records the creating session file and only that session can restore or execute it; omitted/false preserves project-wide behavior. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
67
61
 
68
62
  ${SUBAGENT_SAFETY_GUIDANCE}`;
69
63
 
70
64
  export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for inline orchestration or { workflowScriptPath } to load it from the request cwd. The script inputs are mutually exclusive. Omit action for execution. Use action only for management/control actions.
71
65
 
66
+ ${WORKFLOW_RESOURCE_GUIDANCE}
67
+
72
68
  EXECUTE:
73
69
  • ${EXTERNAL_CLI_RUNNER_GUIDANCE}
74
70
  • Call { action:"list" } first and use only executable/non-disabled agents.
@@ -85,7 +81,7 @@ MANAGE / CONTROL:
85
81
  • A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
86
82
 
87
83
  ASYNC / SAFETY:
88
- • Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
84
+ • Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Ordinary async subagents notify this session natively, so return control and do not call bg_wait merely to get a completion wake. Do not sleep or poll merely to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
89
85
  • ${WORKFLOW_RESUME_KEY_GUIDANCE}
90
86
  • ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
91
87
  • ${WORKFLOW_HOST_GUIDANCE}
@@ -33,6 +33,7 @@ export interface ActiveAsyncCapacityHandle {
33
33
  markStarted(runnerProcessInstanceId: string): void;
34
34
  markWorkflowStarted(): void;
35
35
  rollback(): boolean;
36
+ rollbackBeforeRunnerProceed(runnerProcessInstanceId: string): boolean;
36
37
  reconcile(liveWorkflowRunIds?: ReadonlySet<string>): ActiveAsyncCapacitySnapshot;
37
38
  }
38
39
 
@@ -43,6 +44,7 @@ interface CapacityOptions {
43
44
  abandonedSlotReleaseAfterMs?: number | false;
44
45
  pidLiveness?: (pid: number) => PidLiveness;
45
46
  afterSlotRename?: (releasedDir: string) => void;
47
+ writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwnerV1) => void;
46
48
  }
47
49
 
48
50
  export interface ActiveAsyncCapacityReleaseEvidence {
@@ -380,6 +382,7 @@ function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwnerV1): boolean
380
382
 
381
383
  function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: CapacityOptions, rollbackOwner?: ActiveAsyncCapacityOwnerV1): ActiveAsyncCapacityHandle {
382
384
  const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
385
+ const writeOwner = options.writeOwner ?? writePrivateAtomicJson;
383
386
  const dir = slotDir(sessionDir(owner.ownerSessionId, rootDir), owner.slot);
384
387
  return {
385
388
  owner,
@@ -388,14 +391,10 @@ function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: Ca
388
391
  const current = matchingOwner(dir, owner);
389
392
  if (!current) return false;
390
393
  const next = { ...current, runnerProcessInstanceId, runnerStartedAt: options.now?.() ?? Date.now() };
391
- // Mark memory first. If persistence fails after the process starts, caller
392
- // cleanup must retain the occupied slot instead of rolling it back.
394
+ // Mark memory first so pre-proceed cleanup can distinguish a failed durable
395
+ // bind from an unrelated unstarted reservation.
393
396
  Object.assign(owner, next);
394
- try {
395
- writePrivateAtomicJson(path.join(dir, "owner.json"), next);
396
- } catch (error) {
397
- console.error(`Failed to bind active async capacity to runner '${runnerProcessInstanceId}'; capacity will remain occupied:`, error);
398
- }
397
+ writeOwner(path.join(dir, "owner.json"), next);
399
398
  return true;
400
399
  });
401
400
  if (!claimed.acquired || !claimed.value) throw new Error(`Active async capacity ownership changed for run '${owner.runId}'.`);
@@ -427,6 +426,26 @@ function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: Ca
427
426
  });
428
427
  return claimed.acquired && claimed.value;
429
428
  },
429
+ rollbackBeforeRunnerProceed(runnerProcessInstanceId) {
430
+ const claimed = withSlotClaim(dir, () => {
431
+ const current = matchingOwner(dir, owner);
432
+ if (!current) return false;
433
+ const boundToRunner = current.runnerProcessInstanceId === runnerProcessInstanceId;
434
+ const bindingFailedBeforeProceed = current.runnerProcessInstanceId === undefined && current.runnerStartedAt === undefined && owner.runnerProcessInstanceId === runnerProcessInstanceId;
435
+ if (!boundToRunner && !bindingFailedBeforeProceed) return false;
436
+ if (rollbackOwner) {
437
+ writePrivateAtomicJson(path.join(dir, "owner.json"), rollbackOwner);
438
+ Object.assign(owner, rollbackOwner);
439
+ return true;
440
+ }
441
+ const releasedDir = path.join(path.dirname(dir), `.${path.basename(dir)}.released-${randomUUID()}`);
442
+ fs.renameSync(dir, releasedDir);
443
+ options.afterSlotRename?.(releasedDir);
444
+ fs.rmSync(releasedDir, { recursive: true, force: true });
445
+ return true;
446
+ });
447
+ return claimed.acquired && claimed.value;
448
+ },
430
449
  reconcile(liveWorkflowRunIds) {
431
450
  return reconcileActiveAsyncCapacity(owner.ownerSessionId, limit, { ...options, rootDir, liveWorkflowRunIds });
432
451
  },
@@ -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,14 +19,14 @@ 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";
26
26
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
27
27
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
28
28
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
29
- import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
29
+ import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
30
30
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
31
31
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
32
32
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.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;
@@ -235,6 +235,7 @@ interface AsyncSingleParams {
235
235
  structuredOutputSchema?: JsonSchemaObject;
236
236
  modelOverride?: string;
237
237
  modelOverrideFromParent?: boolean;
238
+ modelOrigin?: ModelOrigin;
238
239
  fast?: boolean;
239
240
  thinkingOverride?: AgentConfig["thinking"];
240
241
  availableModels?: AvailableModelInfo[];
@@ -339,15 +340,15 @@ export function formatAsyncStartedMessage(headline: string, interactive: boolean
339
340
  const guidance = interactive
340
341
  ? [
341
342
  "The async run is detached and running in the background.",
342
- "You are in an interactive session. By default, return control to the user now; Pi will wake you on completion when the run finishes or needs attention. Do NOT call subagent_wait() merely to wait, and do not run sleep/polling loops to wait for it.",
343
- "When you need an explicit wake for one known run but do not need same-turn results, call subagent_wait({ id: \"...\", nonBlocking: true }) to arm a subscription and return immediately.",
344
- "Override the default and call blocking subagent_wait() before ending the turn only when the current request is run-to-completion — for example, the user asked you to report results back here before continuing, or a skill must finish in one turn. In that case, call subagent_wait() to block until the run completes so its results are delivered in this turn instead of deferred.",
343
+ "You are in an interactive session. Return control to the user now; Pi will wake you through the native completion notification when this subagent completes or needs attention. Do not run sleep/polling loops to wait for this async subagent; it does not need a wait call.",
344
+ "Use bg_wait only for provider, detached, or other background work that lacks a native completion notification.",
345
+ "If the current turn must receive results from work without a native notification before it ends, call blocking bg_wait(); ordinary async subagent runs do not need a wait call because their completion is delivered natively.",
345
346
  "Otherwise, continue any independent work or return control to the user. Use subagent({ action: \"status\", id: \"...\" }) for a one-shot status/result or to inspect a blocked/stale run, never as a wait loop.",
346
347
  ]
347
348
  : [
348
349
  "The async run is detached. Do not run sleep timers or polling loops just to wait for it.",
349
- "This is a non-interactive run: Pi auto-drains current-session background work at agent_end so detached children are not abandoned; call subagent_wait() when this turn must receive the run's results before it ends, otherwise let the headless auto-drain finish the work.",
350
- "Use subagent({ action: \"status\", id: \"...\" }) when you need a one-shot status/result or to inspect a blocked/stale run. To block until completion, use subagent_wait() — do not poll in a loop.",
350
+ "This is a non-interactive run: Pi auto-drains current-session subagent work at agent_end so detached children are not abandoned. Use bg_wait only when this turn must receive provider, detached, or other background-work results that have no native completion notification.",
351
+ "Use subagent({ action: \"status\", id: \"...\" }) when you need a one-shot status/result or to inspect a blocked/stale run; do not poll in a loop.",
351
352
  ];
352
353
  return [headline, "", ...guidance].join("\n");
353
354
  }
@@ -431,13 +432,15 @@ function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupS
431
432
  return { ok: false, error: `Timed out after ${timeoutMs}ms waiting for the async runner startup state '${expectedState}'.`, startupDidNotProceed: true };
432
433
  }
433
434
 
435
+ const writePrivateStartupControlJson = createAtomicJsonWriter({ mode: 0o600, ignoreCleanupErrorAfterSuccess: true });
436
+
434
437
  function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "proceed"; token: string }): void {
435
438
  // Delegate to the shared atomic JSON writer (temp file + rename, retrying
436
439
  // transient Windows EPERM/EBUSY/EACCES locks and cleaning up the temp file
437
440
  // on failure), so the startup handshake gets the same locking resilience as
438
441
  // every other async control/result file. This is exercised by
439
442
  // test/unit/atomic-json.test.ts.
440
- writePrivateAtomicJson(filePath, payload);
443
+ writePrivateStartupControlJson(filePath, payload);
441
444
  }
442
445
 
443
446
  function runnerIsAlive(pid: number): boolean {
@@ -519,7 +522,7 @@ export function emitProcessTerminalEvent(ctx: AsyncExecutionContext, proof: unkn
519
522
  }
520
523
  }
521
524
 
522
- 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 {
523
526
  const cwdError = preflightLaunchCwd(requestedCwd, cwd);
524
527
  if (cwdError) return { error: cwdError };
525
528
 
@@ -636,6 +639,24 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
636
639
  });
637
640
  } catch (error) {
638
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);
639
660
  const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
640
661
  return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
641
662
  }
@@ -857,14 +878,15 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
857
878
  const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
858
879
 
859
880
  const modelScopes = resolveModelScopesForAgent(ctx.modelScope, a.name, ctx.currentModel);
860
- const primaryModelFromParent = inheritsParentModel(s.model, a.model, ctx.currentModel);
881
+ const modelOrigin = resolveModelOrigin({ explicitModel: s.model, agentModel: a.model, parentModel: ctx.currentModel });
882
+ const primaryModelFromParent = modelOrigin === "inherited";
861
883
  const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
862
884
  s.model,
863
885
  a.model,
864
886
  ctx.currentModel,
865
887
  availableModels,
866
888
  a.modelProvider ?? ctx.currentModelProvider,
867
- { scope: modelScopes },
889
+ { scope: modelScopes, source: modelOrigin === "explicit" ? "explicit" : "inherited" },
868
890
  );
869
891
  const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
870
892
  const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
@@ -884,15 +906,17 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
884
906
  }
885
907
  const agentContract = s.agentContract ?? params.agentContract;
886
908
  const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
887
- const modelCandidates = externalRunner ? [] : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
888
- scope: modelScopes,
889
- primaryModelFromParent,
890
- }).flatMap((candidate) => {
891
- const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
892
- return resolved ? [resolved] : [];
893
- });
909
+ let modelCandidates: string[] = [];
894
910
  if (!externalRunner) {
895
911
  try {
912
+ modelCandidates = buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
913
+ scope: modelScopes,
914
+ primaryModelFromParent,
915
+ origin: modelOrigin,
916
+ }).flatMap((candidate) => {
917
+ const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
918
+ return resolved ? [resolved] : [];
919
+ });
896
920
  for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
897
921
  } catch (error) {
898
922
  throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
@@ -901,6 +925,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
901
925
  const fast = s.fast ?? params.fast ?? a.fast;
902
926
  const toolPlan = resolvePiLaunchToolPlan({
903
927
  tools: a.tools,
928
+ excludeTools: a.excludeTools,
904
929
  allowNestedSubagents: a.allowNestedSubagents,
905
930
  extensions: a.extensions,
906
931
  subagentOnlyExtensions: a.subagentOnlyExtensions,
@@ -963,6 +988,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
963
988
  ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
964
989
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
965
990
  tools: a.tools,
991
+ excludeTools: a.excludeTools,
966
992
  allowNestedSubagents: a.allowNestedSubagents,
967
993
  extensions: a.extensions,
968
994
  subagentOnlyExtensions: a.subagentOnlyExtensions,
@@ -998,7 +1024,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
998
1024
  acceptanceRole: a.acceptanceRole,
999
1025
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
1000
1026
  ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
1001
- ...(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) }) } : {}),
1002
1028
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1003
1029
  ...(s.worktree ? { worktree: true } : {}),
1004
1030
  };
@@ -1167,7 +1193,7 @@ export function executeAsyncChain(
1167
1193
  chain: chain.map((step) => {
1168
1194
  if (isParallelStep(step)) return { parallel: step.parallel };
1169
1195
  if (isDynamicParallelStep(step)) return { acceptance: step.acceptance, parallel: step.parallel };
1170
- return { acceptance: step.acceptance };
1196
+ return { acceptance: step.acceptance, outputSchema: step.outputSchema };
1171
1197
  }),
1172
1198
  });
1173
1199
  if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
@@ -1328,6 +1354,7 @@ export function executeAsyncChain(
1328
1354
  },
1329
1355
  path.join(asyncDir, "status.json"),
1330
1356
  (proof) => emitProcessTerminalEvent(ctx, proof),
1357
+ (runnerProcessInstanceId) => params.activeAsyncCapacity?.markStarted(runnerProcessInstanceId),
1331
1358
  );
1332
1359
  } catch (error) {
1333
1360
  params.activeAsyncCapacity?.rollback();
@@ -1336,7 +1363,10 @@ export function executeAsyncChain(
1336
1363
  }
1337
1364
 
1338
1365
  if (spawnResult.error) {
1339
- 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();
1340
1370
  else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1341
1371
  return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${spawnResult.error}`);
1342
1372
  }
@@ -1344,8 +1374,6 @@ export function executeAsyncChain(
1344
1374
  params.activeAsyncCapacity?.rollback();
1345
1375
  return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': runner identity unavailable`);
1346
1376
  }
1347
- params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1348
-
1349
1377
  if (spawnResult.pid) {
1350
1378
  const eventFirstStep = eventChain[0];
1351
1379
  if (!eventFirstStep) {
@@ -1588,15 +1616,27 @@ export function executeAsyncSingle(
1588
1616
  : "";
1589
1617
  const taskText = readsInstruction + taskWithOutputInstruction;
1590
1618
  const modelScopes = resolveModelScopesForAgent(ctx.modelScope, agentConfig.name, ctx.currentModel);
1591
- const primaryModel = externalRunner ? undefined : params.modelOverrideFromParent
1592
- ? params.modelOverride
1593
- : resolveSubagentModelOverride(
1594
- params.modelOverride ?? agentConfig.model,
1595
- ctx.currentModel,
1596
- availableModels,
1597
- ctx.currentModelProvider,
1598
- { scope: modelScopes },
1599
- );
1619
+ const modelOrigin = resolveModelOrigin({
1620
+ fromParent: params.modelOverrideFromParent,
1621
+ storedOrigin: params.modelOrigin,
1622
+ explicitModel: params.modelOverrideFromParent ? undefined : params.modelOverride,
1623
+ agentModel: agentConfig.model,
1624
+ parentModel: ctx.currentModel,
1625
+ });
1626
+ let primaryModel: string | undefined;
1627
+ try {
1628
+ primaryModel = externalRunner ? undefined : modelOrigin === "inherited"
1629
+ ? params.modelOverride ?? (ctx.currentModel ? `${ctx.currentModel.provider}/${ctx.currentModel.id}` : undefined)
1630
+ : resolveSubagentModelOverride(
1631
+ params.modelOverride ?? agentConfig.model,
1632
+ ctx.currentModel,
1633
+ availableModels,
1634
+ ctx.currentModelProvider,
1635
+ { scope: modelScopes, source: modelOrigin === "explicit" ? "explicit" : "inherited" },
1636
+ );
1637
+ } catch (error) {
1638
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1639
+ }
1600
1640
  const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
1601
1641
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
1602
1642
  const contextLimit = model ? findModelInfo(model, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider)?.contextWindow : undefined;
@@ -1631,20 +1671,19 @@ export function executeAsyncSingle(
1631
1671
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1632
1672
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1633
1673
  const structuredOutput = params.structuredOutputSchema
1634
- ? 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) })
1635
1675
  : undefined;
1636
- const modelCandidates = externalRunner
1637
- ? []
1638
- : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
1639
- scope: modelScopes,
1640
- primaryModelFromParent: params.modelOverrideFromParent,
1641
- })
1642
- .flatMap((candidate) => {
1676
+ let modelCandidates: string[] = [];
1677
+ if (!externalRunner) {
1678
+ try {
1679
+ modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
1680
+ scope: modelScopes,
1681
+ primaryModelFromParent: modelOrigin === "inherited",
1682
+ origin: modelOrigin,
1683
+ }).flatMap((candidate) => {
1643
1684
  const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1644
1685
  return resolved ? [resolved] : [];
1645
1686
  });
1646
- if (!externalRunner) {
1647
- try {
1648
1687
  for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
1649
1688
  } catch (error) {
1650
1689
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
@@ -1652,6 +1691,7 @@ export function executeAsyncSingle(
1652
1691
  }
1653
1692
  const toolPlan = resolvePiLaunchToolPlan({
1654
1693
  tools: agentConfig.tools,
1694
+ excludeTools: agentConfig.excludeTools,
1655
1695
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1656
1696
  extensions: agentConfig.extensions,
1657
1697
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1698,6 +1738,7 @@ export function executeAsyncSingle(
1698
1738
  inheritSkills: agentConfig.inheritSkills,
1699
1739
  skills: resolvedSkills.map((skill) => skill.name),
1700
1740
  tools: toolPlan.effectiveToolAllowlist,
1741
+ ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1701
1742
  extensions: toolPlan.extensionArgs,
1702
1743
  mcpDirectTools: toolPlan.effectiveMcpTools,
1703
1744
  ...(outputPath ? { outputPath } : {}),
@@ -1730,11 +1771,13 @@ export function executeAsyncSingle(
1730
1771
  ...(model ? { model } : {}),
1731
1772
  ...(params.fast ?? recoveryAgentConfig.fast ? { fast: params.fast ?? recoveryAgentConfig.fast } : {}),
1732
1773
  ...(recoveryAgentConfig.modelProvider ? { modelProvider: recoveryAgentConfig.modelProvider } : {}),
1733
- ...(params.modelOverrideFromParent ? { modelOverrideFromParent: true } : {}),
1774
+ ...(modelOrigin === "inherited" ? { modelOverrideFromParent: true } : {}),
1775
+ modelOrigin,
1734
1776
  ...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
1735
1777
  ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1736
1778
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1737
1779
  ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1780
+ ...(recoveryAgentConfig.excludeTools ? { excludeTools: [...recoveryAgentConfig.excludeTools] } : {}),
1738
1781
  ...(recoveryAgentConfig.allowNestedSubagents !== undefined ? { allowNestedSubagents: recoveryAgentConfig.allowNestedSubagents } : {}),
1739
1782
  ...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
1740
1783
  ...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
@@ -1799,9 +1842,10 @@ export function executeAsyncSingle(
1799
1842
  thinking: resolveEffectiveThinking(model, effectiveThinking),
1800
1843
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1801
1844
  modelCandidates,
1802
- ...(params.modelOverrideFromParent ? { skipPrimaryModelVerification: true } : {}),
1845
+ ...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
1803
1846
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1804
1847
  tools: agentConfig.tools,
1848
+ excludeTools: agentConfig.excludeTools,
1805
1849
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1806
1850
  extensions: agentConfig.extensions,
1807
1851
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1830,6 +1874,7 @@ export function executeAsyncSingle(
1830
1874
  ...(extensionBindings ? { extensionBindings } : {}),
1831
1875
  launchResolvedExtensions,
1832
1876
  effectiveAcceptance: resolvedAcceptance,
1877
+ acceptanceInput: params.acceptance,
1833
1878
  ...(structuredOutput ? { structuredOutput } : {}),
1834
1879
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1835
1880
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
@@ -1898,6 +1943,7 @@ export function executeAsyncSingle(
1898
1943
  },
1899
1944
  path.join(asyncDir, "status.json"),
1900
1945
  (proof) => emitProcessTerminalEvent(ctx, proof),
1946
+ (runnerProcessInstanceId) => params.activeAsyncCapacity?.markStarted(runnerProcessInstanceId),
1901
1947
  params.requestedCwd ?? runnerCwd,
1902
1948
  );
1903
1949
  } catch (error) {
@@ -1907,7 +1953,10 @@ export function executeAsyncSingle(
1907
1953
  }
1908
1954
 
1909
1955
  if (spawnResult.error) {
1910
- 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();
1911
1960
  else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1912
1961
  return formatAsyncStartError("single", `Failed to start async run '${id}': ${spawnResult.error}`);
1913
1962
  }
@@ -1915,8 +1964,6 @@ export function executeAsyncSingle(
1915
1964
  params.activeAsyncCapacity?.rollback();
1916
1965
  return formatAsyncStartError("single", `Failed to start async run '${id}': runner identity unavailable`);
1917
1966
  }
1918
- params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
1919
-
1920
1967
  if (spawnResult.pid) {
1921
1968
  if (inheritedNestedRoute && nestedAddress) {
1922
1969
  const now = Date.now();