pi-subagents 0.56.0 → 0.58.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 (108) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/agents/claude-code-writer.md +15 -0
  3. package/agents/claude-code.md +15 -0
  4. package/agents/codex-exec-writer.md +15 -0
  5. package/agents/codex-exec.md +15 -0
  6. package/agents/cursor-agent-writer.md +14 -0
  7. package/agents/cursor-agent.md +14 -0
  8. package/docs/agents.md +124 -21
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +41 -2
  11. package/docs/models.md +3 -3
  12. package/docs/observability.md +8 -7
  13. package/docs/tool-reference.md +14 -3
  14. package/docs/workflows.md +44 -0
  15. package/package.json +1 -1
  16. package/skills/pi-subagents/SKILL.md +2 -0
  17. package/skills/pi-subagents/references/execution-controls.md +2 -2
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
  19. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  20. package/src/agents/agent-management.ts +36 -5
  21. package/src/agents/agent-refinements.ts +4 -4
  22. package/src/agents/agent-serializer.ts +5 -0
  23. package/src/agents/agents.ts +257 -51
  24. package/src/agents/builtin-names.ts +6 -0
  25. package/src/agents/runtime-agent-events.ts +70 -0
  26. package/src/agents/runtime-agent-registry.ts +18 -4
  27. package/src/api/agents.ts +10 -5
  28. package/src/api/preflight.ts +28 -3
  29. package/src/extension/config.ts +70 -0
  30. package/src/extension/doctor.ts +3 -3
  31. package/src/extension/index.ts +33 -8
  32. package/src/extension/public-execution.ts +29 -13
  33. package/src/extension/rpc.ts +55 -19
  34. package/src/extension/schemas.ts +6 -5
  35. package/src/extension/tool-description.ts +18 -12
  36. package/src/inspectors/herdr/actions.ts +2 -1
  37. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  38. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  39. package/src/integrations/herdr-status.ts +51 -3
  40. package/src/runs/background/active-async-capacity.ts +77 -10
  41. package/src/runs/background/async-execution.ts +127 -19
  42. package/src/runs/background/async-job-tracker.ts +5 -0
  43. package/src/runs/background/async-resume.ts +6 -2
  44. package/src/runs/background/async-retention.ts +20 -3
  45. package/src/runs/background/async-status.ts +7 -0
  46. package/src/runs/background/chain-append.ts +2 -0
  47. package/src/runs/background/chain-root-attachment.ts +15 -1
  48. package/src/runs/background/fleet-view.ts +16 -10
  49. package/src/runs/background/inspect-rpc.ts +8 -8
  50. package/src/runs/background/notify.ts +26 -3
  51. package/src/runs/background/result-delivery-ownership.ts +45 -0
  52. package/src/runs/background/result-files.ts +27 -14
  53. package/src/runs/background/result-watcher.ts +36 -15
  54. package/src/runs/background/run-status.ts +33 -6
  55. package/src/runs/background/scheduled-runs.ts +7 -1
  56. package/src/runs/background/subagent-runner.ts +239 -56
  57. package/src/runs/background/wait-completions.ts +4 -0
  58. package/src/runs/foreground/execution.ts +92 -11
  59. package/src/runs/foreground/foreground-control.ts +6 -0
  60. package/src/runs/foreground/foreground-history.ts +22 -1
  61. package/src/runs/foreground/subagent-executor.ts +322 -102
  62. package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
  63. package/src/runs/shared/child-protocol.ts +21 -7
  64. package/src/runs/shared/claude-code-adapter.ts +129 -0
  65. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  66. package/src/runs/shared/completion-guard.ts +4 -3
  67. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  68. package/src/runs/shared/dynamic-fanout.ts +3 -3
  69. package/src/runs/shared/external-cli-contract.ts +167 -0
  70. package/src/runs/shared/external-cli-preflight.ts +122 -0
  71. package/src/runs/shared/external-cli-runner.ts +348 -55
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/launch-cwd.ts +16 -0
  74. package/src/runs/shared/long-running-guard.ts +2 -1
  75. package/src/runs/shared/mcp-config-sources.ts +386 -0
  76. package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
  77. package/src/runs/shared/model-exclusions.ts +69 -7
  78. package/src/runs/shared/model-fallback.ts +39 -5
  79. package/src/runs/shared/mutation-evidence.ts +7 -2
  80. package/src/runs/shared/nested-events.ts +3 -1
  81. package/src/runs/shared/nested-render.ts +2 -2
  82. package/src/runs/shared/parallel-utils.ts +8 -1
  83. package/src/runs/shared/pi-args.ts +61 -6
  84. package/src/runs/shared/process-signal.ts +13 -0
  85. package/src/runs/shared/run-history.ts +21 -1
  86. package/src/runs/shared/single-output.ts +17 -0
  87. package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
  88. package/src/shared/fork-context.ts +21 -0
  89. package/src/shared/formatters.ts +13 -1
  90. package/src/shared/launch-contract.ts +4 -0
  91. package/src/shared/pruned-fork.ts +450 -0
  92. package/src/shared/session-file-trust.ts +19 -0
  93. package/src/shared/session-tokens.ts +14 -3
  94. package/src/shared/settings.ts +10 -2
  95. package/src/shared/shortcuts.ts +17 -0
  96. package/src/shared/types.ts +160 -10
  97. package/src/shared/utils.ts +6 -29
  98. package/src/shared/workflow-child-permit.ts +116 -0
  99. package/src/slash/delegation-adapters.ts +0 -1
  100. package/src/slash/slash-commands.ts +8 -6
  101. package/src/slash/subagents-admin.ts +3 -0
  102. package/src/tui/fleet-status.ts +27 -10
  103. package/src/tui/fleet-transcript.ts +11 -5
  104. package/src/tui/fleet.ts +28 -13
  105. package/src/tui/render.ts +55 -21
  106. package/src/workflows/scripted-workflow.ts +299 -31
  107. package/src/workflows/workflow-child-summary.ts +117 -0
  108. package/src/workflows/workflow-receipt.ts +155 -5
