pi-subagents 0.32.0 → 0.33.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 (52) hide show
  1. package/CHANGELOG.md +30 -3
  2. package/README.md +147 -58
  3. package/install.mjs +2 -1
  4. package/package.json +3 -2
  5. package/skills/pi-subagents/SKILL.md +70 -14
  6. package/src/agents/agent-management.ts +177 -5
  7. package/src/agents/agent-memory.ts +254 -0
  8. package/src/agents/agent-serializer.ts +11 -0
  9. package/src/agents/agents.ts +142 -12
  10. package/src/agents/chain-serializer.ts +27 -2
  11. package/src/extension/doctor.ts +1 -9
  12. package/src/extension/fanout-child.ts +2 -2
  13. package/src/extension/index.ts +65 -90
  14. package/src/extension/rpc.ts +369 -0
  15. package/src/extension/schemas.ts +52 -8
  16. package/src/extension/tool-description.ts +200 -0
  17. package/src/intercom/intercom-bridge.ts +21 -253
  18. package/src/intercom/native-supervisor-channel.ts +510 -0
  19. package/src/runs/background/async-execution.ts +51 -7
  20. package/src/runs/background/async-job-tracker.ts +12 -2
  21. package/src/runs/background/async-status.ts +27 -2
  22. package/src/runs/background/completion-batcher.ts +166 -0
  23. package/src/runs/background/control-channel.ts +106 -1
  24. package/src/runs/background/fleet-view.ts +515 -0
  25. package/src/runs/background/notify.ts +161 -44
  26. package/src/runs/background/result-watcher.ts +1 -2
  27. package/src/runs/background/run-id-resolver.ts +3 -2
  28. package/src/runs/background/run-status.ts +166 -6
  29. package/src/runs/background/scheduled-runs.ts +514 -0
  30. package/src/runs/background/subagent-runner.ts +409 -35
  31. package/src/runs/background/wait.ts +353 -0
  32. package/src/runs/foreground/chain-execution.ts +95 -21
  33. package/src/runs/foreground/execution.ts +150 -21
  34. package/src/runs/foreground/subagent-executor.ts +378 -64
  35. package/src/runs/shared/dynamic-fanout.ts +1 -1
  36. package/src/runs/shared/model-fallback.ts +167 -20
  37. package/src/runs/shared/model-scope.ts +128 -0
  38. package/src/runs/shared/nested-events.ts +31 -0
  39. package/src/runs/shared/parallel-utils.ts +1 -0
  40. package/src/runs/shared/pi-args.ts +30 -1
  41. package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
  42. package/src/runs/shared/tool-budget.ts +74 -0
  43. package/src/runs/shared/turn-budget.ts +52 -0
  44. package/src/shared/artifacts.ts +1 -0
  45. package/src/shared/atomic-json.ts +15 -2
  46. package/src/shared/child-transcript.ts +212 -0
  47. package/src/shared/settings.ts +3 -1
  48. package/src/shared/types.ts +134 -19
  49. package/src/slash/prompt-workflows.ts +330 -0
  50. package/src/slash/slash-commands.ts +16 -2
  51. package/src/tui/render.ts +16 -8
  52. package/src/extension/companion-suggestions.ts +0 -359
@@ -3,6 +3,7 @@ import { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./
3
3
  import { parseFrontmatter } from "./frontmatter.ts";
4
4
  import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
5
5
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
6
+ import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
6
7
  import type { ChainStep } from "../shared/settings.ts";
7
8
  import type { AgentSource } from "./agents.ts";
8
9
 
@@ -78,6 +79,19 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
78
79
  if (key === "progress") {
79
80
  if (rawValue === "true") step.progress = true;
80
81
  else if (rawValue === "false") step.progress = false;
82
+ continue;
83
+ }
84
+ if (key === "toolbudget") {
85
+ let parsed: unknown;
86
+ try {
87
+ parsed = JSON.parse(rawValue);
88
+ } catch (error) {
89
+ const message = error instanceof Error ? error.message : String(error);
90
+ throw new Error(`Invalid toolBudget in .chain.md step '${agent}': ${message}`);
91
+ }
92
+ const validation = validateToolBudgetConfig(parsed, `toolBudget for step '${agent}'`);
93
+ if (validation.error) throw new Error(validation.error);
94
+ step.toolBudget = parsed as ChainStepConfig["toolBudget"];
81
95
  }
82
96
  }
