pi-subagents 0.66.0 → 0.68.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 (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -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";
@@ -54,6 +60,16 @@ export interface SubagentNotifyDetails {
54
60
  watchdogBlockers?: SubagentNotifyWatchdogBlocker[];
55
61
  }
56
62
 
63
+ export interface IncrementalChildCompletion {
64
+ workflowRunId: string;
65
+ childKey: string;
66
+ childRunId?: string;
67
+ outcome: "completed" | "failed" | "paused" | "stopped";
68
+ outputReference?: string;
69
+ error?: string;
70
+ workflowRunning: boolean;
71
+ }
72
+
57
73
  export interface CompletionNotification {
58
74
  [key: string]: unknown;
59
75
  id?: string | null;
@@ -80,9 +96,12 @@ export interface CompletionNotification {
80
96
  success?: boolean;
81
97
  output?: string;
82
98
  structuredOutput?: unknown;
99
+ structuredOutputPath?: string;
83
100
  outputState?: "present" | "absent" | "unknown";
84
101
  outputReference?: string | { path?: string };
85
102
  artifactPaths?: { outputPath?: string };
103
+ outputSaveError?: string;
104
+ artifactOutputSaveFailed?: true;
86
105
  detached?: boolean;
87
106
  exitCode?: number | null;
88
107
  processSignal?: string | null;
@@ -109,6 +128,7 @@ export interface CompletionNotification {
109
128
  intercomDelivered?: boolean;
110
129
  parallelHandoff?: ParallelHandoffReference;
111
130
  scheduleOrigin?: ScheduleOrigin;
131
+ asyncDir?: string;
112
132
  }
113
133
 
114
134
  interface NotifyTimerApi {
@@ -133,6 +153,11 @@ const CHILD_OUTPUT_PREVIEW_MAX_BYTES = 4 * 1024;
133
153
  const CHILD_OUTPUT_PREVIEW_COUNT = 8;
134
154
  const PREVIEW_TRUNCATION_MARKER = "...[preview truncated]";
135
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 };
136
161
 
137
162
  function truncateUtf8Head(value: string, maxBytes: number): { text: string; truncated: boolean } {
138
163
  const bytes = Buffer.from(value, "utf8");
@@ -161,6 +186,27 @@ function childSavedOutputPath(child: CompletionChild): string | undefined {
161
186
  return outputPathFromReference(child.outputReference);
162
187
  }
163
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
+
164
210
  function childStatus(child: CompletionChild, workflowState?: string): string {
165
211
  const knownStatus = child.status === "complete"
166
212
  ? "completed"
@@ -221,6 +267,11 @@ function formatChildOutputBlock(children: SubagentNotifyChildOutput[] | undefine
221
267
  const status = boundedSafeText(child.status) || "unavailable";
222
268
  lines.push(`- key=${key} run=${runId} status=${status}`);
223
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");
224
275
  if (index >= CHILD_OUTPUT_PREVIEW_COUNT) {
225
276
  lines.push(" Preview: unavailable (notice preview budget exceeded)");
226
277
  continue;
@@ -257,6 +308,7 @@ function formatChildRun(child: { runId: string; workflowKey?: string; agent?: st
257
308
 
258
309
  function formatCorrelationLines(details: SubagentNotifyDetails): string[] {
259
310
  return [
311
+ details.asyncDir ? `Retention-managed async directory: ${boundedSafeText(details.asyncDir)}` : undefined,
260
312
  details.workflowRunId ? `Workflow run: ${details.workflowRunId}` : undefined,
261
313
  details.childRuns?.length ? `Child runs: ${details.childRuns.map(formatChildRun).join(", ")}` : undefined,
262
314
  details.reconciledFromDetachedChild ? `Reconciled detached child: ${details.reconciledFromDetachedChild}` : undefined,
@@ -308,6 +360,40 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
308
360
  .join("\n");
309
361
  }
310
362
 
363
+ export function scheduledCompletionTriggersTurn(origin: ScheduleOrigin | undefined, outcome: string): boolean {
364
+ return !(origin?.quiet === true && outcome === "completed");
365
+ }
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
+
381
+ export function formatIncrementalChildCompletion(child: IncrementalChildCompletion): string {
382
+ const statusText = child.outcome === "completed" ? "completed"
383
+ : child.outcome === "failed" ? "failed"
384
+ : child.outcome === "paused" ? "paused (needs attention)"
385
+ : "stopped";
386
+ const workflowStatus = child.workflowRunning ? "workflow still running" : "workflow finished";
387
+ return [
388
+ `Workflow child ${statusText}: **${child.childKey}**`,
389
+ `Workflow run: ${child.workflowRunId}`,
390
+ ...(child.childRunId ? [`Child run: ${child.childRunId}`] : []),
391
+ ...(child.outputReference ? [`Output: ${child.outputReference}`] : []),
392
+ ...(child.error ? [`Error: ${child.error}`] : []),
393
+ `Status: ${workflowStatus}`,
394
+ ].join("\n");
395
+ }
396
+
311
397
  export function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
312
398
  const lines = content.split("\n");
313
399
  const match = (lines[0] ?? "").match(/^(Background task|Detached foreground task) (completed|failed|paused|stopped): \*\*(.+?)\*\*(?:\s+(\([^)]*\)))?$/);
@@ -529,12 +615,20 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
529
615
  const workflowKey = typeof child.workflowKey === "string" && child.workflowKey.trim() ? child.workflowKey.trim() : undefined;
530
616
  const runId = typeof child.runId === "string" && child.runId.trim() ? child.runId.trim() : undefined;
531
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);
532
622
  return {
533
623
  ...(workflowKey ? { workflowKey } : {}),
534
624
  ...(runId ? { runId } : {}),
535
625
  ...(typeof child.agent === "string" ? { agent: child.agent } : {}),
536
626
  status: childStatus(child, result.state),
537
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 } : {}),
538
632
  ...(inline.preview ? { preview: inline.preview } : { preview: "" }),
539
633
  ...(inline.truncated ? { previewTruncated: true } : {}),
540
634
  ...(inline.unavailableReason ? { previewUnavailableReason: inline.unavailableReason } : {}),
@@ -542,15 +636,18 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
542
636
  })
543
637
  : undefined;
544
638
  const reconciledFromDetachedChild = typeof result.reconciledFromDetachedChild === "string" ? result.reconciledFromDetachedChild : undefined;
639
+ const asyncDir = nonemptyPath(result.asyncDir);
545
640
  const watchdogBlockers: SubagentNotifyWatchdogBlocker[] = [];
546
- const collectWatchdogBlockers = (owner: string, progress: ChildWatchdogProgress | undefined) => {
641
+ const watchdogConcerns: string[] = [];
642
+ const collectWatchdogFindings = (owner: string, progress: ChildWatchdogProgress | undefined) => {
547
643
  for (const warning of progress?.warnings ?? []) {
548
- if (warning.severity !== "blocker") continue;
549
- 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}`);
550
647
  }
551
648
  };
552
- collectWatchdogBlockers(agent, result.watchdog);
553
- 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);
554
651
  const session =
555
652
  result.shareUrl
556
653
  ? { label: "Session", value: result.shareUrl }
@@ -568,9 +665,10 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
568
665
  status,
569
666
  ...(scheduleOrigin ? { scheduleOrigin } : {}),
570
667
  ...(workflowReceiptPath ? { workflowReceiptPath } : {}),
668
+ ...(asyncDir ? { asyncDir } : {}),
571
669
  ...(result.source ? { source: result.source } : {}),
572
670
  ...(taskInfo ? { taskInfo } : {}),
573
- resultPreview,
671
+ resultPreview: watchdogConcerns.length ? `${resultPreview}\n\nHigh-importance watchdog concerns:\n${watchdogConcerns.map((warning) => `- ${warning}`).join("\n")}` : resultPreview,
574
672
  ...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
575
673
  ...(handoffPath ? { handoffPath } : {}),
576
674
  ...(workflowRunId ? { workflowRunId } : {}),
@@ -675,7 +773,7 @@ export default function registerSubagentNotify(
675
773
  details,
676
774
  sessionId: result.sessionId,
677
775
  completionOwnerId: result.completionOwnerId,
678
- triggerTurn: result.triggerTurn !== false,
776
+ triggerTurn: result.triggerTurn !== false && scheduledCompletionTriggersTurn(result.scheduleOrigin, details.status),
679
777
  resolve,
680
778
  };
681
779
  if (notificationDebug.enabled) item.trace = traceIdentity(result);
@@ -35,6 +35,23 @@ function activeProcessGroupMembers(processGroupId: number): number[] | { diagnos
35
35
  return members;
36
36
  }
37
37
 
38
+ function knownDetachedDescendants(processGroupId: number): number[] {
39
+ const result = spawnSync("ps", ["-axo", "pid=,ppid=,pgid=,stat="], { encoding: "utf-8" });
40
+ if (result.error || result.status !== 0) return [];
41
+ const owned = new Set<number>();
42
+ const outside: { pid: number; ppid: number }[] = [];
43
+ for (const line of result.stdout.split("\n")) {
44
+ const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/.exec(line);
45
+ if (!match || match[4]!.startsWith("Z")) continue;
46
+ const pid = Number(match[1]);
47
+ const ppid = Number(match[2]);
48
+ const pgid = Number(match[3]);
49
+ if (pgid === processGroupId) owned.add(pid);
50
+ else outside.push({ pid, ppid });
51
+ }
52
+ return outside.filter((row) => owned.has(row.ppid)).map((row) => row.pid);
53
+ }
54
+
38
55
  async function waitUntilGroupTerminal(
39
56
  processGroupId: number,
40
57
  timeoutMs: number,
@@ -56,6 +73,15 @@ function observed(processGroupId: number): ProcessTreeTerminal {
56
73
  return { state: "observed", mechanism: "posix-process-group", processGroupId, verifiedAt: Date.now() };
57
74
  }
58
75
 
76
+ function observedUnlessLiveDetached(processGroupId: number, detached: readonly number[]): ProcessTreeTerminal {
77
+ const live = detached.filter((pid) => {
78
+ const result = spawnSync("ps", ["-o", "stat=", "-p", String(pid)], { encoding: "utf-8" });
79
+ return result.status === 0 && Boolean(result.stdout.trim()) && !result.stdout.trim().startsWith("Z");
80
+ });
81
+ if (live.length === 0) return observed(processGroupId);
82
+ return { state: "unknown", reason: "verification-failed", diagnostic: `Owned detached descendant(s) still active: ${live.join(", ")}.` };
83
+ }
84
+
59
85
  /** Owns one writer process group and arbitrates its cleanup exactly once. */
60
86
  export interface OwnedProcessTreeController {
61
87
  terminate(): Promise<ProcessTreeTerminal>;
@@ -77,12 +103,13 @@ export function createOwnedProcessTreeController(
77
103
  signalProcess(target, "SIGTERM");
78
104
  return { state: "unknown", reason: "unsupported-platform" };
79
105
  }
106
+ const detached = knownDetachedDescendants(pid);
80
107
  const term = signalProcess(target, "SIGTERM");
81
108
  if (term !== "sent" && term !== "absent") {
82
109
  return { state: "unknown", reason: "signal-failed", diagnostic: term.diagnostic };
83
110
  }
84
111
  const termExit = await waitUntilGroupTerminal(pid, options.termGraceMs ?? DEFAULT_TERM_GRACE_MS);
85
- if (termExit === false) return observed(pid);
112
+ if (termExit === false) return observedUnlessLiveDetached(pid, detached);
86
113
 
87
114
  const kill = signalProcess(target, "SIGKILL");
88
115
  if (kill !== "sent" && kill !== "absent") {
@@ -95,7 +122,7 @@ export function createOwnedProcessTreeController(
95
122
  if (killExit !== false) {
96
123
  return { state: "unknown", reason: "verification-failed", diagnostic: killExit.diagnostic };
97
124
  }
98
- return observed(pid);
125
+ return observedUnlessLiveDetached(pid, detached);
99
126
  })();
100
127
  return termination;
101
128
  };
@@ -241,10 +241,15 @@ function pendingResultExists(resultsDir: string, sessionId: string, runId: strin
241
241
  return firstExistingResultFile(resultPendingPaths(resultsDir, sessionId, runId)) !== undefined;
242
242
  }
243
243
 
244
+ export function resultPayloadMatchesSessionRun(data: unknown, sessionId: string, runId: string): boolean {
245
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
246
+ const record = data as Record<string, unknown>;
247
+ return (nonEmptyString(record.runId) ?? nonEmptyString(record.id)) === runId && nonEmptyString(record.sessionId) === sessionId;
248
+ }
249
+
244
250
  function pendingResultPayloadMatches(filePath: string, sessionId: string, runId: string): boolean {
245
251
  try {
246
- const data = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Record<string, unknown>;
247
- return (nonEmptyString(data.runId) ?? nonEmptyString(data.id)) === runId && nonEmptyString(data.sessionId) === sessionId;
252
+ return resultPayloadMatchesSessionRun(JSON.parse(fs.readFileSync(filePath, "utf-8")), sessionId, runId);
248
253
  } catch (error) {
249
254
  if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid pending async result '${filePath}':`, error);
250
255
  return false;
@@ -252,8 +257,7 @@ function pendingResultPayloadMatches(filePath: string, sessionId: string, runId:
252
257
  }
253
258
 
254
259
  function assertPendingResultPayloadMatches(filePath: string, sessionId: string, runId: string): boolean {
255
- const data = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Record<string, unknown>;
256
- return (nonEmptyString(data.runId) ?? nonEmptyString(data.id)) === runId && nonEmptyString(data.sessionId) === sessionId;
260
+ return resultPayloadMatchesSessionRun(JSON.parse(fs.readFileSync(filePath, "utf-8")), sessionId, runId);
257
261
  }
258
262
 
259
263
  export function promotePendingResultFile(resultsDir: string, sessionId: string, runId: string, file = resultFileName(runId), options: { logFailure?: boolean } = {}): "none" | "promoted" | "pending" {
@@ -70,6 +70,7 @@ type ResultFileChild = {
70
70
  sessionName?: string;
71
71
  output?: string;
72
72
  structuredOutput?: unknown;
73
+ structuredOutputPath?: string;
73
74
  outputState?: SubagentOutputState;
74
75
  error?: string;
75
76
  success?: boolean;
@@ -81,6 +82,8 @@ type ResultFileChild = {
81
82
  processSignal?: string | null;
82
83
  sessionFile?: string;
83
84
  artifactPaths?: { outputPath?: string };
85
+ outputSaveError?: string;
86
+ artifactOutputSaveFailed?: true;
84
87
  intercomTarget?: string;
85
88
  children?: unknown;
86
89
  };
@@ -408,7 +411,9 @@ export function createResultWatcher(
408
411
  if (typeof data.sessionId !== "string" || !data.sessionId) return;
409
412
  const sessionId = data.sessionId;
410
413
  const completionOwnerId = data.completionOwnerId;
411
- const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
414
+ const runId = typeof data.runId === "string" && data.runId
415
+ ? data.runId
416
+ : typeof data.id === "string" && data.id ? data.id : file.replace(/\.json$/i, "");
412
417
  const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined;
413
418
  let observerSucceeded = true;
414
419
  try {
@@ -428,7 +433,7 @@ export function createResultWatcher(
428
433
  if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
429
434
  // Recorded before dedupe and before the unlink below so bg_wait can
430
435
  // use the in-memory record or its bounded durable replay after cleanup.
431
- recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
436
+ const completionPersisted = recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
432
437
  resultsDir,
433
438
  sessionId,
434
439
  });
@@ -456,6 +461,10 @@ export function createResultWatcher(
456
461
  }
457
462
  if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
458
463
  if (markReplacedPayload()) return;
464
+ if (!completionPersisted) {
465
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
466
+ return;
467
+ }
459
468
  if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
460
469
  return;
461
470
  }
@@ -513,6 +522,10 @@ export function createResultWatcher(
513
522
  }
514
523
  if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
515
524
  if (markReplacedPayload()) return;
525
+ if (!completionPersisted) {
526
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
527
+ return;
528
+ }
516
529
  if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
517
530
  return;
518
531
  }
@@ -600,6 +613,10 @@ export function createResultWatcher(
600
613
  return;
601
614
  }
602
615
  if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
616
+ if (!completionPersisted) {
617
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
618
+ return;
619
+ }
603
620
  if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
604
621
  } catch (error) {
605
622
  if (isAccessDenied(error)) {
@@ -19,14 +19,15 @@ import {
19
19
  type ChildWatchdogStatusEvent,
20
20
  } from "../../watchdog/child-status.ts";
21
21
  import { projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState } from "../shared/child-lifecycle.ts";
22
- import { formatSubagentModelVerificationError } from "../shared/model-fallback.ts";
22
+ import { formatSubagentModelVerificationError } from "../shared/model-resolution.ts";
23
+ import { formatChildModelResolutionDiagnostic, isChildModelResolutionFailure } from "../shared/model-resolution-diagnostic.ts";
23
24
  import { isMutatingTool, resolveCurrentPath } from "../shared/long-running-guard.ts";
24
25
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
25
26
  import { createReportedChildSessionInput, type InProcessChildLaunch } from "../shared/child-launch.ts";
26
- import { projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent, type ChildSessionFactory } from "../shared/child-session.ts";
27
+ import { childSessionHasQueuedMessages, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent, type ChildSessionFactory } from "../shared/child-session.ts";
27
28
  import { formatSteerMessage } from "../shared/subagent-prompt-runtime.ts";
28
- import { getReadonlySessionEvidence, requestReadonlySessionEvidence, type SettledReadonlyEvidence } from "../shared/readonly-session-evidence.ts";
29
29
  import type { SteerDeliveryStatus, SteerRequest } from "./control-channel.ts";
30
+ import { takeMatchingAcceptedSteer, unconsumedSteerReason } from "./steering.ts";
30
31
 
31
32
  export interface ChildEventContext {
32
33
  runId: string;
@@ -84,6 +85,8 @@ export interface RunChildSessionInput {
84
85
  registerTimeout?: (interrupt: (() => void) | undefined) => void;
85
86
  registerStop?: (stop: (() => void) | undefined) => void;
86
87
  registerSteer?: (steer: StepSteerHandler | undefined) => void;
88
+ /** Consumption (or unconsumed settlement) after the child accepted a steer or follow-up. */
89
+ onSteerOutcome?: (request: SteerRequest, delivery: SteerDelivery) => void;
87
90
  /** Receives the sink the child's watchdog hook reports status through; the launch's `watchdogStatus` must forward to it. */
88
91
  registerWatchdogStatus?: (sink: ((event: ChildWatchdogStatusEvent) => void) | undefined) => void;
89
92
  timeoutMessage?: string;
@@ -96,16 +99,6 @@ export interface RunChildSessionInput {
96
99
  modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>;
97
100
  modelResponseAliases?: Record<string, string[]>;
98
101
  mutationTools?: readonly string[];
99
- /** Internal guarded continuation handoff; never part of persisted results. */
100
- readonlyContinuation?: { source: ChildSession; expected: SettledReadonlyEvidence; modelId: string };
101
- collectReadonlyEvidence?: boolean;
102
- canContinue?: () => boolean;
103
- }
104
-
105
- const settledChildren = new WeakMap<RunChildSessionResult, ChildSession>();
106
- export function getSettledReadonlyChild(result: RunChildSessionResult): ChildSession | undefined {
107
- const child = settledChildren.get(result);
108
- return child && getReadonlySessionEvidence(child) ? child : undefined;
109
102
  }
110
103
 
111
104
  export interface RunChildSessionResult {
@@ -115,6 +108,7 @@ export interface RunChildSessionResult {
115
108
  toolCount: number;
116
109
  durationMs: number;
117
110
  model?: string;
111
+ nativeMachine?: { provider: "herdr"; machineId: string; initialGit?: import("../../shared/types.ts").HerdrRemoteGitStatus; finalGit?: import("../../shared/types.ts").HerdrRemoteGitStatus };
118
112
  error?: string;
119
113
  finalOutput: string;
120
114
  outputState: SubagentOutputState;
@@ -130,12 +124,12 @@ export interface RunChildSessionResult {
130
124
  currentToolArgs?: string;
131
125
  currentPath?: string;
132
126
  afterCompactionSettlement?: boolean;
127
+ abortRecoveryDiagnostic?: string;
133
128
  /** Set by the runner while it finalizes the attempt. */
134
129
  toolBudget?: ToolBudgetState;
135
130
  toolBudgetBlocked?: boolean;
136
131
  structuredOutput?: unknown;
137
132
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
138
- abortRecoveryDiagnostic?: string;
139
133
  effects?: EffectsProjection;
140
134
  }
141
135
 
@@ -187,12 +181,14 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
187
181
  let currentPath: string | undefined;
188
182
  let toolCount = 0;
189
183
  let session: ChildSession | undefined;
184
+ const acceptedSteers: Array<{ request: SteerRequest; text: string }> = [];
190
185
  let unsubscribe: (() => void) | undefined;
191
186
  let settled = false;
192
187
  let promptSettled = false;
193
188
  let forcedTermination = false;
194
189
  let cleanTerminalAssistantStopReceived = false;
195
190
  let agentSettledReceived = false;
191
+ let queuedDrainHold = false;
196
192
  let compactionStartedReceived = false;
197
193
  let afterCompactionSettlement = false;
198
194
  let finalDrainTimer: NodeJS.Timeout | undefined;
@@ -311,14 +307,28 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
311
307
  }
312
308
  // If the child emits its terminal event but its run never settles (a hook
313
309
  // is stuck), abort it after a short grace period and then finish without it.
310
+ const observeQueuedDrainHold = (): boolean => {
311
+ if (childSessionHasQueuedMessages(session)) queuedDrainHold = true;
312
+ return queuedDrainHold;
313
+ };
314
314
  function startFinalDrain(): void {
315
315
  if (childWatchdogIsActive(childWatchdogState)) {
316
316
  armWatchdogTail();
317
317
  return;
318
318
  }
319
+ if (promptSettled || finalDrainTimer || settled) return;
320
+ if (observeQueuedDrainHold()) return;
321
+ armFinalDrainTimer();
322
+ }
323
+ function armFinalDrainTimer(): void {
319
324
  if (promptSettled || finalDrainTimer || settled) return;
320
325
  finalDrainTimer = setTimeout(() => {
321
326
  if (settled || promptSettled) return;
327
+ if (input.launch.capture.finalDrainHeld() || observeQueuedDrainHold()) {
328
+ finalDrainTimer = undefined;
329
+ armFinalDrainTimer();
330
+ return;
331
+ }
322
332
  forcedTermination = true;
323
333
  if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !error && !assistantError) {
324
334
  error = `Subagent session did not settle within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Aborting it.`;
@@ -334,6 +344,8 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
334
344
  }
335
345
  const applyChildLifecycle = (action: ChildLifecycleAction): void => {
336
346
  if (action === "cancel-drain") {
347
+ cleanTerminalAssistantStopReceived = false;
348
+ agentSettledReceived = false;
337
349
  clearFinalDrainTimers();
338
350
  clearWatchdogTailTimer();
339
351
  return;
@@ -401,6 +413,9 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
401
413
  compactionStartedReceived = false;
402
414
  afterCompactionSettlement = false;
403
415
  }
416
+ if (event.type === "turn_start" || event.type === "agent_start" || event.type === "auto_retry_start") {
417
+ queuedDrainHold = false;
418
+ }
404
419
  if (event.type === "agent_start" || event.type === "auto_retry_start") {
405
420
  compactionStartedReceived = false;
406
421
  afterCompactionSettlement = false;
@@ -470,6 +485,16 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
470
485
  messages.push(event.message);
471
486
  const text = extractTextFromContent(event.message.content);
472
487
  if (text) writeOutputText(text);
488
+ if (event.type === "message_end" && event.message.role === "user" && text) {
489
+ const matched = takeMatchingAcceptedSteer(acceptedSteers, text);
490
+ if (matched) {
491
+ input.onSteerOutcome?.(matched.request, {
492
+ state: "delivered",
493
+ deliveryStatus: "delivered",
494
+ message: "Child consumed the steering input.",
495
+ });
496
+ }
497
+ }
473
498
 
474
499
  if (input.childWatchdog && event.type === "message_end") {
475
500
  const next = applyChildWatchdogMessage(childWatchdogState, event.message);
@@ -529,14 +554,33 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
529
554
  };
530
555
 
531
556
  /** The child run ended (or was forced to end); fold in the outcome once the child's shutdown work is done. */
557
+ const failUnconsumedSteers = (): void => {
558
+ for (const entry of acceptedSteers.splice(0)) {
559
+ input.onSteerOutcome?.(entry.request, { state: "failed", message: unconsumedSteerReason(entry.request.mode) });
560
+ }
561
+ };
532
562
  const settle = (promptError: unknown, forced = false): void => {
533
563
  if (settled) return;
534
564
  settled = true;
565
+ failUnconsumedSteers();
535
566
  const closed = finish();
536
567
  const finalOutput = getFinalOutput(messages);
537
568
  let finalError = error ?? assistantError;
538
- if (!finalError && promptError !== undefined) {
539
- finalError = promptError instanceof Error ? promptError.message : String(promptError);
569
+ const promptErrorMessage = promptError === undefined ? undefined : promptError instanceof Error ? promptError.message : String(promptError);
570
+ if (!finalError && promptErrorMessage !== undefined) {
571
+ finalError = promptErrorMessage;
572
+ }
573
+ // A child launched without the ambient extensions resolves a provider
574
+ // extension's model as "not found". Annotate only a creation/prompt failure
575
+ // that produced no turn; keep the core error and add the rule and the
576
+ // remedies that load the extension for this child.
577
+ if (promptErrorMessage !== undefined
578
+ && finalError === promptErrorMessage
579
+ && isChildModelResolutionFailure(promptErrorMessage)
580
+ && messages.length === 0
581
+ && usage.turns === 0
582
+ && !input.launch.session.ambientExtensions) {
583
+ finalError = `${promptErrorMessage}\n\n${formatChildModelResolutionDiagnostic({ agent: input.launch.config.agent, model: input.launch.session.model, host: "runner", capabilityCeiling: input.launch.toolPlan.capabilityCeiling })}`;
540
584
  }
541
585
  const forcedDrainAfterFinalSuccess = (forced || forcedTermination) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
542
586
  const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(messages);
@@ -556,6 +600,7 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
556
600
  toolCount,
557
601
  durationMs: Date.now() - startedAt,
558
602
  model,
603
+ nativeMachine: session?.machineEvidence ? { provider: "herdr", machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) } : undefined,
559
604
  error: stopped ? stopMessage() : timedOut ? (error ?? timeoutMessage()) : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? undefined : finalError,
560
605
  finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage() : error ?? timeoutMessage()) : finalOutput,
561
606
  outputState: finalOutput.trim() ? "present" : "absent",
@@ -572,7 +617,6 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
572
617
  currentPath,
573
618
  afterCompactionSettlement: afterCompactionSettlement || undefined,
574
619
  });
575
- if (session && !forced && !forcedTermination && !interrupted && !timedOut && !stopped && getReadonlySessionEvidence(session)) settledChildren.set(result, session);
576
620
  resolve(result);
577
621
  });
578
622
  };
@@ -594,42 +638,46 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
594
638
 
595
639
  void (async () => {
596
640
  try {
597
- const continuation = input.readonlyContinuation;
598
- const checkContinuation = () => {
599
- if (!continuation) return;
600
- if (interrupted || timedOut || stopped || input.canContinue?.() !== true
601
- || (input.runDeadlineAt !== undefined && Date.now() >= input.runDeadlineAt)
602
- || getReadonlySessionEvidence(continuation.source) !== continuation.expected
603
- || continuation.source.detached || continuation.source.shutDown) throw new Error("Read-only continuation handoff vetoed");
604
- };
605
- checkContinuation();
606
641
  const createInput = createReportedChildSessionInput(input.launch, input.transcriptWriter);
607
- if (input.collectReadonlyEvidence || continuation) requestReadonlySessionEvidence(createInput, continuation?.expected);
608
642
  const created = await input.factory.create(createInput);
609
643
  if (settled) {
610
644
  void created.dispose();
611
645
  return;
612
646
  }
613
647
  session = created;
614
- checkContinuation();
615
- if (continuation && (created.modelId !== continuation.modelId || input.launch.capture.completionIntentContext?.()?.model?.api !== continuation.expected.api)) throw new Error("Read-only continuation model changed");
648
+ const steer = created.steer.bind(created);
649
+ const followUp = created.followUp.bind(created);
650
+ created.steer = async (text) => {
651
+ if (cleanTerminalAssistantStopReceived || agentSettledReceived) queuedDrainHold = true;
652
+ return steer(text);
653
+ };
654
+ created.followUp = async (text) => {
655
+ if (cleanTerminalAssistantStopReceived || agentSettledReceived) queuedDrainHold = true;
656
+ return followUp(text);
657
+ };
616
658
  unsubscribe = created.subscribe(processEvent);
617
659
  input.registerWatchdogStatus?.((event) => processEvent(event as unknown as ChildSessionEvent));
618
660
  input.registerSteer?.(async (request) => {
619
661
  const text = formatSteerMessage(request);
620
662
  const followUp = request.mode === "follow_up";
663
+ const accepted = { request, text };
664
+ acceptedSteers.push(accepted);
665
+ const queued: SteerDelivery = {
666
+ state: "queued",
667
+ deliveryStatus: "queued",
668
+ message: followUp ? "Pi queued the follow-up input." : "Pi accepted the steering input.",
669
+ };
621
670
  try {
622
671
  if (followUp) await created.followUp(text);
623
672
  else await created.steer(text);
624
673
  } catch (steerError) {
674
+ const index = acceptedSteers.indexOf(accepted);
675
+ if (index >= 0) acceptedSteers.splice(index, 1);
625
676
  return { state: "failed", message: steerError instanceof Error ? steerError.message : String(steerError) };
626
677
  }
627
- return followUp
628
- ? { state: "queued", deliveryStatus: "queued", message: "Pi queued the follow-up input." }
629
- : { state: "delivered", deliveryStatus: "delivered", message: "Pi accepted the steering input." };
678
+ return queued;
630
679
  });
631
680
  if (interrupted || timedOut || stopped) abortChild();
632
- checkContinuation();
633
681
  await created.prompt(input.prompt);
634
682
  promptSettled = true;
635
683
  settle(undefined);