pi-subagents 0.50.0 → 0.52.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 (109) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/async-retention-discovery-worker.mjs +180 -0
  7. package/docs/agents.md +40 -2
  8. package/docs/configuration.md +30 -12
  9. package/docs/extension-api.md +42 -1
  10. package/docs/models.md +2 -0
  11. package/docs/observability.md +41 -5
  12. package/docs/tool-reference.md +38 -39
  13. package/docs/workflows.md +171 -3
  14. package/package.json +4 -2
  15. package/skills/pi-subagents/SKILL.md +6 -4
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
  17. package/skills/pi-subagents/references/execution-controls.md +29 -15
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
  19. package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
  20. package/src/agents/agent-management.ts +124 -351
  21. package/src/agents/agents.ts +157 -31
  22. package/src/agents/skills.ts +1 -1
  23. package/src/api/external-job-provider.ts +185 -0
  24. package/src/api/preflight.ts +36 -8
  25. package/src/api/shared-types.ts +2 -0
  26. package/src/extension/config.ts +3 -3
  27. package/src/extension/doctor.ts +3 -6
  28. package/src/extension/fanout-child.ts +2 -2
  29. package/src/extension/index.ts +166 -88
  30. package/src/extension/public-execution.ts +31 -2
  31. package/src/extension/schemas.ts +12 -35
  32. package/src/extension/tool-description.ts +35 -24
  33. package/src/inspectors/herdr/actions.ts +2 -2
  34. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  35. package/src/inspectors/herdr/project-panes.ts +2 -2
  36. package/src/intercom/native-supervisor-channel.ts +32 -10
  37. package/src/missions/lifecycle.ts +6 -1
  38. package/src/missions/store.ts +4 -9
  39. package/src/missions/workflow-state.ts +2 -2
  40. package/src/profiles/profiles.ts +3 -1
  41. package/src/runs/background/active-run-index.ts +31 -8
  42. package/src/runs/background/async-execution.ts +68 -40
  43. package/src/runs/background/async-job-tracker.ts +20 -4
  44. package/src/runs/background/async-resume.ts +47 -15
  45. package/src/runs/background/async-retention.ts +886 -0
  46. package/src/runs/background/async-status.ts +39 -53
  47. package/src/runs/background/chain-append.ts +3 -33
  48. package/src/runs/background/completion-dedupe.ts +5 -1
  49. package/src/runs/background/completion-replay.ts +22 -12
  50. package/src/runs/background/control-channel.ts +14 -68
  51. package/src/runs/background/fleet-view.ts +68 -19
  52. package/src/runs/background/index-segment.ts +59 -0
  53. package/src/runs/background/inspect-rpc.ts +443 -0
  54. package/src/runs/background/notify.ts +31 -5
  55. package/src/runs/background/result-files.ts +158 -90
  56. package/src/runs/background/result-watcher.ts +116 -20
  57. package/src/runs/background/resume-guidance.ts +8 -5
  58. package/src/runs/background/retained-children.ts +13 -3
  59. package/src/runs/background/run-id-query.ts +7 -0
  60. package/src/runs/background/run-id-resolver.ts +11 -9
  61. package/src/runs/background/run-status.ts +27 -18
  62. package/src/runs/background/scheduled-runs.ts +24 -4
  63. package/src/runs/background/stale-run-reconciler.ts +6 -3
  64. package/src/runs/background/steering.ts +11 -1
  65. package/src/runs/background/subagent-runner.ts +253 -140
  66. package/src/runs/background/subagent-wait.ts +8 -8
  67. package/src/runs/background/terminal-run-index.ts +129 -0
  68. package/src/runs/background/wait-completions.ts +21 -4
  69. package/src/runs/background/wait-subscriptions.ts +81 -2
  70. package/src/runs/foreground/async-steering-action.ts +21 -14
  71. package/src/runs/foreground/execution.ts +3 -1
  72. package/src/runs/foreground/subagent-executor.ts +666 -1579
  73. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  74. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  75. package/src/runs/shared/acceptance.ts +4 -4
  76. package/src/runs/shared/chain-outputs.ts +1 -3
  77. package/src/runs/shared/external-job-bridge.ts +444 -0
  78. package/src/runs/shared/external-job-runner.ts +286 -0
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  80. package/src/runs/shared/model-fallback.ts +98 -38
  81. package/src/runs/shared/orca-progress-tabs.ts +84 -22
  82. package/src/runs/shared/parallel-handoff.ts +46 -4
  83. package/src/runs/shared/parallel-utils.ts +4 -15
  84. package/src/runs/shared/permissions.ts +5 -1
  85. package/src/runs/shared/pi-args.ts +8 -1
  86. package/src/runs/shared/session-lease.ts +0 -6
  87. package/src/runs/shared/subagent-control.ts +26 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  89. package/src/runs/shared/workflow-graph.ts +1 -23
  90. package/src/runs/shared/worktree.ts +14 -2
  91. package/src/shared/atomic-json.ts +22 -2
  92. package/src/shared/capacity-resilient-json.ts +102 -0
  93. package/src/shared/completion-owner.ts +14 -0
  94. package/src/shared/file-system-retry.ts +49 -1
  95. package/src/shared/fork-context.ts +42 -0
  96. package/src/shared/prompt-resources.ts +0 -40
  97. package/src/shared/settings.ts +3 -27
  98. package/src/shared/types.ts +71 -26
  99. package/src/shared/utils.ts +8 -0
  100. package/src/shared/watch-strategy.ts +10 -0
  101. package/src/slash/slash-bridge.ts +2 -1
  102. package/src/slash/slash-commands.ts +29 -3
  103. package/src/tui/fleet.ts +63 -15
  104. package/src/watchdog/change-signature.ts +1 -1
  105. package/src/watchdog/lsp-diagnostics.ts +1 -0
  106. package/src/workflows/chat-progress.ts +2 -2
  107. package/src/workflows/scripted-workflow.ts +220 -67
  108. package/src/runs/foreground/chain-clarify.ts +0 -1354
  109. package/src/runs/foreground/chain-execution.ts +0 -1581