83
97
 
@@ -125,6 +139,11 @@ export function parseChain(content: string, source: AgentSource, filePath: strin
125
139
  };
126
140
  }
127
141
 
142
+ function validateJsonChainToolBudget(value: unknown, label: string): void {
143
+ const validation = validateToolBudgetConfig(value, label);
144
+ if (validation.error) throw new Error(validation.error);
145
+ }
146
+
128
147
  export function parseJsonChain(content: string, source: AgentSource, filePath: string): ChainConfig {
129
148
  let parsed: unknown;
130
149
  try {
@@ -152,6 +171,7 @@ export function parseJsonChain(content: string, source: AgentSource, filePath: s
152
171
  throw new Error(`JSON chain '${filePath}' step ${i + 1} must be an object.`);
153
172
  }
154
173
  const stepRecord = step as Record<string, unknown>;
174
+ if (stepRecord.toolBudget !== undefined) validateJsonChainToolBudget(stepRecord.toolBudget, `step ${i + 1} toolBudget`);
155
175
  const acceptanceErrors = validateAcceptanceInput(stepRecord.acceptance, `step ${i + 1} acceptance`);
156
176
  if (acceptanceErrors.length > 0) {
157
177
  throw new Error(`Invalid JSON chain '${filePath}': ${acceptanceErrors.join(" ")}`);
@@ -161,13 +181,17 @@ export function parseJsonChain(content: string, source: AgentSource, filePath: s
161
181
  for (let taskIndex = 0; taskIndex < parallel.length; taskIndex++) {
162
182
  const task = parallel[taskIndex];
163
183
  if (!task || typeof task !== "object" || Array.isArray(task)) continue;
164
- const taskErrors = validateAcceptanceInput((task as Record<string, unknown>).acceptance, `step ${i + 1} parallel task ${taskIndex + 1} acceptance`);
184
+ const taskRecord = task as Record<string, unknown>;
185
+ if (taskRecord.toolBudget !== undefined) validateJsonChainToolBudget(taskRecord.toolBudget, `step ${i + 1} parallel task ${taskIndex + 1} toolBudget`);
186
+ const taskErrors = validateAcceptanceInput(taskRecord.acceptance, `step ${i + 1} parallel task ${taskIndex + 1} acceptance`);
165
187
  if (taskErrors.length > 0) {
166
188
  throw new Error(`Invalid JSON chain '${filePath}': ${taskErrors.join(" ")}`);
167
189
  }
168
190
  }
169
191
  } else if (parallel && typeof parallel === "object") {
170
- const templateErrors = validateAcceptanceInput((parallel as Record<string, unknown>).acceptance, `step ${i + 1} dynamic template acceptance`);
192
+ const parallelRecord = parallel as Record<string, unknown>;
193
+ if (parallelRecord.toolBudget !== undefined) validateJsonChainToolBudget(parallelRecord.toolBudget, `step ${i + 1} dynamic template toolBudget`);
194
+ const templateErrors = validateAcceptanceInput(parallelRecord.acceptance, `step ${i + 1} dynamic template acceptance`);
171
195
  if (templateErrors.length > 0) {
172
196
  throw new Error(`Invalid JSON chain '${filePath}': ${templateErrors.join(" ")}`);
173
197
  }
@@ -243,6 +267,7 @@ export function serializeChain(config: ChainConfig): string {
243
267
  if (step.skills === false) lines.push("skills: false");
244
268
  else if (Array.isArray(step.skills) && step.skills.length > 0) lines.push(`skills: ${step.skills.join(", ")}`);
245
269
  if (step.progress !== undefined) lines.push(`progress: ${step.progress ? "true" : "false"}`);
270
+ if (step.toolBudget !== undefined) lines.push(`toolBudget: ${JSON.stringify(step.toolBudget)}`);
246
271
  lines.push("");
247
272
  lines.push(step.task ?? "");
248
273
  if (i < config.steps.length - 1) lines.push("");
@@ -39,7 +39,6 @@ interface DoctorReportInput {
39
39
  sessionError?: string;
40
40
  expandTilde?: (value: string) => string;
41
41
  paths?: DoctorPaths;
42
- companionPackageLines?: string[];
43
42
  deps?: Partial<DoctorDeps>;
44
43
  }
45
44
 
@@ -158,14 +157,8 @@ function formatIntercomDiagnostic(diagnostic: IntercomBridgeDiagnostic, context:
158
157
  `- bridge: ${diagnostic.active ? "active" : "inactive"}${diagnostic.reason ? ` (${diagnostic.reason})` : ""}`,
159
158
  `- mode: ${diagnostic.mode}; context: ${context ?? "unspecified"}`,
160
159
  `- orchestrator target: ${diagnostic.orchestratorTarget ?? "not available"}`,
161
- `- pi-intercom: ${diagnostic.piIntercomAvailable ? "available" : "unavailable"} at ${diagnostic.extensionDir}`,
160
+ `- supervisor channel: ${diagnostic.supervisorChannelAvailable ? "available" : "unavailable"} (${diagnostic.extensionDir})`,
162
161
  ];
163
- if (diagnostic.configPath && diagnostic.intercomConfigEnabled !== undefined) {
164
- lines.push(`- intercom config: ${diagnostic.intercomConfigEnabled === false ? "disabled" : "enabled or absent"} (${diagnostic.configPath})`);
165
- }
166
- if (diagnostic.intercomConfigError) {
167
- lines.push(`- intercom config warning: ${diagnostic.intercomConfigError}; runtime assumes enabled`);
168
- }
169
162
  return lines;
170
163
  }
171
164
 
@@ -216,7 +209,6 @@ export function buildDoctorReport(input: DoctorReportInput): string {
216
209
  orchestratorTarget: input.orchestratorTarget,
217
210
  cwd: input.cwd,
218
211
  }), input.context).join("\n")).split("\n"),
219
- ...(input.companionPackageLines?.length ? ["", ...input.companionPackageLines] : []),
220
212
  ];
