pi-subagents 0.49.0 → 0.51.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 (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. package/src/runs/foreground/chain-execution.ts +0 -1565
@@ -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 {
@@ -215,7 +205,7 @@ export interface ControlEvent {
215
205
  nestedRunId?: string;
216
206
  nestingPath?: NestedRunAddress["path"];
217
207
  message: string;
218
- reason?: "idle" | "completion_guard" | "active_long_running" | "tool_failures" | "supervisor_request" | "time_threshold" | "turn_threshold" | "token_threshold";
208
+ reason?: "idle" | "completion_guard" | "active_long_running" | "tool_failures" | "supervisor_request" | "time_threshold" | "turn_threshold" | "token_threshold" | "tool_open_threshold";
219
209
  turns?: number;
220
210
  tokens?: number;
221
211
  toolCount?: number;
@@ -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;
@@ -1031,6 +1025,7 @@ export interface Details {
1031
1025
  controlEvents?: ControlEvent[];
1032
1026
  steering?: SteerActionResult;
1033
1027
  asyncId?: string;
1028
+ background?: boolean;
1034
1029
  asyncDir?: string;
1035
1030
  timeoutMs?: number;
1036
1031
  deadlineAt?: number;
@@ -1056,7 +1051,6 @@ export interface Details {
1056
1051
  totalSteps?: number; // Total steps in chain
1057
1052
  currentStepIndex?: number; // 0-indexed current step (for running chains)
1058
1053
  workflowGraph?: WorkflowGraphSnapshot;
1059
- checkpoint?: ChainCheckpointState;
1060
1054
  outputs?: ChainOutputMap;
1061
1055
  // Aggregated child usage across all agents in the run
1062
1056
  totalChildUsage?: Usage;
@@ -1086,9 +1080,9 @@ export interface Details {
1086
1080
  workflow?: {
1087
1081
  value?: unknown;
1088
1082
  trace: Array<{
1089
- operation: "run" | "status";
1083
+ operation: "run" | "status" | "steer";
1090
1084
  key: string;
1091
- state: "started" | "completed" | "failed" | "stopped" | "reused";
1085
+ state: "started" | "completed" | "failed" | "detached" | "stopped" | "reused" | "queued" | "delivered" | "missed";
1092
1086
  agent?: string;
1093
1087
  runId?: string;
1094
1088
  phase?: string;
@@ -1265,6 +1259,7 @@ export interface AsyncStartedEvent {
1265
1259
  sessionRoot?: string;
1266
1260
  pid?: number;
1267
1261
  sessionId?: string;
1262
+ completionOwnerId?: string;
1268
1263
  mode?: SubagentRunMode;
1269
1264
  agent?: string;
1270
1265
  agents?: string[];
@@ -1276,7 +1271,6 @@ export interface AsyncStartedEvent {
1276
1271
  chainStepCount?: number;
1277
1272
  parallelGroups?: AsyncParallelGroupStatus[];
1278
1273
  workflowGraph?: WorkflowGraphSnapshot;
1279
- checkpoint?: ChainCheckpointState;
1280
1274
  launchContractDigest?: string;
1281
1275
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
1282
1276
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
@@ -1297,6 +1291,11 @@ export type AgentRunnerConfig =
1297
1291
  command: string;
1298
1292
  args?: string[];
1299
1293
  promptDelivery?: "stdin";
1294
+ }
1295
+ | {
1296
+ type: "external-job";
1297
+ provider: string;
1298
+ options?: Record<string, unknown>;
1300
1299
  };
1301
1300
 
1302
1301
  export interface ExternalCliRunnerStatus {
@@ -1313,6 +1312,35 @@ export interface ExternalCliRunnerStatus {
1313
1312
  };
1314
1313
  }
1315
1314
 
1315
+ export interface ExternalJobRunnerStatus {
1316
+ type: "external-job";
1317
+ provider: string;
1318
+ options: Record<string, unknown>;
1319
+ capabilities: {
1320
+ stop: false;
1321
+ steer: false;
1322
+ resume: false;
1323
+ structuredOutput: false;
1324
+ toolEvents: false;
1325
+ };
1326
+ }
1327
+
1328
+ export interface ExternalJobStatus {
1329
+ provider: string;
1330
+ providerJobId?: string;
1331
+ promptDigest: string;
1332
+ options: Record<string, unknown>;
1333
+ handleUrl?: string;
1334
+ conversationUrl?: string;
1335
+ resultArtifactPath?: string;
1336
+ state: "queued" | "running" | "completed" | "failed" | "stopped" | "blocked";
1337
+ failureCode?: string;
1338
+ failureMessage?: string;
1339
+ blockingJobId?: string;
1340
+ startedAt?: number;
1341
+ updatedAt?: number;
1342
+ }
1343
+
1316
1344
  export interface ExternalProcessStatus {
1317
1345
  pid?: number;
1318
1346
  startedAt: number;
@@ -1327,6 +1355,8 @@ export interface ExternalProcessStatus {
1327
1355
  export interface AsyncStatus {
1328
1356
  lifecycleArtifactVersion?: SubagentLifecycleArtifactVersion;
1329
1357
  runId: string;
1358
+ /** Parent Pi process/window that owns local completion delivery. */
1359
+ completionOwnerId?: string;
1330
1360
  /** Host tool-call id retained when it differs from the internal run id. */
1331
1361
  toolCallId?: string;
1332
1362
  sessionId?: string;
@@ -1360,12 +1390,13 @@ export interface AsyncStatus {
1360
1390
  usageBudget?: UsageBudgetState;
1361
1391
  pid?: number;
1362
1392
  cwd?: string;
1393
+ /** Parent-resolved child session root retained for trusted restored transcript lookup. */
1394
+ sessionRoot?: string;
1363
1395
  currentStep?: number;
1364
1396
  chainStepCount?: number;
1365
1397
  pendingAppends?: number;
1366
1398
  parallelGroups?: AsyncParallelGroupStatus[];
1367
1399
  workflowGraph?: WorkflowGraphSnapshot;
1368
- checkpoint?: ChainCheckpointState;
1369
1400
  processTerminal?: ProcessTerminalV1;
1370
1401
  runFanoutBudget?: RunFanoutBudgetSnapshot;
1371
1402
  runFanoutBudgetDescriptor?: RunFanoutBudgetDescriptor;
@@ -1379,8 +1410,9 @@ export interface AsyncStatus {
1379
1410
  workflowKey?: string;
1380
1411
  steps?: Array<{
1381
1412
  agent: string;
1382
- runner?: ExternalCliRunnerStatus;
1413
+ runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
1383
1414
  externalProcess?: ExternalProcessStatus;
1415
+ externalJob?: ExternalJobStatus;
1384
1416
  /** Resolved launch context for this child step. */
1385
1417
  context?: "fresh" | "fork";
1386
1418
  /** Short caller-facing task/goal shown in fleet surfaces when available. */
@@ -1395,7 +1427,6 @@ export interface AsyncStatus {
1395
1427
  parentWorkflowRunId?: string;
1396
1428
  outputName?: string;
1397
1429
  structured?: boolean;
1398
- checkpoint?: ChainCheckpointState;
1399
1430
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused" | "stopped" | "rejected";
1400
1431
  children?: NestedRunSummary[];
1401
1432
  sessionFile?: string;
@@ -1464,6 +1495,8 @@ export type AsyncJobStep = NonNullable<AsyncStatus["steps"]>[number] & {
1464
1495
  export interface AsyncJobState {
1465
1496
  asyncId: string;
1466
1497
  asyncDir: string;
1498
+ /** Host tool-call id retained when it differs from the internal run id. */
1499
+ toolCallId?: string;
1467
1500
  /** Parent-resolved launch directory retained for trusted live artifact lookup. */
1468
1501
  cwd?: string;
1469
1502
  /** Parent-resolved child session root retained for trusted live transcript lookup. */
@@ -1473,6 +1506,7 @@ export interface AsyncJobState {
1473
1506
  description?: string;
1474
1507
  pid?: number;
1475
1508
  sessionId?: string;
1509
+ completionOwnerId?: string;
1476
1510
  activityState?: ActivityState;
1477
1511
  lastActivityAt?: number;
1478
1512
  currentTool?: string;
@@ -1489,7 +1523,6 @@ export interface AsyncJobState {
1489
1523
  chainStepCount?: number;
1490
1524
  parallelGroups?: AsyncParallelGroupStatus[];
1491
1525
  steps?: AsyncJobStep[];
1492
- checkpoint?: ChainCheckpointState;
1493
1526
  stepsTotal?: number;
1494
1527
  runningSteps?: number;
1495
1528
  completedSteps?: number;
@@ -1567,7 +1600,6 @@ export interface ForegroundResumeRun {
1567
1600
  /** Originating parent session. Detached exits can outlive the active session. */
1568
1601
  sessionId?: string;
1569
1602
  updatedAt: number;
1570
- checkpoint?: ChainCheckpointState;
1571
1603
  children: ForegroundResumeChild[];
1572
1604
  }
1573
1605
 
@@ -1660,6 +1692,8 @@ export interface ActiveAsyncCapacitySnapshot {
1660
1692
  export interface SubagentState {
1661
1693
  baseCwd: string;
1662
1694
  currentSessionId: string | null;
1695
+ /** Reload-stable identity for this parent Pi process/window. */
1696
+ completionOwnerId?: string;
1663
1697
  /** Runtime-owned artifact resolution inputs used by Fleet transcript targeting. */
1664
1698
  artifactDirPreference?: ArtifactDirPreference;
1665
1699
  /** Runtime authority snapshot used by optional inspector controls. */
@@ -1767,6 +1801,10 @@ export interface RunSyncOptions {
1767
1801
  interruptSignal?: AbortSignal;
1768
1802
  timeoutMs?: number;
1769
1803
  deadlineAt?: number;
1804
+ /** Per-call per-tool timeout (ms), resolved with the agent/config/environment ladder at execution. */
1805
+ toolTimeoutMs?: number;
1806
+ /** Raw global config.toolTimeoutMs, used by the per-child resolver. */
1807
+ configToolTimeoutMs?: number;
1770
1808
  turnBudget?: ResolvedTurnBudget;
1771
1809
  usageBudget?: UsageBudgetConfig;
1772
1810
  /** Enforce maxTurns + graceTurns as a hard model-turn boundary. */
@@ -1831,6 +1869,8 @@ export interface RunSyncOptions {
1831
1869
  };
1832
1870
  /** Private live callback for the exact child prompt after runtime acceptance injection. */
1833
1871
  onEffectivePrompt?: (prompt: string) => void;
1872
+ /** Internal lifecycle hook for the observer shared across retries of one logical child. */
1873
+ onOrcaProgressTabCreated?: (tab: import("../runs/shared/orca-progress-tabs.ts").OrcaProgressTab) => void;
1834
1874
  }
1835
1875
 
1836
1876
  export type IntercomBridgeMode = "off" | "fork-only" | "always";
@@ -1892,6 +1932,11 @@ export const FLEET_KEYBINDING_ACTIONS = [
1892
1932
  export type FleetKeybindingAction = typeof FLEET_KEYBINDING_ACTIONS[number];
1893
1933
  export type FleetKeybindingsConfig = Partial<Record<FleetKeybindingAction, string[]>>;
1894
1934
 
1935
+ export interface OrcaProgressTabsConfig {
1936
+ /** Create one Orca terminal tab per running subagent. Experimental and opt-in. */
1937
+ enabled?: boolean;
1938
+ }
1939
+
1895
1940
  export interface MainWindowRendererConfig {
1896
1941
  /** Unit of horizontal space in main chat subagent call/result rows. Omit to preserve current spacing. Set 0 for no extra padding. */
1897
1942
  horizontalSpacing?: number;
@@ -1901,6 +1946,10 @@ export interface MainWindowRendererConfig {
1901
1946
 
1902
1947
  export interface ExtensionConfig {
1903
1948
  asyncByDefault?: boolean;
1949
+ /** Set the context for launches that omit an explicit context. */
1950
+ defaultSubagentContext?: "fresh" | "fork";
1951
+ /** Optional shortcut that detaches the active foreground single-subagent run. */
1952
+ foregroundDetachShortcut?: string;
1904
1953
  /** Show the Claude Code-style navigable fleet. Defaults to true. */
1905
1954
  fleetView?: boolean;
1906
1955
  /** Place the persistent FleetView above or below the editor. Defaults to belowEditor. */
@@ -1909,14 +1958,14 @@ export interface ExtensionConfig {
1909
1958
  fleetKeybindings?: FleetKeybindingsConfig;
1910
1959
  /** Show the under-editor async runs widget. Defaults to true, including when FleetView is enabled. */
1911
1960
  asyncWidget?: boolean;
1912
- /** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
1961
+ /** Tool description variant registered for the parent-facing subagent tool. Defaults to split metadata. */
1913
1962
  toolDescriptionMode?: ToolDescriptionMode;
1914
- /** Include legacy append-step and checkpoint controls in the registered tool schema. Defaults to false. */
1915
- legacyChainControls?: boolean;
1916
1963
  /** Inline chat rendering for the subagent tool. Defaults to rich. */
1917
1964
  inlineToolDisplay?: InlineToolDisplay;
1918
1965
  /** Density controls for the main chat subagent call/result renderer. */
1919
1966
  mainWindowRenderer?: MainWindowRendererConfig;
1967
+ /** Experimental observer: mirror each native subagent's progress into a new Orca tab. */
1968
+ orcaProgressTabs?: OrcaProgressTabsConfig;
1920
1969
  forceTopLevelAsync?: boolean;
1921
1970
  waitTool?: WaitToolConfig;
1922
1971
  defaultSessionDir?: string;
@@ -1940,6 +1989,15 @@ export interface ExtensionConfig {
1940
1989
  * Must be a positive integer; invalid values are ignored.
1941
1990
  */
1942
1991
  timeoutMs?: number;
1992
+ /**
1993
+ * Optional hard per-tool-call timeout in milliseconds. Bounds a single
1994
+ * subagent tool call inside the child; the run-level timeout remains
1995
+ * authoritative. Known-fast built-in tools have a five-minute default when
1996
+ * this is undefined. Precedence: call param > agent frontmatter > this config >
1997
+ * PI_SUBAGENT_TOOL_TIMEOUT_MS. Must be a positive integer; invalid values
1998
+ * are rejected with an error.
1999
+ */
2000
+ toolTimeoutMs?: number;
1943
2001
  control?: ControlConfig;
1944
2002
  completionBatch?: CompletionBatchConfig;
1945
2003
  turnBudget?: TurnBudgetConfig;
@@ -2040,7 +2098,10 @@ export function resolveTempScopeId(options?: {
2040
2098
 
2041
2099
  const MAX_PARALLEL = 8;
2042
2100
  export const MAX_CONCURRENCY = 4;
2043
- export const TEMP_ROOT_DIR = path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
2101
+ const configuredTempRoot = process.env.PI_SUBAGENTS_TEMP_ROOT?.trim();
2102
+ export const TEMP_ROOT_DIR = configuredTempRoot
2103
+ ? path.resolve(configuredTempRoot)
2104
+ : path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
2044
2105
  export const RESULTS_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-results");
2045
2106
  export const ASYNC_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-runs");
2046
2107
  export const CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
@@ -2063,7 +2124,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
2063
2124
  export const POLL_INTERVAL_MS = 250;
2064
2125
  export const MAX_WIDGET_JOBS = 4;
2065
2126
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
2066
- 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;
2127
+ 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;
2067
2128
 
2068
2129
  export const DEFAULT_FORK_PREAMBLE =
2069
2130
  "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
+ }
@@ -2,8 +2,8 @@ import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
- import { Key, matchesKey, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
6
- import { BUILTIN_AGENT_NAMES, discoverAgents } from "../agents/agents.ts";
5
+ import { Key, matchesKey, truncateToWidth, type Component, type KeyId, type TUI } from "@earendil-works/pi-tui";
6
+ import { BUILTIN_AGENT_NAMES, discoverAgents, findBlockingAgentDiagnostic, resolveAgentName } from "../agents/agents.ts";
7
7
  import { resolveExistingReadPaths } from "../shared/settings.ts";
8
8
  import {
9
9
  DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,
@@ -631,7 +631,7 @@ function slashRunWorkflowScript(key: string, child: Record<string, unknown>): st
631
631
  export function registerSlashCommands(
632
632
  pi: ExtensionAPI,
633
633
  state: SubagentState,
634
- options: { fleetKeybindings?: FleetKeybindingsConfig } = {},
634
+ options: { fleetKeybindings?: FleetKeybindingsConfig; foregroundDetachShortcut?: string } = {},
635
635
  ): void {
636
636
  let fleetOpen = false;
637
637
  const showFleet = async (ctx: ExtensionContext) => {
@@ -671,8 +671,16 @@ export function registerSlashCommands(
671
671
  const task = firstSpace === -1 ? "" : input.slice(firstSpace + 1).trim();
672
672
 
673
673
  if (!state.baseCwd) { ctx.ui.notify("Subagent session cwd is not initialized yet", "error"); return; }
674
- const agents = discoverAgents(state.baseCwd, "both").agents;
675
- if (!agents.find((a) => a.name === agentName)) { ctx.ui.notify(`Unknown agent: ${agentName}`, "error"); return; }
674
+ const discovered = discoverAgents(state.baseCwd, "both");
675
+ const resolvedAgent = resolveAgentName(agentName, discovered.agents);
676
+ const candidates = resolvedAgent.error
677
+ ? discovered.agents.filter((agent) => resolveAgentName(agentName, [agent]).agent)
678
+ : resolvedAgent.agent;
679
+ const diagnostic = findBlockingAgentDiagnostic(agentName, candidates, discovered.agentDiagnostics);
680
+ if (diagnostic || resolvedAgent.error || !resolvedAgent.agent) {
681
+ ctx.ui.notify(diagnostic ? `Agent '${agentName}' has invalid configuration: ${diagnostic.error}` : resolvedAgent.error ?? `Unknown agent: ${agentName}`, "error");
682
+ return;
683
+ }
676
684
 
677
685
  let finalTask = task;
678
686
  if (inline.reads && Array.isArray(inline.reads) && inline.reads.length > 0) {
@@ -741,33 +749,42 @@ export function registerSlashCommands(
741
749
  handler: async (ctx) => showFleet(ctx),
742
750
  });
743
751
 
752
+ const detachForegroundRun = (args: string, ctx: ExtensionContext): void => {
753
+ const id = args.trim();
754
+ let control: ReturnType<typeof selectForegroundDetachControl>;
755
+ try {
756
+ control = selectForegroundDetachControl(state, id);
757
+ } catch (error) {
758
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
759
+ return;
760
+ }
761
+ if (!control) {
762
+ ctx.ui.notify(id ? `No active foreground run found for '${id}'.` : "No active foreground single-subagent run to detach.", "info");
763
+ return;
764
+ }
765
+ if (control.mode !== "single") {
766
+ ctx.ui.notify("/subagents-detach currently supports single-subagent runs only.", "error");
767
+ return;
768
+ }
769
+ if (!control.detach?.()) {
770
+ ctx.ui.notify(`Foreground run ${control.runId} is not currently detachable.`, "info");
771
+ return;
772
+ }
773
+ sendSlashText(pi, `Detached foreground run ${control.runId} without terminating its child. Use subagent({ action: "status", id: ${JSON.stringify(control.runId)} }) or subagent_wait({ id: ${JSON.stringify(control.runId)} }) to recover the eventual result. This does not daemonize the process or guarantee survival across Pi reload/restart.`);
774
+ };
775
+
744
776
  pi.registerCommand("subagents-detach", {
745
777
  description: "Detach the active foreground single-subagent run without terminating it",
746
- handler: async (args, ctx) => {
747
- const id = args.trim();
748
- let control: ReturnType<typeof selectForegroundDetachControl>;
749
- try {
750
- control = selectForegroundDetachControl(state, id);
751
- } catch (error) {
752
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
753
- return;
754
- }
755
- if (!control) {
756
- ctx.ui.notify(id ? `No active foreground run found for '${id}'.` : "No active foreground single-subagent run to detach.", "info");
757
- return;
758
- }
759
- if (control.mode !== "single") {
760
- ctx.ui.notify("/subagents-detach currently supports single-subagent runs only.", "error");
761
- return;
762
- }
763
- if (!control.detach?.()) {
764
- ctx.ui.notify(`Foreground run ${control.runId} is not currently detachable.`, "info");
765
- return;
766
- }
767
- sendSlashText(pi, `Detached foreground run ${control.runId} without terminating its child. Use subagent({ action: "status", id: ${JSON.stringify(control.runId)} }) or subagent_wait({ id: ${JSON.stringify(control.runId)} }) to recover the eventual result. This does not daemonize the process or guarantee survival across Pi reload/restart.`);
768
- },
778
+ handler: async (args, ctx) => detachForegroundRun(args, ctx),
769
779
  });
770
780
 
781
+ if (options.foregroundDetachShortcut) {
782
+ pi.registerShortcut(options.foregroundDetachShortcut as KeyId, {
783
+ description: "Detach the active foreground subagent and keep it running in the background",
784
+ handler: async (ctx) => detachForegroundRun("", ctx),
785
+ });
786
+ }
787
+
771
788
  pi.registerCommand("subagents-stop", {
772
789
  description: "Stop a current-session async subagent run",
773
790
  handler: async (args, ctx) => {
@@ -81,6 +81,7 @@ function buildParallelInitialResult(params: SubagentParamsLike): AgentToolResult
81
81
  content: [{ type: "text", text: tasks.map((task) => `${task.agent}: ${task.task}`).join("\n\n") }],
82
82
  details: {
83
83
  mode: "parallel",
84
+ ...(params.async ? { background: true } : {}),
84
85
  ...(params.context ? { context: params.context } : {}),
85
86
  results: tasks.map((task, index) => createPlaceholderResult(task.agent, task.task, "running", index)),
86
87
  progress: tasks.map((task, index) => ({
@@ -136,6 +137,7 @@ function buildChainInitialResult(params: SubagentParamsLike): AgentToolResult<De
136
137
  }],
137
138
  details: {
138
139
  mode: "chain",
140
+ ...(params.async ? { background: true } : {}),
139
141
  ...(params.context ? { context: params.context } : {}),
140
142
  results,
141
143
  progress: results.map((result, index) => ({
@@ -164,6 +166,7 @@ function buildSingleInitialResult(params: SubagentParamsLike): AgentToolResult<D
164
166
  content: [{ type: "text", text: task }],
165
167
  details: {
166
168
  mode: "single",
169
+ ...(params.async ? { background: true } : {}),
167
170
  ...(params.context ? { context: params.context } : {}),
168
171
  results: [createPlaceholderResult(agent, task, "running", 0)],
169
172
  progress: [{