@@ -0,0 +1,102 @@
1
+ import { isStorageCapacityError } from "./file-system-retry.ts";
2
+ import { writeAtomicJson } from "./atomic-json.ts";
3
+
4
+ type TimerApi = {
5
+ setTimeout(handler: () => void, delayMs: number): unknown;
6
+ clearTimeout(handle: unknown): void;
7
+ };
8
+
9
+ const defaultTimerApi: TimerApi = {
10
+ setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
11
+ clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
12
+ };
13
+
14
+ export type CapacityResilientJsonWriterOptions = {
15
+ /** The underlying write operation. It must be atomic for each target path. */
16
+ write?: (filePath: string, payload: object) => void;
17
+ /** Keep retry timers referenced by the event loop (needed by child runners). */
18
+ keepAlive?: boolean;
19
+ retryDelayMs?: number;
20
+ timerApi?: TimerApi;
21
+ onError?: (error: unknown, filePath: string) => void;
22
+ onSuccess?: (filePath: string, payload: object) => void;
23
+ };
24
+
25
+ export type CapacityResilientJsonWriter = {
26
+ write(filePath: string, payload: object, writeOperation?: (filePath: string, payload: object) => void): void;
27
+ pendingCount(): number;
28
+ dispose(): void;
29
+ };
30
+
31
+ /**
32
+ * Defers only storage-capacity failures, retaining the newest payload per path.
33
+ * Initial/ordinary writes remain synchronous and preserve their throwing contract;
34
+ * only ENOSPC-like failures enter the guarded retry loop.
35
+ */
36
+ export function createCapacityResilientJsonWriter(options: CapacityResilientJsonWriterOptions = {}): CapacityResilientJsonWriter {
37
+ const defaultWrite = options.write ?? writeAtomicJson;
38
+ const keepAlive = options.keepAlive ?? false;
39
+ const retryDelayMs = options.retryDelayMs ?? 1_000;
40
+ const timerApi = options.timerApi ?? defaultTimerApi;
41
+ const onError = options.onError ?? ((error, filePath) => console.error(`Failed to persist JSON '${filePath}':`, error));
42
+ const onSuccess = options.onSuccess;
43
+ const reportError = (error: unknown, filePath: string): void => {
44
+ try {
45
+ onError(error, filePath);
46
+ } catch (callbackError) {
47
+ console.error(`Failed to report JSON persistence error for '${filePath}':`, callbackError);
48
+ }
49
+ };
50
+ const notifySuccess = (filePath: string, payload: object): void => {
51
+ if (!onSuccess) return;
52
+ try {
53
+ onSuccess(filePath, payload);
54
+ } catch (error) {
55
+ reportError(error, filePath);
56
+ }
57
+ };
58
+ const pending = new Map<string, { payload: object; write: (filePath: string, payload: object) => void }>();
59
+ let retryTimer: unknown;
60
+
61
+ const scheduleRetry = (): void => {
62
+ if (retryTimer !== undefined) return;
63
+ retryTimer = timerApi.setTimeout(() => {
64
+ retryTimer = undefined;
65
+ for (const [filePath, entry] of pending) {
66
+ try {
67
+ entry.write(filePath, entry.payload);
68
+ pending.delete(filePath);
69
+ notifySuccess(filePath, entry.payload);
70
+ } catch (error) {
71
+ if (isStorageCapacityError(error)) continue;
72
+ pending.delete(filePath);
73
+ reportError(error, filePath);
74
+ }
75
+ }
76
+ if (pending.size > 0) scheduleRetry();
77
+ }, retryDelayMs);
78
+ if (!keepAlive && typeof retryTimer === "object" && retryTimer !== null && "unref" in retryTimer && typeof retryTimer.unref === "function") {
79
+ retryTimer.unref();
80
+ }
81
+ };
82
+
83
+ return {
84
+ write(filePath, payload, writeOperation = defaultWrite): void {
85
+ try {
86
+ writeOperation(filePath, payload);
87
+ pending.delete(filePath);
88
+ notifySuccess(filePath, payload);
89
+ } catch (error) {
90
+ if (!isStorageCapacityError(error)) throw error;
91
+ pending.set(filePath, { payload, write: writeOperation });
92
+ scheduleRetry();
93
+ }
94
+ },
95
+ pendingCount: () => pending.size,
96
+ dispose: (): void => {
97
+ if (retryTimer !== undefined) timerApi.clearTimeout(retryTimer);
98
+ retryTimer = undefined;
99
+ pending.clear();
100
+ },
101
+ };
102
+ }
@@ -0,0 +1,14 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ const COMPLETION_OWNER_KEY = Symbol.for("pi-subagents.completion-owner-id");
4
+
5
+ type CompletionOwnerGlobal = typeof globalThis & {
6
+ [COMPLETION_OWNER_KEY]?: string;
7
+ };
8
+
9
+ /** Stable for one parent Pi process across extension reloads. */
10
+ export function currentCompletionOwnerId(): string {
11
+ const runtime = globalThis as CompletionOwnerGlobal;
12
+ runtime[COMPLETION_OWNER_KEY] ??= randomUUID();
13
+ return runtime[COMPLETION_OWNER_KEY];
14
+ }
@@ -1,8 +1,51 @@
1
1
  const WAIT_BUFFER = typeof SharedArrayBuffer !== "undefined" ? new SharedArrayBuffer(4) : undefined;