221
213
  return lines.join("\n");
222
214
  }
@@ -158,8 +158,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
158
158
  label: "Subagent",
159
159
  description: [
160
160
  "Delegate to subagents from child-safe fanout mode.",
161
- "Allowed management/control actions: list, get, status, interrupt, resume, append-step, doctor.",
162
- "Agent config mutation actions create, update, and delete are blocked in this mode.",
161
+ "Allowed management/control actions: list, get, status, interrupt, resume, steer, append-step, doctor.",
162
+ "Agent config mutation actions (create, update, delete, eject, disable, enable, reset) are blocked in this mode.",
163
163
  ].join("\n"),
164
164
  parameters: SubagentParams,
165
165
  execute(id, params, signal, onUpdate, ctx) {
@@ -12,38 +12,36 @@
12
12
  * { "asyncByDefault": true, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
13
13
  */
14
14
 
15
+ import { randomUUID } from "node:crypto";
15
16
  import * as fs from "node:fs";
16
17
  import * as os from "node:os";
17
18
  import * as path from "node:path";
18
19
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
19
- import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
20
+ import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
20
21
  import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
21
22
  import { discoverAgents } from "../agents/agents.ts";
22
23
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
23
24
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
24
25
  import { cleanupOldChainDirs } from "../shared/settings.ts";
25
26
  import { clearLegacyResultAnimationTimer, renderWidget, renderSubagentResult } from "../tui/render.ts";
26
- import { SubagentParams } from "./schemas.ts";
27
+ import { SubagentParams, WaitParams } from "./schemas.ts";
27
28
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
28
29
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
29
30
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
31
+ import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
30
32
  import { registerSlashCommands } from "../slash/slash-commands.ts";
31
33
  import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
32
34
  import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
35
+ import { createNativeSupervisorChannel } from "../intercom/native-supervisor-channel.ts";
36
+ import { registerSubagentRpcBridge } from "./rpc.ts";
33
37
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
34
38
  import { inspectSubagentStatus } from "../runs/background/run-status.ts";
39
+ import { waitForSubagents } from "../runs/background/wait.ts";
35
40
  import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
36
41
  import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
37
42
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
38
43
  import { loadConfig } from "./config.ts";
39
- import {
40
- buildCompanionDoctorLines,
41
- buildCompanionListLines,
42
- collectCompanionStatuses,
43
- handleCompanionCommand,
44
- maybeSendCompanionStartupMessage,
45
- resolveCompanionOrchestratorTarget,
46
- } from "./companion-suggestions.ts";
44
+ import { buildSubagentToolDescription } from "./tool-description.ts";
47
45
  import {
48
46
  type Details,
49
47
  type SubagentState,
@@ -258,7 +256,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
258
256
  ensureAccessibleDir(ASYNC_DIR);
259
257
  cleanupOldChainDirs();
260
258
 
261
- let config = loadConfig();
259
+ const config = loadConfig();
262
260
  const asyncByDefault = config.asyncByDefault === true;
263
261
  const tempArtifactsDir = getArtifactsDir(null);
264
262
  cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
@@ -279,14 +277,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
279
277
  completionSeen: new Map(),
280
278
  watcher: null,
281
279
  watcherRestartTimer: null,
282
- companionSuggestionStartupShown: false,
283
- companionSuggestionListShown: false,
284
280
  resultFileCoalescer: {
285
281
  schedule: () => false,
286
282
  clear: () => {},
287
283
  },
288
284
  };
289
285
 
286
+ const supervisorChannel = createNativeSupervisorChannel(pi, state);
290
287
  const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
291
288
  pi,
292
289
  state,
@@ -298,6 +295,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
298
295
 
299
296
  const runtimeCleanup = () => {
300
297
  stopResultWatcher();
298
+ scheduledRunManager.stop();
299
+ supervisorChannel.dispose();
301
300
  clearPendingForegroundControlNotices(state);
302
301
  if (state.poller) {
303
302
  clearInterval(state.poller);
@@ -307,20 +306,32 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
307
306
  globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
308
307
 
309
308
  const { ensurePoller, handleStarted, handleComplete, resetJobs, restoreActiveJobs } = createAsyncJobTracker(pi, state, ASYNC_DIR);
309
+ let executorExecute: ((id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
310
+ const scheduledRunManager = createScheduledRunManager({
311
+ config,
312
+ launch: (params, ctx, signal) => {
313
+ if (!executorExecute) {
314
+ return Promise.resolve({
315
+ content: [{ type: "text", text: "Scheduled subagent launch is unavailable (executor not ready)." }],
316
+ isError: true,
317
+ details: { mode: "management" as const, results: [] },
318
+ });
319
+ }
320
+ return executorExecute(randomUUID(), params, signal, undefined, ctx);
321
+ },
322
+ });
310
323
  const executor = createSubagentExecutor({
311
324
  pi,
312
325
  state,
313
326
  config,
314
327
  asyncByDefault,
315
- companionSuggestionLines: ({ surface, cwd, context, orchestratorTarget }) => {
316
- const statuses = collectCompanionStatuses({ pi, config, cwd, context, orchestratorTarget });
317
- return surface === "doctor" ? buildCompanionDoctorLines(statuses) : buildCompanionListLines(statuses);
318
- },
328
+ handleScheduledRunAction: (params, ctx) => scheduledRunManager.handleToolCall(params, ctx),
319
329
  tempArtifactsDir,
320
330
  getSubagentSessionRoot,
321
331
  expandTilde,
322
332
  discoverAgents,
323
333
  });
334
+ executorExecute = executor.execute;
324
335
 
325
336
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
326
337
  const details = resolveSlashMessageDetails(message.details);
@@ -360,7 +371,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
360
371
  text += `\n ${theme.fg("dim", `⎿ ${line}`)}`;
361
372
  }
362
373
  if (!options.expanded && trimmedPreview.includes("\n")) {
363
- text += `\n ${theme.fg("dim", "Ctrl+O full notification")}`;
374
+ const expandKey = keyText("app.tools.expand");
375
+ text += `\n ${theme.fg("dim", `${expandKey} full notification`)}`;
364
376
  }
365
377
  if (details.sessionLabel && details.sessionValue) {
366
378
  text += `\n ${theme.fg("muted", `${details.sessionLabel}: ${shortenPath(details.sessionValue)}`)}`;
@@ -425,6 +437,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
425
437
  },
426
438
  });
427
439
 
440
+ const rpcBridge = registerSubagentRpcBridge({
441
+ events: pi.events,
442
+ getContext: () => state.lastUiContext,
443
+ execute: (id, params, signal, onUpdate, ctx) => executor.execute(id, params, signal, onUpdate, ctx),
444
+ });
445
+
428
446
  function effectiveParallelTaskCount(tasks: Array<{ count?: unknown }> | undefined): number {
429
447
  if (!tasks || tasks.length === 0) return 0;
430
448
  return tasks.reduce((total, task) => {
@@ -433,66 +451,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
433
451
  }, 0);
434
452
  }
435
453
 
436
- pi.registerCommand("subagents-companions", {
437
- description: "Show or hide pi-subagents companion package recommendations",
438
- handler: async (args, ctx) => {
439
- try {
440
- const statuses = collectCompanionStatuses({
441
- pi,
442
- config,
443
- cwd: ctx.cwd,
444
- orchestratorTarget: resolveCompanionOrchestratorTarget(pi, ctx),
445
- });
446
- const result = handleCompanionCommand(args, ctx, statuses);
447
- if (result.updatedConfig) config = result.updatedConfig;
448
- pi.sendMessage({ content: result.text, display: true });
449
- if (result.error && ctx.hasUI) ctx.ui.notify(result.text, "error");
450
- } catch (error) {
451
- const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
452
- pi.sendMessage({ content: `Failed to update companion suggestions: ${message}`, display: true });
453
- if (ctx.hasUI) ctx.ui.notify(`Failed to update companion suggestions: ${message}`, "error");
454
- }
455
- },
456
- });
457
-
458
454
  const tool: ToolDefinition<typeof SubagentParams, Details> = {
459
455
  name: "subagent",
460
456
  label: "Subagent",
461
- description: `Delegate to subagents or manage agent definitions.
462
-
463
- EXECUTION (use exactly ONE mode):
464
- • Before executing, use { action: "list" } to inspect configured agents/chains. Only execute agents listed as executable/non-disabled.
465
- • SINGLE: { agent, task? } - one task; omit task for self-contained agents
466
- • CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
467
- • PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
468
- • Optional context: { context: "fresh" | "fork" } (explicit value overrides every child; when omitted, each requested agent uses its own defaultContext, otherwise "fresh"; inspect agent defaults via { action: "list" })
469
- • Optional timeout: { timeoutMs } or { maxRuntimeMs } sets a run-level max runtime for foreground and async/background runs
470
- • If { action: "list" } shows proactive skill subagent suggestions, consider a small fresh-context fanout for broad tasks where one of those skills would materially help
471
-
472
- CHAIN TEMPLATE VARIABLES (use in task strings):
473
- • {task} - The original task/request from the user
474
- • {previous} - Text response from the previous step (empty for first step)
475
- • {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/pi-subagents-<scope>/chain-runs/abc123/)
476
-
477
- Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", task:"Plan based on {previous}"}] }
478
-
479
- MANAGEMENT (use action field, omit agent/task/chain/tasks):
480
- • { action: "list" } - discover executable agents/chains
481
- • { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
482
- • { action: "models", agent?: "name" } - show the runtime-loaded builtin subagent model mapping, optionally filtered to one builtin
483
- • { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
484
- • { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
485
- • { action: "delete", agent: "code-analysis.custom-agent" }
486
- • Use chainName for chain operations; packaged chains also use dotted runtime names
487
-
488
- CONTROL:
489
- • { action: "status", id: "..." } - inspect an async/background run by id or prefix
490
- • { action: "interrupt", id?: "..." } - soft-interrupt the current child turn and leave the run paused
491
- • { action: "resume", id: "...", message: "...", index?: 0 } - interrupt then follow up with a live async child, or revive a completed async/foreground child from its session
492
- • { action: "append-step", id: "...", chain: [{agent:"agent-c", task:"Use {previous}"}] } - append one step to the tail of a running async chain
493
-
494
- DIAGNOSTICS:
495
- • { action: "doctor" } - read-only report for runtime paths, discovery, sessions, and intercom`,
457
+ description: buildSubagentToolDescription(config),
496
458
  parameters: SubagentParams,
497
459
 
498
460
  execute(id, params, signal, onUpdate, ctx) {
@@ -542,6 +504,27 @@ DIAGNOSTICS:
542
504
  };
543
505
 
544
506
  pi.registerTool(tool);
507
+
508
+ const waitTool: ToolDefinition<typeof WaitParams, Details> = {
509
+ name: "wait",
510
+ label: "Wait",
511
+ description: `Block until background (async) subagent runs started in this session finish, then return.
512
+
513
+ Use this after launching async subagents when you have no independent work left and must not end your turn — for example inside a skill that has to run to completion, or any non-interactive run (\`pi -p ...\`) where the whole task is a single turn and ending it would abandon the still-running children.
514
+
515
+ • { } — return as soon as the FIRST active run finishes (default). Ideal for a rolling fleet: launch N, wait, spawn a replacement for the one that finished, wait again — keeping N in flight.
516
+ • { all: true } — block until EVERY active run in this session is finished.
517
+ • { id: "..." } — wait for one specific run (id or prefix) to finish.
518
+ • { timeoutMs: 600000 } — stop waiting after N ms (the runs keep going regardless; default 30 min)
519
+
520
+ wait also returns when a run needs attention (a child that went idle or blocked for a decision), not only on completion — so a stuck child never stalls the loop; the summary names the run(s) to inspect/nudge/resume/interrupt. It wakes the instant a completion or control event arrives (subscribed to Pi's event bus, with a poll fallback that reconciles crashed runners), keeps the turn alive for normal notification delivery, and resolves early if the turn is aborted.`,
521
+ parameters: WaitParams,
522
+ execute(_id, params, signal, _onUpdate, _ctx) {
523
+ return waitForSubagents(params, signal, { state, events: pi.events });
524
+ },
525
+ };
526
+ pi.registerTool(waitTool);
527
+
545
528
  registerSlashCommands(pi, state);
546
529
 
547
530
  const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
@@ -557,7 +540,7 @@ DIAGNOSTICS:
557
540
  }
558
541
  }
559
542
  }
560
- registerSubagentNotify(pi);
543
+ registerSubagentNotify(pi, state, { batchConfig: config.completionBatch });
561
544
 
562
545
  const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
563
546
  const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
@@ -574,6 +557,7 @@ DIAGNOSTICS:
574
557
  pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, handleStarted),
