pi-subagents 0.67.0 → 0.69.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 (122) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +41 -12
  4. package/docs/configuration.md +61 -19
  5. package/docs/extension-api.md +5 -1
  6. package/docs/missions.md +2 -2
  7. package/docs/models.md +11 -79
  8. package/docs/observability.md +18 -8
  9. package/docs/standalone-background.md +13 -3
  10. package/docs/tool-reference.md +38 -14
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +59 -1
  13. package/index.ts +5 -2
  14. package/package.json +4 -2
  15. package/runner-peer-loader.mjs +24 -0
  16. package/runner-peer-preload.mjs +25 -11
  17. package/skills/pi-subagents/SKILL.md +18 -21
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  19. package/skills/pi-subagents/references/execution-controls.md +6 -4
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  21. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  22. package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
  23. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  24. package/src/agents/agent-management.ts +57 -58
  25. package/src/agents/agent-serializer.ts +4 -3
  26. package/src/agents/agents.ts +185 -72
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/agents/skills.ts +1 -1
  30. package/src/api/preflight.ts +20 -16
  31. package/src/api/required-child-extensions.ts +6 -0
  32. package/src/extension/config.ts +10 -37
  33. package/src/extension/fanout-child.ts +3 -0
  34. package/src/extension/herdr-pi-bridge.ts +160 -0
  35. package/src/extension/index.ts +42 -31
  36. package/src/extension/public-execution.ts +3 -3
  37. package/src/extension/schemas.ts +23 -6
  38. package/src/extension/tool-description.ts +8 -7
  39. package/src/inspectors/ghostty/plugin.ts +13 -1
  40. package/src/intercom/native-supervisor-channel.ts +22 -18
  41. package/src/policy/authority.ts +4 -0
  42. package/src/profiles/profiles.ts +12 -6
  43. package/src/runs/background/active-run-index.ts +17 -1
  44. package/src/runs/background/async-execution.ts +309 -126
  45. package/src/runs/background/async-job-tracker.ts +8 -6
  46. package/src/runs/background/async-resume.ts +13 -4
  47. package/src/runs/background/async-status.ts +15 -4
  48. package/src/runs/background/auto-drain.ts +20 -10
  49. package/src/runs/background/binary-bootstrap.ts +5 -0
  50. package/src/runs/background/chain-append.ts +1 -1
  51. package/src/runs/background/chain-root-attachment.ts +14 -33
  52. package/src/runs/background/notify.ts +74 -6
  53. package/src/runs/background/result-files.ts +8 -4
  54. package/src/runs/background/result-watcher.ts +19 -2
  55. package/src/runs/background/run-child-session.ts +20 -29
  56. package/src/runs/background/runner-aliases.ts +4 -33
  57. package/src/runs/background/runner-child-launch.ts +4 -1
  58. package/src/runs/background/runner-child-sessions.ts +2 -2
  59. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  60. package/src/runs/background/scheduled-runs.ts +11 -5
  61. package/src/runs/background/stale-run-reconciler.ts +35 -11
  62. package/src/runs/background/subagent-runner.ts +413 -276
  63. package/src/runs/background/subagent-wait.ts +128 -23
  64. package/src/runs/background/wait-completions.ts +75 -27
  65. package/src/runs/background/wait-subscriptions.ts +9 -3
  66. package/src/runs/background/wait-tool.ts +4 -2
  67. package/src/runs/foreground/async-stop-action.ts +93 -3
  68. package/src/runs/foreground/execution.ts +115 -219
  69. package/src/runs/foreground/foreground-history.ts +2 -1
  70. package/src/runs/foreground/subagent-executor.ts +281 -80
  71. package/src/runs/shared/acceptance.ts +194 -37
  72. package/src/runs/shared/async-status-projection.ts +123 -33
  73. package/src/runs/shared/child-launch-plan.ts +15 -3
  74. package/src/runs/shared/child-launch.ts +19 -6
  75. package/src/runs/shared/child-runtime-config.ts +5 -0
  76. package/src/runs/shared/child-session.ts +94 -50
  77. package/src/runs/shared/child-tool-plan.ts +28 -16
  78. package/src/runs/shared/dynamic-fanout.ts +2 -2
  79. package/src/runs/shared/external-cli-contract.ts +11 -1
  80. package/src/runs/shared/external-cli-preflight.ts +6 -2
  81. package/src/runs/shared/herdr-connection.ts +134 -0
  82. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  83. package/src/runs/shared/herdr-machine.ts +279 -0
  84. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  85. package/src/runs/shared/herdr-placed-run.ts +263 -0
  86. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  87. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  88. package/src/runs/shared/model-scope.ts +1 -1
  89. package/src/runs/shared/nested-events.ts +11 -2
  90. package/src/runs/shared/parallel-utils.ts +7 -2
  91. package/src/runs/shared/pi-spawn.ts +1 -1
  92. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  93. package/src/runs/shared/worktree-setup-command.ts +27 -4
  94. package/src/runs/shared/worktree.ts +30 -8
  95. package/src/shared/child-cache-retention.ts +43 -0
  96. package/src/shared/launch-contract.ts +6 -9
  97. package/src/shared/pruned-fork.ts +1 -1
  98. package/src/shared/required-child-extensions.ts +81 -0
  99. package/src/shared/settings.ts +5 -2
  100. package/src/shared/shortcuts.ts +0 -4
  101. package/src/shared/types.ts +81 -29
  102. package/src/slash/slash-commands.ts +0 -6
  103. package/src/slash/subagents-admin.ts +13 -9
  104. package/src/tui/render.ts +20 -10
  105. package/src/watchdog/child-status.ts +28 -36
  106. package/src/watchdog/lsp-diagnostics.ts +1 -1
  107. package/src/watchdog/model-selection.ts +1 -1
  108. package/src/watchdog/register-child.ts +10 -3
  109. package/src/watchdog/register-main.ts +20 -20
  110. package/src/watchdog/render.ts +1 -1
  111. package/src/watchdog/review.ts +14 -30
  112. package/src/watchdog/rules.ts +1 -1
  113. package/src/watchdog/runtime.ts +23 -12
  114. package/src/watchdog/settings.ts +3 -6
  115. package/src/watchdog/types.ts +3 -5
  116. package/src/watchdog/warning-format.ts +1 -1
  117. package/src/workflows/scripted-workflow.ts +68 -7
  118. package/src/workflows/workflow-receipt.ts +21 -3
  119. package/src/workflows/workflow-resources.ts +13 -2
  120. package/src/runs/shared/model-exclusions.ts +0 -374
  121. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  122. package/src/runs/shared/readonly-session-evidence.ts +0 -307
