pi-subagents 0.57.0 → 0.59.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 (115) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/agents.md +18 -9
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +40 -1
  5. package/docs/models.md +27 -4
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +92 -6
  8. package/docs/workflows.md +115 -0
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +3 -1
  13. package/skills/pi-subagents/references/execution-controls.md +6 -2
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +26 -5
  16. package/src/agents/agent-management.ts +28 -17
  17. package/src/agents/agent-serializer.ts +9 -2
  18. package/src/agents/agents.ts +103 -27
  19. package/src/agents/runtime-agent-events.ts +70 -0
  20. package/src/agents/runtime-agent-registry.ts +19 -18
  21. package/src/api/agents.ts +10 -5
  22. package/src/api/background-work.ts +5 -1
  23. package/src/api/delegation.ts +0 -7
  24. package/src/api/preflight.ts +32 -10
  25. package/src/extension/fanout-child.ts +5 -3
  26. package/src/extension/index.ts +66 -27
  27. package/src/extension/public-execution.ts +15 -2
  28. package/src/extension/rpc.ts +35 -14
  29. package/src/extension/schemas.ts +37 -12
  30. package/src/extension/tool-description.ts +25 -6
  31. package/src/integrations/herdr-status.ts +5 -0
  32. package/src/intercom/result-intercom.ts +2 -0
  33. package/src/profiles/profiles.ts +5 -6
  34. package/src/runs/background/active-async-capacity.ts +2 -2
  35. package/src/runs/background/async-execution.ts +78 -46
  36. package/src/runs/background/async-job-tracker.ts +14 -12
  37. package/src/runs/background/async-resume.ts +29 -27
  38. package/src/runs/background/async-status-snapshot.ts +23 -261
  39. package/src/runs/background/async-status.ts +68 -7
  40. package/src/runs/background/chain-append.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +60 -8
  42. package/src/runs/background/fleet-view.ts +21 -11
  43. package/src/runs/background/notify.ts +184 -9
  44. package/src/runs/background/result-delivery-ownership.ts +45 -0
  45. package/src/runs/background/result-files.ts +2 -1
  46. package/src/runs/background/result-watcher.ts +29 -10
  47. package/src/runs/background/resume-guidance.ts +1 -1
  48. package/src/runs/background/retained-children.ts +1 -1
  49. package/src/runs/background/run-status.ts +18 -8
  50. package/src/runs/background/scheduled-runs.ts +86 -7
  51. package/src/runs/background/stale-run-reconciler.ts +10 -4
  52. package/src/runs/background/steering.ts +4 -14
  53. package/src/runs/background/subagent-runner.ts +389 -350
  54. package/src/runs/background/subagent-wait.ts +58 -10
  55. package/src/runs/background/terminal-run-index.ts +1 -1
  56. package/src/runs/background/wait-completions.ts +22 -1
  57. package/src/runs/background/wait-config.ts +23 -9
  58. package/src/runs/background/wait-tool.ts +9 -2
  59. package/src/runs/foreground/async-steering-action.ts +2 -2
  60. package/src/runs/foreground/execution.ts +134 -121
  61. package/src/runs/foreground/foreground-control.ts +3 -0
  62. package/src/runs/foreground/foreground-history.ts +1 -0
  63. package/src/runs/foreground/subagent-executor.ts +527 -257
  64. package/src/runs/foreground/workflow-detach-reconcile.ts +130 -120
  65. package/src/runs/shared/abort-recovery.ts +119 -0
  66. package/src/runs/shared/async-status-projection.ts +463 -0
  67. package/src/runs/shared/child-identity.ts +19 -4
  68. package/src/runs/shared/child-launch-plan.ts +151 -0
  69. package/src/runs/shared/completion-evidence.ts +89 -0
  70. package/src/runs/shared/completion-guard.ts +5 -4
  71. package/src/runs/shared/dynamic-fanout.ts +3 -3
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/host-step-status.ts +230 -0
  74. package/src/runs/shared/lane-metadata.ts +105 -0
  75. package/src/runs/shared/launch-cwd.ts +16 -0
  76. package/src/runs/shared/long-running-guard.ts +2 -1
  77. package/src/runs/shared/mcp-config-sources.ts +422 -0
  78. package/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
  79. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  80. package/src/runs/shared/model-exclusions.ts +12 -0
  81. package/src/runs/shared/model-fallback.ts +25 -6
  82. package/src/runs/shared/nested-events.ts +6 -2
  83. package/src/runs/shared/nested-render.ts +7 -3
  84. package/src/runs/shared/parallel-handoff.ts +419 -7
  85. package/src/runs/shared/parallel-utils.ts +15 -1
  86. package/src/runs/shared/pi-args.ts +81 -8
  87. package/src/runs/shared/single-output.ts +44 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +98 -13
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +11 -2
  94. package/src/shared/launch-contract.ts +5 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +240 -30
  97. package/src/shared/utils.ts +41 -84
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/slash/subagents-admin.ts +3 -0
  104. package/src/tui/fleet-status.ts +125 -74
  105. package/src/tui/fleet.ts +11 -5
  106. package/src/tui/render.ts +330 -33
  107. package/src/watchdog/turn-delta.ts +1 -1
  108. package/src/workflows/chat-progress.ts +6 -3
  109. package/src/workflows/host-command.ts +230 -0
  110. package/src/workflows/scripted-workflow.ts +496 -35
  111. package/src/workflows/workflow-child-summary.ts +9 -5
  112. package/src/workflows/workflow-preflight.ts +270 -0
  113. package/src/workflows/workflow-receipt.ts +79 -5
  114. package/src/workflows/workflow-settlement.ts +246 -0
  115. package/src/runs/shared/turn-budget.ts +0 -98
