pi-subagents 0.65.1 → 0.67.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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -12,7 +12,7 @@ export const DEFAULT_ABANDONED_SLOT_RELEASE_AFTER_MS = 20 * 60 * 1000;
12
12
  export const MIN_ABANDONED_SLOT_RELEASE_AFTER_MS = 5 * 60 * 1000;
13
13
  export const MAX_ABANDONED_SLOT_RELEASE_AFTER_MS = 24 * 60 * 60 * 1000;
14
14
 
15
- export interface ActiveAsyncCapacityOwnerV1 {
15
+ export interface ActiveAsyncCapacityOwner {
16
16
  version: 1;
17
17
  reservationToken: string;
18
18
  ownerSessionId: string;
@@ -29,7 +29,7 @@ export interface ActiveAsyncCapacityOwnerV1 {
29
29
  }
30
30
 
31
31
  export interface ActiveAsyncCapacityHandle {
32
- readonly owner: ActiveAsyncCapacityOwnerV1;
32
+ readonly owner: ActiveAsyncCapacityOwner;
33
33
  markStarted(runnerProcessInstanceId: string): void;
34
34
  markWorkflowStarted(): void;
35
35
  rollback(): boolean;
@@ -44,7 +44,7 @@ interface CapacityOptions {
44
44
  abandonedSlotReleaseAfterMs?: number | false;
45
45
  pidLiveness?: (pid: number) => PidLiveness;
46
46
  afterSlotRename?: (releasedDir: string) => void;
47
- writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwnerV1) => void;
47
+ writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwner) => void;
48
48
  }
49
49
 
50
50
  export interface ActiveAsyncCapacityReleaseEvidence {
@@ -61,7 +61,7 @@ export type ActiveAsyncCapacityReleaseVerdict =
61
61
  | { state: "not-owned"; reason: string };
62
62
 
63
63
  export interface ActiveAsyncCapacityInspection {
64
- owner?: ActiveAsyncCapacityOwnerV1;
64
+ owner?: ActiveAsyncCapacityOwner;
65
65
  relation: "current" | "source" | "none";
66
66
  slotDir?: string;
67
67
  release: ActiveAsyncCapacityReleaseVerdict;
@@ -100,9 +100,9 @@ function slotDir(poolDir: string, slot: number): string {
100
100
  return path.join(poolDir, `slot-${slot}`);
101
101
  }
102
102
 
103
- function parseOwner(value: unknown): ActiveAsyncCapacityOwnerV1 | undefined {
103
+ function parseOwner(value: unknown): ActiveAsyncCapacityOwner | undefined {
104
104
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
105
- const owner = value as Partial<ActiveAsyncCapacityOwnerV1>;
105
+ const owner = value as Partial<ActiveAsyncCapacityOwner>;
106
106
  if (owner.version !== 1
107
107
  || typeof owner.reservationToken !== "string" || !owner.reservationToken
108
108
  || typeof owner.ownerSessionId !== "string" || !owner.ownerSessionId
@@ -116,10 +116,10 @@ function parseOwner(value: unknown): ActiveAsyncCapacityOwnerV1 | undefined {
116
116
  if (owner.sourceRunId !== undefined && typeof owner.sourceRunId !== "string") return undefined;
117
117
  if (owner.runnerProcessInstanceId !== undefined && (typeof owner.runnerProcessInstanceId !== "string" || !owner.runnerProcessInstanceId)) return undefined;
118
118
  if (owner.runnerStartedAt !== undefined && (typeof owner.runnerStartedAt !== "number" || !Number.isFinite(owner.runnerStartedAt))) return undefined;
119
- return owner as ActiveAsyncCapacityOwnerV1;
119
+ return owner as ActiveAsyncCapacityOwner;
120
120
  }
121
121
 
122
- function readOwner(dir: string): ActiveAsyncCapacityOwnerV1 | undefined {
122
+ function readOwner(dir: string): ActiveAsyncCapacityOwner | undefined {
123
123
  try {
124
124
  return parseOwner(JSON.parse(fs.readFileSync(path.join(dir, "owner.json"), "utf-8")));
125
125
  } catch {
@@ -142,7 +142,7 @@ function snapshotFor(sessionId: string, limit: number | undefined, rootDir: stri
142
142
  return { used: occupiedSlots(sessionDir(sessionId, rootDir)).length, limit: limit ?? 0 };
143
143
  }
144
144
 
145
- function matchingOwner(dir: string, expected: ActiveAsyncCapacityOwnerV1): ActiveAsyncCapacityOwnerV1 | undefined {
145
+ function matchingOwner(dir: string, expected: ActiveAsyncCapacityOwner): ActiveAsyncCapacityOwner | undefined {
146
146
  const owner = readOwner(dir);
147
147
  return owner?.reservationToken === expected.reservationToken
148
148
  && owner.runId === expected.runId
@@ -176,7 +176,7 @@ function withSlotClaim<T>(dir: string, operation: () => T): { acquired: true; va
176
176
  }
177
177
  }
178
178
 
179
- function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwnerV1, options: CapacityOptions, requireUnstarted = false, release?: ActiveAsyncCapacityReleaseVerdict): boolean {
179
+ function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwner, options: CapacityOptions, requireUnstarted = false, release?: ActiveAsyncCapacityReleaseVerdict): boolean {
180
180
  if (requireUnstarted && (expected.runnerProcessInstanceId || expected.runnerStartedAt)) return false;
181
181
  const claimed = withSlotClaim(dir, () => {
182
182
  const current = matchingOwner(dir, expected);
@@ -193,7 +193,7 @@ function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwnerV1, opti
193
193
  return claimed.acquired && claimed.value;
194
194
  }
195
195
 
196
- function appendAbandonedReleaseEvent(asyncDir: string, owner: ActiveAsyncCapacityOwnerV1, evidence: ActiveAsyncCapacityReleaseEvidence, now: number): void {
196
+ function appendAbandonedReleaseEvent(asyncDir: string, owner: ActiveAsyncCapacityOwner, evidence: ActiveAsyncCapacityReleaseEvidence, now: number): void {
197
197
  try {
198
198
  const eventsPath = path.join(asyncDir, "events.jsonl");
199
199
  fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
@@ -213,7 +213,7 @@ function terminalState(state: AsyncStatus["state"]): boolean {
213
213
  return state !== "queued" && state !== "running" && state !== "paused";
214
214
  }
215
215
 
216
- function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus | null, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
216
+ function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncStatus | null, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
217
217
  if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
218
218
  if (!owner.runnerProcessInstanceId) return { state: "retained", reason: "runner process identity has not been recorded" };
219
219
  if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
@@ -262,7 +262,7 @@ function abandonedRunnerReleaseVerdict(status: AsyncStatus, proofState: string,
262
262
  };
263
263
  }
264
264
 
265
- function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus | null, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
265
+ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncStatus | null, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
266
266
  if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
267
267
  if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
268
268
  if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
@@ -280,6 +280,10 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: Async
280
280
  if (!childStatus) return { state: "retained", reason: `async workflow child ${label} status is missing or unreadable` };
281
281
  if (!terminalState(childStatus.state)) return { state: "retained", reason: `async workflow child ${label} is still ${childStatus.state}` };
282
282
  if (!childStatus.processTerminal?.runnerProcessInstanceId) return { state: "retained", reason: `async workflow child ${label} has no runner process identity` };
283
+ if (childStatus.processTerminal.state === "not-started"
284
+ && childStatus.processTerminal.runId === step.runId
285
+ && typeof childStatus.error === "string"
286
+ && childStatus.error) continue;
283
287
  const proof = readProcessTerminal(childDir, {
284
288
  runId: step.runId,
285
289
  runnerProcessInstanceId: childStatus.processTerminal.runnerProcessInstanceId,
@@ -289,7 +293,7 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: Async
289
293
  return { state: "releasable", reason: "workflow is terminal, controller is gone, and async children have observed proof" };
290
294
  }
291
295
 
292
- function ownerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, liveWorkflowRunIds: ReadonlySet<string>, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
296
+ function ownerReleaseVerdict(owner: ActiveAsyncCapacityOwner, liveWorkflowRunIds: ReadonlySet<string>, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
293
297
  const status = readStatus(owner.asyncDir);
294
298
  return owner.kind === "runner"
295
299
  ? runnerReleaseVerdict(owner, status, options)
@@ -364,7 +368,7 @@ export function getActiveAsyncCapacitySnapshot(
364
368
  return reconcileActiveAsyncCapacity(sessionId, limit, options);
365
369
  }
366
370
 
367
- function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwnerV1): boolean {
371
+ function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwner): boolean {
368
372
  const destination = slotDir(poolDir, owner.slot);
369
373
  fs.mkdirSync(poolDir, { recursive: true, mode: 0o700 });
370
374
  try {
@@ -379,7 +383,7 @@ function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwnerV1): boolean
379
383
  return true;
380
384
  }
381
385
 
382
- function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: CapacityOptions, rollbackOwner?: ActiveAsyncCapacityOwnerV1): ActiveAsyncCapacityHandle {
386
+ function handleFor(owner: ActiveAsyncCapacityOwner, limit: number, options: CapacityOptions, rollbackOwner?: ActiveAsyncCapacityOwner): ActiveAsyncCapacityHandle {
383
387
  const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
384
388
  const writeOwner = options.writeOwner ?? writePrivateAtomicJson;
385
389
  const dir = slotDir(sessionDir(owner.ownerSessionId, rootDir), owner.slot);
@@ -462,7 +466,7 @@ export function acquireActiveAsyncCapacity(
462
466
  const poolDir = sessionDir(input.sessionId, rootDir);
463
467
  const token = options.token?.() ?? randomUUID();
464
468
  for (let slot = 0; slot < input.limit; slot++) {
465
- const owner: ActiveAsyncCapacityOwnerV1 = {
469
+ const owner: ActiveAsyncCapacityOwner = {
466
470
  version: 1,
467
471
  reservationToken: token,
468
472
  ownerSessionId: input.sessionId,
@@ -495,7 +499,7 @@ export function transferActiveAsyncCapacity(
495
499
  if (!current || !status || status.runId !== input.sourceRunId || status.state === "queued" || status.state === "running") {
496
500
  throw new Error(`Active async capacity source '${input.sourceRunId}' is not transferable.`);
497
501
  }
498
- const next: ActiveAsyncCapacityOwnerV1 = {
502
+ const next: ActiveAsyncCapacityOwner = {
499
503
  ...current,
500
504
  runId: input.runId,
501
505
  sourceRunId: input.sourceRunId,
@@ -10,23 +10,22 @@ import { fileURLToPath } from "node:url";
10
10
  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
- import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
14
13
  import { createAtomicJsonWriter, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
14
+ import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.ts";
15
15
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
16
  import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
17
- import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
18
- import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
17
+ import { applyThinkingSuffix, getHostBuiltinToolNames, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
18
+ import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
19
19
  import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
20
20
  import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
21
21
  import type { RunnerStep } from "../shared/parallel-utils.ts";
22
22
  import type { ContextMode } from "../shared/context-mode.ts";
23
- import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
23
+ import { PI_CODING_AGENT_PACKAGE, resolveBunPiExecutable, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
24
24
  import { JITI_ALIAS_ENV, resolveHostPeerAliases } from "./runner-aliases.ts";
25
25
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
26
26
  import { resolveNodeExecutable } from "../../shared/node-executable.ts";
27
27
  import { backgroundProcessOptions } from "../shared/background-process-options.ts";
28
- import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
29
- import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
28
+ import { normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
30
29
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
31
30
  import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
32
31
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
@@ -80,7 +79,7 @@ import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
80
79
  import { statusStepDescription } from "./chain-append.ts";
81
80
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
82
81
  import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
83
- import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
82
+ import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
84
83
  import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
85
84
  import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
86
85
  import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
@@ -542,15 +541,20 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
542
541
  const cwdError = preflightLaunchCwd(requestedCwd, cwd);
543
542
  if (cwdError) return { error: cwdError };
544
543
 
545
- if (!jitiCliPath) {
544
+ // The compiled host exposes its SDK only through Pi's extension loader.
545
+ const binaryHost = resolveBunPiExecutable();
546
+ const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
547
+ const bootstrap = path.join(path.dirname(runner), "binary-bootstrap.ts");
548
+ if (binaryHost && !fs.existsSync(bootstrap)) return { error: `Background runner bootstrap not found: ${bootstrap}` };
549
+ if (!binaryHost && !jitiCliPath) {
546
550
  return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
547
551
  }
548
- if (!piPackageRoot) {
549
- return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}); a standalone pi binary has no package directory, so the async runner cannot create child sessions. Run this child in the foreground (async: false) or install pi from npm.` };
552
+ if (!binaryHost && !piPackageRoot) {
553
+ return { error: `Background children require a supported standalone Pi host or the installed npm package (${PI_CODING_AGENT_PACKAGE}); neither is available.` };
550
554
  }
551
- const hostPeerAliases = resolveHostPeerAliases(piPackageRoot);
555
+ const hostPeerAliases = !binaryHost && piPackageRoot ? resolveHostPeerAliases(piPackageRoot) : { aliases: {}, missing: [] };
552
556
  if (hostPeerAliases.missing.length > 0) {
553
- return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}, so the async runner cannot create child sessions. A standalone pi binary cannot run background children.` };
557
+ return { error: `Background children require the host npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}.` };
554
558
  }
555
559
 
556
560
  fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
@@ -560,8 +564,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
560
564
  const launchBarrierToken = hasRevivalLease ? undefined : runnerProcessInstanceId;
561
565
  const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}) };
562
566
  writePrivateAtomicJson(cfgPath, launchConfig);
563
- const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
564
- const nodeCommand = resolveNodeExecutable();
567
+ const command = binaryHost ?? resolveNodeExecutable();
565
568
  const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
566
569
  const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
567
570
  const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
@@ -587,17 +590,23 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
587
590
  stdoutFd = fs.openSync(logPaths.stdoutPath, "a");
588
591
  stderrFd = fs.openSync(logPaths.stderrPath, "a");
589
592
  }
590
- const preload = hostPeerAliases.supplemental.length > 0
591
- ? ["--import", new URL("../../../runner-server-preload.mjs", import.meta.url).href]
593
+ const preload = Object.keys(hostPeerAliases.aliases).length > 0
594
+ ? ["--import", new URL("../../../runner-peer-preload.mjs", import.meta.url).href]
592
595
  : [];
593
- const proc = spawn(nodeCommand, [...preload, jitiCliPath, runner, cfgPath], {
596
+ const args = binaryHost
597
+ ? ["--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session", "--mode", "rpc", "--extension", bootstrap]
598
+ : [...preload, jitiCliPath!, runner, cfgPath];
599
+ const proc = spawn(command, args, {
594
600
  cwd,
595
601
  ...backgroundProcessOptions(),
596
602
  stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
597
603
  env: {
598
604
  ...omitExtensionBindingsEnv(process.env),
599
- [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot,
600
- [JITI_ALIAS_ENV]: JSON.stringify(hostPeerAliases.aliases),
605
+ [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: binaryHost ? undefined : piPackageRoot,
606
+ // npm must override inherited bundled layouts (#2071); binaries retain release assets.
607
+ PI_PACKAGE_DIR: binaryHost ? process.env.PI_PACKAGE_DIR : piPackageRoot,
608
+ [JITI_ALIAS_ENV]: binaryHost ? undefined : JSON.stringify(hostPeerAliases.aliases),
609
+ PI_SUBAGENT_RUNNER_CONFIG: binaryHost ? cfgPath : undefined,
601
610
  },
602
611
  });
603
612
  closeFd(stdoutFd);
@@ -891,22 +900,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
891
900
  );
892
901
  if (missingSkills.includes("pi-subagents")) throw new UnavailableSubagentSkillError(UNAVAILABLE_SUBAGENT_SKILL_ERROR);
893
902
 
894
- let systemPrompt = a.systemPrompt?.trim() ?? "";
895
- if (resolvedSkills.length > 0) {
896
- const injection = buildSkillInjection(resolvedSkills);
897
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
898
- }
899
- const memoryInjection = buildAgentMemoryInjection(a, stepCwd);
900
- if (memoryInjection) {
901
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
902
- }
903
- systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: stepCwd, agentName: a.name });
903
+ // A namespaced parallel output is injected by the runner, not the prompt.
904
+ const systemPrompt = buildEffectiveSystemPrompt({ agent: a, resolvedSkills, cwd: stepCwd, ...(!namespaceOutputPath && outputPath ? { outputPath } : {}) });
904
905
 
905
906
  const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false, undefined, readExistenceCwd);
906
907
  const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
907
908
  if (behavior.progress) progressInstructionCreated = true;
908
909
  const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
909
- if (!namespaceOutputPath) systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, a);
910
910
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
911
911
  if (validationError) throw new AsyncStartValidationError(validationError);
912
912
  let taskTemplate = s.task ?? "{previous}";
@@ -963,6 +963,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
963
963
  const launchRuleError = applyWatchdogLaunchRules({ cwd: stepCwd, agent: a.name, model: modelCandidates[0] ?? model, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
964
964
  if (launchRuleError) throw new AsyncStartValidationError(launchRuleError);
965
965
  const fast = s.fast ?? params.fast ?? a.fast;
966
+ const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
966
967
  const toolPlan = resolvePiLaunchToolPlan({
967
968
  tools: a.tools,
968
969
  excludeTools: a.excludeTools,
@@ -981,6 +982,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
981
982
  agentName: a.name,
982
983
  permissionRules,
983
984
  runtimeSnapshotHost: ctx.pi,
985
+ hostAvailableBuiltins,
984
986
  });
985
987
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
986
988
  if (externalRunner && permissionRules) {
@@ -1377,6 +1379,7 @@ export function executeAsyncChain(
1377
1379
  deadlineAt,
1378
1380
  globalConcurrencyLimit: params.globalConcurrencyLimit,
1379
1381
  runFanoutBudget,
1382
+ hostAvailableBuiltins: getHostBuiltinToolNames(ctx.pi),
1380
1383
  workflowGraph,
1381
1384
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1382
1385
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
@@ -1636,16 +1639,6 @@ export function executeAsyncSingle(
1636
1639
  agentConfig.filePath ? path.dirname(agentConfig.filePath) : runnerCwd,
1637
1640
  );
1638
1641
  if (missingSkills.includes("pi-subagents")) return formatAsyncStartError("single", UNAVAILABLE_SUBAGENT_SKILL_ERROR);
1639
- let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
1640
- if (resolvedSkills.length > 0) {
1641
- const injection = buildSkillInjection(resolvedSkills);
1642
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
1643
- }
1644
- const memoryInjection = buildAgentMemoryInjection(agentConfig, runnerCwd);
1645
- if (memoryInjection) {
1646
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
1647
- }
1648
- systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: runnerCwd, agentName: agentConfig.name });
1649
1642
 
1650
1643
  const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
1651
1644
  const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
@@ -1668,7 +1661,7 @@ export function executeAsyncSingle(
1668
1661
 
1669
1662
  const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
1670
1663
  const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
1671
- systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
1664
+ const systemPrompt = buildEffectiveSystemPrompt({ agent: agentConfig, resolvedSkills, cwd: runnerCwd, ...(outputPath ? { outputPath } : {}) });
1672
1665
  const outputMode = params.outputMode ?? agentConfig.outputMode ?? "inline";
1673
1666
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
1674
1667
  if (validationError) return formatAsyncStartError("single", validationError);
@@ -1759,6 +1752,7 @@ export function executeAsyncSingle(
1759
1752
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1760
1753
  }
1761
1754
  }
1755
+ const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
1762
1756
  const toolPlan = resolvePiLaunchToolPlan({
1763
1757
  tools: agentConfig.tools,
1764
1758
  excludeTools: agentConfig.excludeTools,
@@ -1777,6 +1771,7 @@ export function executeAsyncSingle(
1777
1771
  agentName: agentConfig.name,
1778
1772
  permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
1779
1773
  runtimeSnapshotHost: ctx.pi,
1774
+ hostAvailableBuiltins,
1780
1775
  });
1781
1776
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1782
1777
  if (!externalRunner) {
@@ -1793,24 +1788,17 @@ export function executeAsyncSingle(
1793
1788
  });
1794
1789
  if (contractError) return formatAsyncStartError("single", contractError);
1795
1790
  }
1796
- const launchContractDigest = launchBindingDigest({
1797
- definitionDigest: agentDefinitionDigest(agentConfig),
1791
+ const fast = params.fast ?? agentConfig.fast;
1792
+ const launchThinking = resolveEffectiveThinking(model, effectiveThinking);
1793
+ const { definitionDigest, launchContractDigest } = resolveLaunchBinding({
1794
+ agent: agentConfig,
1798
1795
  task,
1799
- ...(model ? { model } : {}),
1800
1796
  modelCandidates,
1801
- ...((params.fast ?? agentConfig.fast) !== undefined ? { fast: params.fast ?? agentConfig.fast } : {}),
1802
- ...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1803
- ...(thinkingCeiling ? { thinkingCeiling } : {}),
1797
+ ...(fast !== undefined ? { fast } : {}),
1798
+ ...(launchThinking ? { thinking: launchThinking } : {}),
1804
1799
  systemPrompt,
1805
- systemPromptMode: agentConfig.systemPromptMode,
1806
- inheritProjectContext: agentConfig.inheritProjectContext,
1807
- inheritGlobalContext: agentConfig.inheritGlobalContext,
1808
- inheritSkills: agentConfig.inheritSkills,
1809
1800
  skills: resolvedSkills.map((skill) => skill.name),
1810
- tools: toolPlan.effectiveToolAllowlist,
1811
- ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1812
- extensions: toolPlan.extensionArgs,
1813
- mcpDirectTools: toolPlan.effectiveMcpTools,
1801
+ toolPlan,
1814
1802
  ...(outputPath ? { outputPath } : {}),
1815
1803
  outputMode,
1816
1804
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
@@ -1939,7 +1927,7 @@ export function executeAsyncSingle(
1939
1927
  waitToolEnabled: params.waitToolEnabled,
1940
1928
  waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
1941
1929
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1942
- definitionDigest: agentDefinitionDigest(agentConfig),
1930
+ definitionDigest,
1943
1931
  launchBindingTask: task,
1944
1932
  launchContractDigest,
1945
1933
  ...(extensionBindings ? { extensionBindings } : {}),
@@ -1988,6 +1976,7 @@ export function executeAsyncSingle(
1988
1976
  launchContractDigest,
1989
1977
  launchResolvedExtensions,
1990
1978
  runFanoutBudget,
1979
+ hostAvailableBuiltins,
1991
1980
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1992
1981
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1993
1982
  ...(lane ? { lane } : {}),
@@ -26,6 +26,7 @@ import { EXTERNAL_JOB_BRIDGE_REQUEST_DIR, serviceExternalJobBridgeRequests } fro
26
26
  import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
27
27
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
28
28
  import { validHostStepNodes } from "../shared/host-step-status.ts";
29
+ import { readProcessTerminal } from "./process-terminal.ts";
29
30
  import { withCachedUiContext } from "../../shared/extension-context.ts";
30
31
 
31
32
  interface AsyncJobTrackerOptions {
@@ -78,6 +79,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
78
79
  let widgetRerenderTimer: ReturnType<typeof setTimeout> | undefined;
79
80
  const runningJobIds = new Set<string>();
80
81
  const externalJobBridgeRuns = new Set<string>();
82
+ // Early native failure is visible before its publisher finishes. Retain only
83
+ // that scoped observation, using the existing liveness sweep to renew delivery.
84
+ const terminalPublications = new Map<string, { instanceId: string; pending: true } | { pending: false }>();
81
85
  const externalJobBridgeEligibility = (steps: AsyncJobState["steps"]): "required" | "not-required" | "unknown" => {
82
86
  if (!Array.isArray(steps)) return "unknown";
83
87
  for (const step of steps) {
@@ -380,6 +384,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
380
384
  refreshTimers.delete(asyncId);
381
385
  runningJobIds.delete(asyncId);
382
386
  externalJobBridgeRuns.delete(asyncId);
387
+ terminalPublications.delete(asyncId);
383
388
  };
384
389
 
385
390
  const refreshJob = (job: AsyncJobState): boolean => {
@@ -440,6 +445,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
440
445
  if (status) {
441
446
  const previousStatus = job.status;
442
447
  job.status = status.state;
448
+ if (!terminalStatus(job.status)) terminalPublications.delete(job.asyncId);
443
449
  if (job.status === "running") runningJobIds.add(job.asyncId);
444
450
  else runningJobIds.delete(job.asyncId);
445
451
  if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
@@ -497,9 +503,30 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
497
503
  job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
498
504
  job.sessionFile = status.sessionFile ?? job.sessionFile;
499
505
  if (terminalStatus(job.status)) {
500
- if (!terminalStatus(previousStatus)) options.onJobTerminal?.();
506
+ let publication = terminalPublications.get(job.asyncId);
507
+ if (!publication && status.mode !== "workflow" && status.processTerminal?.state === "pending"
508
+ && status.runId === job.asyncId && status.sessionId === state.currentSessionId
509
+ && status.completionOwnerId && status.completionOwnerId === state.completionOwnerId) {
510
+ publication = { instanceId: status.processTerminal.runnerProcessInstanceId, pending: true };
511
+ terminalPublications.set(job.asyncId, publication);
512
+ }
513
+ const wasPending = publication?.pending;
514
+ if (publication?.pending) {
515
+ // Reconciliation resolves pending as well as public payloads. The
516
+ // watcher already discovers exact tracked run IDs without promotion.
517
+ const published = reconciliation.resultPath && fs.existsSync(reconciliation.resultPath);
518
+ const closed = !published && readProcessTerminal(job.asyncDir, {
519
+ runId: job.asyncId, runnerProcessInstanceId: publication.instanceId,
520
+ })?.state === "observed";
521
+ if (published || closed) {
522
+ publication = { pending: false };
523
+ terminalPublications.set(job.asyncId, publication);
524
+ } else cancelCleanup(job.asyncId);
525
+ }
526
+ // Scan on close too: publication may have raced the payload check.
527
+ if (!terminalStatus(previousStatus) || (wasPending && !publication?.pending)) options.onJobTerminal?.();
501
528
  rememberFleetJob(state, job);
502
- if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
529
+ if (!publication?.pending && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
503
530
  scheduleCleanup(job.asyncId);
504
531
  }
505
532
  }
@@ -526,7 +553,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
526
553
  }
527
554
  runningJobIds.delete(job.asyncId);
528
555
  rememberFleetJob(state, job);
529
- if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
556
+ if (!terminalPublications.get(job.asyncId)?.pending && !hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
530
557
  }
531
558
  return widgetRenderKey(job, widgetExpanded) !== widgetStateBefore;
532
559
  };
@@ -668,6 +695,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
668
695
  const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
669
696
  state.liveAsyncSessionRoots?.delete(info.id);
670
697
  externalJobBridgeRuns.delete(info.id);
698
+ terminalPublications.delete(info.id);
671
699
  state.asyncJobs.set(info.id, {
672
700
  asyncId: info.id,
673
701
  asyncDir,
@@ -721,6 +749,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
721
749
  job.asyncDir = result.asyncDir;
722
750
  watchJob(job);
723
751
  }
752
+ // Delivery can precede the first terminal status refresh. Remember its
753
+ // settlement even when no pending publication has been observed yet.
754
+ terminalPublications.set(asyncId, { pending: false });
724
755
  try {
725
756
  updateAsyncJobNestedProjection(job);
726
757
  } catch (error) {
@@ -745,6 +776,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
745
776
  widgetRerenderTimer = undefined;
746
777
  runningJobIds.clear();
747
778
  externalJobBridgeRuns.clear();
779
+ terminalPublications.clear();
748
780
  };
749
781
 
750
782
  const resetJobs = (ctx?: ExtensionContext) => {
@@ -15,6 +15,7 @@ import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/paral
15
15
  import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
16
16
  import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
17
17
  import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
18
+ import { validateIntercomBridgeConfig } from "../../intercom/intercom-bridge.ts";
18
19
  import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
19
20
 
20
21
  export interface AsyncResumeParams {
@@ -318,7 +319,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
318
319
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
319
320
  const parsed = value as Record<string, unknown>;
320
321
  const allowedFields = new Set([
321
- "modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
322
+ "modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "fast", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
322
323
  "subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
323
324
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
324
325
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
@@ -352,6 +353,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
352
353
  if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
353
354
  if (parsed.context !== undefined && parsed.context !== "fresh" && parsed.context !== "fork") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': context is invalid.`);
354
355
  if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
356
+ if (parsed.fast !== undefined && typeof parsed.fast !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': fast must be a boolean.`);
355
357
  if (parsed.modelOrigin !== undefined && parsed.modelOrigin !== "explicit" && parsed.modelOrigin !== "inherited" && parsed.modelOrigin !== "configured") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOrigin must be 'explicit', 'inherited', or 'configured'.`);
356
358
  if (parsed.modelOrigin === undefined && parsed.model !== undefined) parsed.modelOrigin = parsed.modelOverrideFromParent ? "inherited" : "configured";
357
359
  for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
@@ -411,14 +413,8 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
411
413
  if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
412
414
  }
413
415
  if (parsed.intercomBridge !== undefined) {
414
- if (!parsed.intercomBridge || typeof parsed.intercomBridge !== "object" || Array.isArray(parsed.intercomBridge)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge must be an object.`);
415
- const bridge = parsed.intercomBridge as Record<string, unknown>;
416
- for (const field of Object.keys(bridge)) {
417
- if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
418
- }
419
- if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.mode is invalid.`);
420
- if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
421
- if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
416
+ const bridge = validateIntercomBridgeConfig({ value: parsed.intercomBridge, label: "intercomBridge" });
417
+ if (!bridge.ok) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${bridge.error}`);
422
418
  }
423
419
  if (parsed.controlConfig !== undefined) {
424
420
  if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
@@ -1,8 +1,8 @@
1
1
  import type { AsyncJobState, SubagentState } from "../../shared/types.ts";
2
2
  import {
3
- projectAsyncStatusSnapshot,
3
+ projectAsyncStatusSnapshot as buildAsyncStatusSnapshot,
4
4
  type AsyncStatusSnapshotOptions,
5
- type AsyncStatusSnapshotV1,
5
+ type AsyncStatusSnapshot,
6
6
  } from "../shared/async-status-projection.ts";
7
7
 
8
8
  export {
@@ -10,22 +10,20 @@ export {
10
10
  ASYNC_STATUS_SNAPSHOT_VERSION,
11
11
  } from "../shared/async-status-projection.ts";
12
12
  export type {
13
- AsyncStatusSnapshotActivityV1,
14
- AsyncStatusSnapshotCapsV1,
15
- AsyncStatusSnapshotHostStepV1,
13
+ AsyncStatusSnapshotActivity,
14
+ AsyncStatusSnapshotCaps,
15
+ AsyncStatusSnapshotHostStep,
16
16
  AsyncStatusSnapshotKind,
17
- AsyncStatusSnapshotNodeV1,
18
- AsyncStatusSnapshotOmittedV1,
17
+ AsyncStatusSnapshotNode,
18
+ AsyncStatusSnapshotOmitted,
19
19
  AsyncStatusSnapshotOptions,
20
20
  AsyncStatusSnapshotState,
21
- AsyncStatusSnapshotV1,
21
+ AsyncStatusSnapshot,
22
22
  } from "../shared/async-status-projection.ts";
23
23
 
24
24
  export const ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX = "PI_SUBAGENT_ASYNC_JSON:";
25
25
 
26
- export function buildAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
27
- return projectAsyncStatusSnapshot(jobs, options);
28
- }
26
+ export { buildAsyncStatusSnapshot };
29
27
 
30
28
  export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined, sessionId: string | null | undefined): AsyncJobState[] {
31
29
  if (!state || !sessionId || state.currentSessionId !== sessionId) return [];
@@ -39,7 +37,7 @@ export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined
39
37
  return [...jobs.values()];
40
38
  }
41
39
 
42
- export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
40
+ export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshot {
43
41
  return buildAsyncStatusSnapshot(asyncStatusSnapshotJobsForState(state, sessionId), options);
44
42
  }
45
43