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
@@ -9,7 +9,7 @@
9
9
  * Toggle: async parameter (default: true; set asyncByDefault:false in config.json to opt out)
10
10
  *
11
11
  * Config file: ~/.pi/agent/extensions/subagent/config.json
12
- * { "asyncByDefault": true, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
12
+ * { "asyncByDefault": true, "defaultSubagentContext": "fork", "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
13
13
  */
14
14
 
15
15
  import { randomUUID } from "node:crypto";
@@ -23,6 +23,7 @@ import { discoverAgents } from "../agents/agents.ts";
23
23
  import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
24
24
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
25
25
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
26
+ import { currentCompletionOwnerId } from "../shared/completion-owner.ts";
26
27
  import { cleanupOldChainDirs } from "../shared/settings.ts";
27
28
  import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
28
29
  import { openSubagentFleet } from "../tui/fleet.ts";
@@ -31,6 +32,8 @@ import { createSubagentParamsSchema } from "./schemas.ts";
31
32
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
32
33
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
33
34
  import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
35
+ import { cleanupResultIndexes, missionObserverResultCandidateFiles } from "../runs/background/result-files.ts";
36
+ import { ASYNC_RETENTION_DELAY_MS, cleanupAsyncRetention } from "../runs/background/async-retention.ts";
34
37
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
35
38
  import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
36
39
  import { registerSlashCommands } from "../slash/slash-commands.ts";
@@ -52,7 +55,7 @@ import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/
52
55
  import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
53
56
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
54
57
  import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
55
- import { buildSubagentToolDescription } from "./tool-description.ts";
58
+ import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
56
59
  import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
57
60
  import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.ts";
58
61
  import { resolveMissionStoreLocation } from "../missions/store.ts";
@@ -82,6 +85,44 @@ import {
82
85
 
83
86
  export { loadConfig, resolveAsyncByDefault } from "./config.ts";
84
87
 
88
+ const SLOW_RELOAD_PHASE_MS = 250;
89
+ const RUNTIME_REGISTRY_STORE_KEY = "__piSubagentRuntimeRegistry";
90
+
91
+ interface SubagentRuntimeEntry {
92
+ cleanup(): void;
93
+ sessionManager: object | null;
94
+ visibleControlNotices: Set<string>;
95
+ }
96
+
97
+ interface SubagentRuntimeRegistry {
98
+ bySessionManager: WeakMap<object, SubagentRuntimeEntry>;
99
+ visibleControlNoticesBySessionManager: WeakMap<object, Set<string>>;
100
+ activeEntries: Set<SubagentRuntimeEntry>;
101
+ }
102
+
103
+ function getRuntimeRegistry(): SubagentRuntimeRegistry {
104
+ const globalStore = globalThis as Record<string, unknown>;
105
+ const existing = globalStore[RUNTIME_REGISTRY_STORE_KEY] as Partial<SubagentRuntimeRegistry> | undefined;
106
+ if (existing?.bySessionManager instanceof WeakMap && existing.activeEntries instanceof Set) {
107
+ if (!(existing.visibleControlNoticesBySessionManager instanceof WeakMap)) {
108
+ existing.visibleControlNoticesBySessionManager = new WeakMap();
109
+ }
110
+ return existing as SubagentRuntimeRegistry;
111
+ }
112
+ const registry: SubagentRuntimeRegistry = {
113
+ bySessionManager: new WeakMap(),
114
+ visibleControlNoticesBySessionManager: new WeakMap(),
115
+ activeEntries: new Set(),
116
+ };
117
+ globalStore[RUNTIME_REGISTRY_STORE_KEY] = registry;
118
+ return registry;
119
+ }
120
+
121
+ function logSlowPhase(label: string, startedAt: number): void {
122
+ const elapsed = Date.now() - startedAt;
123
+ if (elapsed >= SLOW_RELOAD_PHASE_MS) console.error(`Subagent reload phase '${label}' took ${elapsed}ms.`);
124
+ }
125
+
85
126
  function workflowLaneKeys(script: string): string[] {
86
127
  const keys: string[] = [];
87
128
  const seen = new Set<string>();
@@ -255,12 +296,13 @@ function rebuildSlashResultContainer(
255
296
  options: { expanded: boolean },
256
297
  theme: ExtensionContext["ui"]["theme"],
257
298
  rendererConfig?: MainWindowRendererConfig,
299
+ foregroundDetachShortcut?: string,
258
300
  ): void {
259
301
  container.clear();
260
302
  container.addChild(new Spacer(1));
261
303
  const boxTheme = isSlashResultRunning(result) ? "toolPendingBg" : isSlashResultError(result) ? "toolErrorBg" : "toolSuccessBg";
262
304
  const box = new Box(1, 1, (text: string) => theme.bg(boxTheme, text));
263
- box.addChild(renderSubagentResult(result, options, theme, undefined, rendererConfig));
305
+ box.addChild(renderSubagentResult(result, options, theme, undefined, rendererConfig, foregroundDetachShortcut));
264
306
  container.addChild(box);
265
307
  }
266
308
 
@@ -269,6 +311,7 @@ function createSlashResultComponent(
269
311
  options: { expanded: boolean },
270
312
  theme: ExtensionContext["ui"]["theme"],
271
313
  rendererConfig?: MainWindowRendererConfig,
314
+ foregroundDetachShortcut?: string,
272
315
  ): Container {
273
316
  const container = new Container();
274
317
  let lastVersion = -1;
@@ -276,7 +319,7 @@ function createSlashResultComponent(
276
319
  const snapshot = getSlashRenderableSnapshot(details);
277
320
  if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
278
321
  lastVersion = snapshot.version;
279
- rebuildSlashResultContainer(container, snapshot.result, options, theme, rendererConfig);
322
+ rebuildSlashResultContainer(container, snapshot.result, options, theme, rendererConfig, foregroundDetachShortcut);
280
323
  }
281
324
  return Container.prototype.render.call(container, width);
282
325
  };
@@ -348,16 +391,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
348
391
  if (process.env[SUBAGENT_CHILD_ENV] === "1") {
349
392
  return;
350
393
  }
351
- const globalStore = globalThis as Record<string, unknown>;
352
- const runtimeCleanupStoreKey = "__piSubagentRuntimeCleanup";
353
- const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
354
- if (typeof previousRuntimeCleanup === "function") {
355
- try {
356
- previousRuntimeCleanup();
357
- } catch {
358
- // Best effort cleanup for stale timers from an older reload.
359
- }
360
- }
394
+ const runtimeRegistry = getRuntimeRegistry();
361
395
 
362
396
  DIRS.results = ensureAccessibleDir(DIRS.results);
363
397
  DIRS.async = ensureAccessibleDir(DIRS.async);
@@ -373,10 +407,19 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
373
407
  const tempArtifactsDir = getArtifactsDir(null);
374
408
  const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
375
409
  cleanupAllArtifactDirs(artifactCleanupDays);
410
+ const resultIndexCleanupTimer = setTimeout(() => {
411
+ try {
412
+ cleanupResultIndexes(DIRS.results);
413
+ } catch (error) {
414
+ console.error("Failed to clean stale subagent result indexes:", error);
415
+ }
416
+ }, 30_000);
417
+ resultIndexCleanupTimer.unref?.();
376
418
 
377
419
  const state: SubagentState = {
378
420
  baseCwd: "",
379
421
  currentSessionId: null,
422
+ completionOwnerId: currentCompletionOwnerId(),
380
423
  artifactDirPreference: config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
381
424
  ...(config.authorityPolicy ? { authorityPolicy: config.authorityPolicy } : {}),
382
425
  ...(config.missions ? { missionStoreConfig: config.missions } : {}),
@@ -422,6 +465,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
422
465
  : undefined;
423
466
  let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
424
467
  let goalTurnId = 0;
468
+ let parentSessionEnvValue: string | null = null;
425
469
  const scheduledStoreRoot = config.scheduledRuns?.storeRoot === undefined ? undefined : resolveScheduledStoreRoot(config.scheduledRuns.storeRoot);
426
470
  const scheduledRunManager = createScheduledRunManager({
427
471
  config,
@@ -438,10 +482,18 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
438
482
  },
439
483
  resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
440
484
  });
485
+ let refreshResultDelivery = () => {};
486
+ const hasResultDeliveryDemand = () => {
487
+ if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
488
+ if (state.foregroundControls.size > 0) return true;
489
+ if (scheduledRunManager.observedCompletionRunIds().size > 0) return true;
490
+ return missionObserverResultCandidateFiles(DIRS.results).length > 0;
491
+ };
441
492
  const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose: disposeAsyncJobTracker } = createAsyncJobTracker(pi, state, DIRS.async, {
442
493
  widgetEnabled: asyncWidgetEnabled,
494
+ onJobTerminal: () => refreshResultDelivery(),
443
495
  });
444
- const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
496
+ const resultWatcher = createResultWatcher(
445
497
  pi,
446
498
  state,
447
499
  DIRS.results,
@@ -450,22 +502,30 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
450
502
  notifier: completionNotifier,
451
503
  observeCompletion: (result) => scheduledRunManager.handleAsyncCompletion(result),
452
504
  observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
505
+ hasDeliveryDemand: hasResultDeliveryDemand,
453
506
  deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
454
507
  },
455
508
  );
456
-
457
- const runtimeCleanup = () => {
458
- stopResultWatcher();
459
- state.currentSessionId = null;
460
- completionNotifier.dispose();
461
- mainWatchdog.dispose();
462
- scheduledRunManager.stop();
463
- supervisorChannel.dispose();
464
- waitSubscriptionManager.dispose();
465
- fleetStatus?.dispose();
466
- disposeAsyncJobTracker();
467
- };
468
- globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
509
+ const { startResultWatcher, primeExistingResults, stopResultWatcher } = resultWatcher;
510
+ refreshResultDelivery = resultWatcher.refreshResultDelivery;
511
+ const asyncRetentionAbort = new AbortController();
512
+ const asyncRetentionTimer = setTimeout(async () => {
513
+ try {
514
+ await cleanupAsyncRetention({
515
+ asyncDirRoot: DIRS.async,
516
+ resultsDir: DIRS.results,
517
+ signal: asyncRetentionAbort.signal,
518
+ protectedRunIds: new Set([
519
+ ...state.asyncJobs.keys(),
520
+ ...(state.workflowControllers?.keys() ?? []),
521
+ ...scheduledRunManager.referencedAsyncRunIds(),
522
+ ]),
523
+ });
524
+ } catch (error) {
525
+ console.error("Failed to clean retained async subagent state:", error);
526
+ }
527
+ }, ASYNC_RETENTION_DELAY_MS);
528
+ asyncRetentionTimer.unref?.();
469
529
 
470
530
  const executor = createSubagentExecutor({
471
531
  pi,
@@ -479,13 +539,15 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
479
539
  getSubagentSessionRoot,
480
540
  expandTilde,
481
541
  discoverAgents,
542
+ activateSupervisorTransport: () => supervisorChannel.activateTransport(),
543
+ refreshResultDelivery: () => refreshResultDelivery(),
482
544
  });
483
545
  executorScheduled = executor.executeScheduled;
484
546
 
485
547
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
486
548
  const details = resolveSlashMessageDetails(message.details);
487
549
  if (!details) return undefined;
488
- return createSlashResultComponent(details, options, theme, config.mainWindowRenderer);
550
+ return createSlashResultComponent(details, options, theme, config.mainWindowRenderer, config.foregroundDetachShortcut);
489
551
  });
