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
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
import * as fs from "node:fs";
|
|
42
|
+
import * as path from "node:path";
|
|
42
43
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
43
44
|
import {
|
|
44
45
|
listBackgroundWorkWakeChannels,
|
|
@@ -61,6 +62,7 @@ import {
|
|
|
61
62
|
type Usage,
|
|
62
63
|
type WaitCompletion,
|
|
63
64
|
} from "../../shared/types.ts";
|
|
65
|
+
import { nestedRunScope } from "../shared/nested-events.ts";
|
|
64
66
|
import { formatDuration, shortenPath } from "../../shared/formatters.ts";
|
|
65
67
|
import { toAgentToolUsage } from "../../shared/utils.ts";
|
|
66
68
|
import { collectWaitCompletions } from "./wait-completions.ts";
|
|
@@ -103,6 +105,8 @@ export interface SubagentWaitDeps {
|
|
|
103
105
|
onUpdate?: (result: AgentToolResult<Details>) => void;
|
|
104
106
|
asyncDirRoot?: string;
|
|
105
107
|
resultsDir?: string;
|
|
108
|
+
/** Root from the child's validated inherited route, not a tool argument. */
|
|
109
|
+
nestedRootRunId?: string;
|
|
106
110
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
107
111
|
now?: () => number;
|
|
108
112
|
pollIntervalMs?: number;
|
|
@@ -118,6 +122,8 @@ export interface SubagentWaitDeps {
|
|
|
118
122
|
failOnFailedRuns?: boolean;
|
|
119
123
|
/** Internal auto-drain mode surfaces actionable attention as an error. */
|
|
120
124
|
failOnAttention?: boolean;
|
|
125
|
+
/** Durable owned supervisor-request barrier used by headless auto-drain. */
|
|
126
|
+
hasPendingSupervisorRequest?: () => boolean;
|
|
121
127
|
/** Arm a durable exact-target wait subscription in a long-lived interactive runtime. */
|
|
122
128
|
subscribe?: (input: { targetKind: "async" | "foreground"; runId: string; requestedId: string; timeoutMs: number }) => { token: string; expiresAt: number };
|
|
123
129
|
/** Injectable provider protocol surfaces for deterministic tests. */
|
|
@@ -214,14 +220,13 @@ function matchesId(run: AsyncRunSummary, id: string): boolean {
|
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
function activeDetachedForegroundRuns(params: SubagentWaitParams, deps: SubagentWaitDeps): ForegroundResumeRun[] {
|
|
217
|
-
if (!params.id || !deps.state.foregroundRuns) return [];
|
|
218
223
|
const sessionId = deps.state.currentSessionId;
|
|
219
|
-
if (!sessionId) return [];
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
&& run.sessionId === sessionId
|
|
223
|
-
&& run.children.some((child) => child.status === "detached")
|
|
224
|
+
if (!sessionId || !deps.state.foregroundRuns) return [];
|
|
225
|
+
const runs = [...deps.state.foregroundRuns.values()].filter((run) =>
|
|
226
|
+
run.sessionId === sessionId && run.children.some((child) => child.status === "detached")
|
|
224
227
|
);
|
|
228
|
+
if (params.id) return runs.filter((run) => run.runId === params.id || run.runId.startsWith(params.id!));
|
|
229
|
+
return params.all === true ? runs : [];
|
|
225
230
|
}
|
|
226
231
|
|
|
227
232
|
function summarizeForegroundChildren(run: ForegroundResumeRun, indices: Set<number>): string {
|
|
@@ -265,22 +270,39 @@ function backgroundWorkForSession(deps: SubagentWaitDeps, nowMs: number): Backgr
|
|
|
265
270
|
return deps.backgroundWork?.snapshot(sessionId, nowMs) ?? snapshotBackgroundWork(sessionId, nowMs);
|
|
266
271
|
}
|
|
267
272
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
273
|
+
export function waitRunScopes(deps: Pick<SubagentWaitDeps, "asyncDirRoot" | "resultsDir" | "nestedRootRunId">): Array<{ asyncDirRoot: string; resultsDir: string }> {
|
|
274
|
+
return [
|
|
275
|
+
{ asyncDirRoot: deps.asyncDirRoot ?? DIRS.async, resultsDir: deps.resultsDir ?? DIRS.results },
|
|
276
|
+
...(deps.nestedRootRunId ? [nestedRunScope(deps.nestedRootRunId)] : []),
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function collectScopedCompletions(terminal: AsyncRunSummary[], deps: SubagentWaitDeps, references: string[]): WaitCompletion[] | undefined {
|
|
281
|
+
const completions = waitRunScopes(deps).flatMap((scope) => collectWaitCompletions(
|
|
282
|
+
terminal.filter((run) => path.resolve(path.dirname(run.asyncDir)) === path.resolve(scope.asyncDirRoot)),
|
|
283
|
+
deps.state, scope.resultsDir, (reference) => references.push(reference),
|
|
284
|
+
) ?? []);
|
|
285
|
+
return completions.length ? completions : undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Immediate-session runs only; named lookups can also recover already-terminal results. */
|
|
289
|
+
function runsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps, activeOnly = true): AsyncRunSummary[] {
|
|
290
|
+
const runs = waitRunScopes(deps).flatMap(({ asyncDirRoot, resultsDir }) => listAsyncRuns(asyncDirRoot, {
|
|
291
|
+
states: activeOnly ? [...ACTIVE_STATES] : undefined,
|
|
274
292
|
sessionId: deps.state.currentSessionId ?? undefined,
|
|
275
293
|
resultsDir,
|
|
276
294
|
kill: deps.kill,
|
|
277
295
|
now: deps.now,
|
|
278
296
|
includeNested: false,
|
|
279
297
|
...(params.id ? { runId: params.id } : {}),
|
|
280
|
-
});
|
|
298
|
+
}));
|
|
281
299
|
return params.id ? runs.filter((run) => matchesId(run, params.id!)) : runs;
|
|
282
300
|
}
|
|
283
301
|
|
|
302
|
+
function activeRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps): AsyncRunSummary[] {
|
|
303
|
+
return runsForSession(params, deps);
|
|
304
|
+
}
|
|
305
|
+
|
|
284
306
|
/** Runs (from the initial set) currently flagged needs_attention, for reporting. */
|
|
285
307
|
function attentionRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps, initialIds: Set<string>): AsyncRunSummary[] {
|
|
286
308
|
return activeRunsForSession(params, deps).filter((run) => needsAttention(run) && initialIds.has(run.id));
|
|
@@ -288,9 +310,7 @@ function attentionRunsForSession(params: SubagentWaitParams, deps: SubagentWaitD
|
|
|
288
310
|
|
|
289
311
|
/** Exact initial runs in any state, for the final summary. */
|
|
290
312
|
function runsForIds(runIds: Iterable<string>, deps: SubagentWaitDeps): AsyncRunSummary[] {
|
|
291
|
-
|
|
292
|
-
const resultsDir = deps.resultsDir ?? DIRS.results;
|
|
293
|
-
return [...runIds].flatMap((runId) => listAsyncRuns(asyncDirRoot, {
|
|
313
|
+
return waitRunScopes(deps).flatMap(({ asyncDirRoot, resultsDir }) => [...runIds].flatMap((runId) => listAsyncRuns(asyncDirRoot, {
|
|
294
314
|
sessionId: deps.state.currentSessionId ?? undefined,
|
|
295
315
|
resultsDir,
|
|
296
316
|
kill: deps.kill,
|
|
@@ -298,7 +318,7 @@ function runsForIds(runIds: Iterable<string>, deps: SubagentWaitDeps): AsyncRunS
|
|
|
298
318
|
includeNested: false,
|
|
299
319
|
runId,
|
|
300
320
|
exactRunId: true,
|
|
301
|
-
}));
|
|
321
|
+
})));
|
|
302
322
|
}
|
|
303
323
|
|
|
304
324
|
function summarizeTerminalRuns(runs: AsyncRunSummary[], providerFinishedCount = 0): string {
|
|
@@ -378,6 +398,49 @@ function windowElapsedResult(
|
|
|
378
398
|
};
|
|
379
399
|
}
|
|
380
400
|
|
|
401
|
+
function supervisorYieldResult(
|
|
402
|
+
activeRunIds: string[],
|
|
403
|
+
activeProviderItems: readonly RegisteredBackgroundWorkItem[] = [],
|
|
404
|
+
): AgentToolResult<Details> {
|
|
405
|
+
return {
|
|
406
|
+
content: [{ type: "text", text: "Wait yielded for a pending supervisor request. Background work remains active and will continue after the supervisor reply." }],
|
|
407
|
+
details: {
|
|
408
|
+
mode: "management",
|
|
409
|
+
results: [],
|
|
410
|
+
wait: {
|
|
411
|
+
reason: "supervisor_request",
|
|
412
|
+
timedOut: false,
|
|
413
|
+
activeRunIds,
|
|
414
|
+
activeProviderItems: activeProviderItems.map(({ provider, id }) => ({ provider, id })),
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
interface InitialWaitScope {
|
|
421
|
+
asyncRunIds: Set<string>;
|
|
422
|
+
foregroundRuns: Array<{ runId: string; sessionId?: string; detachedIndices: Set<number> }>;
|
|
423
|
+
providerIds: Set<string>;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function supervisorYieldForScope(scope: InitialWaitScope, params: SubagentWaitParams, deps: SubagentWaitDeps, nowMs: number): AgentToolResult<Details> {
|
|
427
|
+
try {
|
|
428
|
+
const activeAsyncIds = new Set(activeRunsForSession(params.id ? { id: params.id } : {}, deps).map((run) => run.id));
|
|
429
|
+
const activeRunIds = new Set([...scope.asyncRunIds].filter((id) => activeAsyncIds.has(id)));
|
|
430
|
+
for (const initial of scope.foregroundRuns) {
|
|
431
|
+
const current = deps.state.foregroundRuns?.get(initial.runId);
|
|
432
|
+
if (!current || current.sessionId !== initial.sessionId) continue;
|
|
433
|
+
if (current.children.some((child) => initial.detachedIndices.has(child.index) && child.status === "detached")) activeRunIds.add(initial.runId);
|
|
434
|
+
}
|
|
435
|
+
const activeProviderItems = scope.providerIds.size > 0
|
|
436
|
+
? backgroundWorkForSession(deps, nowMs).items.filter((item) => scope.providerIds.has(backgroundWorkIdentity(item)))
|
|
437
|
+
: [];
|
|
438
|
+
return supervisorYieldResult([...activeRunIds], activeProviderItems);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
return result(error instanceof Error ? error.message : String(error), true);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
381
444
|
/** Build the live status shown while async work keeps bg_wait blocked. */
|
|
382
445
|
function asyncWaitUpdate(runs: AsyncRunSummary[], providerCount: number, elapsedMs: number): AgentToolResult<Details> {
|
|
383
446
|
const activity = runs.flatMap((run) => {
|
|
@@ -500,6 +563,7 @@ async function waitForDetachedForegroundRun(
|
|
|
500
563
|
now: () => number,
|
|
501
564
|
pollIntervalMs: number,
|
|
502
565
|
timeoutMs: number,
|
|
566
|
+
supervisorYield: () => AgentToolResult<Details>,
|
|
503
567
|
): Promise<AgentToolResult<Details>> {
|
|
504
568
|
const initialDetachedIndices = new Set(run.children.filter((child) => child.status === "detached").map((child) => child.index));
|
|
505
569
|
while (true) {
|
|
@@ -510,6 +574,7 @@ async function waitForDetachedForegroundRun(
|
|
|
510
574
|
if (!current || current.sessionId !== run.sessionId) {
|
|
511
575
|
return result(`Remembered foreground run "${run.runId}" disappeared before a terminal child result was recorded. Completion cannot be confirmed; do not launch a replacement without checking the originating child session.`, true);
|
|
512
576
|
}
|
|
577
|
+
if (deps.hasPendingSupervisorRequest?.()) return supervisorYield();
|
|
513
578
|
const pending = current.children.filter((child) => initialDetachedIndices.has(child.index) && child.status === "detached");
|
|
514
579
|
const attention = foregroundChildrenNeedingAttention(current, initialDetachedIndices);
|
|
515
580
|
if (attention.length > 0) return formatForegroundAttention(current, attention, now() - startedAt);
|
|
@@ -534,6 +599,38 @@ async function waitForDetachedForegroundRun(
|
|
|
534
599
|
}
|
|
535
600
|
}
|
|
536
601
|
|
|
602
|
+
async function waitForSessionDetachedForegroundRuns(
|
|
603
|
+
runs: ForegroundResumeRun[],
|
|
604
|
+
signal: AbortSignal | undefined,
|
|
605
|
+
deps: SubagentWaitDeps,
|
|
606
|
+
startedAt: number,
|
|
607
|
+
now: () => number,
|
|
608
|
+
pollIntervalMs: number,
|
|
609
|
+
timeoutMs: number,
|
|
610
|
+
supervisorYield: () => AgentToolResult<Details>,
|
|
611
|
+
): Promise<AgentToolResult<Details>> {
|
|
612
|
+
const texts: string[] = [];
|
|
613
|
+
for (const run of runs) {
|
|
614
|
+
const one = await waitForDetachedForegroundRun(run, signal, deps, startedAt, now, pollIntervalMs, timeoutMs, supervisorYield);
|
|
615
|
+
if (one.isError) return one;
|
|
616
|
+
if (one.details.wait?.reason === "supervisor_request") return one;
|
|
617
|
+
if (one.details.wait?.reason === "window_elapsed") {
|
|
618
|
+
const activeRunIds = runs.filter((initial) => {
|
|
619
|
+
const current = deps.state.foregroundRuns?.get(initial.runId);
|
|
620
|
+
if (!current || current.sessionId !== initial.sessionId) return false;
|
|
621
|
+
return initial.children.some((child) => child.status === "detached"
|
|
622
|
+
&& current.children.some((candidate) => candidate.index === child.index && candidate.status === "detached"));
|
|
623
|
+
}).map((activeRun) => activeRun.runId);
|
|
624
|
+
return windowElapsedResult(
|
|
625
|
+
one.content.map((part) => part.type === "text" ? part.text : "").join("\n"),
|
|
626
|
+
activeRunIds,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
texts.push(one.content.map((part) => part.type === "text" ? part.text : "").join("\n").trim());
|
|
630
|
+
}
|
|
631
|
+
return result(texts.filter(Boolean).join("\n") || `Waited ${formatDuration(now() - startedAt)} for remembered detached foreground run(s); done.`);
|
|
632
|
+
}
|
|
633
|
+
|
|
537
634
|
/**
|
|
538
635
|
* Block until the targeted async or remembered detached foreground run finishes,
|
|
539
636
|
* the timeout elapses, or the turn is aborted. Resolves with a short
|
|
@@ -557,6 +654,7 @@ export async function waitForSubagents(
|
|
|
557
654
|
? params.timeoutMs
|
|
558
655
|
: deps.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
559
656
|
const startedAt = now();
|
|
657
|
+
const sessionId = deps.state.currentSessionId;
|
|
560
658
|
const waitForAll = params.id ? true : params.all === true;
|
|
561
659
|
if (params.nonBlocking && !params.id) {
|
|
562
660
|
return result("Non-blocking wait subscriptions require id so the registration can bind one exact run identity.", true);
|
|
@@ -569,13 +667,17 @@ export async function waitForSubagents(
|
|
|
569
667
|
let foreground: ForegroundResumeRun[];
|
|
570
668
|
let providerSnapshot: BackgroundWorkSnapshot;
|
|
571
669
|
try {
|
|
572
|
-
active =
|
|
573
|
-
foreground = activeDetachedForegroundRuns(params, deps)
|
|
670
|
+
active = runsForSession(params, deps, !params.id);
|
|
671
|
+
foreground = activeDetachedForegroundRuns(params, deps).map((run) => ({
|
|
672
|
+
...run,
|
|
673
|
+
children: run.children.map((child) => ({ ...child })),
|
|
674
|
+
}));
|
|
574
675
|
providerSnapshot = params.id ? { providers: [], items: [] } : backgroundWorkForSession(deps, startedAt);
|
|
575
676
|
} catch (error) {
|
|
576
677
|
return result(error instanceof Error ? error.message : String(error), true);
|
|
577
678
|
}
|
|
578
679
|
|
|
680
|
+
let selectedForeground: ForegroundResumeRun | undefined;
|
|
579
681
|
if (params.id) {
|
|
580
682
|
const candidates = [
|
|
581
683
|
...active.map((run) => ({ kind: "async" as const, id: run.id, run })),
|
|
@@ -584,9 +686,21 @@ export async function waitForSubagents(
|
|
|
584
686
|
const exact = candidates.filter((candidate) => candidate.id === params.id);
|
|
585
687
|
const matches = exact.length > 0 ? exact : candidates;
|
|
586
688
|
if (matches.length > 1) {
|
|
587
|
-
return result(`Ambiguous subagent run id prefix "${params.id}" matched ${matches.length}
|
|
689
|
+
return result(`Ambiguous subagent run id prefix "${params.id}" matched ${matches.length} runs: ${matches.map((candidate) => candidate.id).join(", ")}. Pass a longer id.`, true);
|
|
588
690
|
}
|
|
589
691
|
const selected = matches[0];
|
|
692
|
+
if (selected?.kind === "async" && !ACTIVE_STATES.includes(selected.run.state)) {
|
|
693
|
+
try {
|
|
694
|
+
const references: string[] = [];
|
|
695
|
+
const completions = collectScopedCompletions([selected.run], deps, references);
|
|
696
|
+
return result(
|
|
697
|
+
`Run "${selected.id}" is terminal. Outcome: ${summarizeTerminalRuns([selected.run])}.${formatCompletionRecovery(completions)}${references.length ? `\n${references.join("\n")}` : ""}`,
|
|
698
|
+
deps.failOnFailedRuns === true && (selected.run.state === "failed" || selected.run.state === "partial"), completions,
|
|
699
|
+
);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
return result(error instanceof Error ? error.message : String(error), true);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
590
704
|
if (selected && params.nonBlocking) {
|
|
591
705
|
if (!deps.subscribe) {
|
|
592
706
|
return result("Non-blocking wait subscriptions require a long-lived interactive subagent runtime; this runtime can only use blocking bg_wait calls.", true);
|
|
@@ -598,27 +712,47 @@ export async function waitForSubagents(
|
|
|
598
712
|
return result(error instanceof Error ? error.message : String(error), true);
|
|
599
713
|
}
|
|
600
714
|
}
|
|
601
|
-
|
|
602
|
-
return waitForDetachedForegroundRun(selected.run, signal, deps, startedAt, now, pollIntervalMs, timeoutMs);
|
|
603
|
-
}
|
|
715
|
+
selectedForeground = selected?.kind === "foreground" ? selected.run : undefined;
|
|
604
716
|
active = selected?.kind === "async" ? [selected.run] : [];
|
|
605
717
|
}
|
|
606
718
|
|
|
607
719
|
let providerActive = providerSnapshot.items;
|
|
720
|
+
const scopedForeground = selectedForeground ? [selectedForeground] : params.id ? [] : foreground;
|
|
721
|
+
const initialAsyncIds = new Set(active.map((run) => run.id));
|
|
722
|
+
const initialProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
723
|
+
const initialScope: InitialWaitScope = {
|
|
724
|
+
asyncRunIds: initialAsyncIds,
|
|
725
|
+
foregroundRuns: scopedForeground.map((run) => ({
|
|
726
|
+
runId: run.runId,
|
|
727
|
+
sessionId: run.sessionId,
|
|
728
|
+
detachedIndices: new Set(run.children.filter((child) => child.status === "detached").map((child) => child.index)),
|
|
729
|
+
})),
|
|
730
|
+
providerIds: initialProviderIds,
|
|
731
|
+
};
|
|
732
|
+
const supervisorYield = () => supervisorYieldForScope(initialScope, params, deps, now());
|
|
733
|
+
if (selectedForeground) {
|
|
734
|
+
return waitForDetachedForegroundRun(selectedForeground, signal, deps, startedAt, now, pollIntervalMs, timeoutMs, supervisorYield);
|
|
735
|
+
}
|
|
608
736
|
if (active.length === 0 && providerActive.length === 0) {
|
|
737
|
+
if (waitForAll && !params.id && foreground.length > 0) {
|
|
738
|
+
return waitForSessionDetachedForegroundRuns(foreground, signal, deps, startedAt, now, pollIntervalMs, timeoutMs, supervisorYield);
|
|
739
|
+
}
|
|
609
740
|
return result(params.id
|
|
610
741
|
? `No active run matched "${params.id}". Nothing to wait for.`
|
|
611
742
|
: "No active async runs or registered provider work in this session. Nothing to wait for.");
|
|
612
743
|
}
|
|
613
744
|
const waitParams = params.id ? { ...params, id: active[0]!.id } : params;
|
|
614
|
-
const initialAsyncIds = new Set(active.map((run) => run.id));
|
|
615
|
-
const initialProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
616
745
|
const initialProviderNames = new Set(providerActive.map((item) => item.provider));
|
|
617
746
|
const initialCount = initialAsyncIds.size + initialProviderIds.size;
|
|
618
747
|
const stopOnAttention = params.stopOnAttention ?? deps.stopOnAttention !== false;
|
|
619
748
|
let attention = active.filter((run) => needsAttention(run));
|
|
749
|
+
let supervisorBarrier = false;
|
|
620
750
|
|
|
621
751
|
const isDone = (): boolean => {
|
|
752
|
+
if (deps.hasPendingSupervisorRequest?.()) {
|
|
753
|
+
supervisorBarrier = true;
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
622
756
|
if (attention.some((run) => initialAsyncIds.has(run.id) && (stopOnAttention || hasSupervisorTool(run)))) return true;
|
|
623
757
|
const activeAsyncIds = new Set(active.map((run) => run.id));
|
|
624
758
|
const activeProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
@@ -650,6 +784,7 @@ export async function waitForSubagents(
|
|
|
650
784
|
}
|
|
651
785
|
try {
|
|
652
786
|
await waitForWake(pollIntervalMs, signal, deps);
|
|
787
|
+
if (deps.state.currentSessionId !== sessionId) return result("Wait stopped because the active session changed.", true);
|
|
653
788
|
active = activeRunsForSession(waitParams, deps);
|
|
654
789
|
attention = attentionRunsForSession(waitParams, deps, initialAsyncIds);
|
|
655
790
|
providerSnapshot = params.id ? providerSnapshot : backgroundWorkForSession(deps, now());
|
|
@@ -663,12 +798,16 @@ export async function waitForSubagents(
|
|
|
663
798
|
return result(error instanceof Error ? error.message : String(error), true);
|
|
664
799
|
}
|
|
665
800
|
}
|
|
801
|
+
if (supervisorBarrier) {
|
|
802
|
+
return supervisorYield();
|
|
803
|
+
}
|
|
666
804
|
|
|
667
805
|
let terminalSummary: string;
|
|
668
806
|
let finishedAsyncCount: number;
|
|
669
807
|
let failedAsyncCount: number;
|
|
670
808
|
let completions: WaitCompletion[] | undefined;
|
|
671
809
|
let resumeGuidance = "";
|
|
810
|
+
const references: string[] = [];
|
|
672
811
|
const activeProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
673
812
|
const providerFinishedCount = [...initialProviderIds].filter((id) => !activeProviderIds.has(id)).length;
|
|
674
813
|
try {
|
|
@@ -678,7 +817,7 @@ export async function waitForSubagents(
|
|
|
678
817
|
failedAsyncCount = terminal.filter((run) => run.state === "failed" || run.state === "partial").length;
|
|
679
818
|
terminalSummary = summarizeTerminalRuns(terminal, providerFinishedCount);
|
|
680
819
|
resumeGuidance = formatResumeFirstFailedRunsNote(terminal);
|
|
681
|
-
completions =
|
|
820
|
+
completions = collectScopedCompletions(terminal, deps, references);
|
|
682
821
|
} catch (error) {
|
|
683
822
|
return result(error instanceof Error ? error.message : String(error), true);
|
|
684
823
|
}
|
|
@@ -694,9 +833,18 @@ export async function waitForSubagents(
|
|
|
694
833
|
+ providerActive.filter((item) => initialProviderIds.has(backgroundWorkIdentity(item))).length;
|
|
695
834
|
const elapsed = formatDuration(now() - startedAt);
|
|
696
835
|
const outcome = terminalSummary ? ` Outcome: ${terminalSummary}.` : "";
|
|
697
|
-
const recoveryNote = formatCompletionRecovery(completions);
|
|
836
|
+
const recoveryNote = formatCompletionRecovery(completions) + (references.length ? `\n${references.join("\n")}\n` : "");
|
|
698
837
|
|
|
699
838
|
if (waitForAll) {
|
|
839
|
+
const foregroundResult = !params.id && foreground.length > 0 && relevantAttention.length === 0
|
|
840
|
+
? await waitForSessionDetachedForegroundRuns(foreground, signal, deps, startedAt, now, pollIntervalMs, timeoutMs, supervisorYield)
|
|
841
|
+
: undefined;
|
|
842
|
+
if (foregroundResult?.isError) return foregroundResult;
|
|
843
|
+
if (foregroundResult?.details.wait?.reason === "supervisor_request") return foregroundResult;
|
|
844
|
+
if (foregroundResult?.details.wait?.reason === "window_elapsed") return foregroundResult;
|
|
845
|
+
const foregroundNote = foregroundResult
|
|
846
|
+
? `\n${foregroundResult.content.map((part) => part.type === "text" ? part.text : "").join("\n")}`
|
|
847
|
+
: "";
|
|
700
848
|
const scope = params.id
|
|
701
849
|
? `run "${params.id}"`
|
|
702
850
|
: initialProviderIds.size === 0
|
|
@@ -704,7 +852,7 @@ export async function waitForSubagents(
|
|
|
704
852
|
: `${initialAsyncIds.size} async run(s) and ${initialProviderIds.size} provider item(s)`;
|
|
705
853
|
const status = relevantAttention.length > 0 ? "attention required" : "done";
|
|
706
854
|
return result(
|
|
707
|
-
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
855
|
+
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote}${foregroundNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
708
856
|
(deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
|
|
709
857
|
completions,
|
|
710
858
|
);
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import type { ArtifactPaths, SubagentState, Usage, WaitCompletion, WaitCompletionChild } from "../../shared/types.ts";
|
|
3
3
|
import type { AsyncRunSummary } from "./async-status.ts";
|
|
4
4
|
import { readCompletionReplay, writeCompletionReplay } from "./completion-replay.ts";
|
|
5
|
-
import { fallbackResultPayloadPathForSessionRun, resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
|
|
5
|
+
import { fallbackResultPayloadPathForSessionRun, resultFilePath, resultPayloadMatchesSessionRun, resultPayloadPathForSessionRun } from "./result-files.ts";
|
|
6
6
|
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
7
7
|
import { projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
|
|
8
8
|
|
|
@@ -44,6 +44,27 @@ function errorMessage(error: unknown): string {
|
|
|
44
44
|
|
|
45
45
|
const STRUCTURED_OUTPUT_INLINE_LIMIT_BYTES = 4 * 1024;
|
|
46
46
|
|
|
47
|
+
function waitResultPayloadCandidate(resultsDir: string, run: AsyncRunSummary): string {
|
|
48
|
+
const publicPath = resultFilePath(resultsDir, run.id);
|
|
49
|
+
if (!run.sessionId) return publicPath;
|
|
50
|
+
try {
|
|
51
|
+
const indexedPath = resultPayloadPathForSessionRun(resultsDir, run.sessionId, run.id);
|
|
52
|
+
return indexedPath ?? publicPath;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!isAccessDenied(error)) throw error;
|
|
55
|
+
const pendingPath = fallbackResultPayloadPathForSessionRun(resultsDir, run.sessionId, run.id);
|
|
56
|
+
return pendingPath ?? publicPath;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readWaitResultPayload(candidatePath: string, run: AsyncRunSummary): Record<string, unknown> | undefined {
|
|
61
|
+
// A terminal run without session ownership cannot safely claim a payload.
|
|
62
|
+
if (!run.sessionId) return undefined;
|
|
63
|
+
const payload: unknown = JSON.parse(fs.readFileSync(candidatePath, "utf-8"));
|
|
64
|
+
if (!resultPayloadMatchesSessionRun(payload, run.sessionId, run.id)) return undefined;
|
|
65
|
+
return payload as Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
export function projectStructuredOutput(value: unknown): unknown {
|
|
48
69
|
if (value === undefined) return undefined;
|
|
49
70
|
const serialized = JSON.stringify(value);
|
|
@@ -118,8 +139,9 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
118
139
|
/**
|
|
119
140
|
* Record a consumed terminal payload for later surfacing by bg_wait, pruning
|
|
120
141
|
* stale entries with the same TTL that dedupes completion notifications. The result
|
|
121
|
-
* file is deleted after
|
|
122
|
-
* the watcher has consumed it.
|
|
142
|
+
* file is deleted after durable replay succeeds, so this record is the in-process
|
|
143
|
+
* source once the watcher has consumed it. Payload ownership must be explicit and
|
|
144
|
+
* agree with persistence ownership. Returns whether that replay is durable.
|
|
123
145
|
*/
|
|
124
146
|
export function recordWaitCompletion(
|
|
125
147
|
state: SubagentState,
|
|
@@ -128,7 +150,10 @@ export function recordWaitCompletion(
|
|
|
128
150
|
now: number,
|
|
129
151
|
ttlMs: number,
|
|
130
152
|
persistence?: { resultsDir: string; sessionId: string },
|
|
131
|
-
):
|
|
153
|
+
): boolean {
|
|
154
|
+
const sessionId = asNonEmptyString(data.sessionId);
|
|
155
|
+
if (!sessionId || !resultPayloadMatchesSessionRun(data, sessionId, runId)) return false;
|
|
156
|
+
if (persistence && persistence.sessionId !== sessionId) return false;
|
|
132
157
|
const store = state.completedResults ??= new Map();
|
|
133
158
|
for (const [key, entry] of store) {
|
|
134
159
|
if (now - entry.seenAt > ttlMs) store.delete(key);
|
|
@@ -148,7 +173,8 @@ export function recordWaitCompletion(
|
|
|
148
173
|
console.error(`Failed to persist completion replay for '${runId}':`, error);
|
|
149
174
|
}
|
|
150
175
|
}
|
|
151
|
-
store.set(runId, { seenAt: now, completion });
|
|
176
|
+
store.set(runId, { sessionId, seenAt: now, completion });
|
|
177
|
+
return completion.archivePath !== undefined;
|
|
152
178
|
}
|
|
153
179
|
|
|
154
180
|
/**
|
|
@@ -157,37 +183,59 @@ export function recordWaitCompletion(
|
|
|
157
183
|
* so a direct read never observes a torn write; the read is deliberately read-only —
|
|
158
184
|
* the watcher owns notification and cleanup.
|
|
159
185
|
*/
|
|
160
|
-
export function collectWaitCompletions(terminal: AsyncRunSummary[], state: SubagentState, resultsDir: string): WaitCompletion[] | undefined {
|
|
186
|
+
export function collectWaitCompletions(terminal: AsyncRunSummary[], state: SubagentState, resultsDir: string, onReference?: (text: string) => void): WaitCompletion[] | undefined {
|
|
161
187
|
if (terminal.length === 0) return undefined;
|
|
162
188
|
const completions: WaitCompletion[] = [];
|
|
189
|
+
const add = (completion: WaitCompletion, resultPath?: string): void => {
|
|
190
|
+
completions.push(completion);
|
|
191
|
+
// Tool details are not model context. Publish the evidence address in content
|
|
192
|
+
// without copying potentially unbounded reviewer output into every wait.
|
|
193
|
+
const reference = completion.archivePath ?? resultPath;
|
|
194
|
+
if (reference) onReference?.(`Result [${completion.runId}]: ${reference}`);
|
|
195
|
+
};
|
|
163
196
|
for (const run of terminal) {
|
|
164
197
|
const recorded = state.completedResults?.get(run.id);
|
|
165
|
-
if (recorded) {
|
|
166
|
-
|
|
198
|
+
if (recorded && recorded.sessionId === run.sessionId) {
|
|
199
|
+
if (recorded.completion.archivePath) {
|
|
200
|
+
add(recorded.completion);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const candidatePath = waitResultPayloadCandidate(resultsDir, run);
|
|
205
|
+
if (!readWaitResultPayload(candidatePath, run)) {
|
|
206
|
+
const replay = readCompletionReplay(resultsDir, run.id, { sessionId: run.sessionId });
|
|
207
|
+
add(replay?.completion ?? recorded.completion);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
add(recorded.completion, candidatePath);
|
|
211
|
+
continue;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
214
|
+
}
|
|
215
|
+
const replay = readCompletionReplay(resultsDir, run.id, { sessionId: run.sessionId });
|
|
216
|
+
add(replay?.completion ?? recorded.completion);
|
|
167
217
|
continue;
|
|
168
218
|
}
|
|
169
|
-
|
|
170
|
-
let resultPath = publicResultPath;
|
|
219
|
+
let candidatePath: string;
|
|
171
220
|
try {
|
|
172
|
-
|
|
173
|
-
? resultPayloadPathForSessionRun(resultsDir, run.sessionId, run.id) ?? publicResultPath
|
|
174
|
-
: publicResultPath;
|
|
221
|
+
candidatePath = waitResultPayloadCandidate(resultsDir, run);
|
|
175
222
|
} catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
throw new Error(`Failed to read subagent result '${publicResultPath}': ${errorMessage(fallbackError)}`, {
|
|
181
|
-
cause: fallbackError instanceof Error ? fallbackError : undefined,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
223
|
+
const publicResultPath = resultFilePath(resultsDir, run.id);
|
|
224
|
+
throw new Error(`Failed to read subagent result '${publicResultPath}': ${errorMessage(error)}`, {
|
|
225
|
+
cause: error instanceof Error ? error : undefined,
|
|
226
|
+
});
|
|
184
227
|
}
|
|
185
228
|
try {
|
|
186
|
-
const
|
|
187
|
-
|
|
229
|
+
const payload = readWaitResultPayload(candidatePath, run);
|
|
230
|
+
if (!payload) {
|
|
231
|
+
const replay = readCompletionReplay(resultsDir, run.id, { sessionId: run.sessionId });
|
|
232
|
+
if (replay) add(replay.completion);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
add(toWaitCompletion(payload, run.id), candidatePath);
|
|
188
236
|
} catch (error) {
|
|
189
237
|
if (errorCode(error) !== "ENOENT") {
|
|
190
|
-
throw new Error(`Failed to read subagent result '${
|
|
238
|
+
throw new Error(`Failed to read subagent result '${candidatePath}': ${errorMessage(error)}`, {
|
|
191
239
|
cause: error instanceof Error ? error : undefined,
|
|
192
240
|
});
|
|
193
241
|
}
|
|
@@ -195,13 +243,13 @@ export function collectWaitCompletions(terminal: AsyncRunSummary[], state: Subag
|
|
|
195
243
|
// read. Prefer its in-memory record, then the durable replay written before
|
|
196
244
|
// result cleanup so watcher reloads do not lose completion details.
|
|
197
245
|
const late = state.completedResults?.get(run.id);
|
|
198
|
-
if (late) {
|
|
199
|
-
|
|
246
|
+
if (late && late.sessionId === run.sessionId) {
|
|
247
|
+
add(late.completion);
|
|
200
248
|
continue;
|
|
201
249
|
}
|
|
202
250
|
try {
|
|
203
251
|
const replay = readCompletionReplay(resultsDir, run.id, { sessionId: run.sessionId });
|
|
204
|
-
if (replay)
|
|
252
|
+
if (replay) add(replay.completion);
|
|
205
253
|
} catch (replayError) {
|
|
206
254
|
throw new Error(`Failed to read completion replay for '${run.id}': ${errorMessage(replayError)}`, {
|
|
207
255
|
cause: replayError instanceof Error ? replayError : undefined,
|
|
@@ -44,6 +44,7 @@ export interface ArmWaitSubscriptionInput {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface WaitSubscriptionManager {
|
|
47
|
+
start(): void;
|
|
47
48
|
arm(input: ArmWaitSubscriptionInput): WaitSubscriptionRecord;
|
|
48
49
|
restore(): void;
|
|
49
50
|
reconcile(): void;
|
|
@@ -109,6 +110,7 @@ export function createWaitSubscriptionManager(
|
|
|
109
110
|
state.waitSubscriptions = subscriptions;
|
|
110
111
|
const unresolvedRestoredForegroundTokens = new Set<string>();
|
|
111
112
|
let disposed = false;
|
|
113
|
+
let interval: ReturnType<typeof setInterval> | undefined;
|
|
112
114
|
let lastForeignSweepAt = 0;
|
|
113
115
|
|
|
114
116
|
/**
|
|
@@ -283,10 +285,13 @@ export function createWaitSubscriptionManager(
|
|
|
283
285
|
SUBAGENT_RESULT_INTERCOM_EVENT,
|
|
284
286
|
];
|
|
285
287
|
const unsubscribes = wakeChannels.map((channel) => pi.events.on(channel, reconcile));
|
|
286
|
-
const interval = setInterval(reconcile, options.pollIntervalMs ?? RECONCILE_INTERVAL_MS);
|
|
287
|
-
interval.unref?.();
|
|
288
288
|
|
|
289
289
|
return {
|
|
290
|
+
start() {
|
|
291
|
+
if (disposed || interval) return;
|
|
292
|
+
interval = setInterval(reconcile, options.pollIntervalMs ?? RECONCILE_INTERVAL_MS);
|
|
293
|
+
interval.unref?.();
|
|
294
|
+
},
|
|
290
295
|
arm(input) {
|
|
291
296
|
const sessionId = state.currentSessionId;
|
|
292
297
|
if (!sessionId) throw new Error("A wait subscription requires an active session identity.");
|
|
@@ -338,7 +343,8 @@ export function createWaitSubscriptionManager(
|
|
|
338
343
|
dispose() {
|
|
339
344
|
if (disposed) return;
|
|
340
345
|
disposed = true;
|
|
341
|
-
clearInterval(interval);
|
|
346
|
+
if (interval) clearInterval(interval);
|
|
347
|
+
interval = undefined;
|
|
342
348
|
for (const unsubscribe of unsubscribes) {
|
|
343
349
|
try { unsubscribe(); } catch { /* best effort */ }
|
|
344
350
|
}
|
|
@@ -11,14 +11,15 @@ export function registerWaitTool(
|
|
|
11
11
|
enabled = resolveWaitToolConfig().enabled,
|
|
12
12
|
subscriptions?: Pick<WaitSubscriptionManager, "arm">,
|
|
13
13
|
defaultTimeoutMs?: number,
|
|
14
|
+
child?: { nestedRootRunId?: string },
|
|
14
15
|
): void {
|
|
15
16
|
const description = `Wait for background, provider, or detached work that has no native completion notification, then return.
|
|
16
17
|
|
|
17
|
-
Ordinary async subagent runs already notify this session natively when they complete or need attention. In an interactive chat, return control instead of calling this merely to wait. Use this tool for provider jobs, remembered detached foreground runs, or other background work without a native notification path. Headless runs auto-drain current-session subagent work at agent_end; use this tool only when the current turn must receive non-notifying background work results.
|
|
18
|
+
${child ? "This child runtime does not install the root session's native completion notifier. Use blocking bg_wait to collect your owned descendants during this turn, then read the returned result references before synthesizing findings. Automatic draining at agent_end keeps owned work alive but does not synthesize its results." : "Ordinary async subagent runs already notify this session natively when they complete or need attention. In an interactive chat, return control instead of calling this merely to wait. Use this tool for provider jobs, remembered detached foreground runs, or other background work without a native notification path. Headless runs auto-drain current-session subagent work at agent_end; use this tool only when the current turn must receive non-notifying background work results."}
|
|
18
19
|
|
|
19
20
|
• { } — return when the first initially active async run or registered provider item finishes, or when a subagent needs attention.
|
|
20
|
-
• { all: true } — wait for every async run
|
|
21
|
-
• { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
|
|
21
|
+
• { all: true } — wait for every async run, provider item, and remembered detached foreground descendant that was active when the call began.
|
|
22
|
+
• { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix). Named async runs that already finished return their terminal result references.
|
|
22
23
|
• { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. Use this for detached work without native completion delivery; the originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
|
|
23
24
|
• { stopOnAttention: false } — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
|
|
24
25
|
• { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running. Omitted values use waitTool.defaultTimeoutMs, then 30 minutes. Window expiry returns a non-error window_elapsed result with active work identities.
|
|
@@ -26,6 +27,7 @@ Ordinary async subagent runs already notify this session natively when they comp
|
|
|
26
27
|
Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`bg_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: bg_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`;
|
|
27
28
|
const execute: ToolDefinition<typeof SubagentWaitParams, Details>["execute"] = async (_id, params, signal, onUpdate, ctx) => finalizeToolResult(await waitForSubagents(params, signal, {
|
|
28
29
|
state,
|
|
30
|
+
nestedRootRunId: child?.nestedRootRunId,
|
|
29
31
|
events: pi.events,
|
|
30
32
|
enabled,
|
|
31
33
|
...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
|