@@ -21,10 +21,12 @@ import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition
21
21
  import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
22
22
  import { discoverAgents, discoverAgentsAll, type AgentConfig, type AgentScope } from "../agents/agents.ts";
23
23
  import { clearRuntimeAgentsForPi, listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
24
+ import { registerRuntimeAgentEventListener } from "../agents/runtime-agent-events.ts";
24
25
  import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
25
26
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
26
27
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
27
28
  import { getAgentDir } from "../shared/utils.ts";
29
+ import { isStaleExtensionContextError, withCachedUiContext } from "../shared/extension-context.ts";
28
30
  import { currentCompletionOwnerId } from "../shared/completion-owner.ts";
29
31
  import { cleanupOldChainDirs } from "../shared/settings.ts";
30
32
  import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
@@ -37,6 +39,7 @@ import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, res
37
39
  import { cleanupResultIndexes, missionObserverResultCandidateFiles } from "../runs/background/result-files.ts";
38
40
  import { ASYNC_RETENTION_DELAY_MS, cleanupAsyncRetention } from "../runs/background/async-retention.ts";
39
41
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
42
+ import { createResultDeliveryOwnership } from "../runs/background/result-delivery-ownership.ts";
40
43
  import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
41
44
  import { registerSlashCommands } from "../slash/slash-commands.ts";
42
45
  import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
@@ -47,7 +50,6 @@ import { registerHerdrStatusBridge, type HerdrStatusRun } from "../integrations/
47
50
  import { listHerdrProjectPaneRoots, restoreHerdrProjectPaneSnapshots } from "../inspectors/herdr/project-panes.ts";
48
51
  import { registerSubagentRpcBridge } from "./rpc.ts";
49
52
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
50
- import { inspectSubagentStatus } from "../runs/background/run-status.ts";
51
53
  import { resolveWaitToolConfig } from "../runs/background/subagent-wait.ts";
52
54
  import { registerWaitTool } from "../runs/background/wait-tool.ts";
53
55
  import { createWaitSubscriptionManager } from "../runs/background/wait-subscriptions.ts";
@@ -59,6 +61,7 @@ import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capabili
59
61
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
60
62
  import { applyModelExclusionsConfig, loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
61
63
  import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
64
+ import { formatWorkflowPreflightSummary, normalizeWorkflowPreflight } from "../workflows/workflow-preflight.ts";
62
65
  import { finalizeToolResult } from "./tool-result.ts";
63
66
  import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
64
67
  import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.ts";
@@ -249,15 +252,25 @@ function workflowLaneKeys(script: string): string[] {
249
252
  return keys;
250
253
  }
251
254
 
252
- function formatWorkflowManifest(script: string, async: unknown, clarify: unknown): string {
255
+ function formatWorkflowPreflightCall(input: unknown): string {
256
+ if (input === undefined) return "";
257
+ try {
258
+ return formatWorkflowPreflightSummary(normalizeWorkflowPreflight(input));
259
+ } catch (error) {
260
+ return `preflight · rejected: ${error instanceof Error ? error.message : String(error)}`;
261
+ }
262
+ }
263
+
264
+ function formatWorkflowManifest(script: string, async: unknown, clarify: unknown, preflightInput?: unknown): string {
253
265
  if (clarify === true) return "workflow script · rejected: clarify UI unsupported";
254
266
  const keys = workflowLaneKeys(script);
255
267
  // The workflow executor starts background work unless callers pass async:false.
256
268
  const mode = async === false ? "foreground" : "background";
257
- if (keys.length === 0) return `workflow script · ${mode}`;
269
+ const preflight = formatWorkflowPreflightCall(preflightInput);
270
+ if (keys.length === 0) return `workflow script · ${mode}${preflight ? ` · ${preflight}` : ""}`;
258
271
  const visibleKeys = keys.slice(0, 4).join(", ");
259
272
  const remainder = keys.length > 4 ? `, +${keys.length - 4}` : "";
260
- return `workflow · ${mode} · ${keys.length} lane${keys.length === 1 ? "" : "s"}: ${visibleKeys}${remainder}`;
273
+ return `workflow · ${mode} · ${keys.length} lane${keys.length === 1 ? "" : "s"}: ${visibleKeys}${remainder}${preflight ? ` · ${preflight}` : ""}`;
261
274
  }
262
275
 
263
276
  /**
@@ -290,10 +303,6 @@ function isSlashResultError(result: { details?: Details }): boolean {
290
303
  return result.details?.results.some((entry) => entry.exitCode !== 0 && entry.progress?.status !== "running") || false;
291
304
  }
292
305
 
293
- function isStaleExtensionContextError(error: unknown): boolean {
294
- return error instanceof Error && error.message.includes("Extension context no longer active");
295
- }
296
-
297
306
  function rebuildSlashResultContainer(
298
307
  container: Container,
299
308
  result: AgentToolResult<Details>,
@@ -462,16 +471,31 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
462
471
  clear: () => {},
463
472
  },
464
473
  };
474
+ const withLastUiContext = <T>(run: (ctx: ExtensionContext) => T): T | undefined => {
475
+ const cached = state.lastUiContext;
476
+ return withCachedUiContext(cached, () => {
477
+ if (state.lastUiContext === cached) state.lastUiContext = null;
478
+ }, run);
479
+ };
465
480
 
466
481
  const supervisorChannel = createNativeSupervisorChannel(pi, state);
467
482
  const waitSubscriptionManager = createWaitSubscriptionManager(pi, state);
468
483
  const mainWatchdog = registerMainWatchdog(pi);
469
- const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch });
484
+ const resultDeliveryOwnership = createResultDeliveryOwnership(state);
485
+ const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch, ownership: resultDeliveryOwnership });
470
486
  const fleetStatus = fleetViewEnabled
471
487
  ? new SubagentFleetStatus(state, async (itemKey) => {
472
- const ctx = state.lastUiContext;
473
- if (!ctx?.hasUI) return;
474
- await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: config.fleetKeybindings });
488
+ const ctx = withLastUiContext((current) => current);
489
+ if (!ctx) return;
490
+ try {
491
+ await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: config.fleetKeybindings });
492
+ } catch (error) {
493
+ if (isStaleExtensionContextError(error)) {
494
+ if (state.lastUiContext === ctx) state.lastUiContext = null;
495
+ return;
496
+ }
497
+ throw error;
498
+ }
475
499
  }, { placement: fleetViewPlacement })
476
500
  : undefined;
477
501
  let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
@@ -500,10 +524,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
500
524
  if (scheduledRunManager.observedCompletionRunIds().size > 0) return true;
501
525
  return missionObserverResultCandidateFiles(DIRS.results).length > 0;
502
526
  };
503
- const discoverAgentsForRuntime = (cwd: string, scope: AgentScope) => {
504
- const discovered = discoverAgents(cwd, scope);
527
+ const discoverAgentsForRuntime = (cwd: string, scope: AgentScope, preferredModelProvider?: string) => {
528
+ const discovered = discoverAgents(cwd, scope, preferredModelProvider);
505
529
  if (listRuntimeAgentConfigs(pi).length === 0) return discovered;
506
- const all = discoverAgentsAll(cwd);
530
+ const all = discoverAgentsAll(cwd, preferredModelProvider);
507
531
  const configuredAgents: AgentConfig[] = [
508
532
  ...all.builtin,
509
533
  ...all.package,
@@ -528,6 +552,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
528
552
  10 * 60 * 1000,
529
553
  {
530
554
  notifier: completionNotifier,
555
+ ownership: resultDeliveryOwnership,
531
556
  observeCompletion: (result) => scheduledRunManager.handleAsyncCompletion(result),
532
557
  observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
533
558
  hasDeliveryDemand: hasResultDeliveryDemand,
@@ -535,7 +560,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
535
560
  resultScanLogging: config.resultScanLogging,
536
561
  },
537
562
  );
538
- const { startResultWatcher, primeExistingResults, stopResultWatcher } = resultWatcher;
563
+ const { startResultWatcher, transitionResultDelivery, primeExistingResults, stopResultWatcher } = resultWatcher;
539
564
  refreshResultDelivery = resultWatcher.refreshResultDelivery;
540
565
  const asyncRetentionAbort = new AbortController();
541
566
  const asyncRetentionTimer = setTimeout(async () => {
@@ -562,6 +587,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
562
587
  config,
563
588
  asyncByDefault,
564
589
  waitToolEnabled: waitToolConfig.enabled,
590
+ waitToolDefaultTimeoutMs: waitToolConfig.defaultTimeoutMs,
565
591
  handleScheduledRunAction: (params, ctx) => scheduledRunManager.handleToolCall(params, ctx),
566
592
  watchdog: mainWatchdog,
567
593
  tempArtifactsDir,
@@ -649,7 +675,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
649
675
 
650
676
  const slashBridge = registerSlashSubagentBridge({
651
677
  events: pi.events,
652
- asyncByDefault,
653
678
  getContext: () => state.lastUiContext,
654
679
  execute: (id, params, signal, onUpdate, ctx) =>
655
680
  executeSubagentCollapsed(id, params, signal, onUpdate, ctx),
@@ -698,13 +723,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
698
723
  }
699
724
  if (args.workflowScript)
700
725
  return new Text(
701
- `${title}${gap}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
726
+ `${title}${gap}${formatWorkflowManifest(args.workflowScript, args.async, false, args.preflight)}`,
702
727
  0,
703
728
  0,
704
729
  );
705
730
  if (args.workflowScriptPath)
706
731
  return new Text(
707
- `${title}${gap}${theme.fg("accent", args.workflowScriptPath)}${args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : ""}`,
732
+ `${title}${gap}${theme.fg("accent", args.workflowScriptPath)}${args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : ""}${args.preflight !== undefined ? `${gap}${theme.fg("dim", formatWorkflowPreflightCall(args.preflight))}` : ""}`,
708
733
  0,
709
734
  0,
710
735
  );
@@ -728,7 +753,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
728
753
 
729
754
  pi.registerTool(tool);
730
755
 
731
- registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager);
756
+ registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager, waitToolConfig.defaultTimeoutMs);
732
757
 
733
758
  pi.on("agent_end", async (_event, ctx) => {
734
759
  if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events });
@@ -751,7 +776,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
751
776
  }
752
777
  });
753
778
 
754
- registerSlashCommands(pi, state, {
779
+ const disposeSlashCommands = registerSlashCommands(pi, state, {
755
780
  fleetKeybindings: config.fleetKeybindings,
756
781
  foregroundDetachShortcut: config.foregroundDetachShortcut,
757
782
  });
@@ -791,6 +816,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
791
816
  fleetStatus?.refresh();
792
817
  };
793
818
  const eventUnsubscribes = [
819
+ registerRuntimeAgentEventListener(pi),
794
820
  pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
795
821
  pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
796
822
  pi.events.on(SUBAGENT_PROCESS_TERMINAL_EVENT, () => {
@@ -807,7 +833,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
807
833
  if (event.toolName !== "subagent") return;
808
834
  if (!ctx.hasUI) return;
809
835
  state.lastUiContext = ctx;
836
+ const activeJobCount = state.asyncJobs.size;
810
837
  restoreActiveJobs(ctx);
838
+ if (state.asyncJobs.size > activeJobCount) herdrStatusBridge.syncRuns();
811
839
  fleetStatus?.setContext(ctx);
812
840
  fleetStatus?.refresh();
813
841
  if (state.asyncJobs.size > 0) {
@@ -830,14 +858,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
830
858
  const suspendWidgetsForCompaction = () => {
831
859
  if (state.widgetsSuspended) return;
832
860
  state.widgetsSuspended = true;
833
- if (state.lastUiContext?.hasUI) state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
861
+ withLastUiContext((ctx) => ctx.ui.setWidget(WIDGET_KEY, undefined));
834
862
  fleetStatus?.refresh();
835
863
  };
836
864
  const resumeWidgetsAfterCompaction = () => {
837
865
  if (!state.widgetsSuspended) return;
838
866
  state.widgetsSuspended = false;
839
- const ctx = state.lastUiContext;
840
- if (ctx?.hasUI) refreshWidget(ctx);
867
+ withLastUiContext((ctx) => refreshWidget(ctx));
841
868
  fleetStatus?.refresh();
842
869
  };
843
870
 
@@ -853,11 +880,14 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
853
880
  );
854
881
  };
855
882
 
856
- const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
883
+ const resetSessionState = (ctx: ExtensionContext, recovering: boolean, previousSessionFile?: string) => {
857
884
  state.widgetsSuspended = false;
858
885
  state.baseCwd = ctx.cwd;
859
886
  goalTurnId = 0;
887
+ const previousRuntimeSessionId = state.currentSessionId;
888
+ resultDeliveryOwnership.claimPredecessor(previousSessionFile, previousRuntimeSessionId);
860
889
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
890
+ transitionResultDelivery();
861
891
  state.parentSessionFile = ctx.sessionManager.getSessionFile();
862
892
  state.trustedSessionFileRoot = state.parentSessionFile ? path.join(getAgentDir(), "sessions") : undefined;
863
893
  state.trustedSessionRoots = [...new Set([
@@ -929,11 +959,19 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
929
959
  if (runtimeCleaned) return;
930
960
  runtimeCleaned = true;
931
961
  const shuttingDownParentSession = parentSessionEnvValue;
962
+ // Workflow continuations retain their launch context; abort them before
963
+ // teardown so a reload cannot launch through a stale context.
964
+ for (const controller of state.workflowControllers?.values() ?? []) {
965
+ if (!controller.signal.aborted) controller.abort(new Error("Workflow stopped because the extension session was replaced or reloaded."));
966
+ }
967
+ state.workflowControllers?.clear();
968
+ state.workflowChildStops?.clear();
932
969
  clearRuntimeAgentsForPi(pi);
933
970
  clearTimeout(resultIndexCleanupTimer);
934
971
  clearTimeout(asyncRetentionTimer);
935
972
  asyncRetentionAbort.abort();
936
973
  stopResultWatcher();
974
+ resultDeliveryOwnership.clear();
937
975
  completionNotifier.dispose();
938
976
  mainWatchdog.dispose();
939
977
  scheduledRunManager.stop();
@@ -951,6 +989,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
951
989
  // Best effort cleanup during shutdown or reload.
952
990
  }
953
991
  }
992
+ disposeSlashCommands.dispose();
954
993
  slashBridge.cancelAll();
955
994
  slashBridge.dispose();
956
995
  promptTemplateBridge.cancelAll();
@@ -1020,7 +1059,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1020
1059
 
1021
1060
  pi.on("session_compact", () => {
1022
1061
  const hasActiveAsyncWork = [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
1023
- if (!hasActiveAsyncWork || state.lastUiContext?.hasUI !== true) return;
1062
+ if (!hasActiveAsyncWork || !withLastUiContext(() => true)) return;
1024
1063
  pi.sendMessage(
1025
1064
  {
1026
1065
  customType: "subagent-compaction-resume",
@@ -1034,7 +1073,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1034
1073
  pi.on("session_start", (event, ctx) => {
1035
1074
  installRuntime(ctx);
1036
1075
  const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
1037
- resetSessionState(ctx, recovering);
1076
+ resetSessionState(ctx, recovering, event.previousSessionFile);
1038
1077
  herdrStatusBridge.sessionStarted({
1039
1078
  hasUI: ctx.hasUI === true,
1040
1079
  runs: activeHerdrRuns(),
@@ -1,7 +1,14 @@
1
1
  export interface PublicSubagentExecutionParams {
2
2
  action?: unknown;
3
+ mode?: unknown;
4
+ repo?: unknown;
5
+ planId?: unknown;
3
6
  agent?: unknown;
4
7
  task?: unknown;
8
+ handoffPath?: unknown;
9
+ laneId?: unknown;
10
+ merge?: unknown;
11
+ supersession?: unknown;
5
12
  step?: unknown;
6
13
  tasks?: unknown;
7
14
  chain?: unknown;
@@ -12,8 +19,10 @@ export interface PublicSubagentExecutionParams {
12
19
  config?: unknown;
13
20
  workflowScript?: unknown;
14
21
  workflowScriptPath?: unknown;
22
+ preflight?: unknown;
15
23
  isolation?: unknown;
16
24
  worktree?: unknown;
25
+ lane?: unknown;
17
26
  async?: unknown;
18
27
  output?: unknown;
19
28
  resume?: unknown;
@@ -22,6 +31,7 @@ export interface PublicSubagentExecutionParams {
22
31
  workflowKey?: unknown;
23
32
  workflowChildAsyncId?: unknown;
24
33
  workflowAwaitAsync?: unknown;
34
+ workflowAwaitDetached?: unknown;
25
35
  workflowParentDeadlineAt?: unknown;
26
36
  suppressRoutineResultIntercom?: unknown;
27
37
  runFanoutBudget?: unknown;
@@ -38,11 +48,14 @@ export type PublicSubagentExecutionNormalization<T> =
38
48
  * Enforce the public execution cutover before requests reach the executor.
39
49
  * Internal runs.run children and structured owned delegation bypass this boundary.
40
50
  */
41
- export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T, options: { asyncByDefault?: boolean } = {}): PublicSubagentExecutionNormalization<T> {
51
+ export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
42
52
  if (params.workflowScript !== undefined && params.workflowScriptPath !== undefined) {
43
53
  return { ok: false, error: "workflowScript and workflowScriptPath are mutually exclusive.", mode: "workflow" };
44
54
  }
45
55
  const hasWorkflowInput = params.workflowScript !== undefined || params.workflowScriptPath !== undefined;
56
+ if (params.preflight !== undefined && !hasWorkflowInput) {
57
+ return { ok: false, error: "preflight requires workflowScript or workflowScriptPath.", mode: params.action === undefined ? "workflow" : "management" };
58
+ }
46
59
  const hasValidWorkflowInput = (typeof params.workflowScript === "string" && Boolean(params.workflowScript.trim()))
47
60
  || (typeof params.workflowScriptPath === "string" && Boolean(params.workflowScriptPath.trim()));
48
61
  if (params.isolation !== undefined) {
@@ -59,7 +72,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
59
72
  if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
60
73
  return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: hasWorkflowInput ? "workflow" : "management" };
61
74
  }
62
- if (params.workflowParentRunId !== undefined || params.workflowKey !== undefined || params.workflowChildAsyncId !== undefined || params.workflowAwaitAsync !== undefined || params.workflowParentDeadlineAt !== undefined || params.suppressRoutineResultIntercom !== undefined) {
75
+ if (params.workflowParentRunId !== undefined || params.workflowKey !== undefined || params.workflowChildAsyncId !== undefined || params.workflowAwaitAsync !== undefined || params.workflowAwaitDetached !== undefined || params.workflowParentDeadlineAt !== undefined || params.suppressRoutineResultIntercom !== undefined) {
63
76
  return { ok: false, error: "Public execution does not accept internal workflow child fields.", mode: hasWorkflowInput ? "workflow" : "management" };
64
77
  }
65
78
  const action = params.action;
@@ -557,7 +557,7 @@ function stopAsyncRun(
557
557
  childId: stoppedChild.id,
558
558
  status: "stopping",
559
559
  ts,
560
- reason: "user",
560
+ reason: "rpc",
561
561
  source: "rpc",
562
562
  asyncDir,
563
563
  stepIndex: stoppedChild.index,
@@ -579,20 +579,21 @@ function stopAsyncRun(
579
579
  if (initialStatus.mode === "workflow" && initialStatus.state === "running") {
580
580
  if (child) {
581
581
  const stopChild = options.state?.workflowChildStops?.get(initialRunId);
582
- if (!stopChild) throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; child stop is unavailable.`);
583
- if (!stopChild(child.id, `Workflow child '${child.id}' stopped by RPC.`)) throw new SubagentRpcError("invalid_state", `Child '${childId}' in workflow ${initialRunId} is not available to stop.`);
584
- emitChildStopping(initialRunId, location.asyncDir, child);
585
- return {
586
- runId: initialRunId,
587
- asyncDir: location.asyncDir,
588
- previousState: initialStatus.state,
589
- state: "stopping",
590
- childId: child.id,
591
- message: `Stop requested for child ${child.id} in async run ${initialRunId}.`,
592
- };
582
+ if (stopChild) {
583
+ if (!stopChild(child.id, `Workflow child '${child.id}' stopped by RPC.`)) throw new SubagentRpcError("invalid_state", `Child '${childId}' in workflow ${initialRunId} is not available to stop.`);
584
+ emitChildStopping(initialRunId, location.asyncDir, child);
585
+ return {
586
+ runId: initialRunId,
587
+ asyncDir: location.asyncDir,
588
+ previousState: initialStatus.state,
589
+ state: "stopping",
590
+ childId: child.id,
591
+ message: `Stop requested for child ${child.id} in async run ${initialRunId}.`,
592
+ };
593
+ }
593
594
  }
594
595
  const workflowController = options.state?.workflowControllers?.get(initialRunId);
595
- if (workflowController) {
596
+ if (workflowController && !child) {
596
597
  workflowController.abort(new Error("Workflow stopped by RPC."));
597
598
  return {
598
599
  runId: initialRunId,
@@ -602,7 +603,27 @@ function stopAsyncRun(
602
603
  message: `Stop requested for async run ${initialRunId}.`,
603
604
  };
604
605
  }
605
- throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; reload recovery cannot stop it safely.`);
606
+ try {
607
+ deliverStopRequest({
608
+ asyncDir: location.asyncDir,
609
+ pid: initialStatus.pid,
610
+ kill: options.kill,
611
+ now: options.now,
612
+ source: "rpc-stop",
613
+ ...(child ? { targetIndex: child.index, childId: child.id } : {}),
614
+ });
615
+ } catch (error) {
616
+ throw new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
617
+ }
618
+ if (child) emitChildStopping(initialRunId, location.asyncDir, child);
619
+ return {
620
+ runId: initialRunId,
621
+ asyncDir: location.asyncDir,
622
+ previousState: initialStatus.state,
623
+ state: "stopping",
624
+ ...(child ? { childId: child.id } : {}),
625
+ message: child ? `Stop requested for child ${child.id} in async run ${initialRunId}.` : `Stop requested for async run ${initialRunId}.`,
626
+ };
606
627
  }
607
628
 
608
629
  let status;
@@ -101,10 +101,14 @@ const ChainGateOverride = Type.String({
101
101
  description: "For chain steps with agentContract, choose whether the chain advances on execution success or acceptance success. Defaults to execution.",
102
102
  });
103
103
 
104
- const TurnBudgetOverride = Type.Object({
105
- maxTurns: Type.Integer({ minimum: 1 }),
106
- graceTurns: Type.Optional(Type.Integer({ minimum: 0 })),
107
- }, { additionalProperties: false, description: "Optional assistant-turn budget. At maxTurns the child is asked to wrap up; after graceTurns additional assistant turns it is aborted and partial output is returned." });
104
+ const WorkflowLaneMetadata = Type.Object({
105
+ version: Type.Integer({ minimum: 1, maximum: 1 }),
106
+ key: Type.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" }),
107
+ mode: Type.Optional(Type.String({ enum: ["mutation", "review", "scout", "gate"] })),
108
+ sourceRef: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
109
+ claims: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 160 }), { maxItems: 20 })),
110
+ outputPaths: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 10 })),
111
+ }, { additionalProperties: false, description: "Optional bounded child lane metadata. Display/triage only; sourceRef is opaque and never resolved during status rendering." });
108
112
 