490
552
 
491
553
  pi.registerMessageRenderer<undefined>(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
@@ -573,11 +635,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
573
635
  });
574
636
 
575
637
 
576
- const parameters = createSubagentParamsSchema(config);
638
+ const parameters = createSubagentParamsSchema();
577
639
  const tool: ToolDefinition<typeof parameters, Details> = {
578
640
  name: "subagent",
579
641
  label: "Subagent",
580
642
  description: buildSubagentToolDescription(config),
643
+ ...buildSubagentToolPromptMetadata(config),
581
644
  parameters,
582
645
 
583
646
  execute(id, params, signal, onUpdate, ctx) {
@@ -588,7 +651,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
588
651
  const gap = " ".repeat(config.mainWindowRenderer?.horizontalSpacing ?? 1);
589
652
  const title = theme.fg("toolTitle", theme.bold("subagent"));
590
653
  if (args.action) {
591
- const target = args.agent || args.chainName || "";
654
+ const target = args.agent || "";
592
655
  return new Text(
593
656
  `${title}${gap}${args.action}${target ? `${gap}${theme.fg("accent", target)}` : ""}`,
594
657
  0, 0,
@@ -613,7 +676,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
613
676
  const renderedResult = { ...result, isError: context.isError };
614
677
  return summaryInlineToolDisplay
615
678
  ? renderSubagentSummary(renderedResult, options, theme)
616
- : renderSubagentResult(renderedResult, options, theme, undefined, config.mainWindowRenderer);
679
+ : renderSubagentResult(renderedResult, options, theme, undefined, config.mainWindowRenderer, config.foregroundDetachShortcut);
617
680
  },
618
681
 
619
682
  };
@@ -643,24 +706,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
643
706
  }
644
707
  });