2
2
  const WAIT_VIEW = WAIT_BUFFER ? new Int32Array(WAIT_BUFFER) : undefined;
3
3
  const RETRYABLE_FILE_SYSTEM_ERROR_CODES = new Set(["EACCES", "EBUSY", "EPERM"]);
4
+ const STORAGE_CAPACITY_ERROR_CODES = new Set(["EDQUOT", "EMFILE", "ENFILE", "ENOSPC"]);
4
5
 
5
- export const DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS = [10, 25, 50, 100, 200, 500, 1000, 2000, 4000] as const;
6
+ export const FS_RETRY_MAX_TOTAL_MS_ENV = "PI_SUBAGENT_FS_RETRY_MAX_TOTAL_MS";
7
+
8
+ const BASE_FILE_SYSTEM_RETRY_DELAYS_MS = [10, 25, 50, 100, 200, 500, 1000, 2000, 4000] as const;
9
+
10
+ /**
11
+ * Clamp the retry ladder to a total sleep budget, preserving its length.
12
+ *
13
+ * waitForFileSystemRetry blocks the calling thread, so the ladder above is also
14
+ * a ceiling on how long a single contended rename can stall that thread: about
15
+ * 7.9s. That is fine for a short-lived CLI. A long-lived host that loads this
16
+ * extension in-process runs the same writers on its event loop, where an 8s
17
+ * stall stops it serving anything at all -- and because Atomics.wait parks
18
+ * rather than spins, it presents as an unresponsive process at 0% CPU.
19
+ *
20
+ * The length is preserved deliberately: run-fanout-budget.ts and
21
+ * workflow-state.ts index this array by attempt number and treat running off
22
+ * the end as "timed out acquiring lock". Shortening it would quietly shrink
23
+ * those attempt budgets too, so only the sleeps shrink here.
24
+ *
25
+ * Unset by default, so behaviour is unchanged unless a host opts in. Opting in
26
+ * trades lock-wait tolerance for responsiveness: entries clamped to 0 return
27
+ * immediately, so contention that would previously have been waited out is
28
+ * surfaced as an error sooner.
29
+ */
30
+ export function resolveFileSystemRetryDelays(
31
+ env: NodeJS.ProcessEnv = process.env,
32
+ base: readonly number[] = BASE_FILE_SYSTEM_RETRY_DELAYS_MS,
33
+ ): readonly number[] {
34
+ const raw = env[FS_RETRY_MAX_TOTAL_MS_ENV];
35
+ if (raw === undefined || raw.trim() === "") return base;
36
+ const budget = Number(raw);
37
+ if (!Number.isInteger(budget) || budget < 0) {
38
+ throw new Error(`${FS_RETRY_MAX_TOTAL_MS_ENV} must be a non-negative integer number of milliseconds.`);
39
+ }
40
+ let spent = 0;
41
+ return base.map((delayMs) => {
42
+ const allowed = Math.max(0, Math.min(delayMs, budget - spent));
43
+ spent += allowed;
44
+ return allowed;
45
+ });
46
+ }
47
+
48
+ export const DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS = resolveFileSystemRetryDelays();
6
49
 