575
558
  pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete),
576
559
  pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
560
+ rpcBridge.dispose,
577
561
  ];
578
562
  globalStore[eventUnsubscribeStoreKey] = eventUnsubscribes;
579
563
 
@@ -615,30 +599,19 @@ DIAGNOSTICS:
615
599
  }
616
600
  }
617
601
  state.lastUiContext = ctx;
618
- state.companionSuggestionStartupShown = false;
619
- state.companionSuggestionListShown = false;
620
602
  cleanupSessionArtifacts(ctx);
621
603
  clearPendingForegroundControlNotices(state);
622
604
  resetJobs(ctx);
623
605
  restoreActiveJobs(ctx);
606
+ scheduledRunManager.bindSession(ctx);
624
607
  restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
625
608
  primeExistingResults();
626
609
  };
627
610
 
628
611
  pi.on("session_start", (_event, ctx) => {
629
612
  resetSessionState(ctx);
630
- maybeSendCompanionStartupMessage({
631
- pi,
632
- ctx,
633
- state,
634
- statuses: collectCompanionStatuses({
635
- pi,
636
- config,
637
- cwd: ctx.cwd,
638
- orchestratorTarget: resolveCompanionOrchestratorTarget(pi, ctx),
639
- fast: true,
640
- }),
641
- });
613
+ rpcBridge.emitReady(ctx);
614
+ supervisorChannel.start();
642
615
  });
643
616
 
644
617
  pi.on("session_shutdown", () => {
@@ -654,6 +627,7 @@ DIAGNOSTICS:
654
627
  delete globalStore[eventUnsubscribeStoreKey];
655
628
  }
656
629
  stopResultWatcher();
630
+ scheduledRunManager.stop();
657
631
  if (state.poller) clearInterval(state.poller);
658
632
  state.poller = null;
659
633
  clearPendingForegroundControlNotices(state);
@@ -667,6 +641,7 @@ DIAGNOSTICS:
667
641
  slashBridge.dispose();
668
642
  promptTemplateBridge.cancelAll();
669
643
  promptTemplateBridge.dispose();
644
+ supervisorChannel.dispose();
670
645
  if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
671
646
  delete globalStore[runtimeCleanupStoreKey];
672
647
  }