109
113
  const ToolBudgetBlock = Type.Unsafe({
110
114
  anyOf: [
@@ -129,6 +133,21 @@ const UsageBudgetOverride = Type.Object({
129
133
  costUsd: Type.Optional(UsageBudgetLimitOverride),
130
134
  }, { additionalProperties: false, description: "Optional root-only reported-usage budget. Hard limits prevent future child launches; running children are not stopped." });
131
135
 
136
+ const WorkflowPreflightLane = Type.Object({
137
+ key: Type.String({ minLength: 1, maxLength: 128 }),
138
+ mode: Type.Optional(Type.String({ enum: ["mutation", "review", "scout", "gate"] })),
139
+ decision: Type.Optional(Type.String({ maxLength: 256 })),
140
+ claims: Type.Optional(Type.Array(Type.String({ maxLength: 256 }), { maxItems: 16 })),
141
+ expectedOutput: Type.Optional(Type.String({ maxLength: 256 })),
142
+ independence: Type.Optional(Type.String({ maxLength: 256 })),
143
+ }, { additionalProperties: false });
144
+
145
+ const WorkflowPreflightOverride = Type.Object({
146
+ version: Type.Integer({ minimum: 1, maximum: 1 }),
147
+ coverage: Type.Optional(Type.String({ enum: ["complete", "partial"] })),
148
+ lanes: Type.Array(WorkflowPreflightLane, { maxItems: 64 }),
149
+ }, { additionalProperties: false, description: "Bounded display-only lane hints for workflow launch/status. V1 coverage mismatches warn but never change launch authority or execution." });
150
+
132
151
  // Parallel task item (within a parallel step)
133
152
  export const ParallelTaskSchema = Type.Object({
134
153
  agent: Type.String(),
@@ -273,7 +292,12 @@ const SubagentParamProperties = {
273
292
  dir: Type.Optional(Type.String({
274
293
  description: "Async run directory for status/debug.run, stop, resume, or steer."
275
294
  })),
276
- handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
295
+ handoffPath: Type.Optional(Type.String({ description: "Existing parallel handoff manifest for worktree.discard, worktree.cleanup metadata, or lane evidence actions." })),
296
+ repo: Type.Optional(Type.String({ description: "Repository path for action='worktree.cleanup'; defaults to cwd." })),
297
+ planId: Type.Optional(Type.String({ description: "Cleanup plan id reserved for a future worktree.cleanup apply action." })),
298
+ laneId: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Exact manifest run id for lane.status, lane.recordMerge, or lane.recordSupersession." })),
299
+ merge: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true, description: "Attested merge evidence for lane.recordMerge: prNumber, reviewedHead, mergeCommit, treeEquivalent, postMergeChecks, attestedBy, and attestedAt." })),
300
+ supersession: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true, description: "Attested replacement-lane evidence for lane.recordSupersession: supersededBy, attestedBy, and attestedAt." })),
277
301
  index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