@@ -52,11 +52,14 @@ const DEFAULT_LIVENESS_INTERVAL_MS = 5000;
52
52
  const EVENT_REFRESH_DEBOUNCE_MS = 25;
53
53
  const WATCH_ATTACHMENT_RETRY_MS = 100;
54
54
 
55
+ const isTerminalJobStatus = (status: AsyncJobState["status"]): boolean =>
56
+ status === "complete" || status === "failed" || status === "partial" || status === "paused" || status === "rejected" || status === "stopped";
57
+
55
58
  function rememberFleetJob(state: SubagentState, job: AsyncJobState): void {
56
59
  state.fleetJobs ??= new Map();
57
60
  state.fleetJobs.set(job.asyncId, job);
58
61
  const terminal = [...state.fleetJobs.values()]
59
- .filter((candidate) => candidate.status === "complete" || candidate.status === "failed" || candidate.status === "paused" || candidate.status === "stopped")
62
+ .filter((candidate) => isTerminalJobStatus(candidate.status))
60
63
  .sort((left, right) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0));
61
64
  for (const stale of terminal.slice(MAX_RECENT_FLEET_JOBS)) state.fleetJobs.delete(stale.asyncId);
62
65
  }
@@ -100,7 +103,6 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
100
103
  let nextWidgetAnimationAt = Date.now() + WIDGET_ANIMATION_INTERVAL_MS;
101
104
  const watch = options.watch ?? fs.watch;
102
105
  const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