7
50
  export type FileSystemRetryOptions = {
8
51
  retryDelaysMs?: readonly number[];
@@ -32,6 +75,11 @@ export function isRetryableFileSystemError(error: unknown): boolean {
32
75
  return typeof code === "string" && RETRYABLE_FILE_SYSTEM_ERROR_CODES.has(code);
33
76
  }
34
77
 
78
+ export function isStorageCapacityError(error: unknown): boolean {
79
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
80
+ return typeof code === "string" && STORAGE_CAPACITY_ERROR_CODES.has(code);
81
+ }
82
+
35
83
  export function runFileSystemOperationWithRetry<T>(operation: () => T, options: FileSystemRetryOptions = {}): T {
36
84
  const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS;
37
85
  const wait = options.wait ?? waitForFileSystemRetry;
@@ -56,6 +56,48 @@ export function resolveSubagentContext(value: unknown): SubagentExecutionContext
56
56
  return value === "fork" ? "fork" : "fresh";
57
57
  }
58
58
 
59
+ export interface PreferredForkAvailability {
60
+ getSessionFile(): string | undefined;
61
+ getLeafId?: () => string | null;
62
+ }
63
+
64
+ export interface PreferredForkSnapshot {
65
+ parentSessionFile?: string | null;
66
+ leafId?: string | null;
67
+ }
68
+
69
+ export interface SubagentLaunchContextInput {
70
+ explicitContext?: SubagentExecutionContext;
71
+ agentDefaultContext?: SubagentExecutionContext;
72
+ defaultSubagentContext?: SubagentExecutionContext;
73
+ canUseImplicitFork: boolean;
74
+ }
75
+
76
+ /** Resolve the actual launch context from explicit, global, and agent preferences. */
77
+ export function resolveSubagentLaunchContext(input: SubagentLaunchContextInput): SubagentExecutionContext {
78
+ if (input.explicitContext !== undefined) return input.explicitContext;
79
+ const preferredContext = input.defaultSubagentContext ?? input.agentDefaultContext ?? "fresh";
80
+ return preferredContext === "fork" && input.canUseImplicitFork ? "fork" : "fresh";
81
+ }
82
+
83
+ /** True when an implicit `defaultContext: fork` can create a real branch now.
84
+ * Explicit `context: "fork"` stays strict and does not use this preference. */
85
+ export function canPreferFork(sessionManager: PreferredForkAvailability): boolean {
86
+ return canPreferForkFromSnapshot({
87
+ parentSessionFile: sessionManager.getSessionFile(),
88
+ leafId: sessionManager.getLeafId?.() ?? null,
89
+ });
90
+ }
91
+
92
+ export function canPreferForkFromSnapshot(input: PreferredForkSnapshot): boolean {
93
+ if (!input.parentSessionFile || !input.leafId) return false;
94
+ try {
95
+ return fs.existsSync(input.parentSessionFile);
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
59
101
  /** Decide whether a resolved child model uses Anthropic's provider or message API, which
60
102
  * requires the sanitized fork to disable thinking. Unknown models stay conservative. */
61
103
  export function forkedChildRequiresThinkingOff(
@@ -1,14 +1,7 @@
1
- import * as fs from "node:fs";
2
1
  import * as path from "node:path";
3
2
  import { fileURLToPath } from "node:url";
4
- import { parseFrontmatter } from "../agents/frontmatter.ts";
5
3
  import { getAgentDir, getProjectConfigDir } from "./utils.ts";
6
4
 
7
- const PROMPT_REF_PATTERN = /^(package|user|project):([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/;
8
- const PROMPT_VARIABLE_PATTERN = /\{\{(\w+)\}\}/g;
9
-
10
- type PromptVariable = string | number | boolean;
11
-
12
5
  export function getPromptDirectories(cwd: string) {
13
6
  return {
14
7
  package: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "prompts"),
@@ -16,36 +9,3 @@ export function getPromptDirectories(cwd: string) {
16
9
  project: path.join(getProjectConfigDir(cwd), "prompts"),
17
10
  };
18
11
  }
19
-
20
- function promptVariables(vars: unknown): Record<string, PromptVariable> {
21
- if (vars === undefined) return {};
22
- if (!vars || typeof vars !== "object" || Array.isArray(vars)) throw new Error("prompts.render vars must be a plain object.");
23
- const prototype = Object.getPrototypeOf(vars);
24
- if (prototype !== null && prototype !== Object.prototype) throw new Error("prompts.render vars must be a plain object.");
25
- for (const [name, value] of Object.entries(vars)) {
26
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
27
- throw new Error(`prompts.render variable '${name}' must be a string, number, or boolean.`);
28
- }
29
- }
30
- return vars as Record<string, PromptVariable>;
31
- }
32
-
33
- export function renderWorkflowPrompt(ref: string, vars: unknown, cwd: string): string {
34
- const match = ref.match(PROMPT_REF_PATTERN);
35
- if (!match) throw new Error("prompts.render ref must use package:<name>, user:<name>, or project:<name>.");
36
- const scope = match[1] as keyof ReturnType<typeof getPromptDirectories>;
37
- const name = match[2]!;
38
- const filePath = path.join(getPromptDirectories(cwd)[scope], `${name}.md`);
39
- let content: string;
40
- try {
41
- if (!fs.lstatSync(filePath).isFile()) throw new Error("not a regular file");
42
- content = fs.readFileSync(filePath, "utf-8");
43
- } catch (error) {
44
- const detail = error instanceof Error ? error.message : String(error);
45
- throw new Error(`Could not read prompt fragment '${ref}': ${detail}`);
46
- }
47
- const variables = promptVariables(vars);
48
- return parseFrontmatter(content).body.trim().replace(PROMPT_VARIABLE_PATTERN, (placeholder, variable: string) => {
49
- return Object.hasOwn(variables, variable) ? String(variables[variable]) : placeholder;
50
- });
51
- }
@@ -64,6 +64,8 @@ export interface SequentialStep {
64
64
  acceptance?: AcceptanceInput;
65
65
  agentContract?: AgentContract;
66
66
  gateOn?: ChainGateLayer;
67
+ /** Internal workflow child isolation; public workflowScript supplies this on runs.run. */
68
+ worktree?: boolean;
67
69
  }
68
70
 
69
71
  /** Parallel task item within a parallel step */
@@ -119,24 +121,6 @@ export interface DynamicParallelStep {
119
121
  gateOn?: ChainGateLayer;
120
122
  }
121
123
 
122
- /** Approval checkpoint: pause before later chain steps until parent approval. */
123
- export interface CheckpointStep {
124
- checkpoint: string;
125
- message?: string;
126
- phase?: string;
127
- label?: string;
128
- agent?: string;
129
- task?: string;
130
- as?: string;
131
- output?: OutputOverrideInput;
132
- outputMode?: OutputMode;
133
- reads?: string[] | false;
134
- progress?: boolean;
135
- skill?: string | string[] | false;
136
- skills?: string[] | false;
137
- model?: string;
138
- }
139
-
140
124
  /** Parallel step: multiple agents running concurrently */
141
125
  export interface ParallelStep {
142
126
  parallel: ParallelTaskItem[];
@@ -149,16 +133,12 @@ export interface ParallelStep {
149
133
  }
150
134
 
151
135
  /** Union type for chain steps */
152
- export type ChainStep = SequentialStep | ParallelStep | DynamicParallelStep | CheckpointStep;
136
+ export type ChainStep = SequentialStep | ParallelStep | DynamicParallelStep;
153
137
 
154
138
  // =============================================================================
155
139
  // Type Guards
156
140
  // =============================================================================
157
141
 
158
- export function isCheckpointStep(step: ChainStep): step is CheckpointStep {
159
- return "checkpoint" in step;
160
- }
161
-
162
142
  export function isParallelStep(step: ChainStep): step is ParallelStep {
163
143
  return "parallel" in step && Array.isArray((step as ParallelStep).parallel);
164
144
  }
@@ -169,9 +149,6 @@ export function isDynamicParallelStep(step: ChainStep): step is DynamicParallelS
169
149
 
170
150
  /** Get all agent names in a step (single for sequential, multiple for parallel) */
171
151
  export function getStepAgents(step: ChainStep): string[] {
172
- if (isCheckpointStep(step)) {
173
- return [];
174
- }
175
152
  if (isParallelStep(step)) {
176
153
  return step.parallel.map((t) => t.agent);
177
154
  }
@@ -239,7 +216,6 @@ export function resolveChainTemplates(
239
216
  steps: ChainStep[],
240
217
  ): ResolvedTemplates {
241
218
  return steps.map((step, i) => {
242
- if (isCheckpointStep(step)) return "";
243
219
  if (isParallelStep(step)) {
244
220
  // Parallel step: resolve each task's template
245
221
  return step.parallel.map((task) => {
@@ -41,7 +41,7 @@ export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed"
41
41
 
42
42
  export interface WorkflowGraphNode {
43
43
  id: string;
44
- kind: "step" | "parallel-group" | "dynamic-parallel-group" | "agent" | "checkpoint";
44
+ kind: "step" | "parallel-group" | "dynamic-parallel-group" | "agent";
45
45
  agent?: string;
46
46
  phase?: string;
47
47
  label: string;
@@ -61,16 +61,6 @@ export interface WorkflowGraphNode {
61
61
  structured?: boolean;
62
62
  acceptanceStatus?: AcceptanceLedgerStatus;
63
63
  error?: string;
64
- checkpoint?: ChainCheckpointState;
65
- }
66
-
67
- export interface ChainCheckpointState {
68
- name: string;
69
- message?: string;
70
- status: "pending" | "approved" | "rejected";
71
- stepIndex: number;
72
- approvedAt?: number;
73
- rejectedAt?: number;
74
64
  }
75
65
 
76
66
  export interface WorkflowGraphSnapshot {
@@ -224,6 +214,10 @@ export interface ControlEvent {
224
214
  currentPath?: string;
225
215
  elapsedMs?: number;
226
216
  recentFailureSummary?: string;
217
+ workflowKey?: string;
218
+ phase?: string;
219
+ label?: string;
220
+ taskPreview?: string;
227
221
  }
228
222
 
229
223
  export type SubagentResultStatus = "completed" | "failed" | "paused" | "stopped" | "detached";
@@ -282,7 +276,7 @@ export interface ParallelHandoffGroup {
282
276
  export interface ParallelHandoffManifest {
283
277
  version: 1;
284
278
  runId: string;
285
- mode: "parallel" | "chain";
279
+ mode: "single" | "parallel" | "chain";
286
280
  source: "foreground" | "async";
287
281
  cwd: string;
288
282
  createdAt: number;
@@ -415,6 +409,12 @@ export type ProcessTerminalV1 =
415
409
  diagnostic?: string;
416
410
  });
417
411
 
412
+ /** Identifies the durable schedule that launched a run, so its completion is attributable. */
413
+ export interface ScheduleOrigin {
414
+ id: string;
415
+ name?: string;
416
+ }
417
+
418
418
  export type SteeringActionState = "delivered" | "scheduled" | "pending" | "partial" | "recovered" | "failed";
419
419
  export type SteeringTargetState = "scheduled" | "pending" | "routed" | "queued" | "delivered" | "late" | "failed" | "recovered";
420
420
 
@@ -508,6 +508,7 @@ export interface SteeringRecoveryDescriptor {
508
508
  sessionFile?: string;
509
509
  cwd: string;
510
510
  model?: string;
511
+ modelOverrideFromParent?: boolean;
511
512
  fallbackModels?: string[];
512
513
  thinking?: string;
513
514
  tools?: string[];
@@ -1057,7 +1058,6 @@ export interface Details {
1057
1058
  totalSteps?: number; // Total steps in chain
1058
1059
  currentStepIndex?: number; // 0-indexed current step (for running chains)
1059
1060
  workflowGraph?: WorkflowGraphSnapshot;
1060
- checkpoint?: ChainCheckpointState;
1061
1061
  outputs?: ChainOutputMap;
1062
1062
  // Aggregated child usage across all agents in the run
1063
1063
  totalChildUsage?: Usage;
@@ -1087,9 +1087,9 @@ export interface Details {
1087
1087
  workflow?: {
1088
1088
  value?: unknown;
1089
1089
  trace: Array<{
1090
- operation: "run" | "status";
1090
+ operation: "run" | "status" | "steer";
1091
1091
  key: string;
1092
- state: "started" | "completed" | "failed" | "detached" | "stopped" | "reused";
1092
+ state: "started" | "completed" | "failed" | "detached" | "stopped" | "reused" | "queued" | "delivered" | "missed";
1093
1093
  agent?: string;
1094
1094
  runId?: string;
1095
1095
  phase?: string;
@@ -1266,6 +1266,7 @@ export interface AsyncStartedEvent {
1266
1266
  sessionRoot?: string;
1267
1267
  pid?: number;
1268
1268
  sessionId?: string;
1269
+ completionOwnerId?: string;
1269
1270
  mode?: SubagentRunMode;
1270
1271
  agent?: string;
1271
1272
  agents?: string[];
@@ -1277,7 +1278,6 @@ export interface AsyncStartedEvent {
1277
1278
  chainStepCount?: number;
1278
1279
  parallelGroups?: AsyncParallelGroupStatus[];
1279
1280
  workflowGraph?: WorkflowGraphSnapshot;
1280
- checkpoint?: ChainCheckpointState;
1281
1281
  launchContractDigest?: string;
1282
1282
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
1283
1283
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
@@ -1298,6 +1298,11 @@ export type AgentRunnerConfig =
1298
1298
  command: string;
1299
1299
  args?: string[];
1300
1300
  promptDelivery?: "stdin";
1301
+ }
1302
+ | {
1303
+ type: "external-job";
1304
+ provider: string;
1305
+ options?: Record<string, unknown>;
1301
1306
  };
1302
1307
 
1303
1308
  export interface ExternalCliRunnerStatus {
@@ -1314,6 +1319,35 @@ export interface ExternalCliRunnerStatus {
1314
1319
  };
1315
1320
  }
1316
1321
 
1322
+ export interface ExternalJobRunnerStatus {
1323
+ type: "external-job";
1324
+ provider: string;
1325
+ options: Record<string, unknown>;
1326
+ capabilities: {
1327
+ stop: false;
1328
+ steer: false;
1329
+ resume: false;
1330
+ structuredOutput: false;
1331
+ toolEvents: false;
1332
+ };
1333
+ }
1334
+
1335
+ export interface ExternalJobStatus {
1336
+ provider: string;
1337
+ providerJobId?: string;
1338
+ promptDigest: string;
1339
+ options: Record<string, unknown>;
1340
+ handleUrl?: string;
1341
+ conversationUrl?: string;
1342
+ resultArtifactPath?: string;
1343
+ state: "queued" | "running" | "completed" | "failed" | "stopped" | "blocked";
1344
+ failureCode?: string;
1345
+ failureMessage?: string;
1346
+ blockingJobId?: string;
1347
+ startedAt?: number;
1348
+ updatedAt?: number;
1349
+ }
1350
+
1317
1351
  export interface ExternalProcessStatus {
1318
1352
  pid?: number;
1319
1353
  startedAt: number;
@@ -1328,6 +1362,8 @@ export interface ExternalProcessStatus {
1328
1362
  export interface AsyncStatus {
1329
1363
  lifecycleArtifactVersion?: SubagentLifecycleArtifactVersion;
1330
1364
  runId: string;
1365
+ /** Parent Pi process/window that owns local completion delivery. */
1366
+ completionOwnerId?: string;
1331
1367
  /** Host tool-call id retained when it differs from the internal run id. */
1332
1368
  toolCallId?: string;
1333
1369
  sessionId?: string;
@@ -1361,12 +1397,13 @@ export interface AsyncStatus {
1361
1397
  usageBudget?: UsageBudgetState;
1362
1398
  pid?: number;
1363
1399
  cwd?: string;
1400
+ /** Parent-resolved child session root retained for trusted restored transcript lookup. */
1401
+ sessionRoot?: string;
1364
1402
  currentStep?: number;
1365
1403
  chainStepCount?: number;
1366
1404
  pendingAppends?: number;
1367
1405
  parallelGroups?: AsyncParallelGroupStatus[];
1368
1406
  workflowGraph?: WorkflowGraphSnapshot;
1369
- checkpoint?: ChainCheckpointState;
1370
1407
  processTerminal?: ProcessTerminalV1;
1371
1408
  runFanoutBudget?: RunFanoutBudgetSnapshot;
1372
1409
  runFanoutBudgetDescriptor?: RunFanoutBudgetDescriptor;
@@ -1378,10 +1415,13 @@ export interface AsyncStatus {
1378
1415
  workflow?: Details["workflow"];
1379
1416
  parentWorkflowRunId?: string;
1380
1417
  workflowKey?: string;
1418
+ /** Set when a durable schedule launched this run, so completions can name their origin. */
1419
+ scheduleOrigin?: ScheduleOrigin;
1381
1420
  steps?: Array<{
1382
1421
  agent: string;
1383
- runner?: ExternalCliRunnerStatus;
1422
+ runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
1384
1423
  externalProcess?: ExternalProcessStatus;
1424
+ externalJob?: ExternalJobStatus;
1385
1425
  /** Resolved launch context for this child step. */
1386
1426
  context?: "fresh" | "fork";
1387
1427
  /** Short caller-facing task/goal shown in fleet surfaces when available. */
@@ -1396,7 +1436,6 @@ export interface AsyncStatus {
1396
1436
  parentWorkflowRunId?: string;
1397
1437
  outputName?: string;
1398
1438
  structured?: boolean;
1399
- checkpoint?: ChainCheckpointState;
1400
1439
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused" | "stopped" | "rejected";
1401
1440
  children?: NestedRunSummary[];
1402
1441
  sessionFile?: string;
@@ -1476,6 +1515,7 @@ export interface AsyncJobState {
1476
1515
  description?: string;
1477
1516
  pid?: number;
1478
1517
  sessionId?: string;
1518
+ completionOwnerId?: string;
1479
1519
  activityState?: ActivityState;
1480
1520
  lastActivityAt?: number;
1481
1521
  currentTool?: string;
@@ -1492,7 +1532,6 @@ export interface AsyncJobState {
1492
1532
  chainStepCount?: number;
1493
1533
  parallelGroups?: AsyncParallelGroupStatus[];
1494
1534
  steps?: AsyncJobStep[];
1495
- checkpoint?: ChainCheckpointState;
1496
1535
  stepsTotal?: number;
1497
1536
  runningSteps?: number;
1498
1537
  completedSteps?: number;
@@ -1570,7 +1609,6 @@ export interface ForegroundResumeRun {
1570
1609
  /** Originating parent session. Detached exits can outlive the active session. */
1571
1610
  sessionId?: string;
1572
1611
  updatedAt: number;
1573
- checkpoint?: ChainCheckpointState;
1574
1612
  children: ForegroundResumeChild[];
1575
1613
  }
1576
1614
 
@@ -1663,6 +1701,8 @@ export interface ActiveAsyncCapacitySnapshot {
1663
1701
  export interface SubagentState {
1664
1702
  baseCwd: string;
1665
1703
  currentSessionId: string | null;
1704
+ /** Reload-stable identity for this parent Pi process/window. */
1705
+ completionOwnerId?: string;
1666
1706
  /** Runtime-owned artifact resolution inputs used by Fleet transcript targeting. */
1667
1707
  artifactDirPreference?: ArtifactDirPreference;
1668
1708
  /** Runtime authority snapshot used by optional inspector controls. */
@@ -1811,6 +1851,8 @@ export interface RunSyncOptions {
1811
1851
  nestedRoute?: NestedRouteInfo;
1812
1852
  /** Override the agent's default model (format: "provider/id" or just "id") */
1813
1853
  modelOverride?: string;
1854
+ /** The override came from the running parent session, not configuration. */
1855
+ modelOverrideFromParent?: boolean;
1814
1856
  /** LLM intent arbiter for the completion mutation guard (rescues read-only review runs). */
1815
1857
  llmIntentArbiter?: import("../runs/shared/llm-intent-arbiter.ts").TaskMutationArbiter;
1816
1858
  /** Override the agent's default thinking level for this run */
@@ -1915,6 +1957,8 @@ export interface MainWindowRendererConfig {
1915
1957
 
1916
1958
  export interface ExtensionConfig {
1917
1959
  asyncByDefault?: boolean;
1960
+ /** Set the context for launches that omit an explicit context. */
1961
+ defaultSubagentContext?: "fresh" | "fork";
1918
1962
  /** Optional shortcut that detaches the active foreground single-subagent run. */
1919
1963
  foregroundDetachShortcut?: string;
1920
1964
  /** Show the Claude Code-style navigable fleet. Defaults to true. */
@@ -1925,10 +1969,8 @@ export interface ExtensionConfig {
1925
1969
  fleetKeybindings?: FleetKeybindingsConfig;
1926
1970
  /** Show the under-editor async runs widget. Defaults to true, including when FleetView is enabled. */
1927
1971
  asyncWidget?: boolean;
1928
- /** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
1972
+ /** Tool description variant registered for the parent-facing subagent tool. Defaults to split metadata. */
1929
1973
  toolDescriptionMode?: ToolDescriptionMode;
1930
- /** Include legacy append-step and checkpoint controls in the registered tool schema. Defaults to false. */
1931
- legacyChainControls?: boolean;
1932
1974
  /** Inline chat rendering for the subagent tool. Defaults to rich. */
1933
1975
  inlineToolDisplay?: InlineToolDisplay;
1934
1976
  /** Density controls for the main chat subagent call/result renderer. */
@@ -2067,7 +2109,10 @@ export function resolveTempScopeId(options?: {
2067
2109
 
2068
2110
  const MAX_PARALLEL = 8;
2069
2111
  export const MAX_CONCURRENCY = 4;
2070
- export const TEMP_ROOT_DIR = path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
2112
+ const configuredTempRoot = process.env.PI_SUBAGENTS_TEMP_ROOT?.trim();
2113
+ export const TEMP_ROOT_DIR = configuredTempRoot
2114
+ ? path.resolve(configuredTempRoot)
2115
+ : path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
2071
2116
  export const RESULTS_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-results");
2072
2117
  export const ASYNC_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-runs");
2073
2118
  export const CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
@@ -2090,7 +2135,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
2090
2135
  export const POLL_INTERVAL_MS = 250;
2091
2136
  export const MAX_WIDGET_JOBS = 4;
2092
2137
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
2093
- export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
2138
+ export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
2094
2139
 
2095
2140
  export const DEFAULT_FORK_PREAMBLE =
2096
2141
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -102,6 +102,7 @@ export function getAgentDir(): string {
102
102
  }
103
103
 
104
104
  const statusCache = new Map<string, { mtime: number; ctime: number; size: number; ino: number; status: AsyncStatus }>();
105
+ const MAX_STATUS_CACHE_ENTRIES = 512;
105
106
 
106
107
  export function pruneStatusCacheForAsyncRoot(asyncDirRoot: string, runIds: Iterable<string>): number {
107
108
  const root = path.resolve(asyncDirRoot);
@@ -163,6 +164,8 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
163
164
  && cached.size === stat.size
164
165
  && cached.ino === stat.ino
165
166
  ) {
167
+ statusCache.delete(statusPath);
168
+ statusCache.set(statusPath, cached);
166
169
  return cached.status;
167
170
  }
168
171
 
@@ -195,6 +198,11 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
195
198
  ino: stat.ino,
196
199
  status,
197
200
  });
201
+ while (statusCache.size > MAX_STATUS_CACHE_ENTRIES) {
202
+ const oldest = statusCache.keys().next().value as string | undefined;
203
+ if (oldest === undefined) break;
204
+ statusCache.delete(oldest);
205
+ }
198
206
  return status;
199
207
  }
200
208
 
@@ -0,0 +1,10 @@
1
+ export type FileWatchPurpose =
2
+ | "result-delivery"
3
+ | "supervisor-channel"
4
+ | "async-job-tracker"
5
+ | "runner-control-inbox"
6
+ | "child-steering-inbox";
7
+
8
+ export function shouldUseNativeFsWatch(_purpose: FileWatchPurpose, platform: NodeJS.Platform = process.platform): boolean {
9
+ return platform !== "darwin";
10
+ }