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.
- package/CHANGELOG.md +138 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +43 -15
- package/docs/configuration.md +67 -23
- package/docs/extension-api.md +38 -19
- package/docs/missions.md +10 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +22 -12
- package/docs/standalone-background.md +59 -0
- package/docs/tool-reference.md +33 -20
- package/docs/watchdog.md +39 -10
- package/docs/workflows.md +37 -13
- package/index.ts +5 -2
- package/inspector-runner.mjs +2 -2
- package/package.json +4 -2
- package/prompts/parallel-review.md +1 -1
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +32 -0
- package/skills/pi-subagents/SKILL.md +32 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +11 -9
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +190 -71
- package/src/agents/builtin-names.ts +1 -0
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +94 -59
- package/src/api/required-child-extensions.ts +6 -0
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +66 -4
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +62 -39
- package/src/extension/public-execution.ts +7 -5
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +81 -81
- package/src/extension/tool-description.ts +30 -82
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +44 -31
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +348 -176
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +17 -12
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +23 -10
- package/src/runs/background/binary-bootstrap.ts +38 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +105 -7
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +81 -33
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +12 -33
- package/src/runs/background/runner-child-launch.ts +6 -1
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +51 -18
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +441 -304
- package/src/runs/background/subagent-wait.ts +176 -28
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +134 -248
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +374 -178
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +38 -11
- package/src/runs/shared/async-status-projection.ts +127 -33
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +28 -5
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +8 -1
- package/src/runs/shared/child-session.ts +127 -52
- package/src/runs/shared/child-tool-plan.ts +142 -11
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +45 -15
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +68 -8
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +74 -30
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -7
- package/src/slash/subagents-admin.ts +24 -13
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +168 -37
- package/src/watchdog/child-status.ts +28 -28
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +21 -1
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +10 -2
- package/src/watchdog/register-main.ts +39 -35
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +123 -74
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +100 -27
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +3 -0
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +5 -3
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +279 -10
- package/src/workflows/workflow-checklist.ts +2 -2
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/runner-server-preload.mjs +0 -13
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
- /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
|
|
641
|
+
const watchdogConcerns: string[] = [];
|
|
642
|
+
const collectWatchdogFindings = (owner: string, progress: ChildWatchdogProgress | undefined) => {
|
|
547
643
|
for (const warning of progress?.warnings ?? []) {
|
|
548
|
-
if (warning.
|
|
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
|
-
|
|
553
|
-
for (const child of result.results ?? [])
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
539
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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
|
|
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);
|