278
302
  childId: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Stable child identity for child-scoped stop requests." })),
279
303
  view: Type.Optional(Type.String({
@@ -283,13 +307,13 @@ const SubagentParamProperties = {
283
307
  lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })),
284
308
  topic: Type.Optional(Type.String()),
285
309
  message: Type.Optional(Type.String({ description: "Follow-up message for resume, live guidance for steer, or optional startup prompt for project.open." })),
286
- mode: Type.Optional(Type.String({ enum: ["steer", "follow_up", "auto"], description: "Delivery mode for action='steer'. steer interrupts at the next safe point (default), follow_up waits for the next turn boundary, and auto follows up mid-turn but delivers immediately between turns." })),
310
+ mode: Type.Optional(Type.String({ enum: ["steer", "follow_up", "auto", "plan", "apply"], description: "Delivery mode for action='steer', or plan/apply mode for worktree.cleanup. worktree.cleanup currently supports plan only; apply/removal is not available yet." })),
287
311
  steeringRecovery: Type.Optional(Type.Boolean({ description: "For action='steer', allow pause-and-revive recovery after a missed acknowledgment. Defaults true for direct tool calls in steer mode; extension RPC steering forces false so callers retain exact child ownership." })),
288
312
  additional: Type.Optional(Type.Integer({ minimum: 1, description: "Positive launches to add with action='grant-spawn-budget'. Root interactive parent with native user confirmation only; total grants cannot exceed the original configured cap." })),
289
313
  scope: Type.Optional(Type.String({ enum: ["session", "user", "project"], description: "Scope for action='watchdog.configure'. Defaults to session to avoid persistent settings writes unless user/project is explicit." })),
290
314
  target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
291
315
  focus: Type.Optional(Type.Boolean({ description: "Focus the new Herdr pane for inspector.open or project.open." })),
292
- thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean", enum: [false] }], description: "Thinking level for action='watchdog.configure' (off/minimal/low/medium/high/xhigh/max, inherit, or false for off)." })),
316
+ thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean", enum: [false] }], description: "Thinking level for action='watchdog.configure' only (off/minimal/low/medium/high/xhigh/max, inherit, or false for off). Ignored on dispatch; set per-run child thinking with a suffix on the model string, e.g. model: 'provider/id:high'." })),
293
317
  at: Type.Optional(Type.String({ description: "One-shot trigger for action='schedule.create': a relative delay such as '+10m' or an ISO timestamp with timezone." })),
294
318
  every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })),