645
708
 
646
- registerSlashCommands(pi, state, { fleetKeybindings: config.fleetKeybindings });
709
+ registerSlashCommands(pi, state, {
710
+ fleetKeybindings: config.fleetKeybindings,
711
+ foregroundDetachShortcut: config.foregroundDetachShortcut,
712
+ });
647
713
 
648
- const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
649
- const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
650
- const previousEventUnsubscribes = globalStore[eventUnsubscribeStoreKey];
651
- if (Array.isArray(previousEventUnsubscribes)) {
652
- for (const unsubscribe of previousEventUnsubscribes) {
653
- if (typeof unsubscribe !== "function") continue;
654
- try {
655
- unsubscribe();
656
- } catch {
657
- // Best effort cleanup for stale handlers from an older reload.
658
- }
659
- }
660
- }
661
- const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
662
- const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
663
- globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
714
+ let visibleControlNotices = new Set<string>();
664
715
  const activeHerdrRuns = () => projectActiveHerdrRuns(state);
665
716
  const herdrStatusBridge = registerHerdrStatusBridge({
666
717
  events: pi.events,
@@ -682,10 +733,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
682
733
  };
683
734
  const asyncStartedHandler = (payload: unknown) => {
684
735
  handleStarted(payload);
736
+ supervisorChannel.activateTransport();
737
+ refreshResultDelivery();
685
738
  fleetStatus?.refresh();
686
739
  };
687
740
  const asyncCompleteHandler = (payload: unknown) => {
688
741
  handleComplete(payload);
742
+ refreshResultDelivery();
689
743
  refreshActiveAsyncCapacity();
690
744
  scheduledRunManager.handleAsyncCompletion(payload);
691
745
  fleetStatus?.refresh();
@@ -702,7 +756,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
702
756
  herdrStatusBridge.dispose,
703
757
  rpcBridge.dispose,
704
758
  ];
705
- globalStore[eventUnsubscribeStoreKey] = eventUnsubscribes;
706
759
 
707
760
  pi.on("tool_result", (event, ctx) => {
708
761
  if (event.toolName !== "subagent") return;
@@ -780,24 +833,128 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
780
833
  const sessionId = ctx.sessionManager.getSessionId();
781
834
  if (sessionId) {
782
835
  process.env[SUBAGENT_PARENT_SESSION_ENV] = sessionId;
836
+ parentSessionEnvValue = sessionId;
783
837
  }
784
838
  }
785
839
  state.lastUiContext = ctx;
840
+ let phaseStartedAt = Date.now();
786
841
  refreshActiveAsyncCapacity();
842
+ logSlowPhase("active-capacity", phaseStartedAt);
843
+ phaseStartedAt = Date.now();
787
844
  cleanupSessionArtifacts(ctx);
845
+ logSlowPhase("session-artifact-cleanup", phaseStartedAt);
788
846
  state.foregroundControls.clear();
789
847
  state.lastForegroundControlId = null;
848
+ phaseStartedAt = Date.now();
790
849
  resetJobs(ctx);
850
+ logSlowPhase("reset-jobs", phaseStartedAt);
851
+ phaseStartedAt = Date.now();
791
852
  restoreForegroundRunHistory(state, { resultsDir: DIRS.results });
853
+ logSlowPhase("foreground-history", phaseStartedAt);
854
+ phaseStartedAt = Date.now();
792
855
  restoreActiveJobs(ctx);
856
+ logSlowPhase("active-job-restore", phaseStartedAt);
857
+ phaseStartedAt = Date.now();
793
858
  scheduledRunManager.bindSession(ctx);
859
+ logSlowPhase("scheduled-runs", phaseStartedAt);
860
+ phaseStartedAt = Date.now();
794
861
  restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
862
+ logSlowPhase("slash-snapshots", phaseStartedAt);
863
+ phaseStartedAt = Date.now();
795
864
  waitSubscriptionManager.restore();
865
+ logSlowPhase("wait-subscriptions", phaseStartedAt);
866
+ phaseStartedAt = Date.now();
796
867
  startResultWatcher();
868
+ logSlowPhase("result-watcher-start", phaseStartedAt);
869
+ phaseStartedAt = Date.now();
797
870
  primeExistingResults({ triggerTurn: !recovering });
871
+ logSlowPhase("result-prime", phaseStartedAt);
798
872
  fleetStatus?.setContext(ctx);
799
873
  };
800
874
 
875
+ let runtimeCleaned = false;
876
+ const runtimeEntry: SubagentRuntimeEntry = {
877
+ sessionManager: null,
878
+ visibleControlNotices,
879
+ cleanup() {
880
+ if (runtimeCleaned) return;
881
+ runtimeCleaned = true;
882
+ const shuttingDownParentSession = parentSessionEnvValue;
883
+ clearTimeout(resultIndexCleanupTimer);
884
+ clearTimeout(asyncRetentionTimer);
885
+ asyncRetentionAbort.abort();
886
+ stopResultWatcher();
887
+ completionNotifier.dispose();
888
+ mainWatchdog.dispose();
889
+ scheduledRunManager.stop();
890
+ supervisorChannel.dispose();
891
+ waitSubscriptionManager.dispose();
892
+ fleetStatus?.dispose();
893
+ disposeAsyncJobTracker();
894
+ for (const timer of state.cleanupTimers.values()) clearTimeout(timer);
895
+ state.cleanupTimers.clear();
896
+ state.asyncJobs.clear();
897
+ for (const unsubscribe of eventUnsubscribes) {
898
+ try {
899
+ unsubscribe();
900
+ } catch {
901
+ // Best effort cleanup during shutdown or reload.
902
+ }
903
+ }
904
+ slashBridge.cancelAll();
905
+ slashBridge.dispose();
906
+ promptTemplateBridge.cancelAll();
907
+ promptTemplateBridge.dispose();
908
+ state.widgetsSuspended = false;
909
+ state.currentSessionId = null;
910
+ state.parentSessionFile = null;
911
+ parentSessionEnvValue = null;
912
+ if (shuttingDownParentSession && process.env[SUBAGENT_PARENT_SESSION_ENV] === shuttingDownParentSession) {
913
+ delete process.env[SUBAGENT_PARENT_SESSION_ENV];
914
+ }
915
+ if (runtimeEntry.sessionManager && runtimeRegistry.bySessionManager.get(runtimeEntry.sessionManager) === runtimeEntry) {
916
+ runtimeRegistry.bySessionManager.delete(runtimeEntry.sessionManager);
917
+ }
918
+ runtimeRegistry.activeEntries.delete(runtimeEntry);
919
+ if (runtimeRegistry.activeEntries.size === 0) clearSlashSnapshots();
920
+ try {
921
+ if (state.lastUiContext?.hasUI) state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
922
+ } catch (error) {
923
+ if (!isStaleExtensionContextError(error)) throw error;
924
+ }
925
+ state.lastUiContext = null;
926
+ },
927
+ };
928
+
929
+ const installRuntime = (ctx: ExtensionContext) => {
930
+ if (runtimeCleaned) {
931
+ throw new Error("Cannot restart a cleaned pi-subagents extension runtime; register a new extension instance.");
932
+ }
933
+ const sessionManager = ctx.sessionManager as object;
934
+ const previousRuntime = runtimeRegistry.bySessionManager.get(sessionManager);
935
+ const existingVisibleControlNotices = runtimeRegistry.visibleControlNoticesBySessionManager.get(sessionManager);
936
+ if (existingVisibleControlNotices) {
937
+ visibleControlNotices = existingVisibleControlNotices;
938
+ } else {
939
+ runtimeRegistry.visibleControlNoticesBySessionManager.set(sessionManager, visibleControlNotices);
940
+ }
941
+ runtimeEntry.visibleControlNotices = visibleControlNotices;
942
+ if (runtimeEntry.sessionManager && runtimeEntry.sessionManager !== sessionManager
943
+ && runtimeRegistry.bySessionManager.get(runtimeEntry.sessionManager) === runtimeEntry) {
944
+ runtimeRegistry.bySessionManager.delete(runtimeEntry.sessionManager);
945
+ }
946
+ runtimeEntry.sessionManager = sessionManager;
947
+ runtimeRegistry.bySessionManager.set(sessionManager, runtimeEntry);
948
+ runtimeRegistry.activeEntries.add(runtimeEntry);
949
+ if (previousRuntime && previousRuntime !== runtimeEntry) {
950
+ try {
951
+ previousRuntime.cleanup();
952
+ } catch {
953
+ // Best effort cleanup for stale resources from the replaced runtime.
954
+ }
955
+ }
956
+ };
957
+
801
958
  pi.on("agent_start", () => {
802
959
  resumeWidgetsAfterCompaction();
803
960
  herdrStatusBridge.agentStarted();
@@ -825,6 +982,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
825
982
  });
826
983
 
827
984
  pi.on("session_start", (event, ctx) => {
985
+ installRuntime(ctx);
828
986
  const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
829
987
  resetSessionState(ctx, recovering);
830
988
  herdrStatusBridge.sessionStarted({
@@ -833,49 +991,11 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
833
991
  });
834
992
  rpcBridge.emitReady(ctx);
835
993
  supervisorChannel.start();
994
+ supervisorChannel.activateTransport();
836
995
  });
837
996
 
838
997
  pi.on("session_shutdown", async () => {
839
- state.widgetsSuspended = false;
840
- stopResultWatcher();
841
- state.currentSessionId = null;
842
- state.parentSessionFile = null;
843
- completionNotifier.dispose();
844
- delete process.env[SUBAGENT_PARENT_SESSION_ENV];
845
- for (const unsubscribe of eventUnsubscribes) {
846
- try {
847
- unsubscribe();
848
- } catch {
849
- // Best effort cleanup during shutdown.
850
- }
851
- }
852
- if (globalStore[eventUnsubscribeStoreKey] === eventUnsubscribes) {
853
- delete globalStore[eventUnsubscribeStoreKey];
854
- }
855
- scheduledRunManager.stop();
856
- disposeAsyncJobTracker();
857
- for (const timer of state.cleanupTimers.values()) {
858
- clearTimeout(timer);
859
- }
860
- state.cleanupTimers.clear();
861
- state.asyncJobs.clear();
862
- clearSlashSnapshots();
863
- slashBridge.cancelAll();
864
- slashBridge.dispose();
865
- promptTemplateBridge.cancelAll();
866
- promptTemplateBridge.dispose();
867
- supervisorChannel.dispose();
868
- fleetStatus?.dispose();
869
- if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
870
- delete globalStore[runtimeCleanupStoreKey];
871
- }
872
- try {
873
- if (state.lastUiContext?.hasUI) {
874
- state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
875
- }
876
- } catch (error) {
877
- if (!isStaleExtensionContextError(error)) throw error;
878
- }
998
+ runtimeEntry.cleanup();
879
999
  await herdrStatusBridge.flush();
880
1000
  });