@@ -25,6 +25,7 @@ import { recordWaitCompletion } from "./wait-completions.ts";
25
25
  import { MISSION_BINDING_FILE, syncMissionFromAsyncCompletion } from "../../missions/lifecycle.ts";
26
26
  import { missionObserverResultCandidateFiles, promotePendingResultFile, removeMissionObserverIndex, removeResultIndex, resultCandidateFilesForSession, resultPayloadPathForIndexedRun, resultPayloadPathForMissionObserverRun, resultPayloadPathForSessionRun, writeAsyncResultFile, writeResultIndexForData } from "./result-files.ts";
27
27
  import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
28
+ import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
28
29
 
29
30
  const WATCHER_RESTART_DELAY_MS = 3000;
30
31
  const POLL_INTERVAL_MS = 3000;
@@ -60,6 +61,8 @@ type ResultWatcherDeps = {
60
61
  /** Control how slow result-index scans are logged. Defaults to \"activity\". */
61
62
  resultScanLogging?: "all" | "activity" | "off";
62
63
  platform?: NodeJS.Platform;
64
+ /** Shared current/predecessor session ownership used by the notifier. */
65
+ ownership?: Pick<ResultDeliveryOwnership, "owns" | "claimedSessionIds">;
63
66
  };
64
67
 