295
319
  on: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "integer" }], description: "Calendar selector reserved for a later schedule slice." })),
@@ -312,11 +336,13 @@ const SubagentParamProperties = {
312
336
  ],
313
337
  description: "Agent config for create/update. Object or JSON string."
314
338
  })),
315
- workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
339
+ workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Each workflow key identifies one result lane: use a new stable workflow key for every distinct retained resume pass; same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected. Use runs.all([...]), runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For bounded parallel sequential chains, use runs.lanes([{key,stages:[{key,agent,task},{key,resume:'previous',task},...]}]); first stages run together, later stages sequence per lane, and the bounded board reports lane-local failures. Only an explicit structuredOutput.verdict === 'blocked' blocks a successful stage; reviewer prose is not parsed. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals except through runs.host." })),
316
340
  workflowScriptPath: Type.Optional(Type.String({ minLength: 1, description: "Path to a trusted JavaScript workflow file. Mutually exclusive with workflowScript. Relative paths resolve against the request cwd. The host reads the file before the filesystem-free workflow sandbox starts." })),
341
+ preflight: Type.Optional(WorkflowPreflightOverride),
317
342
  chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise. Explicit live-card requires same-repository async:false; async workflows should omit chatProgress or use auto/off." })),
318
343
  isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
319
344
  worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