881
1001
  }
@@ -8,7 +8,12 @@ export interface PublicSubagentExecutionParams {
8
8
  parallel?: unknown;
9
9
  concurrency?: unknown;
10
10
  chainDir?: unknown;
11
+ chainName?: unknown;
12
+ config?: unknown;
11
13
  workflowScript?: unknown;
14
+ isolation?: unknown;
15
+ worktree?: unknown;
16
+ output?: unknown;
12
17
  resume?: unknown;
13
18
  clarify?: unknown;
14
19
  runFanoutBudget?: unknown;
@@ -26,6 +31,17 @@ export type PublicSubagentExecutionNormalization<T> =
26
31
  * Internal runs.run children and structured owned delegation bypass this boundary.
27
32
  */
28
33
  export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
34
+ if (params.isolation !== undefined) {
35
+ if (params.isolation !== "none" && params.isolation !== "worktree") {
36
+ return { ok: false, error: "isolation must be 'none' or 'worktree'.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
37
+ }
38
+ const isolationWorktree = params.isolation === "worktree";
39
+ if (params.worktree !== undefined && params.worktree !== isolationWorktree) {
40
+ return { ok: false, error: `isolation '${params.isolation}' conflicts with worktree: ${String(params.worktree)}.`, mode: params.workflowScript !== undefined ? "workflow" : "management" };
41
+ }
42
+ const { isolation: _isolation, ...normalizedParams } = params;
43
+ params = { ...normalizedParams, worktree: isolationWorktree } as T;
44
+ }
29
45
  if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
30
46
  return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
31
47
  }
@@ -37,6 +53,12 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
37
53
  if (params.clarify !== undefined) {
38
54
  return { ok: false, error: "Public workflowScript execution does not support clarify UI.", mode: "workflow" };
39
55
  }
56
+ if (params.chainName !== undefined) {
57
+ return { ok: false, error: "Durable chain management was removed; use workflowScript or /prompt-workflow for repeatable workflows.", mode: "management" };
58
+ }
59
+ if (params.config && typeof params.config === "object" && !Array.isArray(params.config) && Object.prototype.hasOwnProperty.call(params.config, "steps")) {
60
+ return { ok: false, error: "Durable chain definitions were removed; use workflowScript or /prompt-workflow for repeatable workflows.", mode: "management" };
61
+ }
40
62
  if (params.resume !== undefined) {
41
63
  return { ok: false, error: "Top-level resume execution is not available. Put resume on a workflowScript runs.run/runs.all item.", mode: "workflow" };
42
64
  }
@@ -46,6 +68,12 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
46
68
  }