65
68
  type ResultFileChild = {
@@ -148,6 +151,10 @@ function isNotFound(error: unknown): boolean {
148
151
  return errorCode(error) === "ENOENT";
149
152
  }
150
153
 
154
+ function isAbsentResultCandidate(error: unknown): boolean {
155
+ return isNotFound(error) || errorCode(error) === "ENAMETOOLONG";
156
+ }
157
+
151
158
  function isAccessDenied(error: unknown): boolean {
152
159
  const code = errorCode(error);
153
160
  return code === "EPERM" || code === "EACCES";
@@ -196,6 +203,7 @@ export function createResultWatcher(
196
203
  deps: ResultWatcherDeps = {},
197
204
  ): {
198
205
  startResultWatcher: () => void;
206
+ transitionResultDelivery: () => void;
199
207
  primeExistingResults: (options?: { triggerTurn?: boolean }) => void;
200
208
  refreshResultDelivery: () => void;
201
209
  stopResultWatcher: () => void;
@@ -214,14 +222,16 @@ export function createResultWatcher(
214
222
  // The sole in-memory ownership lease. It is acquired for one active session
215
223
  // and revoked before the watcher, queues, or callbacks are torn down.
216
224
  let activeSessionId: string | null = null;
225
+ const ownsResult = deps.ownership?.owns
226
+ ?? ((sessionId: string, completionOwnerId: unknown) => sessionId === state.currentSessionId
227
+ && typeof completionOwnerId === "string"
228
+ && completionOwnerId === state.completionOwnerId);
229
+ const claimedSessionIds = () => deps.ownership?.claimedSessionIds() ?? [];
217
230
 
218
231
  const ownsCompletion = (sessionId: string, completionOwnerId: unknown, epoch: number) => {
219
232
  if (!deliveryActive || epoch !== deliveryEpoch) return false;
220
233
  if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
221
- return activeSessionId === sessionId
222
- && state.currentSessionId === sessionId
223
- && typeof completionOwnerId === "string"
224
- && completionOwnerId === state.completionOwnerId;
234
+ return activeSessionId === state.currentSessionId && ownsResult(sessionId, completionOwnerId);
225
235
  };
226
236
 
227
237
  const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
@@ -235,15 +245,18 @@ export function createResultWatcher(
235
245
  try {
236
246
  return fsApi.statSync(publicResultPath(file)).isFile();
237
247
  } catch (error) {
238
- if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${publicResultPath(file)}':`, error);
248
+ if (!isAbsentResultCandidate(error)) console.error(`Failed to inspect subagent result file '${publicResultPath(file)}':`, error);
239
249
  return false;
240
250
  }
241
251
  };
242
252
  const resultPayloadPath = (file: string, observed?: ReadonlySet<string>): string | undefined => {
243
253
  if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
244
254
  const runId = file.replace(/\.json$/i, "");
245
- const sessionResult = state.currentSessionId ? resultPayloadPathForSessionRun(resultsDir, state.currentSessionId, runId) : undefined;
246
- if (sessionResult) return sessionResult;
255
+ for (const sessionId of [state.currentSessionId, ...claimedSessionIds()]) {
256
+ if (!sessionId) continue;
257
+ const sessionResult = resultPayloadPathForSessionRun(resultsDir, sessionId, runId);
258
+ if (sessionResult) return sessionResult;
259
+ }
247
260
  const observerResult = resultPayloadPathForMissionObserverRun(resultsDir, runId);
248
261
  if (observerResult) return observerResult;
249
262
  if (observed?.has(runId)) {
@@ -265,7 +278,7 @@ export function createResultWatcher(
265
278
  } catch (error) {
266
279
  identityCache.delete(file);
267
280
  if (isAccessDenied(error)) throw error;
268
- if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
281
+ if (!isAbsentResultCandidate(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
269
282
  return undefined;
270
283
  }
271
284
  };
@@ -283,7 +296,7 @@ export function createResultWatcher(
283
296
  } catch (error) {
284
297
  identityCache.delete(file);
285
298
  if (isAccessDenied(error)) throw error;
286
- if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
299
+ if (!isAbsentResultCandidate(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
287
300
  return undefined;
288
301
  }
289
302
  };
@@ -304,7 +317,7 @@ export function createResultWatcher(
304
317
  // Missing identity stays on the normal parser path so malformed or legacy
305
318
  // files keep their existing diagnostics and compatibility behavior.
306
319
  if (!identity.sessionId) return true;
307
- if (identity.sessionId === state.currentSessionId && identity.completionOwnerId === state.completionOwnerId) return true;
320
+ if (ownsResult(identity.sessionId, identity.completionOwnerId)) return true;
308
321
  if (identity.asyncDir && fsApi.existsSync(path.join(identity.asyncDir, MISSION_BINDING_FILE))) return true;
309
322
  if (identity.runId && (observed ?? observedRunIds()).has(identity.runId)) return true;
310
323
  return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
@@ -317,7 +330,7 @@ export function createResultWatcher(
317
330
  removeResultIndex(resultsDir, sessionId, runId, toolCallId);
318
331
  return true;
319
332
  } catch (error) {
320
- if (!isNotFound(error)) {
333
+ if (!isAbsentResultCandidate(error)) {
321
334
  console.error(`Failed to remove delivered subagent result '${publicResultPath(file)}'; will retry:`, error);
322
335
  return false;
323
336
  }
@@ -384,7 +397,7 @@ export function createResultWatcher(
384
397
  if (!resultPayloadWasReplaced(data, readPublicResultIdentity())) return false;
385
398
  } catch (error) {
386
399
  if (isAccessDenied(error)) throw error;
387
- if (isNotFound(error)) return false;
400
+ if (isAbsentResultCandidate(error)) return false;
388
401
  console.error(`Failed to re-read subagent result file '${publicResultPath(file)}':`, error);
389
402
  }
390
403
  identityCache.delete(file);
@@ -617,8 +630,9 @@ export function createResultWatcher(
617
630
  };
618
631
  const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
619
632
  const files = new Set<string>();
620
- if (state.currentSessionId) {
621
- for (const file of resultCandidateFilesForSession(resultsDir, state.currentSessionId)) files.add(file);
633
+ for (const sessionId of [state.currentSessionId, ...claimedSessionIds()]) {
634
+ if (!sessionId) continue;
635
+ for (const file of resultCandidateFilesForSession(resultsDir, sessionId)) files.add(file);
622
636
  }
623
637
  for (const runId of state.asyncJobs.keys()) files.add(`${runId}.json`);
624
638
  for (const file of missionObserverResultCandidateFiles(resultsDir)) files.add(file);
@@ -751,6 +765,13 @@ export function createResultWatcher(
751
765
  }
752
766
  };
753
767
 
768
+ const transitionResultDelivery = () => {
769
+ deliveryActive = true;
770
+ activeSessionId = state.currentSessionId;
771
+ deliveryEpoch += 1;
772
+ identityCache.clear();
773
+ };
774
+
754
775
  const stopResultWatcher = () => {
755
776
  deliveryActive = false;
756
777
  activeSessionId = null;
@@ -765,5 +786,5 @@ export function createResultWatcher(
765
786
  identityCache.clear();
766
787
  };
767
788
 
768
- return { startResultWatcher, primeExistingResults, stopResultWatcher, refreshResultDelivery: () => { primeExistingResults(); startDemandPolling(); } };
789
+ return { startResultWatcher, transitionResultDelivery, primeExistingResults, stopResultWatcher, refreshResultDelivery: () => { primeExistingResults(); startDemandPolling(); } };
769
790
  }
@@ -11,6 +11,7 @@ import { DIRS, type AsyncStatus, type Details, type ForegroundResumeRun, type Ne
11
11
  import { inspectActiveAsyncCapacityOwner, type ActiveAsyncCapacityInspection } from "./active-async-capacity.ts";
12
12
  import { readStatus } from "../../shared/utils.ts";
13
13
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
14
+ import { normalizeExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
14
15
  import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
15
16
  import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
16
17
  import { formatWaitSubscriptions } from "./wait-subscriptions.ts";
@@ -21,6 +22,7 @@ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-
21
22
  import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot, type NestedRunResolutionScope } from "../shared/nested-events.ts";
22
23
  import { readMissionBinding } from "../../missions/lifecycle.ts";
23
24
  import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts";
25
+ import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
24
26
  import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
25
27
  import { getExternalJobProvider } from "../../api/external-job-provider.ts";
26
28
 
@@ -103,6 +105,7 @@ interface RunStatusDeps {
103
105
  nested?: NestedRunResolutionScope;
104
106
  sessionRoots?: string[];
105
107
  activeCapacityRoot?: string;
108
+ abandonedSlotReleaseAfterMs?: number | false;
106
109
  }
107
110
 
108
111
  function hasExistingSessionFile(value: unknown): value is string {
@@ -405,7 +408,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
405
408
  if (!status && diskStatus?.displayDismissedAt !== undefined) {
406
409
  if (params.action === "debug.run") {
407
410
  const { sidecar, overlay } = debugProcessTerminal(asyncDir, diskStatus);
408
- const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
411
+ const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: deps.abandonedSlotReleaseAfterMs });
409
412
  return {
410
413
  content: [{ type: "text", text: formatRunLifecycleDebug({ status: diskStatus, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
411
414
  details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
@@ -432,7 +435,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
432
435
  if (status) {
433
436
  if (params.action === "debug.run") {
434
437
  const { sidecar, overlay } = debugProcessTerminal(asyncDir, status);
435
- const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
438
+ const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: deps.abandonedSlotReleaseAfterMs });
436
439
  return {
437
440
  content: [{ type: "text", text: formatRunLifecycleDebug({ status, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
438
441
  details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
@@ -536,9 +539,29 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
536
539
  const phase = step.phase ? `[${step.phase}] ` : "";
537
540
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
538
541
  if (step.runner?.type === "external-cli") {
539
- lines.push(` Runner: external-cli (${step.runner.command}${step.runner.args.length ? ` ${step.runner.args.join(" ")}` : ""})`);
542
+ const runner = normalizeExternalCliRunnerStatus(step.runner);
543
+ if (runner) {
544
+ lines.push(` Runner: external-cli (${runner.command}${runner.args.length ? ` ${runner.args.join(" ")}` : ""})`);
545
+ lines.push(` Adapter: ${runner.adapter.id} v${runner.adapter.version} (${runner.adapter.executionMode})`);
546
+ if (runner.safety) {
547
+ if ("approvalPolicy" in runner.safety) lines.push(` Safety: ${"access" in runner.safety ? `access=${runner.safety.access}, ` : ""}sandbox=${runner.safety.sandbox}, approval=${runner.safety.approvalPolicy}, ephemeral=${runner.safety.ephemeral}`);
548
+ else if ("mode" in runner.safety) lines.push(` Safety: access=${runner.safety.access}, auth=${runner.safety.authentication}, mode=${runner.safety.mode}, sandbox=${runner.safety.sandbox}, workspaceTrust=${runner.safety.workspaceTrust}, sessionReuse=${runner.safety.sessionReuse}`);
549
+ else if ("authentication" in runner.safety) lines.push(` Safety: access=${runner.safety.access}, auth=${runner.safety.authentication}, permission=${runner.safety.permissionMode}, tools=${runner.safety.tools}, mcp=${runner.safety.mcp}, settings=${runner.safety.settingSources}, settingsTrust=${runner.safety.userSettingsTrust}, persistence=${runner.safety.sessionPersistence}`);
550
+ else lines.push(` Safety: access=read-only, permission=${runner.safety.permissionMode}, tools=${runner.safety.tools}, mcp=${runner.safety.mcp}, settings=${runner.safety.settingSources}, persistence=${runner.safety.sessionPersistence}`);
551
+ }
552
+ lines.push(` Capabilities: stop=${runner.capabilities.stop}, steer=false, resume=false, structuredOutput=false, toolEvents=false, supervisor=unsupported, forkContext=false, extensionBindings=false`);
553
+ lines.push(` Unsupported steer: ${runner.unsupportedReasons.steer}`);
554
+ lines.push(` Unsupported resume: ${runner.nonResumableReason}`);
555
+ lines.push(` Unsupported supervisor: ${runner.unsupportedReasons.supervisor}`);
556
+ lines.push(` Context handoff: fresh only (${runner.unsupportedReasons.forkContext})`);
557
+ } else {
558
+ lines.push(" Runner: external-cli (invalid persisted runner metadata)");
559
+ }
540
560
  if (step.externalProcess?.pid !== undefined) lines.push(` Process: ${step.externalProcess.pid}`);
541
- if (step.externalProcess) lines.push(` Stdout: ${step.externalProcess.stdoutPath}`, ` Stderr: ${step.externalProcess.stderrPath}`);
561
+ if (step.externalProcess) {
562
+ lines.push(` Stdout: ${step.externalProcess.stdoutPath}`, ` Stderr: ${step.externalProcess.stderrPath}`);
563
+ if (step.externalProcess.finalOutputPath) lines.push(` Final output: ${step.externalProcess.finalOutputPath}`);
564
+ }
542
565
  } else if (step.runner?.type === "external-job") {
543
566
  lines.push(` Runner: external-job (${step.runner.provider})`);
544
567
  if (step.externalJob?.providerJobId) lines.push(` Provider job: ${step.externalJob.providerJobId}`);
@@ -583,7 +606,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
583
606
  if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
584
607
  if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
585
608
 
586
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
609
+ const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
610
+ if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
611
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
587
612
  }
588
613
  }
589
614
 
@@ -627,7 +652,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
627
652
  const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
628
653
  lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
629
654
  if (data.summary) lines.push("", data.summary);
630
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
655
+ const workflowChildren = parseWorkflowChildSummary((data as unknown as Record<string, unknown>).workflowChildren);
656
+ if (workflowChildren && workflowChildren.workflowRunId !== runId) throw new Error("workflowChildren.workflowRunId does not match the result run id.");
657
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowChildren ? { workflowChildren } : {}) } };
631
658
  } catch (error) {
632
659
  const message = error instanceof Error ? error.message : String(error);
633
660
  return {
@@ -294,6 +294,10 @@ function resolveMaxPending(config: ExtensionConfig): number {
294
294
  return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : DEFAULT_MAX_PENDING;
295
295
  }
296
296
 
297
+ function hasPendingScheduleWork(schedule: ScheduleRecord): boolean {
298
+ return schedule.activeRunId !== undefined || schedule.trigger.nextRunAt !== undefined;
299
+ }
300
+
297
301
  function nextAfter(trigger: ScheduleTrigger, plannedAt: number, now: number): string | undefined {
298
302
  if (trigger.kind === "once") return undefined;
299
303
  let next = plannedAt + trigger.everyMs;
@@ -479,7 +483,9 @@ export class ScheduledRunManager {
479
483
  if (params.on !== undefined || params.timezone !== undefined || every === "day" || every === "week" || every === "month" || every === "year") return textResult("Calendar schedules are deferred from this first safe slice. Use a fixed interval such as every:'24h' or every:'7d'.", undefined, undefined, true);
480
484
  const sessionId = ctx.sessionManager.getSessionId() ?? "unknown";
481
485
  if (this.deps.resolveCapabilityCeiling?.(sessionId)) return textResult("Cannot persist a schedule while a capability ceiling is active.", undefined, undefined, true);
482
- if (store.list().length >= resolveMaxPending(this.deps.config)) return textResult(`Schedule limit reached (${resolveMaxPending(this.deps.config)}).`, undefined, undefined, true);
486
+ const pendingCount = store.list().filter(hasPendingScheduleWork).length;
487
+ const maxPending = resolveMaxPending(this.deps.config);
488
+ if (pendingCount >= maxPending) return textResult(`Schedule limit reached (${maxPending}).`, undefined, undefined, true);
483
489
  const id = validateScheduleId((params.id?.trim() || this.randomId()));
484
490
  if (store.ids().includes(id)) return textResult(`Schedule '${id}' already exists.`, undefined, undefined, true);
485
491
  const now = this.now();