103
- const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
104
106
  const withLastUiContext = <T>(run: (ctx: ExtensionContext) => T): T | undefined => {
105
107
  const cached = state.lastUiContext;
106
108
  return withCachedUiContext(cached, () => {
@@ -445,10 +447,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
445
447
  if (status) {
446
448
  const previousStatus = job.status;
447
449
  job.status = status.state;
448
- if (!terminalStatus(job.status)) terminalPublications.delete(job.asyncId);
450
+ if (!isTerminalJobStatus(job.status)) terminalPublications.delete(job.asyncId);
449
451
  if (job.status === "running") runningJobIds.add(job.asyncId);
450
452
  else runningJobIds.delete(job.asyncId);
451
- if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
453
+ if (!isTerminalJobStatus(job.status)) cancelCleanup(job.asyncId);
452
454
  job.sessionId = status.sessionId ?? job.sessionId;
453
455
  job.activityState = status.activityState;
454
456
  job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
@@ -502,7 +504,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
502
504
  job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
503
505
  job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
504
506
  job.sessionFile = status.sessionFile ?? job.sessionFile;
505
- if (terminalStatus(job.status)) {
507
+ if (isTerminalJobStatus(job.status)) {
506
508
  let publication = terminalPublications.get(job.asyncId);
507
509
  if (!publication && status.mode !== "workflow" && status.processTerminal?.state === "pending"
508
510
  && status.runId === job.asyncId && status.sessionId === state.currentSessionId
@@ -524,7 +526,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
524
526
  } else cancelCleanup(job.asyncId);
525
527
  }
526
528
  // Scan on close too: publication may have raced the payload check.
527
- if (!terminalStatus(previousStatus) || (wasPending && !publication?.pending)) options.onJobTerminal?.();
529
+ if (!isTerminalJobStatus(previousStatus) || (wasPending && !publication?.pending)) options.onJobTerminal?.();
528
530
  rememberFleetJob(state, job);
529
531
  if (!publication?.pending && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
530
532
  scheduleCleanup(job.asyncId);
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import { DIRS, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor, type SubagentRunMode } from "../../shared/types.ts";
4
4
  import type { AgentConfig } from "../../agents/agents.ts";
5
5
  import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
6
+ import { snapshotRequiredChildExtensions } from "../../shared/required-child-extensions.ts";
6
7
  import { normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
7
8
  import { validateAcceptanceInput } from "../shared/acceptance.ts";
8
9
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
@@ -319,12 +320,13 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
319
320
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
320
321
  const parsed = value as Record<string, unknown>;
321
322
  const allowedFields = new Set([
322
- "modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "fast", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
323
+ "modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fast", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
323
324
  "subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
324
325
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
325
326
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
326
327
  "launchResolvedExtensions", "runFanoutBudget", "lane", "baseRef",
327
328
  "extensionBindings",
329
+ "requiredExtensions",
328
330
  ]);
329
331
  for (const field of Object.keys(parsed)) {
330
332
  if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
@@ -343,6 +345,10 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
343
345
  if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
344
346
  if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
345
347
  if (parsed.extensionBindings !== undefined) parsed.extensionBindings = normalizeExtensionBindings(parsed.extensionBindings)!.value;
348
+ if (parsed.requiredExtensions !== undefined) {
349
+ try { parsed.requiredExtensions = snapshotRequiredChildExtensions(parsed.requiredExtensions, "requiredExtensions"); }
350
+ catch (error) { throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`); }
351
+ }
346
352
  if (parsed.lane !== undefined) parsed.lane = normalizeWorkflowLaneMetadata(parsed.lane, `Invalid async recovery descriptor '${descriptorPath}': lane`);
347
353
  if (parsed.agentContract !== undefined) {
348
354
  if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
@@ -363,7 +369,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
363
369
  else if (typeof parsed.inheritGlobalContext !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': inheritGlobalContext must be a boolean.`);
364
370
  if (parsed.allowNestedSubagents !== undefined && typeof parsed.allowNestedSubagents !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': allowNestedSubagents must be a boolean.`);
365
371
  if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
366
- for (const field of ["fallbackModels", "tools", "excludeTools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
372
+ for (const field of ["tools", "excludeTools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
367
373
  const item = parsed[field];
368
374
  if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
369
375
  }
@@ -551,7 +557,11 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
551
557
  const index = requestedIndex ?? 0;
552
558
  if (!Number.isInteger(index)) throw new Error(`Async run '${runId}' index must be an integer.`);
553
559
  if (index < 0 || index >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${index} is out of range.`);
554
- const agent = statusSteps[index]?.agent ?? resultSteps[index]?.agent ?? result?.agent;
560
+ const selectedStatusStep = statusSteps[index];
561
+ if (selectedStatusStep?.status === "stopped" || selectedStatusStep?.stopped === true) {
562
+ throw new Error(`Async run '${runId}' child ${index} was stopped and cannot be resumed. Start a new run instead.`);
563
+ }
564
+ const agent = selectedStatusStep?.agent ?? resultSteps[index]?.agent ?? result?.agent;
555
565
  if (!agent) throw new Error(`Could not determine child agent for async run '${runId}'.`);
556
566
  if (recoveryDescriptor && recoveryDescriptor.agent !== agent) throw new Error(`Async run '${runId}' has a recovery descriptor for '${recoveryDescriptor.agent}', not '${agent}'.`);
557
567
  const sessionFile = statusSteps[index]?.sessionFile
@@ -596,7 +606,6 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
596
606
  ...agentConfig,
597
607
  model: descriptor.model,
598
608
  modelProvider: descriptor.modelProvider,
599
- fallbackModels: descriptor.fallbackModels ? [...descriptor.fallbackModels] : undefined,
600
609
  thinking: descriptor.thinking,
601
610
  maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
602
611
  tools: descriptor.tools ? [...descriptor.tools] : undefined,
@@ -26,6 +26,7 @@ import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
26
26
  import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
27
27
  import { formatWorkflowChecklistText, projectWorkflowChecklist } from "../../workflows/workflow-checklist.ts";
28
28
  import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
29
+ import { childWatchdogProgressForModel } from "../../watchdog/child-status.ts";
29
30
 
30
31
  interface AsyncRunStepSummary {
31
32
  index: number;
@@ -66,7 +67,7 @@ interface AsyncRunStepSummary {
66
67
  model?: string;
67
68
  contextLimit?: number;
68
69
  thinking?: string;
69
- attemptedModels?: string[];
70
+ requestedModel?: string;
70
71
  sessionFile?: string;
71
72
  transcriptPath?: string;
72
73
  error?: string;
@@ -367,7 +368,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
367
368
  ...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
368
369
  ...(step.thinking ? { thinking: step.thinking } : {}),
369
370
  ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
370
- ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
371
+ ...(step.requestedModel ? { requestedModel: step.requestedModel } : {}),
371
372
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
372
373
  ...(step.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
373
374
  ...(step.error ? { error: step.error } : {}),
@@ -387,7 +388,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
387
388
  ...(step.execution ? { execution: step.execution } : {}),
388
389
  ...(step.review ? { review: step.review } : {}),
389
390
  ...(step.effects ? { effects: step.effects } : {}),
390
- ...(step.watchdog ? { watchdog: step.watchdog } : {}),
391
+ ...(step.watchdog ? { watchdog: childWatchdogProgressForModel(step.watchdog) } : {}),
391
392
  ...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
392
393
  ...(timeoutRecovery ? { timeoutRecovery } : {}),
393
394
  ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
@@ -545,6 +546,14 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
545
546
  const asyncDir = path.join(asyncDirRoot, entry);
546
547
  let status: (AsyncStatus & { cwd?: string }) | null;
547
548
  try {
549
+ // Reconciliation can rewrite state; session-scoped discovery does not own foreign runs.
550
+ if (options.sessionId !== undefined) {
551
+ const stored = readStatus(asyncDir);
552
+ if (stored && stored.sessionId !== options.sessionId) {
553
+ observeStatus?.(stored);
554
+ continue;
555
+ }
556
+ }
548
557
  const reconciliation = options.reconcile === false
549
558
  ? undefined
550
559
  : reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now }, observeStatus);
@@ -563,7 +572,9 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
563
572
  }
564
573
  if (activeEntries.has(entry) && !isActiveAsyncState(status.state)) {
565
574
  const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
566
- if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS) releaseActiveRunIndex(asyncDir);
575
+ if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS) {
576
+ updateActiveRunIndex(asyncDir, status.state, status.toolCallId, { terminalIndexBeforeRelease: true });
577
+ }
567
578
  }
568
579
  if (status.displayDismissedAt !== undefined) continue;
569
580
  // Filter before the nested-route lookup: the lookup builds an index over
@@ -1,13 +1,13 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import { snapshotBackgroundWork } from "../../api/background-work.ts";
3
- import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
3
+ import type { Details, SubagentState } from "../../shared/types.ts";
4
4
  import { listAsyncRuns } from "./async-status.ts";
5
5
  import type { ReadonlyDrainObservation } from "../shared/readonly-drain-observation.ts";
6
- import { waitForSubagents, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
6
+ import { waitForSubagents, waitRunScopes, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
7
7
 
8
8
  export const DEFAULT_AUTO_DRAIN_TIMEOUT_MS = 30 * 60 * 1000;
9
9
 
10
- export interface AutoDrainDeps {
10
+ export interface AutoDrainDeps extends Pick<SubagentWaitDeps, "asyncDirRoot" | "resultsDir" | "nestedRootRunId"> {
11
11
  state: SubagentState;
12
12
  events?: WaitEventBus;
13
13
  timeoutMs?: number;
@@ -18,20 +18,22 @@ export interface AutoDrainDeps {
18
18
  deps: SubagentWaitDeps,
19
19
  ) => Promise<AgentToolResult<Details>>;
20
20
  hasWork?: (sessionId: string, nowMs: number) => boolean;
21
+ hasPendingSupervisorRequest?: () => boolean;
21
22
  }
22
23
 
23
24
  function resultText(value: AgentToolResult<Details>): string {
24
25
  return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
25
26
  }
26
27
 
27
- function hasOutstandingWork(state: SubagentState, sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
28
- const asyncRuns = listAsyncRuns(DIRS.async, {
28
+ function hasOutstandingWork(deps: AutoDrainDeps, sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
29
+ const asyncRuns = waitRunScopes(deps).flatMap(({ asyncDirRoot, resultsDir }) => listAsyncRuns(asyncDirRoot, {
29
30
  states: ["queued", "running"],
30
31
  sessionId,
31
- resultsDir: DIRS.results,
32
+ resultsDir,
32
33
  now: () => nowMs,
33
- }, observation?.status);
34
- const detachedForeground = [...(state.foregroundRuns?.values() ?? [])].some((run) =>
34
+ includeNested: false,
35
+ }, observation?.status));
36
+ const detachedForeground = [...(deps.state.foregroundRuns?.values() ?? [])].some((run) =>
35
37
  run.sessionId === sessionId && run.children.some((child) => child.status === "detached")
36
38
  );
37
39
  return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0 || detachedForeground;
@@ -40,17 +42,19 @@ function hasOutstandingWork(state: SubagentState, sessionId: string, nowMs: numb
40
42
  /** Drain all work owned by the current headless session, including work added while draining. */
41
43
  export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: ReadonlyDrainObservation): Promise<void> {
42
44
  const sessionId = deps.state.currentSessionId;
43
- observation?.begin(sessionId, !deps.hasWork && !deps.wait && !deps.now);
45
+ observation?.begin(sessionId, !deps.hasWork && !deps.wait && !deps.now && !deps.hasPendingSupervisorRequest);
44
46
  try {
45
47
  if (!sessionId) throw new Error("Cannot auto-drain background work without an active session identity.");
46
48
  const now = deps.now ?? Date.now;
47
49
  const timeoutMs = deps.timeoutMs ?? DEFAULT_AUTO_DRAIN_TIMEOUT_MS;
48
50
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Auto-drain timeoutMs must be a positive finite number.");
49
51
  const deadlineAt = now() + timeoutMs;
50
- const hasWork = deps.hasWork ?? ((id: string, time: number) => hasOutstandingWork(deps.state, id, time, observation));
52
+ const hasWork = deps.hasWork ?? ((id: string, time: number) => hasOutstandingWork(deps, id, time, observation));
51
53
  const wait = deps.wait ?? waitForSubagents;
52
54
 
53
55
  while (true) {
56
+ if (deps.state.currentSessionId !== sessionId) throw new Error("Auto-drain stopped because the active session changed.");
57
+ if (deps.hasPendingSupervisorRequest?.()) break;
54
58
  const work = hasWork(sessionId, now());
55
59
  observation?.predicate(work);
56
60
  if (!work) break;
@@ -63,16 +67,22 @@ export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: Re
63
67
  undefined,
64
68
  {
65
69
  state: deps.state,
70
+ asyncDirRoot: deps.asyncDirRoot,
71
+ resultsDir: deps.resultsDir,
72
+ nestedRootRunId: deps.nestedRootRunId,
66
73
  events: deps.events,
67
74
  now,
68
75
  stopOnAttention: false,
69
76
  failOnFailedRuns: true,
70
77
  failOnAttention: true,
78
+ hasPendingSupervisorRequest: deps.hasPendingSupervisorRequest,
71
79
  },
72
80
  );
73
81
  if (waitResult.isError) {
74
82
  throw new Error(`Auto-drain failed for session '${sessionId}': ${resultText(waitResult) || "bg_wait returned an error without details"}.`);
75
83
  }
84
+ if (waitResult.details.wait?.reason === "supervisor_request") break;
85
+ if (deps.hasPendingSupervisorRequest?.()) break;
76
86
  }
77
87
  observation?.complete();
78
88
  } catch (error) {
@@ -1,7 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import * as sdk from "@earendil-works/pi-coding-agent";
4
+ import { installRunnerHttpDispatcher } from "./runner-http-dispatcher.ts";
4
5
  import { runConfiguredSubagent, type SubagentRunConfig } from "./subagent-runner.ts";
6
+ import { getAgentDir } from "../../shared/utils.ts";
5
7
 
6
8
  /**
7
9
  * Pi's extension loader supplies the embedded SDK. Bare Bun/Node cannot replace
@@ -24,6 +26,9 @@ export default async function runBinaryBootstrap(): Promise<never> {
24
26
  } catch {
25
27
  // Temp-config cleanup is best effort, as in the Node entrypoint.
26
28
  }
29
+ // Pi applies httpIdleTimeoutMs to its dispatcher only after extension
30
+ // factories return; this factory never does, so install the runner's own.
31
+ installRunnerHttpDispatcher({ agentDir: getAgentDir(), cwd: process.cwd() });
27
32
  await runConfiguredSubagent(config, { loadPiCodingAgent: async () => sdk });
28
33
  process.exit(0);
29
34
  } catch (error) {
@@ -177,7 +177,7 @@ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
177
177
  model: task.model,
178
178
  ...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
179
179
  thinking: task.thinking,
180
- attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
180
+ requestedModel: task.requestedModel,
181
181
  recentTools: [],
182
182
  recentOutput: [],
183
183
  };
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
4
- import type { AcceptanceLedger, ArtifactPaths, AsyncStatus, CostSummary, EffectsProjection, ExecutionProjection, ModelAttempt, Usage } from "../../shared/types.ts";
4
+ import type { AcceptanceLedger, ArtifactPaths, AsyncStatus, CostSummary, EffectsProjection, ExecutionProjection, Usage } from "../../shared/types.ts";
5
5
  import { readStatus } from "../../shared/utils.ts";
6
6
 
7
7
  export interface ImportedAsyncRoot {
@@ -13,6 +13,7 @@ export interface ImportedAsyncRoot {
13
13
 
14
14
  export interface ImportedAsyncRootResult {
15
15
  agent: string;
16
+ importedPublication?: { sessionId?: string; toolCallId?: string };
16
17
  /** Human-readable display name for the child session, when derived at launch. */
17
18
  sessionName?: string;
18
19
  output: string;
@@ -22,8 +23,7 @@ export interface ImportedAsyncRootResult {
22
23
  sessionFile?: string;
23
24
  intercomTarget?: string;
24
25
  model?: string;
25
- attemptedModels?: string[];
26
- modelAttempts?: ModelAttempt[];
26
+ requestedModel?: string;
27
27
  contextOverflow?: boolean;
28
28
  totalCost?: CostSummary;
29
29
  usage?: Usage;
@@ -43,6 +43,8 @@ export interface ImportedAsyncRootResult {
43
43
  }
44
44
 
45
45
  interface AsyncResultFile {
46
+ sessionId?: string;
47
+ toolCallId?: string;
46
48
  state?: string;
47
49
  success?: boolean;
48
50
  summary?: string;
@@ -62,8 +64,7 @@ interface AsyncResultFile {
62
64
  sessionFile?: string;
63
65
  intercomTarget?: string;
64
66
  model?: string;
65
- attemptedModels?: string[];
66
- modelAttempts?: ModelAttempt[];
67
+ requestedModel?: string;
67
68
  contextOverflow?: boolean;
68
69
  totalCost?: CostSummary;
69
70
  usage?: Usage;
@@ -82,23 +83,6 @@ interface AsyncResultFile {
82
83
  const TERMINAL_STATES = new Set(["complete", "failed", "partial", "paused", "stopped"]);
83
84
  const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "partial", "paused", "stopped"]);
84
85
 
85
- function usageFromAttempts(attempts: ModelAttempt[] | undefined): Usage | undefined {
86
- if (!attempts?.length) return undefined;
87
- const usage: Usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
88
- for (const attempt of attempts) {
89
- if (!attempt.usage) continue;
90
- usage.input += attempt.usage.input;
91
- usage.output += attempt.usage.output;
92
- usage.cacheRead += attempt.usage.cacheRead;
93
- usage.cacheWrite += attempt.usage.cacheWrite;
94
- usage.cost += attempt.usage.cost;
95
- usage.turns += attempt.usage.turns;
96
- }
97
- return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0
98
- ? usage
99
- : undefined;
100
- }
101
-
102
86
  function readResultFile(resultPath: string): AsyncResultFile | undefined {
103
87
  try {
104
88
  return JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultFile;
@@ -153,7 +137,6 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
153
137
  const execution = step?.execution ?? (partialStatus
154
138
  ? { status: "partial" as const, success: false, exitCode: 1, error: message }
155
139
  : undefined);
156
- const usage = usageFromAttempts(step?.modelAttempts);
157
140
  return {
158
141
  agent,
159
142
  output: message,
@@ -165,11 +148,9 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
165
148
  ...(step?.sessionName ? { sessionName: step.sessionName } : {}),
166
149
  ...(step?.sessionFile ?? status.sessionFile ? { sessionFile: step?.sessionFile ?? status.sessionFile } : {}),
167
150
  ...(step?.model ? { model: step.model } : {}),
168
- ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
169
- ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
151
+ ...(step?.requestedModel ? { requestedModel: step.requestedModel } : {}),
170
152
  ...(step?.contextOverflow ? { contextOverflow: true } : {}),
171
153
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
172
- ...(usage ? { usage } : {}),
173
154
  ...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
174
155
  ...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
175
156
  ...(step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: step.structuredOutputSchemaPath } : {}),
@@ -182,7 +163,6 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
182
163
 
183
164
  function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null, message: string): ImportedAsyncRootResult {
184
165
  const step = selectedStatusStep(status, root.index);
185
- const usage = usageFromAttempts(step?.modelAttempts);
186
166
  return {
187
167
  agent: step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent",
188
168
  output: message,
@@ -193,11 +173,9 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
193
173
  ...(step?.sessionName ? { sessionName: step.sessionName } : {}),
194
174
  ...(step?.sessionFile ?? status?.sessionFile ? { sessionFile: step?.sessionFile ?? status?.sessionFile } : {}),
195
175
  ...(step?.model ? { model: step.model } : {}),
196
- ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
197
- ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
176
+ ...(step?.requestedModel ? { requestedModel: step.requestedModel } : {}),
198
177
  ...(step?.contextOverflow ? { contextOverflow: true } : {}),
199
178
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
200
- ...(usage ? { usage } : {}),
201
179
  ...(step?.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
202
180
  };
203
181
  }
@@ -213,9 +191,13 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
213
191
  const success = state === "complete" && !timedOut && !stopped;
214
192
  const error = child?.error ?? (success ? undefined : stopped ? "Subagent stopped by user." : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
215
193
  const execution = state ? { status: state === "complete" ? "completed" as const : state, success, exitCode: success ? 0 : 1, ...(error ? { error } : {}) } : undefined;
216
- const usage = child?.usage ?? usageFromAttempts(step?.modelAttempts);
194
+ const usage = child?.usage;
217
195
  return {
218
196
  agent,
197
+ importedPublication: {
198
+ ...(typeof result.sessionId === "string" ? { sessionId: result.sessionId } : {}),
199
+ ...(typeof result.toolCallId === "string" ? { toolCallId: result.toolCallId } : {}),
200
+ },
219
201
  output: success ? output : (output || error || ""),
220
202
  success,
221
203
  exitCode: success ? 0 : 1,
@@ -226,8 +208,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
226
208
  ...(child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile ? { sessionFile: child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile } : {}),
227
209
  ...(child?.intercomTarget ? { intercomTarget: child.intercomTarget } : {}),
228
210
  ...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
229
- ...(child?.attemptedModels ?? step?.attemptedModels ? { attemptedModels: child?.attemptedModels ?? step?.attemptedModels } : {}),
230
- ...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
211
+ ...(child?.requestedModel ?? step?.requestedModel ? { requestedModel: child?.requestedModel ?? step?.requestedModel } : {}),
231
212
  ...(child?.contextOverflow || step?.contextOverflow ? { contextOverflow: true } : {}),
232
213
  ...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
233
214
  ...(usage ? { usage } : {}),
@@ -6,6 +6,7 @@
6
6
  * observation channel, not a delivery acknowledgement.
7
7
  */
8
8
 
9
+ import { statSync } from "node:fs";
9
10
  import { debuglog } from "node:util";
10
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
12
  import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
@@ -27,6 +28,10 @@ export interface SubagentNotifyChildOutput {
27
28
  agent?: string;
28
29
  status: string;
29
30
  savedOutputPath?: string;
31
+ outputArtifactPath?: string;
32
+ structuredOutputPath?: string;
33
+ outputArtifactError?: RetainedPathError;
34
+ structuredOutputError?: RetainedPathError;
30
35
  preview: string;
31
36
  previewTruncated?: boolean;
32
37
  previewUnavailableReason?: string;
@@ -36,6 +41,7 @@ export type SubagentNotifyWatchdogBlocker = Pick<ChildWatchdogWarningSummary, "s
36
41
 
37
42
  export interface SubagentNotifyDetails {
38
43
  workflowReceiptPath?: string;
44
+ asyncDir?: string;
39
45
  agent: string;
40
46
  status: "completed" | "failed" | "paused" | "stopped";
41
47
  source?: "async" | "foreground";
@@ -90,9 +96,12 @@ export interface CompletionNotification {
90
96
  success?: boolean;
91
97
  output?: string;
92
98
  structuredOutput?: unknown;
99
+ structuredOutputPath?: string;
93
100
  outputState?: "present" | "absent" | "unknown";
94
101
  outputReference?: string | { path?: string };
95
102
  artifactPaths?: { outputPath?: string };
103
+ outputSaveError?: string;
104
+ artifactOutputSaveFailed?: true;
96
105
  detached?: boolean;
97
106
  exitCode?: number | null;
98
107
  processSignal?: string | null;
@@ -119,6 +128,7 @@ export interface CompletionNotification {
119
128
  intercomDelivered?: boolean;
120
129
  parallelHandoff?: ParallelHandoffReference;
121
130
  scheduleOrigin?: ScheduleOrigin;
131
+ asyncDir?: string;
122
132
  }
123
133
 
124
134
  interface NotifyTimerApi {
@@ -143,6 +153,11 @@ const CHILD_OUTPUT_PREVIEW_MAX_BYTES = 4 * 1024;
143
153
  const CHILD_OUTPUT_PREVIEW_COUNT = 8;
144
154
  const PREVIEW_TRUNCATION_MARKER = "...[preview truncated]";
145
155
  type CompletionChild = NonNullable<CompletionNotification["results"]>[number];
156
+ interface RetainedPathError { path: string; code?: string; message: string }
157
+ type RetainedPathProjection =
158
+ | { status: "verified"; path: string }
159
+ | { status: "unavailable" }
160
+ | { status: "error"; error: RetainedPathError };
146
161
 
147
162
  function truncateUtf8Head(value: string, maxBytes: number): { text: string; truncated: boolean } {
148
163
  const bytes = Buffer.from(value, "utf8");
@@ -171,6 +186,27 @@ function childSavedOutputPath(child: CompletionChild): string | undefined {
171
186
  return outputPathFromReference(child.outputReference);
172
187
  }
173
188
 
189
+ function nonemptyPath(value: unknown): string | undefined {
190
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
191
+ }
192
+
193
+ function retainedChildPath(value: unknown, producerConfirmed = false): RetainedPathProjection {
194
+ const path = nonemptyPath(value);
195
+ if (!path) return { status: "unavailable" };
196
+ if (producerConfirmed) return { status: "verified", path };
197
+ try {
198
+ return statSync(path).isFile() ? { status: "verified", path } : { status: "unavailable" };
199
+ } catch (error) {
200
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : undefined;
201
+ if (code === "ENOENT" || code === "ENOTDIR") return { status: "unavailable" };
202
+ return { status: "error", error: { path, ...(code ? { code } : {}), message: error instanceof Error ? error.message : String(error) } };
203
+ }
204
+ }
205
+
206
+ function formatRetainedPathError(label: string, error: RetainedPathError): string {
207
+ return ` ${label} verification failed: stat ${error.code ? boundedSafeText(error.code, 32) : "unknown"}; path=${boundedSafeText(error.path, 384)}; ${boundedSafeText(error.message, 512)}`;
208
+ }
209
+
174
210
  function childStatus(child: CompletionChild, workflowState?: string): string {
175
211
  const knownStatus = child.status === "complete"
176
212
  ? "completed"
@@ -231,6 +267,11 @@ function formatChildOutputBlock(children: SubagentNotifyChildOutput[] | undefine
231
267
  const status = boundedSafeText(child.status) || "unavailable";
232
268
  lines.push(`- key=${key} run=${runId} status=${status}`);
233
269
  lines.push(` Saved output: ${child.savedOutputPath ? boundedSafeText(child.savedOutputPath) : "unavailable"}`);
270
+ if (child.outputArtifactPath) lines.push(` Output artifact (retention-managed): ${boundedSafeText(child.outputArtifactPath)}`);
271
+ if (child.structuredOutputPath) lines.push(` Structured output (retention-managed): ${boundedSafeText(child.structuredOutputPath)}`);
272
+ if (child.outputArtifactError) lines.push(formatRetainedPathError("Output artifact", child.outputArtifactError));
273
+ if (child.structuredOutputError) lines.push(formatRetainedPathError("Structured output", child.structuredOutputError));
274
+ if (child.previewTruncated && !child.savedOutputPath && !child.outputArtifactPath) lines.push(" Full output unavailable");
234
275
  if (index >= CHILD_OUTPUT_PREVIEW_COUNT) {
235
276
  lines.push(" Preview: unavailable (notice preview budget exceeded)");
236
277
  continue;
@@ -267,6 +308,7 @@ function formatChildRun(child: { runId: string; workflowKey?: string; agent?: st
267
308
 
268
309
  function formatCorrelationLines(details: SubagentNotifyDetails): string[] {
269
310
  return [
311
+ details.asyncDir ? `Retention-managed async directory: ${boundedSafeText(details.asyncDir)}` : undefined,
270
312
  details.workflowRunId ? `Workflow run: ${details.workflowRunId}` : undefined,
271
313
  details.childRuns?.length ? `Child runs: ${details.childRuns.map(formatChildRun).join(", ")}` : undefined,
272
314
  details.reconciledFromDetachedChild ? `Reconciled detached child: ${details.reconciledFromDetachedChild}` : undefined,
@@ -322,6 +364,20 @@ export function scheduledCompletionTriggersTurn(origin: ScheduleOrigin | undefin
322
364
  return !(origin?.quiet === true && outcome === "completed");
323
365
  }
324
366
 
367
+ /**
368
+ * Child settlement is useful context, but an ordinary successful child does not
369
+ * establish the workflow's dependency barrier while its workflow is running.
370
+ * Keep actionable outcomes waking the parent, and preserve a terminal child as
371
+ * the barrier for hosts that do not emit a separate workflow completion wake.
372
+ */
373
+ export function incrementalChildCompletionTriggersTurn(
374
+ child: Pick<IncrementalChildCompletion, "outcome" | "workflowRunning">,
375
+ origin: ScheduleOrigin | undefined,
376
+ ): boolean {
377
+ if (child.workflowRunning && child.outcome === "completed") return false;
378
+ return scheduledCompletionTriggersTurn(origin, child.outcome);
379
+ }
380
+
325
381
  export function formatIncrementalChildCompletion(child: IncrementalChildCompletion): string {
326
382
  const statusText = child.outcome === "completed" ? "completed"
327
383
  : child.outcome === "failed" ? "failed"
@@ -559,12 +615,20 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
559
615
  const workflowKey = typeof child.workflowKey === "string" && child.workflowKey.trim() ? child.workflowKey.trim() : undefined;
560
616
  const runId = typeof child.runId === "string" && child.runId.trim() ? child.runId.trim() : undefined;
561
617
  const savedOutputPath = childSavedOutputPath(child);
618
+ const outputArtifact = child.artifactOutputSaveFailed === true ? { status: "unavailable" as const } : retainedChildPath(child.artifactPaths?.outputPath);
619
+ const structuredOutput = child.stopped === true || child.timedOut === true
620
+ ? { status: "unavailable" as const }
621
+ : retainedChildPath(child.structuredOutputPath, child.structuredOutput !== undefined);
562
622
  return {
563
623
  ...(workflowKey ? { workflowKey } : {}),
564
624
  ...(runId ? { runId } : {}),
565
625
  ...(typeof child.agent === "string" ? { agent: child.agent } : {}),
566
626
  status: childStatus(child, result.state),
567
627
  ...(savedOutputPath ? { savedOutputPath } : {}),
628
+ ...(outputArtifact.status === "verified" ? { outputArtifactPath: outputArtifact.path } : {}),
629
+ ...(structuredOutput.status === "verified" ? { structuredOutputPath: structuredOutput.path } : {}),
630
+ ...(outputArtifact.status === "error" ? { outputArtifactError: outputArtifact.error } : {}),
631
+ ...(structuredOutput.status === "error" ? { structuredOutputError: structuredOutput.error } : {}),
568
632
  ...(inline.preview ? { preview: inline.preview } : { preview: "" }),
569
633
  ...(inline.truncated ? { previewTruncated: true } : {}),
570
634
  ...(inline.unavailableReason ? { previewUnavailableReason: inline.unavailableReason } : {}),
@@ -572,15 +636,18 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
572
636
  })
573
637
  : undefined;
574
638
  const reconciledFromDetachedChild = typeof result.reconciledFromDetachedChild === "string" ? result.reconciledFromDetachedChild : undefined;
639
+ const asyncDir = nonemptyPath(result.asyncDir);
575
640
  const watchdogBlockers: SubagentNotifyWatchdogBlocker[] = [];
576
- const collectWatchdogBlockers = (owner: string, progress: ChildWatchdogProgress | undefined) => {
641
+ const watchdogConcerns: string[] = [];
642
+ const collectWatchdogFindings = (owner: string, progress: ChildWatchdogProgress | undefined) => {
577
643
  for (const warning of progress?.warnings ?? []) {
578
- if (warning.severity !== "blocker") continue;
579
- watchdogBlockers.push({ agent: owner, summary: warning.summary, addressed: warning.addressed, stalemate: warning.stalemate });
644
+ if (warning.importance !== "high") continue;
645
+ if (warning.severity === "blocker") watchdogBlockers.push({ agent: owner, summary: warning.summary, addressed: warning.addressed, stalemate: warning.stalemate });
646
+ else watchdogConcerns.push(`${owner}: ${warning.summary}\nEvidence: ${warning.evidence}\nRecommended action: ${warning.recommendedAction}`);
580
647
  }
581
648
  };
582
- collectWatchdogBlockers(agent, result.watchdog);
583
- for (const child of result.results ?? []) collectWatchdogBlockers(typeof child.agent === "string" ? child.agent : agent, child.watchdog);
649
+ collectWatchdogFindings(agent, result.watchdog);
650
+ for (const child of result.results ?? []) collectWatchdogFindings(typeof child.agent === "string" ? child.agent : agent, child.watchdog);
584
651
  const session =
585
652
  result.shareUrl
586
653
  ? { label: "Session", value: result.shareUrl }
@@ -598,9 +665,10 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
598
665
  status,
599
666
  ...(scheduleOrigin ? { scheduleOrigin } : {}),
600
667
  ...(workflowReceiptPath ? { workflowReceiptPath } : {}),
668
+ ...(asyncDir ? { asyncDir } : {}),
601
669
  ...(result.source ? { source: result.source } : {}),
602
670
  ...(taskInfo ? { taskInfo } : {}),
603
- resultPreview,
671
+ resultPreview: watchdogConcerns.length ? `${resultPreview}\n\nHigh-importance watchdog concerns:\n${watchdogConcerns.map((warning) => `- ${warning}`).join("\n")}` : resultPreview,
604
672
  ...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
605
673
  ...(handoffPath ? { handoffPath } : {}),
606
674
  ...(workflowRunId ? { workflowRunId } : {}),