345
+ lane: Type.Optional(WorkflowLaneMetadata),
320
346
  context: Type.Optional(Type.String({
321
347
  enum: ["fresh", "fork", "profile"],
322
348
  description: "'fresh' or 'fork' to branch from parent session, or 'profile' to require the selected agent's declared defaultContext. Explicit fresh/fork overrides every child; profile ignores config defaultSubagentContext and fails when an agent has no defaultContext. If omitted, config defaultSubagentContext wins over each agent defaultContext; implicit fork needs a persisted parent session and leaf, else fresh. Config forkContext may prune resolved forks before spawn without adding another context value.",
@@ -325,7 +351,6 @@ const SubagentParamProperties = {
325
351
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
326
352
  maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline." })),
327
353
  toolTimeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional hard per-tool-call timeout in milliseconds; known-fast built-in tools have a five-minute default." })),
328
- turnBudget: Type.Optional(TurnBudgetOverride),
329
354
  toolBudget: Type.Optional(ToolBudgetOverride),
330
355
  usageBudget: Type.Optional(UsageBudgetOverride),
331
356
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
@@ -343,11 +368,11 @@ const SubagentParamProperties = {
343
368
  { type: "string" },
344
369
  { type: "boolean" },
345
370
  ],
346
- description: "Default child output file (string), or false to disable. Relative paths resolve against cwd.",
371
+ description: "Default child output file (string), or false to disable. Relative workflow child paths use managed artifact routing. Task filename prose is not an output declaration; for durable workflow handoff, return the child's outputReference, outputPathMapping, or artifactPaths.",
347
372
  })),
348
373
  outputMode: Type.Optional(OutputModeOverride),
349
374
  skill: Type.Optional(SkillOverride),
350
- model: Type.Optional(Type.String({ description: "Default child model override. Full provider/id values are accepted; bare ids resolve from the active registry." })),
375
+ model: Type.Optional(Type.String({ description: "Default child model override. Full provider/id values are accepted; bare ids resolve from the active registry. Append a thinking suffix (off/minimal/low/medium/high/xhigh/max, e.g. 'provider/id:low') to set the child's thinking level for the run; the suffix wins over the agent's thinking default." })),
351
376
  fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. Default false. This can increase quota or cost." })),
352
377
  outputSchema: Type.Optional(JsonSchemaObject),
353
378
  agentContract: Type.Optional(AgentContractOverride),
@@ -375,7 +400,7 @@ const SubagentWaitParamsSchema = Type.Object({
375
400
  })),
376
401
  timeoutMs: Type.Optional(Type.Integer({
377
402
  minimum: 1,
378
- description: "Give up waiting after this many milliseconds (the runs keep going regardless). Defaults to 1800000 (30 minutes).",
403
+ description: "Give up waiting after this many milliseconds (the runs keep going regardless). Defaults to config waitTool.defaultTimeoutMs, then 1800000 (30 minutes). Window expiry is a non-error active-work result.",
379
404
  })),
380
405
  stopOnAttention: Type.Optional(Type.Boolean({
381
406
  description: "Blocking waits stop when a run needs attention by default. Set false to keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.",