47
69
  if (normalizedAction !== undefined) {
48
70
  const legacyAction = normalizedAction.toLowerCase();
71
+ if (legacyAction === "append-step") {
72
+ return { ok: false, error: "Legacy append-step control was removed from the public subagent tool; use current workflowScript orchestration.", mode: "management" };
73
+ }
74
+ if (legacyAction === "approve-checkpoint" || legacyAction === "reject-checkpoint") {
75
+ return { ok: false, error: "Legacy checkpoint approval controls were removed from the public subagent tool; use current workflowScript orchestration.", mode: "management" };
76
+ }
49
77
  if (legacyAction === "single") {
50
78
  return { ok: false, error: "action='single' is not supported. Omit action and pass { agent, task } for one child.", mode: "workflow" };
51
79
  }
@@ -70,7 +98,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
70
98
  return { ok: true, params: { ...params, action: normalizedAction } };
71
99
  }
72
100
  if (params.step !== undefined) {
73
- return { ok: false, error: "step is only available with action='append-step'; it is not an execution mode.", mode: "workflow" };
101
+ return { ok: false, error: "step is not a public execution field; use workflowScript for orchestration.", mode: "workflow" };
74
102
  }
75
103
  if (params.workflowScript !== undefined && (params.agent !== undefined || params.task !== undefined)) {
76
104
  return { ok: false, error: "Structured single-child execution cannot be combined with workflowScript.", mode: "workflow" };
@@ -82,10 +110,11 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
82
110
  if (params.task !== undefined && typeof params.task !== "string") {
83
111
  return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
84
112
  }
85
- const { agent: _agent, task: _task, ...workflowDefaults } = params;
113
+ const { agent: _agent, task: _task, output, ...workflowDefaults } = params;
86
114
  const child = {
87
115
  agent: params.agent.trim(),
88
116
  ...(params.task !== undefined ? { task: params.task } : {}),
117
+ output: output === undefined ? true : output,
89
118
  };
90
119
  return {
91
120
  ok: true,
@@ -20,6 +20,7 @@ import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text
20
20
  import { readStatus } from "../shared/utils.ts";
21
21
  import { SubagentParams } from "./schemas.ts";
22
22
  import { normalizePublicSubagentExecution } from "./public-execution.ts";
23
+ import { ASYNC_STATUS_SNAPSHOT_KIND, ASYNC_STATUS_SNAPSHOT_VERSION, buildAsyncStatusSnapshotForState } from "../runs/background/async-status-snapshot.ts";
23
24
 
24
25
  export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
25
26
  export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
@@ -375,6 +376,7 @@ function pingData(ctx: ExtensionContext | null) {
375
376
  capabilities: {
376
377
  status: true,
377
378
  fleetStatus: { version: 1 },
379
+ asyncStatusSnapshot: { kind: ASYNC_STATUS_SNAPSHOT_KIND, version: ASYNC_STATUS_SNAPSHOT_VERSION },
378
380
  asyncSpawn: true,
379
381
  steer: true,
380
382
  nonRecoveringSteer: true,
@@ -544,13 +546,15 @@ async function handleRequest(
544
546
  request.method,
545
547
  { action: "status", ...normalizeTargetParams(request.params, "status") },
546
548
  );
549
+ const sessionId = resolveCurrentSessionId(ctx.sessionManager);
547
550
  return {
548
551
  ...status,
549
552
  fleet: buildFleetStatus(
550
553
  options.state,
551
554
  fleetKeys,
552
- resolveCurrentSessionId(ctx.sessionManager),
555
+ sessionId,
553
556
  ),
557
+ asyncSnapshot: buildAsyncStatusSnapshotForState(options.state, sessionId),
554
558
  };
555
559
  }
556
560
  if (request.method === "steer") {