pi-subagents 0.59.0 → 0.61.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 +69 -0
- package/docs/agents.md +2 -2
- package/docs/configuration.md +9 -5
- package/docs/extension-api.md +14 -7
- package/docs/models.md +1 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +15 -3
- package/docs/workflows.md +15 -14
- package/install.mjs +2 -1
- package/package.json +1 -1
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +89 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +30 -234
- package/skills/pi-subagents/references/execution-controls.md +79 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +35 -28
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +256 -88
- package/src/agents/agents.ts +527 -221
- package/src/api/background-work.ts +7 -2
- package/src/api/external-runs.ts +67 -4
- package/src/api/preflight.ts +13 -8
- package/src/api/shared-types.ts +3 -0
- package/src/extension/index.ts +7 -4
- package/src/extension/public-execution.ts +48 -4
- package/src/extension/rpc.ts +62 -4
- package/src/extension/schemas.ts +12 -7
- package/src/extension/tool-description.ts +16 -16
- package/src/runs/background/async-execution.ts +53 -37
- package/src/runs/background/async-job-tracker.ts +65 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-status.ts +104 -11
- package/src/runs/background/auto-drain.ts +1 -1
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +1 -1
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/run-status.ts +15 -4
- package/src/runs/background/subagent-runner.ts +6 -3
- package/src/runs/background/subagent-wait.ts +30 -23
- package/src/runs/background/wait-completions.ts +4 -1
- package/src/runs/background/wait-tool.ts +24 -18
- package/src/runs/foreground/execution.ts +72 -4
- package/src/runs/foreground/subagent-executor.ts +276 -59
- package/src/runs/shared/acceptance.ts +43 -18
- package/src/runs/shared/async-status-projection.ts +138 -4
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/host-step-status.ts +1 -0
- package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/src/runs/shared/model-fallback.ts +61 -17
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +47 -1
- package/src/runs/shared/single-output.ts +45 -18
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/workflow-graph.ts +16 -0
- package/src/shared/types.ts +74 -4
- package/src/shared/workflow-child-permit.ts +91 -0
- package/src/slash/prompt-template-bridge.ts +37 -1
- package/src/slash/slash-commands.ts +18 -26
- package/src/tui/fleet-status.ts +11 -3
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +673 -145
- package/src/watchdog/change-signature.ts +40 -1
- package/src/workflows/host-command.ts +6 -1
- package/src/workflows/scripted-workflow.ts +206 -6
- package/src/workflows/workflow-child-summary.ts +1 -1
- package/src/workflows/workflow-receipt.ts +41 -4
- package/src/workflows/workflow-resources.ts +150 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
|
+
import { previewDisplayText } from "../../shared/display-text.ts";
|
|
4
5
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
|
-
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNodeV1, type HostStepState, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorkflowPreflightV1 } from "../../shared/types.ts";
|
|
6
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNodeV1, type HostStepState, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TimeoutRecoveryProjection, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorkflowPreflightV1, type WorkflowGraphSnapshot } from "../../shared/types.ts";
|
|
6
7
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
|
|
7
8
|
import { readStatus } from "../../shared/utils.ts";
|
|
8
9
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
@@ -21,6 +22,8 @@ import { assertWorkflowGraphHostSteps, hostStepReportName, hostStepVerdictLabel,
|
|
|
21
22
|
import { projectAsyncWorkflowRows } from "../shared/async-status-projection.ts";
|
|
22
23
|
import { validateAsyncStatusLaneMetadata } from "../shared/lane-metadata.ts";
|
|
23
24
|
import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
|
|
25
|
+
import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
|
|
26
|
+
import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
|
|
24
27
|
|
|
25
28
|
interface AsyncRunStepSummary {
|
|
26
29
|
index: number;
|
|
@@ -75,6 +78,7 @@ interface AsyncRunStepSummary {
|
|
|
75
78
|
review?: AsyncJobStep["review"];
|
|
76
79
|
effects?: AsyncJobStep["effects"];
|
|
77
80
|
processTerminal?: AsyncJobStep["processTerminal"];
|
|
81
|
+
timeoutRecovery?: TimeoutRecoveryProjection;
|
|
78
82
|
launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
|
|
79
83
|
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
|
|
80
84
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
@@ -116,6 +120,7 @@ export interface AsyncRunSummary {
|
|
|
116
120
|
pendingAppends?: number;
|
|
117
121
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
118
122
|
hostSteps?: HostStepNodeV1[];
|
|
123
|
+
workflowGraph?: AsyncStatus["workflowGraph"];
|
|
119
124
|
steps: AsyncRunStepSummary[];
|
|
120
125
|
sessionDir?: string;
|
|
121
126
|
outputFile?: string;
|
|
@@ -161,6 +166,40 @@ function getErrorMessage(error: unknown): string {
|
|
|
161
166
|
return error instanceof Error ? error.message : String(error);
|
|
162
167
|
}
|
|
163
168
|
|
|
169
|
+
function isAsyncStatusIsolationError(asyncDir: string, error: unknown): boolean {
|
|
170
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
171
|
+
const message = getErrorMessage(error);
|
|
172
|
+
return /^(?:Failed to (?:inspect|read|parse|validate) async status file|Invalid async status file) '/.test(message)
|
|
173
|
+
|| message.startsWith(`Invalid async status '${statusPath}'`)
|
|
174
|
+
|| message.startsWith(`Invalid host step '${statusPath}`)
|
|
175
|
+
|| /^(workflowChildren|Invalid workflowChildren)/.test(message);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isolateCorruptActiveRun(asyncDir: string, runId: string, error: unknown, now?: () => number): void {
|
|
179
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
180
|
+
const processTerminal = readProcessTerminal(asyncDir, { runId });
|
|
181
|
+
let markerAge: number | undefined;
|
|
182
|
+
try {
|
|
183
|
+
markerAge = activeRunMarkerAgeMs(asyncDir, now?.());
|
|
184
|
+
} catch (markerError) {
|
|
185
|
+
console.error(`Failed to inspect corrupt async active-run marker for '${runId}':`, markerError);
|
|
186
|
+
}
|
|
187
|
+
const markerCanBeReleased = processTerminal?.state === "observed"
|
|
188
|
+
|| (markerAge !== undefined && markerAge > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS);
|
|
189
|
+
let markerAction = "active marker retained because runner liveness is unknown";
|
|
190
|
+
if (markerCanBeReleased) {
|
|
191
|
+
try {
|
|
192
|
+
releaseActiveRunIndex(asyncDir);
|
|
193
|
+
markerAction = processTerminal?.state === "observed"
|
|
194
|
+
? "active marker released after observed process-terminal proof"
|
|
195
|
+
: "stale active marker released";
|
|
196
|
+
} catch (releaseError) {
|
|
197
|
+
markerAction = `failed to release active marker: ${getErrorMessage(releaseError)}`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
console.error(`[pi-subagents] Skipping corrupt active async run '${runId}' at '${statusPath}': ${getErrorMessage(error)}; ${markerAction}.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
164
203
|
function isNotFoundError(error: unknown): boolean {
|
|
165
204
|
return typeof error === "object"
|
|
166
205
|
&& error !== null
|
|
@@ -245,14 +284,16 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
|
|
|
245
284
|
}
|
|
246
285
|
|
|
247
286
|
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = [], nestedRoute?: NestedRoute): AsyncRunSummary {
|
|
248
|
-
|
|
287
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
288
|
+
validateAsyncStatusLaneMetadata(status, `Invalid async status '${statusPath}'`);
|
|
249
289
|
const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
|
|
250
|
-
if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error(`Invalid async status '${
|
|
251
|
-
assertWorkflowGraphHostSteps(status.workflowGraph,
|
|
290
|
+
if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error(`Invalid async status '${statusPath}': workflowChildren.workflowRunId does not match.`);
|
|
291
|
+
assertWorkflowGraphHostSteps(status.workflowGraph, statusPath, status.runId);
|
|
252
292
|
const hostSteps = validHostStepNodes(status.workflowGraph);
|
|
253
293
|
if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
|
|
254
|
-
throw new Error(`Invalid async status '${
|
|
294
|
+
throw new Error(`Invalid async status '${statusPath}': sessionId must be a string.`);
|
|
255
295
|
}
|
|
296
|
+
if (status.outputFile !== undefined && typeof status.outputFile !== "string") throw new Error(`Invalid async status '${statusPath}': outputFile must be a string.`);
|
|
256
297
|
const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
|
|
257
298
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
|
|
258
299
|
?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
|
|
@@ -282,6 +323,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
282
323
|
const summarizedSteps = steps.map((step, index) => {
|
|
283
324
|
const stepActivityState = step.activityState;
|
|
284
325
|
const stepLastActivityAt = step.lastActivityAt;
|
|
326
|
+
const timeoutRecovery = projectTimeoutRecovery(step.timeoutRecovery);
|
|
285
327
|
return {
|
|
286
328
|
index,
|
|
287
329
|
childId: asyncStatusChildIdentity(step, index),
|
|
@@ -341,6 +383,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
341
383
|
...(step.effects ? { effects: step.effects } : {}),
|
|
342
384
|
...(step.watchdog ? { watchdog: step.watchdog } : {}),
|
|
343
385
|
...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
|
|
386
|
+
...(timeoutRecovery ? { timeoutRecovery } : {}),
|
|
344
387
|
...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
|
|
345
388
|
...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
|
|
346
389
|
...(step.children?.length ? { children: step.children } : {}),
|
|
@@ -381,6 +424,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
381
424
|
...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
|
|
382
425
|
...(parallelGroups.length ? { parallelGroups } : {}),
|
|
383
426
|
...(hostSteps.length ? { hostSteps } : {}),
|
|
427
|
+
...(status.mode === "workflow" && status.workflowGraph ? { workflowGraph: status.workflowGraph } : {}),
|
|
384
428
|
steps: summarizedSteps,
|
|
385
429
|
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
386
430
|
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
@@ -491,10 +535,17 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
491
535
|
};
|
|
492
536
|
for (const entry of entries) {
|
|
493
537
|
const asyncDir = path.join(asyncDirRoot, entry);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
538
|
+
let status: (AsyncStatus & { cwd?: string }) | null;
|
|
539
|
+
try {
|
|
540
|
+
const reconciliation = options.reconcile === false
|
|
541
|
+
? undefined
|
|
542
|
+
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
543
|
+
status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
544
|
+
} catch (error) {
|
|
545
|
+
if (!activeEntries.has(entry) || !isAsyncStatusIsolationError(asyncDir, error)) throw error;
|
|
546
|
+
isolateCorruptActiveRun(asyncDir, entry, error, options.now);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
498
549
|
if (!status) {
|
|
499
550
|
if (activeEntries.has(entry)) updateActiveRunIndex(asyncDir, "failed");
|
|
500
551
|
continue;
|
|
@@ -520,7 +571,14 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
520
571
|
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
521
572
|
}
|
|
522
573
|
}
|
|
523
|
-
|
|
574
|
+
let summary: AsyncRunSummary;
|
|
575
|
+
try {
|
|
576
|
+
summary = statusToSummary(asyncDir, status, nestedWarnings, nestedRoute);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (!activeEntries.has(entry) || !isAsyncStatusIsolationError(asyncDir, error)) throw error;
|
|
579
|
+
isolateCorruptActiveRun(asyncDir, entry, error, options.now);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
524
582
|
runs.push(summary);
|
|
525
583
|
}
|
|
526
584
|
|
|
@@ -575,12 +633,41 @@ function formatHostStepLine(row: ReturnType<typeof projectAsyncWorkflowRows>[num
|
|
|
575
633
|
return `host ${row.kind}: ${row.name} | ${state}${details.length ? ` | ${details.join(" | ")}` : ""}`;
|
|
576
634
|
}
|
|
577
635
|
|
|
636
|
+
function workflowStageStateLabel(status: WorkflowGraphSnapshot["nodes"][number]["status"]): string {
|
|
637
|
+
switch (status) {
|
|
638
|
+
case "completed":
|
|
639
|
+
return "complete";
|
|
640
|
+
case "detached":
|
|
641
|
+
return "paused";
|
|
642
|
+
default:
|
|
643
|
+
return status;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export function formatWorkflowStageLine(node: WorkflowGraphSnapshot["nodes"][number], index: number, total: number): string {
|
|
648
|
+
const state = workflowStageStateLabel(node.status);
|
|
649
|
+
const id = previewDisplayText(node.id, 160);
|
|
650
|
+
const label = node.label ? previewDisplayText(node.label, 160) : "";
|
|
651
|
+
const display = label && label !== id ? ` | ${label}` : "";
|
|
652
|
+
const agent = node.agent ? ` | ${previewDisplayText(node.agent, 80)}` : "";
|
|
653
|
+
const error = node.error ? ` | ${previewDisplayText(node.error, 240)}` : "";
|
|
654
|
+
return `stage ${index + 1}/${total}: ${id}${display}${agent} | ${state}${error}`;
|
|
655
|
+
}
|
|
656
|
+
|
|
578
657
|
export function formatAsyncRunOutputPath(run: Pick<AsyncRunSummary, "asyncDir" | "outputFile">): string | undefined {
|
|
579
658
|
if (!run.outputFile) return undefined;
|
|
580
659
|
return path.isAbsolute(run.outputFile) ? run.outputFile : path.join(run.asyncDir, run.outputFile);
|
|
581
660
|
}
|
|
582
661
|
|
|
583
|
-
export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps">): string {
|
|
662
|
+
export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps"> & { workflowGraph?: WorkflowGraphSnapshot }): string {
|
|
663
|
+
const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
|
|
664
|
+
if (graphStages.length > 0) {
|
|
665
|
+
const currentNode = graphStages.find((node) => node.id === run.workflowGraph?.currentNodeId);
|
|
666
|
+
if (currentNode && currentNode.status !== "completed") return `stage ${graphStages.indexOf(currentNode) + 1}/${graphStages.length}`;
|
|
667
|
+
const activeNode = graphStages.find((node) => node.status === "running") ?? graphStages.find((node) => node.status !== "completed");
|
|
668
|
+
if (activeNode) return `stage ${graphStages.indexOf(activeNode) + 1}/${graphStages.length}`;
|
|
669
|
+
return `stage ${graphStages.length}/${graphStages.length}`;
|
|
670
|
+
}
|
|
584
671
|
const stepCount = run.steps.length || 1;
|
|
585
672
|
const chainStepCount = run.chainStepCount ?? stepCount;
|
|
586
673
|
const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, chainStepCount);
|
|
@@ -622,8 +709,14 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
|
|
|
622
709
|
if (preflightWarning) lines.push(preflightWarning);
|
|
623
710
|
for (const step of run.steps) {
|
|
624
711
|
lines.push(` ${formatStepLine(step)}`);
|
|
712
|
+
lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));
|
|
625
713
|
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
|
|
626
714
|
}
|
|
715
|
+
const loadedWorkflowKeys = new Set(run.steps.flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
|
|
716
|
+
const graphStages = run.mode === "workflow" ? workflowGraphStageNodes(run.workflowGraph) : [];
|
|
717
|
+
for (const [index, node] of graphStages.entries()) {
|
|
718
|
+
if (!loadedWorkflowKeys.has(node.id)) lines.push(` ${formatWorkflowStageLine(node, index, graphStages.length)}`);
|
|
719
|
+
}
|
|
627
720
|
for (const row of projectAsyncWorkflowRows([], run.hostSteps)) {
|
|
628
721
|
const line = formatHostStepLine(row);
|
|
629
722
|
if (line) lines.push(` ${line}`);
|
|
@@ -62,7 +62,7 @@ export async function drainOutstandingWork(deps: AutoDrainDeps): Promise<void> {
|
|
|
62
62
|
},
|
|
63
63
|
);
|
|
64
64
|
if (waitResult.isError) {
|
|
65
|
-
throw new Error(`Auto-drain failed for session '${sessionId}': ${resultText(waitResult) || "
|
|
65
|
+
throw new Error(`Auto-drain failed for session '${sessionId}': ${resultText(waitResult) || "bg_wait returned an error without details"}.`);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-OS control channel for async subagent runs.
|
|
3
3
|
*
|
|
4
|
-
* Background runs
|
|
5
|
-
*
|
|
4
|
+
* Background runs use child processes. Unix detaches them from the parent process.
|
|
5
|
+
* The original control path delivered an interrupt with
|
|
6
|
+
* `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot
|
|
6
7
|
* deliver those signals cross-process via `process.kill` and throws `ENOSYS`,
|
|
7
8
|
* which left async runs uninterruptible (no stop, no live steer) on Windows.
|
|
8
9
|
*
|
|
@@ -330,7 +330,7 @@ function formatDetachedForegroundFleetLines(runs: ForegroundRun[]): string[] {
|
|
|
330
330
|
const childSummary = detachedChildren.map((child) => `${fleetChildDisplayName(child)} #${child.index}`).join(", ");
|
|
331
331
|
lines.push(`- ${run.runId} | detached | ${run.mode}${childSummary ? ` | ${childSummary}` : ""}`);
|
|
332
332
|
lines.push(` status: subagent({ action: "status", id: "${run.runId}" })`);
|
|
333
|
-
lines.push(` recovery: reply to the supervisor request first, then wait with
|
|
333
|
+
lines.push(` recovery: reply to the supervisor request first, then wait with bg_wait({ id: "${run.runId}" }); do not resume or launch a replacement while any child remains detached.`);
|
|
334
334
|
}
|
|
335
335
|
return lines;
|
|
336
336
|
}
|
|
@@ -426,7 +426,7 @@ export function createResultWatcher(
|
|
|
426
426
|
if (observerSucceeded) removeMissionObserverIndex(resultsDir, runId);
|
|
427
427
|
const epoch = deliveryEpoch;
|
|
428
428
|
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
429
|
-
// Recorded before dedupe and before the unlink below so
|
|
429
|
+
// Recorded before dedupe and before the unlink below so bg_wait can
|
|
430
430
|
// use the in-memory record or its bounded durable replay after cleanup.
|
|
431
431
|
recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
|
|
432
432
|
resultsDir,
|
|
@@ -12,7 +12,7 @@ function isIntercomDetached(run: AsyncRunSummary): boolean {
|
|
|
12
12
|
|
|
13
13
|
function formatIntercomDetachGuidance(run: AsyncRunSummary): string | undefined {
|
|
14
14
|
if (!isIntercomDetached(run)) return undefined;
|
|
15
|
-
return `Run "${run.id}" detached for intercom coordination. Reply to the supervisor request first, then wait with
|
|
15
|
+
return `Run "${run.id}" detached for intercom coordination. Reply to the supervisor request first, then wait with bg_wait({ id: "${run.id}" }). Use subagent({ action: "status", id: "${run.id}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefined {
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
4
|
import { safeTerminalText } from "../../shared/display-text.ts";
|
|
5
|
-
import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
|
|
5
|
+
import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, formatWorkflowStageLine, listAsyncRuns } from "./async-status.ts";
|
|
6
6
|
import { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
|
|
7
7
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
8
8
|
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
@@ -25,7 +25,9 @@ import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts"
|
|
|
25
25
|
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
26
26
|
import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary } from "../../workflows/workflow-preflight.ts";
|
|
27
27
|
import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
28
|
+
import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
|
|
28
29
|
import { getExternalJobProvider } from "../../api/external-job-provider.ts";
|
|
30
|
+
import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
|
|
29
31
|
|
|
30
32
|
interface RunStatusParams {
|
|
31
33
|
action?: string;
|
|
@@ -126,7 +128,7 @@ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent
|
|
|
126
128
|
const resumableWorkflowChildren = workflowChildren.filter(({ child }) => !(child.status === "paused" && child.activityState === "needs_attention"));
|
|
127
129
|
if (workflowChildren.length > 0) {
|
|
128
130
|
return [
|
|
129
|
-
...supervisorDetachedWorkflowChildren.map(({ child }) => `Recovery workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: reply to the supervisor request first, then wait with
|
|
131
|
+
...supervisorDetachedWorkflowChildren.map(({ child }) => `Recovery workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: reply to the supervisor request first, then wait with bg_wait({ id: "${child.runId}" }). Use subagent({ action: "status", id: "${child.runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.`),
|
|
130
132
|
...resumableWorkflowChildren.map(({ child }) => `Revive workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: subagent({ action: "resume", id: "${child.runId}", message: "..." })`),
|
|
131
133
|
].join("\n");
|
|
132
134
|
}
|
|
@@ -226,7 +228,7 @@ function formatRememberedForegroundStatus(run: ForegroundResumeRun): string {
|
|
|
226
228
|
const detached = run.children.some((child) => child.status === "detached");
|
|
227
229
|
const resumable = run.children.find((child) => hasExistingSessionFile(child.sessionFile));
|
|
228
230
|
if (detached) {
|
|
229
|
-
lines.push(`Recovery: reply to the supervisor request first, then wait with
|
|
231
|
+
lines.push(`Recovery: reply to the supervisor request first, then wait with bg_wait({ id: "${run.runId}" }); do not resume or launch a replacement while any child remains detached.`);
|
|
230
232
|
} else if (resumable) {
|
|
231
233
|
lines.push(run.children.length === 1
|
|
232
234
|
? `Revive: subagent({ action: "resume", id: "${run.runId}", message: "..." })`
|
|
@@ -481,6 +483,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
481
483
|
currentStep: status.currentStep,
|
|
482
484
|
chainStepCount: status.chainStepCount,
|
|
483
485
|
parallelGroups: status.parallelGroups,
|
|
486
|
+
workflowGraph: status.workflowGraph,
|
|
484
487
|
steps: (status.steps ?? []).map((step, index) => ({ index, agent: step.agent, status: step.status })),
|
|
485
488
|
});
|
|
486
489
|
const started = new Date(status.startedAt).toISOString();
|
|
@@ -548,6 +551,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
548
551
|
const display = runStatusStepDisplayName(step);
|
|
549
552
|
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
550
553
|
lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
|
|
554
|
+
lines.push(...formatTimeoutRecoveryLines(step.timeoutRecovery, " "));
|
|
551
555
|
if (step.runner?.type === "external-cli") {
|
|
552
556
|
const runner = normalizeExternalCliRunnerStatus(step.runner);
|
|
553
557
|
if (runner) {
|
|
@@ -593,6 +597,11 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
593
597
|
lines.push(" Steer: unavailable; external runners do not accept live messages.");
|
|
594
598
|
}
|
|
595
599
|
}
|
|
600
|
+
const loadedWorkflowKeys = new Set((status.steps ?? []).flatMap((step) => step.workflowKey ? [step.workflowKey] : []));
|
|
601
|
+
const graphStages = status.mode === "workflow" ? workflowGraphStageNodes(status.workflowGraph) : [];
|
|
602
|
+
for (const [index, node] of graphStages.entries()) {
|
|
603
|
+
if (!loadedWorkflowKeys.has(node.id)) lines.push(` ${formatWorkflowStageLine(node, index, graphStages.length)}`);
|
|
604
|
+
}
|
|
596
605
|
const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
597
606
|
const unattached = nestedChildren.filter((child) => !attached.has(child.id));
|
|
598
607
|
lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
|
|
@@ -632,7 +641,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
632
641
|
}
|
|
633
642
|
try {
|
|
634
643
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
635
|
-
const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; sessionName?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
|
|
644
|
+
const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; timeoutRecovery?: unknown; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; sessionName?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null; timeoutRecovery?: unknown }> };
|
|
636
645
|
if (params.view === "transcript") {
|
|
637
646
|
try {
|
|
638
647
|
return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
|
|
@@ -660,6 +669,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
660
669
|
const lines = [`Run: ${runId}`, data.toolCallId ? `Tool call: ${data.toolCallId}` : undefined, `State: ${status}`, `Result: ${resultPath}`].filter((line): line is string => Boolean(line));
|
|
661
670
|
if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
|
|
662
671
|
const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
|
|
672
|
+
lines.push(...formatTimeoutRecoveryLines(data.timeoutRecovery, " "));
|
|
673
|
+
for (const child of children) lines.push(...formatTimeoutRecoveryLines(child.timeoutRecovery, " "));
|
|
663
674
|
lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
|
|
664
675
|
if (data.summary) lines.push("", data.summary);
|
|
665
676
|
const workflowChildren = parseWorkflowChildSummary((data as unknown as Record<string, unknown>).workflowChildren);
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
|
|
76
76
|
Semaphore,
|
|
77
77
|
} from "../shared/parallel-utils.ts";
|
|
78
|
-
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
|
|
78
|
+
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
|
|
79
79
|
import { deriveChildSessionName } from "../../shared/child-session-name.ts";
|
|
80
80
|
import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
|
|
81
81
|
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
@@ -1640,6 +1640,7 @@ async function runSingleStepInner(
|
|
|
1640
1640
|
const writerProcesses: PiWriterProcessInstanceExitV1[] = [];
|
|
1641
1641
|
let writerAttemptCount = 0;
|
|
1642
1642
|
const attemptNotes: string[] = [];
|
|
1643
|
+
let finalRequiredOutputMissing: boolean | undefined;
|
|
1643
1644
|
const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
|
|
1644
1645
|
let finalResult: RunPiStreamingResult | undefined;
|
|
1645
1646
|
let finalOutputSnapshot: SingleOutputSnapshot | undefined;
|
|
@@ -1698,6 +1699,7 @@ async function runSingleStepInner(
|
|
|
1698
1699
|
: undefined;
|
|
1699
1700
|
const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
|
|
1700
1701
|
parentSessionId: step.parentSessionId,
|
|
1702
|
+
forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
|
|
1701
1703
|
baseArgs: ["--mode", "json", "-p"],
|
|
1702
1704
|
task: attemptTask,
|
|
1703
1705
|
taskDelivery: taskDeliveryOverride,
|
|
@@ -1929,6 +1931,7 @@ async function runSingleStepInner(
|
|
|
1929
1931
|
: effectiveStructuredOutput
|
|
1930
1932
|
? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
|
|
1931
1933
|
: undefined;
|
|
1934
|
+
finalRequiredOutputMissing = requiredOutput?.missing;
|
|
1932
1935
|
const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
|
|
1933
1936
|
const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
|
|
1934
1937
|
const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
|
|
@@ -2117,6 +2120,7 @@ async function runSingleStepInner(
|
|
|
2117
2120
|
? buildTimeoutRecoverySummary({
|
|
2118
2121
|
termination: "timed-out",
|
|
2119
2122
|
evidence: finalMutationEvidence,
|
|
2123
|
+
requiredOutputMissing: finalRequiredOutputMissing,
|
|
2120
2124
|
currentTool: finalResult?.currentTool,
|
|
2121
2125
|
currentToolArgs: finalResult?.currentToolArgs,
|
|
2122
2126
|
currentPath: finalResult?.currentPath,
|
|
@@ -2145,7 +2149,7 @@ async function runSingleStepInner(
|
|
|
2145
2149
|
report: structuredAcceptanceReport as import("../../shared/types.ts").AcceptanceReport | undefined,
|
|
2146
2150
|
reportError: structuredAcceptanceReportError,
|
|
2147
2151
|
fileOutput: childWrittenOutput !== undefined && step.outputPath
|
|
2148
|
-
? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only" }
|
|
2152
|
+
? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only", durable: resolvedOutput.savedPath !== undefined }
|
|
2149
2153
|
: undefined,
|
|
2150
2154
|
cwd: step.cwd ?? ctx.cwd,
|
|
2151
2155
|
signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
|
|
@@ -5343,7 +5347,6 @@ async function runSubagent(
|
|
|
5343
5347
|
statusPayload.error = `Step failed: ${failedStep.agent}`;
|
|
5344
5348
|
}
|
|
5345
5349
|
}
|
|
5346
|
-
writeStatusPayload();
|
|
5347
5350
|
try {
|
|
5348
5351
|
runPersistence.write(resultPath, {
|
|
5349
5352
|
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
2
|
+
* `bg_wait` tool: block the current turn until outstanding async runs
|
|
3
3
|
* or a named remembered detached foreground run finishes.
|
|
4
4
|
*
|
|
5
5
|
* Background subagent runs are detached. In an interactive session the parent
|
|
@@ -8,33 +8,33 @@
|
|
|
8
8
|
* cannot work at all non-interactively (`pi -p ...`), where the run is a single
|
|
9
9
|
* turn: once the turn ends there is nothing left to receive the notification.
|
|
10
10
|
*
|
|
11
|
-
* `
|
|
11
|
+
* `bg_wait` closes that gap. It keeps the turn alive until a tracked async
|
|
12
12
|
* run for this session reaches a terminal state (complete / failed / paused),
|
|
13
13
|
* the caller-supplied timeout elapses, or the turn is aborted. Because it awaits
|
|
14
14
|
* inside the turn, the completion the model was told to wait for is actually
|
|
15
15
|
* observed before the tool returns.
|
|
16
16
|
*
|
|
17
|
-
* By default `
|
|
17
|
+
* By default `bg_wait` returns as soon as ONE run finishes, so a fleet
|
|
18
18
|
* manager can use it in a rolling-replacement loop: launch N workers, wait for
|
|
19
|
-
* the next one to finish, spawn its replacement, then call `
|
|
19
|
+
* the next one to finish, spawn its replacement, then call `bg_wait`
|
|
20
20
|
* again — keeping N in flight instead of draining to zero between batches.
|
|
21
21
|
* Pass `all: true` to block until every tracked async run is terminal, or `id`
|
|
22
22
|
* to block on one specific async or remembered detached foreground run.
|
|
23
23
|
*
|
|
24
|
-
* `
|
|
24
|
+
* `bg_wait` also returns when a run needs attention — not just on
|
|
25
25
|
* completion. A child that goes idle or blocks for a decision surfaces
|
|
26
26
|
* `needs_attention` (the same signal Pi shows as a control notice and,
|
|
27
|
-
* interactively, wakes the parent with). Since `
|
|
27
|
+
* interactively, wakes the parent with). Since `bg_wait` is used exactly
|
|
28
28
|
* where there is no next turn to receive that notice, it must break on it too,
|
|
29
29
|
* or a stuck child would stall the loop until the timeout. Attention runs are
|
|
30
30
|
* reported so the caller can inspect / nudge / resume / interrupt them.
|
|
31
31
|
*
|
|
32
|
-
* Wake mechanism: when given Pi's event bus (`deps.events`), `
|
|
32
|
+
* Wake mechanism: when given Pi's event bus (`deps.events`), `bg_wait`
|
|
33
33
|
* subscribes to the subagent completion/control channels and wakes the instant
|
|
34
34
|
* any fires, rather than waiting out a fixed poll interval. A poll still runs
|
|
35
35
|
* on the interval as a reconciliation fallback (crashed runners, missed
|
|
36
36
|
* events), and the poll is the source of truth for what actually changed — the
|
|
37
|
-
* event only ends the sleep early. With no bus, `
|
|
37
|
+
* event only ends the sleep early. With no bus, `bg_wait` degrades to pure
|
|
38
38
|
* polling.
|
|
39
39
|
*/
|
|
40
40
|
|
|
@@ -65,6 +65,7 @@ import { formatDuration, shortenPath } from "../../shared/formatters.ts";
|
|
|
65
65
|
import { toAgentToolUsage } from "../../shared/utils.ts";
|
|
66
66
|
import { collectWaitCompletions } from "./wait-completions.ts";
|
|
67
67
|
import { formatResumeFirstFailedRunsNote } from "./resume-guidance.ts";
|
|
68
|
+
import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
|
|
68
69
|
export { WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV, WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, type ResolvedWaitToolConfig } from "./wait-config.ts";
|
|
69
70
|
|
|
70
71
|
/** States that mean a run is still in flight (not yet resolved). */
|
|
@@ -81,9 +82,8 @@ export interface SubagentWaitParams {
|
|
|
81
82
|
nonBlocking?: boolean;
|
|
82
83
|
/**
|
|
83
84
|
* When true, block until EVERY active run in this session (or matching `id`)
|
|
84
|
-
* is terminal. Default false: return
|
|
85
|
-
*
|
|
86
|
-
* targets a single run.
|
|
85
|
+
* is terminal. Default false: return when the first tracked run or provider
|
|
86
|
+
* item finishes or needs attention. Ignored when `id` targets a single run.
|
|
87
87
|
*/
|
|
88
88
|
all?: boolean;
|
|
89
89
|
/** Give up after this many milliseconds. Defaults to waitTool.defaultTimeoutMs, then 30 minutes. */
|
|
@@ -240,7 +240,7 @@ function foregroundChildrenNeedingAttention(run: ForegroundResumeRun, indices: S
|
|
|
240
240
|
function formatForegroundAttention(run: ForegroundResumeRun, children: ReturnType<typeof foregroundChildrenNeedingAttention>, elapsedMs: number): AgentToolResult<Details> {
|
|
241
241
|
const childList = children.map((child) => `${child.agent}${child.index !== undefined ? `#${child.index}` : ""}`).join(", ");
|
|
242
242
|
return result(
|
|
243
|
-
`Waited ${formatDuration(elapsedMs)} for remembered detached foreground run "${run.runId}"; attention required. ${children.length} child run(s) need attention: ${childList}. Reply to any pending supervisor request, then call
|
|
243
|
+
`Waited ${formatDuration(elapsedMs)} for remembered detached foreground run "${run.runId}"; attention required. ${children.length} child run(s) need attention: ${childList}. Reply to any pending supervisor request, then call bg_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
|
|
244
244
|
);
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -261,7 +261,7 @@ function backgroundWorkIdentity(item: RegisteredBackgroundWorkItem): string {
|
|
|
261
261
|
|
|
262
262
|
function backgroundWorkForSession(deps: SubagentWaitDeps, nowMs: number): BackgroundWorkSnapshot {
|
|
263
263
|
const sessionId = deps.state.currentSessionId;
|
|
264
|
-
if (!sessionId) throw new Error("
|
|
264
|
+
if (!sessionId) throw new Error("bg_wait requires an active session identity to scope background work safely.");
|
|
265
265
|
return deps.backgroundWork?.snapshot(sessionId, nowMs) ?? snapshotBackgroundWork(sessionId, nowMs);
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -349,6 +349,12 @@ function result(text: string, isError = false, completions?: WaitCompletion[]):
|
|
|
349
349
|
};
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
function formatCompletionRecovery(completions: WaitCompletion[] | undefined): string {
|
|
353
|
+
const lines = (completions ?? []).flatMap((completion) =>
|
|
354
|
+
(completion.results ?? []).flatMap((child) => formatTimeoutRecoveryLines(child.timeoutRecovery)));
|
|
355
|
+
return lines.length > 0 ? `\n${lines.join("\n")}` : "";
|
|
356
|
+
}
|
|
357
|
+
|
|
352
358
|
function windowElapsedResult(
|
|
353
359
|
text: string,
|
|
354
360
|
activeRunIds: string[],
|
|
@@ -369,7 +375,7 @@ function windowElapsedResult(
|
|
|
369
375
|
};
|
|
370
376
|
}
|
|
371
377
|
|
|
372
|
-
/** Build the live status shown while async work keeps
|
|
378
|
+
/** Build the live status shown while async work keeps bg_wait blocked. */
|
|
373
379
|
function asyncWaitUpdate(runs: AsyncRunSummary[], providerCount: number, elapsedMs: number): AgentToolResult<Details> {
|
|
374
380
|
const activity = runs.flatMap((run) => {
|
|
375
381
|
const activeSteps = run.steps.filter((step) => step.status === "pending" || step.status === "running");
|
|
@@ -517,7 +523,7 @@ async function waitForDetachedForegroundRun(
|
|
|
517
523
|
}
|
|
518
524
|
if (now() - startedAt >= timeoutMs) {
|
|
519
525
|
return windowElapsedResult(
|
|
520
|
-
`Wait window elapsed after ${formatDuration(timeoutMs)} with remembered foreground run "${run.runId}" still detached. Reply to any pending supervisor request, then call
|
|
526
|
+
`Wait window elapsed after ${formatDuration(timeoutMs)} with remembered foreground run "${run.runId}" still detached. Reply to any pending supervisor request, then call bg_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
|
|
521
527
|
[run.runId],
|
|
522
528
|
);
|
|
523
529
|
}
|
|
@@ -536,10 +542,10 @@ export async function waitForSubagents(
|
|
|
536
542
|
deps: SubagentWaitDeps,
|
|
537
543
|
): Promise<AgentToolResult<Details>> {
|
|
538
544
|
if (deps.enabled === false) {
|
|
539
|
-
return result("
|
|
545
|
+
return result("bg_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED; returning immediately without blocking background work. Active work keeps going, and you can inspect subagents with subagent({ action: \"status\" }) or rely on completion notifications.");
|
|
540
546
|
}
|
|
541
547
|
if (!deps.state.currentSessionId) {
|
|
542
|
-
return result("
|
|
548
|
+
return result("bg_wait requires an active session identity to scope background work safely.", true);
|
|
543
549
|
}
|
|
544
550
|
|
|
545
551
|
const now = deps.now ?? Date.now;
|
|
@@ -580,7 +586,7 @@ export async function waitForSubagents(
|
|
|
580
586
|
const selected = matches[0];
|
|
581
587
|
if (selected && params.nonBlocking) {
|
|
582
588
|
if (!deps.subscribe) {
|
|
583
|
-
return result("Non-blocking wait subscriptions require a long-lived interactive subagent runtime; this runtime can only use blocking
|
|
589
|
+
return result("Non-blocking wait subscriptions require a long-lived interactive subagent runtime; this runtime can only use blocking bg_wait calls.", true);
|
|
584
590
|
}
|
|
585
591
|
try {
|
|
586
592
|
const registration = deps.subscribe({ targetKind: selected.kind, runId: selected.id, requestedId: params.id, timeoutMs });
|
|
@@ -634,7 +640,7 @@ export async function waitForSubagents(
|
|
|
634
640
|
}
|
|
635
641
|
if (now() - startedAt >= timeoutMs) {
|
|
636
642
|
return windowElapsedResult(
|
|
637
|
-
`Wait window elapsed after ${formatDuration(timeoutMs)} with ${activeInitialRuns.length} async run(s) and ${activeInitialProviderItems.length} provider item(s) still active: ${stillActive}. The work keeps going; call
|
|
643
|
+
`Wait window elapsed after ${formatDuration(timeoutMs)} with ${activeInitialRuns.length} async run(s) and ${activeInitialProviderItems.length} provider item(s) still active: ${stillActive}. The work keeps going; call bg_wait again or inspect subagent status.`,
|
|
638
644
|
activeInitialRuns.map((run) => run.id),
|
|
639
645
|
activeInitialProviderItems,
|
|
640
646
|
);
|
|
@@ -646,7 +652,7 @@ export async function waitForSubagents(
|
|
|
646
652
|
providerSnapshot = params.id ? providerSnapshot : backgroundWorkForSession(deps, now());
|
|
647
653
|
for (const provider of initialProviderNames) {
|
|
648
654
|
if (!providerSnapshot.providers.includes(provider)) {
|
|
649
|
-
return result(`Background-work provider '${provider}' disappeared while
|
|
655
|
+
return result(`Background-work provider '${provider}' disappeared while bg_wait was tracking its active work; completion cannot be confirmed.`, true);
|
|
650
656
|
}
|
|
651
657
|
}
|
|
652
658
|
providerActive = providerSnapshot.items;
|
|
@@ -685,6 +691,7 @@ export async function waitForSubagents(
|
|
|
685
691
|
+ providerActive.filter((item) => initialProviderIds.has(backgroundWorkIdentity(item))).length;
|
|
686
692
|
const elapsed = formatDuration(now() - startedAt);
|
|
687
693
|
const outcome = terminalSummary ? ` Outcome: ${terminalSummary}.` : "";
|
|
694
|
+
const recoveryNote = formatCompletionRecovery(completions);
|
|
688
695
|
|
|
689
696
|
if (waitForAll) {
|
|
690
697
|
const scope = params.id
|
|
@@ -694,7 +701,7 @@ export async function waitForSubagents(
|
|
|
694
701
|
: `${initialAsyncIds.size} async run(s) and ${initialProviderIds.size} provider item(s)`;
|
|
695
702
|
const status = relevantAttention.length > 0 ? "attention required" : "done";
|
|
696
703
|
return result(
|
|
697
|
-
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
704
|
+
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
698
705
|
(deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
|
|
699
706
|
completions,
|
|
700
707
|
);
|
|
@@ -703,7 +710,7 @@ export async function waitForSubagents(
|
|
|
703
710
|
const finishedCount = finishedAsyncCount + providerFinishedCount;
|
|
704
711
|
const subject = initialProviderIds.size === 0 ? "run(s)" : "item(s)";
|
|
705
712
|
const remainder = stillRunning > 0
|
|
706
|
-
? ` ${stillRunning} ${subject} still in flight — call
|
|
713
|
+
? ` ${stillRunning} ${subject} still in flight — call bg_wait again to catch the next one.`
|
|
707
714
|
: relevantAttention.length > 0
|
|
708
715
|
? " No other work is waitable until attention is handled."
|
|
709
716
|
: initialProviderIds.size === 0 ? " No runs remain in flight." : " No work remains in flight.";
|
|
@@ -711,7 +718,7 @@ export async function waitForSubagents(
|
|
|
711
718
|
? `${relevantAttention.length} of ${initialCount} ${subject} need attention`
|
|
712
719
|
: `${finishedCount} of ${initialCount} ${subject} finished`;
|
|
713
720
|
return result(
|
|
714
|
-
`Waited ${elapsed}; ${progress}.${outcome}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
721
|
+
`Waited ${elapsed}; ${progress}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
715
722
|
(deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
|
|
716
723
|
completions,
|
|
717
724
|
);
|
|
@@ -4,6 +4,7 @@ import type { AsyncRunSummary } from "./async-status.ts";
|
|
|
4
4
|
import { readCompletionReplay, writeCompletionReplay } from "./completion-replay.ts";
|
|
5
5
|
import { fallbackResultPayloadPathForSessionRun, resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
|
|
6
6
|
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
7
|
+
import { projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
|
|
7
8
|
|
|
8
9
|
function asNonEmptyString(value: unknown): string | undefined {
|
|
9
10
|
return typeof value === "string" && value ? value : undefined;
|
|
@@ -65,6 +66,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
65
66
|
const error = asNonEmptyString(child.error);
|
|
66
67
|
const model = asNonEmptyString(child.model);
|
|
67
68
|
const contextOverflow = child.contextOverflow === true;
|
|
69
|
+
const timeoutRecovery = projectTimeoutRecovery(child.timeoutRecovery);
|
|
68
70
|
return [{
|
|
69
71
|
...(agent ? { agent } : {}),
|
|
70
72
|
...(childRunId ? { runId: childRunId } : {}),
|
|
@@ -76,6 +78,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
76
78
|
...(model ? { model } : {}),
|
|
77
79
|
...(contextOverflow ? { contextOverflow: true } : {}),
|
|
78
80
|
...(artifactPaths ? { artifactPaths } : {}),
|
|
81
|
+
...(timeoutRecovery ? { timeoutRecovery } : {}),
|
|
79
82
|
}];
|
|
80
83
|
})
|
|
81
84
|
: undefined;
|
|
@@ -96,7 +99,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
/**
|
|
99
|
-
* Record a consumed terminal payload for later surfacing by
|
|
102
|
+
* Record a consumed terminal payload for later surfacing by bg_wait, pruning
|
|
100
103
|
* stale entries with the same TTL that dedupes completion notifications. The result
|
|
101
104
|
* file is deleted after delivery, so this record is the only in-process source once
|
|
102
105
|
* the watcher has consumed it.
|