pi-subagents 0.65.0 → 0.66.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 +87 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +17 -3
- package/docs/configuration.md +34 -0
- package/docs/extension-api.md +94 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +11 -5
- package/docs/workflows.md +22 -7
- package/package.json +4 -1
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/execution-controls.md +19 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/src/agents/advertised-agent-prompt.ts +63 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +8 -0
- package/src/api/preflight.ts +5 -1
- package/src/api/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/config.ts +4 -2
- package/src/extension/index.ts +71 -4
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +9 -7
- package/src/extension/tool-description.ts +10 -5
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +102 -88
- package/src/intercom/supervisor-ui.ts +3 -2
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-execution.ts +8 -1
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +40 -29
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/notify.ts +88 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +62 -33
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +46 -8
- package/src/runs/background/runner-child-launch.ts +86 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/subagent-runner.ts +430 -208
- package/src/runs/background/subagent-wait.ts +3 -0
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/async-steering-action.ts +19 -0
- package/src/runs/foreground/execution.ts +116 -26
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +531 -227
- package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
- package/src/runs/shared/acceptance.ts +16 -3
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +47 -47
- package/src/runs/shared/child-hooks.ts +151 -2
- package/src/runs/shared/child-launch.ts +18 -13
- package/src/runs/shared/child-session.ts +55 -24
- package/src/runs/shared/child-tool-plan.ts +2 -2
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +1 -0
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +30 -20
- package/src/runs/shared/model-exclusions.ts +2 -1
- package/src/runs/shared/model-fallback.ts +41 -8
- package/src/runs/shared/nested-events.ts +8 -8
- package/src/runs/shared/orca-progress-tabs.ts +6 -0
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +3 -2
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
- package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +403 -210
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/types.ts +89 -60
- package/src/shared/utils.ts +10 -2
- package/src/shared/watch-strategy.ts +2 -0
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +44 -15
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +70 -16
- package/src/workflows/workflow-checklist.ts +15 -18
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
|
@@ -12,7 +12,7 @@ export const DEFAULT_ABANDONED_SLOT_RELEASE_AFTER_MS = 20 * 60 * 1000;
|
|
|
12
12
|
export const MIN_ABANDONED_SLOT_RELEASE_AFTER_MS = 5 * 60 * 1000;
|
|
13
13
|
export const MAX_ABANDONED_SLOT_RELEASE_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
14
14
|
|
|
15
|
-
export interface
|
|
15
|
+
export interface ActiveAsyncCapacityOwner {
|
|
16
16
|
version: 1;
|
|
17
17
|
reservationToken: string;
|
|
18
18
|
ownerSessionId: string;
|
|
@@ -29,7 +29,7 @@ export interface ActiveAsyncCapacityOwnerV1 {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface ActiveAsyncCapacityHandle {
|
|
32
|
-
readonly owner:
|
|
32
|
+
readonly owner: ActiveAsyncCapacityOwner;
|
|
33
33
|
markStarted(runnerProcessInstanceId: string): void;
|
|
34
34
|
markWorkflowStarted(): void;
|
|
35
35
|
rollback(): boolean;
|
|
@@ -44,7 +44,7 @@ interface CapacityOptions {
|
|
|
44
44
|
abandonedSlotReleaseAfterMs?: number | false;
|
|
45
45
|
pidLiveness?: (pid: number) => PidLiveness;
|
|
46
46
|
afterSlotRename?: (releasedDir: string) => void;
|
|
47
|
-
writeOwner?: (filePath: string, owner:
|
|
47
|
+
writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwner) => void;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export interface ActiveAsyncCapacityReleaseEvidence {
|
|
@@ -61,7 +61,7 @@ export type ActiveAsyncCapacityReleaseVerdict =
|
|
|
61
61
|
| { state: "not-owned"; reason: string };
|
|
62
62
|
|
|
63
63
|
export interface ActiveAsyncCapacityInspection {
|
|
64
|
-
owner?:
|
|
64
|
+
owner?: ActiveAsyncCapacityOwner;
|
|
65
65
|
relation: "current" | "source" | "none";
|
|
66
66
|
slotDir?: string;
|
|
67
67
|
release: ActiveAsyncCapacityReleaseVerdict;
|
|
@@ -100,9 +100,9 @@ function slotDir(poolDir: string, slot: number): string {
|
|
|
100
100
|
return path.join(poolDir, `slot-${slot}`);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function parseOwner(value: unknown):
|
|
103
|
+
function parseOwner(value: unknown): ActiveAsyncCapacityOwner | undefined {
|
|
104
104
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
105
|
-
const owner = value as Partial<
|
|
105
|
+
const owner = value as Partial<ActiveAsyncCapacityOwner>;
|
|
106
106
|
if (owner.version !== 1
|
|
107
107
|
|| typeof owner.reservationToken !== "string" || !owner.reservationToken
|
|
108
108
|
|| typeof owner.ownerSessionId !== "string" || !owner.ownerSessionId
|
|
@@ -116,10 +116,10 @@ function parseOwner(value: unknown): ActiveAsyncCapacityOwnerV1 | undefined {
|
|
|
116
116
|
if (owner.sourceRunId !== undefined && typeof owner.sourceRunId !== "string") return undefined;
|
|
117
117
|
if (owner.runnerProcessInstanceId !== undefined && (typeof owner.runnerProcessInstanceId !== "string" || !owner.runnerProcessInstanceId)) return undefined;
|
|
118
118
|
if (owner.runnerStartedAt !== undefined && (typeof owner.runnerStartedAt !== "number" || !Number.isFinite(owner.runnerStartedAt))) return undefined;
|
|
119
|
-
return owner as
|
|
119
|
+
return owner as ActiveAsyncCapacityOwner;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function readOwner(dir: string):
|
|
122
|
+
function readOwner(dir: string): ActiveAsyncCapacityOwner | undefined {
|
|
123
123
|
try {
|
|
124
124
|
return parseOwner(JSON.parse(fs.readFileSync(path.join(dir, "owner.json"), "utf-8")));
|
|
125
125
|
} catch {
|
|
@@ -142,7 +142,7 @@ function snapshotFor(sessionId: string, limit: number | undefined, rootDir: stri
|
|
|
142
142
|
return { used: occupiedSlots(sessionDir(sessionId, rootDir)).length, limit: limit ?? 0 };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function matchingOwner(dir: string, expected:
|
|
145
|
+
function matchingOwner(dir: string, expected: ActiveAsyncCapacityOwner): ActiveAsyncCapacityOwner | undefined {
|
|
146
146
|
const owner = readOwner(dir);
|
|
147
147
|
return owner?.reservationToken === expected.reservationToken
|
|
148
148
|
&& owner.runId === expected.runId
|
|
@@ -176,7 +176,7 @@ function withSlotClaim<T>(dir: string, operation: () => T): { acquired: true; va
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
function removeOwnedSlot(dir: string, expected:
|
|
179
|
+
function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwner, options: CapacityOptions, requireUnstarted = false, release?: ActiveAsyncCapacityReleaseVerdict): boolean {
|
|
180
180
|
if (requireUnstarted && (expected.runnerProcessInstanceId || expected.runnerStartedAt)) return false;
|
|
181
181
|
const claimed = withSlotClaim(dir, () => {
|
|
182
182
|
const current = matchingOwner(dir, expected);
|
|
@@ -193,7 +193,7 @@ function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwnerV1, opti
|
|
|
193
193
|
return claimed.acquired && claimed.value;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
function appendAbandonedReleaseEvent(asyncDir: string, owner:
|
|
196
|
+
function appendAbandonedReleaseEvent(asyncDir: string, owner: ActiveAsyncCapacityOwner, evidence: ActiveAsyncCapacityReleaseEvidence, now: number): void {
|
|
197
197
|
try {
|
|
198
198
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
199
199
|
fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
|
|
@@ -213,7 +213,7 @@ function terminalState(state: AsyncStatus["state"]): boolean {
|
|
|
213
213
|
return state !== "queued" && state !== "running" && state !== "paused";
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
function runnerReleaseVerdict(owner:
|
|
216
|
+
function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncStatus | null, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
|
|
217
217
|
if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
|
|
218
218
|
if (!owner.runnerProcessInstanceId) return { state: "retained", reason: "runner process identity has not been recorded" };
|
|
219
219
|
if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
|
|
@@ -262,7 +262,7 @@ function abandonedRunnerReleaseVerdict(status: AsyncStatus, proofState: string,
|
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
function workflowReleaseVerdict(owner:
|
|
265
|
+
function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncStatus | null, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
|
|
266
266
|
if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
|
|
267
267
|
if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
|
|
268
268
|
if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
|
|
@@ -289,7 +289,7 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: Async
|
|
|
289
289
|
return { state: "releasable", reason: "workflow is terminal, controller is gone, and async children have observed proof" };
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
function ownerReleaseVerdict(owner:
|
|
292
|
+
function ownerReleaseVerdict(owner: ActiveAsyncCapacityOwner, liveWorkflowRunIds: ReadonlySet<string>, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
|
|
293
293
|
const status = readStatus(owner.asyncDir);
|
|
294
294
|
return owner.kind === "runner"
|
|
295
295
|
? runnerReleaseVerdict(owner, status, options)
|
|
@@ -364,7 +364,7 @@ export function getActiveAsyncCapacitySnapshot(
|
|
|
364
364
|
return reconcileActiveAsyncCapacity(sessionId, limit, options);
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
function createSlot(poolDir: string, owner:
|
|
367
|
+
function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwner): boolean {
|
|
368
368
|
const destination = slotDir(poolDir, owner.slot);
|
|
369
369
|
fs.mkdirSync(poolDir, { recursive: true, mode: 0o700 });
|
|
370
370
|
try {
|
|
@@ -379,7 +379,7 @@ function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwnerV1): boolean
|
|
|
379
379
|
return true;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
function handleFor(owner:
|
|
382
|
+
function handleFor(owner: ActiveAsyncCapacityOwner, limit: number, options: CapacityOptions, rollbackOwner?: ActiveAsyncCapacityOwner): ActiveAsyncCapacityHandle {
|
|
383
383
|
const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
|
|
384
384
|
const writeOwner = options.writeOwner ?? writePrivateAtomicJson;
|
|
385
385
|
const dir = slotDir(sessionDir(owner.ownerSessionId, rootDir), owner.slot);
|
|
@@ -462,7 +462,7 @@ export function acquireActiveAsyncCapacity(
|
|
|
462
462
|
const poolDir = sessionDir(input.sessionId, rootDir);
|
|
463
463
|
const token = options.token?.() ?? randomUUID();
|
|
464
464
|
for (let slot = 0; slot < input.limit; slot++) {
|
|
465
|
-
const owner:
|
|
465
|
+
const owner: ActiveAsyncCapacityOwner = {
|
|
466
466
|
version: 1,
|
|
467
467
|
reservationToken: token,
|
|
468
468
|
ownerSessionId: input.sessionId,
|
|
@@ -495,7 +495,7 @@ export function transferActiveAsyncCapacity(
|
|
|
495
495
|
if (!current || !status || status.runId !== input.sourceRunId || status.state === "queued" || status.state === "running") {
|
|
496
496
|
throw new Error(`Active async capacity source '${input.sourceRunId}' is not transferable.`);
|
|
497
497
|
}
|
|
498
|
-
const next:
|
|
498
|
+
const next: ActiveAsyncCapacityOwner = {
|
|
499
499
|
...current,
|
|
500
500
|
runId: input.runId,
|
|
501
501
|
sourceRunId: input.sourceRunId,
|
|
@@ -146,6 +146,7 @@ interface AsyncExecutionContext {
|
|
|
146
146
|
currentModel?: ParentModel;
|
|
147
147
|
/** Optional model-scope enforcement resolved from subagent settings. */
|
|
148
148
|
modelScope?: ModelScopeConfig;
|
|
149
|
+
modelResponseAliases?: Record<string, string[]>;
|
|
149
150
|
/** Whether the parent session has an interactive UI. */
|
|
150
151
|
interactive?: boolean;
|
|
151
152
|
/** The executor's own child runtime when the launch comes from an in-process child. */
|
|
@@ -586,7 +587,10 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
586
587
|
stdoutFd = fs.openSync(logPaths.stdoutPath, "a");
|
|
587
588
|
stderrFd = fs.openSync(logPaths.stderrPath, "a");
|
|
588
589
|
}
|
|
589
|
-
const
|
|
590
|
+
const preload = hostPeerAliases.supplemental.length > 0
|
|
591
|
+
? ["--import", new URL("../../../runner-server-preload.mjs", import.meta.url).href]
|
|
592
|
+
: [];
|
|
593
|
+
const proc = spawn(nodeCommand, [...preload, jitiCliPath, runner, cfgPath], {
|
|
590
594
|
cwd,
|
|
591
595
|
...backgroundProcessOptions(),
|
|
592
596
|
stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
|
|
@@ -1021,6 +1025,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
1021
1025
|
modelCandidates: externalRunner ? undefined : modelCandidates,
|
|
1022
1026
|
...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
|
|
1023
1027
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
1028
|
+
...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
|
|
1024
1029
|
tools: a.tools,
|
|
1025
1030
|
excludeTools: a.excludeTools,
|
|
1026
1031
|
allowNestedSubagents: a.allowNestedSubagents,
|
|
@@ -1822,6 +1827,7 @@ export function executeAsyncSingle(
|
|
|
1822
1827
|
});
|
|
1823
1828
|
const recoveryAgentConfig = params.recoveryAgentConfig ?? agentConfig;
|
|
1824
1829
|
const recoveryDescriptor: SteeringRecoveryDescriptor = {
|
|
1830
|
+
...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
|
|
1825
1831
|
version: 1,
|
|
1826
1832
|
...(lane ? { lane } : {}),
|
|
1827
1833
|
launchContractDigest,
|
|
@@ -1910,6 +1916,7 @@ export function executeAsyncSingle(
|
|
|
1910
1916
|
modelCandidates,
|
|
1911
1917
|
...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
|
|
1912
1918
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
1919
|
+
...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
|
|
1913
1920
|
tools: agentConfig.tools,
|
|
1914
1921
|
excludeTools: agentConfig.excludeTools,
|
|
1915
1922
|
allowNestedSubagents: agentConfig.allowNestedSubagents,
|
|
@@ -26,6 +26,7 @@ import { EXTERNAL_JOB_BRIDGE_REQUEST_DIR, serviceExternalJobBridgeRequests } fro
|
|
|
26
26
|
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
27
27
|
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
28
28
|
import { validHostStepNodes } from "../shared/host-step-status.ts";
|
|
29
|
+
import { readProcessTerminal } from "./process-terminal.ts";
|
|
29
30
|
import { withCachedUiContext } from "../../shared/extension-context.ts";
|
|
30
31
|
|
|
31
32
|
interface AsyncJobTrackerOptions {
|
|
@@ -78,6 +79,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
78
79
|
let widgetRerenderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
79
80
|
const runningJobIds = new Set<string>();
|
|
80
81
|
const externalJobBridgeRuns = new Set<string>();
|
|
82
|
+
// Early native failure is visible before its publisher finishes. Retain only
|
|
83
|
+
// that scoped observation, using the existing liveness sweep to renew delivery.
|
|
84
|
+
const terminalPublications = new Map<string, { instanceId: string; pending: true } | { pending: false }>();
|
|
81
85
|
const externalJobBridgeEligibility = (steps: AsyncJobState["steps"]): "required" | "not-required" | "unknown" => {
|
|
82
86
|
if (!Array.isArray(steps)) return "unknown";
|
|
83
87
|
for (const step of steps) {
|
|
@@ -380,6 +384,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
380
384
|
refreshTimers.delete(asyncId);
|
|
381
385
|
runningJobIds.delete(asyncId);
|
|
382
386
|
externalJobBridgeRuns.delete(asyncId);
|
|
387
|
+
terminalPublications.delete(asyncId);
|
|
383
388
|
};
|
|
384
389
|
|
|
385
390
|
const refreshJob = (job: AsyncJobState): boolean => {
|
|
@@ -440,6 +445,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
440
445
|
if (status) {
|
|
441
446
|
const previousStatus = job.status;
|
|
442
447
|
job.status = status.state;
|
|
448
|
+
if (!terminalStatus(job.status)) terminalPublications.delete(job.asyncId);
|
|
443
449
|
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
444
450
|
else runningJobIds.delete(job.asyncId);
|
|
445
451
|
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
|
|
@@ -497,9 +503,30 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
497
503
|
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
498
504
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
499
505
|
if (terminalStatus(job.status)) {
|
|
500
|
-
|
|
506
|
+
let publication = terminalPublications.get(job.asyncId);
|
|
507
|
+
if (!publication && status.mode !== "workflow" && status.processTerminal?.state === "pending"
|
|
508
|
+
&& status.runId === job.asyncId && status.sessionId === state.currentSessionId
|
|
509
|
+
&& status.completionOwnerId && status.completionOwnerId === state.completionOwnerId) {
|
|
510
|
+
publication = { instanceId: status.processTerminal.runnerProcessInstanceId, pending: true };
|
|
511
|
+
terminalPublications.set(job.asyncId, publication);
|
|
512
|
+
}
|
|
513
|
+
const wasPending = publication?.pending;
|
|
514
|
+
if (publication?.pending) {
|
|
515
|
+
// Reconciliation resolves pending as well as public payloads. The
|
|
516
|
+
// watcher already discovers exact tracked run IDs without promotion.
|
|
517
|
+
const published = reconciliation.resultPath && fs.existsSync(reconciliation.resultPath);
|
|
518
|
+
const closed = !published && readProcessTerminal(job.asyncDir, {
|
|
519
|
+
runId: job.asyncId, runnerProcessInstanceId: publication.instanceId,
|
|
520
|
+
})?.state === "observed";
|
|
521
|
+
if (published || closed) {
|
|
522
|
+
publication = { pending: false };
|
|
523
|
+
terminalPublications.set(job.asyncId, publication);
|
|
524
|
+
} else cancelCleanup(job.asyncId);
|
|
525
|
+
}
|
|
526
|
+
// Scan on close too: publication may have raced the payload check.
|
|
527
|
+
if (!terminalStatus(previousStatus) || (wasPending && !publication?.pending)) options.onJobTerminal?.();
|
|
501
528
|
rememberFleetJob(state, job);
|
|
502
|
-
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
529
|
+
if (!publication?.pending && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
503
530
|
scheduleCleanup(job.asyncId);
|
|
504
531
|
}
|
|
505
532
|
}
|
|
@@ -526,7 +553,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
526
553
|
}
|
|
527
554
|
runningJobIds.delete(job.asyncId);
|
|
528
555
|
rememberFleetJob(state, job);
|
|
529
|
-
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
|
|
556
|
+
if (!terminalPublications.get(job.asyncId)?.pending && !hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
|
|
530
557
|
}
|
|
531
558
|
return widgetRenderKey(job, widgetExpanded) !== widgetStateBefore;
|
|
532
559
|
};
|
|
@@ -668,6 +695,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
668
695
|
const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
|
|
669
696
|
state.liveAsyncSessionRoots?.delete(info.id);
|
|
670
697
|
externalJobBridgeRuns.delete(info.id);
|
|
698
|
+
terminalPublications.delete(info.id);
|
|
671
699
|
state.asyncJobs.set(info.id, {
|
|
672
700
|
asyncId: info.id,
|
|
673
701
|
asyncDir,
|
|
@@ -721,6 +749,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
721
749
|
job.asyncDir = result.asyncDir;
|
|
722
750
|
watchJob(job);
|
|
723
751
|
}
|
|
752
|
+
// Delivery can precede the first terminal status refresh. Remember its
|
|
753
|
+
// settlement even when no pending publication has been observed yet.
|
|
754
|
+
terminalPublications.set(asyncId, { pending: false });
|
|
724
755
|
try {
|
|
725
756
|
updateAsyncJobNestedProjection(job);
|
|
726
757
|
} catch (error) {
|
|
@@ -745,6 +776,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
745
776
|
widgetRerenderTimer = undefined;
|
|
746
777
|
runningJobIds.clear();
|
|
747
778
|
externalJobBridgeRuns.clear();
|
|
779
|
+
terminalPublications.clear();
|
|
748
780
|
};
|
|
749
781
|
|
|
750
782
|
const resetJobs = (ctx?: ExtensionContext) => {
|
|
@@ -15,6 +15,7 @@ import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/paral
|
|
|
15
15
|
import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
|
|
16
16
|
import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
17
17
|
import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
|
|
18
|
+
import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
|
|
18
19
|
|
|
19
20
|
export interface AsyncResumeParams {
|
|
20
21
|
id?: string;
|
|
@@ -317,7 +318,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
317
318
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
318
319
|
const parsed = value as Record<string, unknown>;
|
|
319
320
|
const allowedFields = new Set([
|
|
320
|
-
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
321
|
+
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
321
322
|
"subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
322
323
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
323
324
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
@@ -337,6 +338,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
337
338
|
} catch (error) {
|
|
338
339
|
throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
339
340
|
}
|
|
341
|
+
validateModelResponseAliases(parsed.modelResponseAliases, `async recovery descriptor '${descriptorPath}' modelResponseAliases`);
|
|
340
342
|
if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
|
|
341
343
|
if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
|
|
342
344
|
if (parsed.extensionBindings !== undefined) parsed.extensionBindings = normalizeExtensionBindings(parsed.extensionBindings)!.value;
|
|
@@ -320,7 +320,16 @@ function readCursor(root: string): RetentionCursor {
|
|
|
320
320
|
return value?.version === 1 ? value as unknown as RetentionCursor : { version: 1 };
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
let currentProcessStartIdentity: string | undefined | null = null;
|
|
324
|
+
|
|
323
325
|
function processStartIdentity(pid: number): string | undefined {
|
|
326
|
+
// Foreign PIDs can be reused while this process lives, so resolve them afresh.
|
|
327
|
+
if (pid !== process.pid) return computeProcessStartIdentity(pid);
|
|
328
|
+
if (currentProcessStartIdentity === null) currentProcessStartIdentity = computeProcessStartIdentity(pid);
|
|
329
|
+
return currentProcessStartIdentity;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function computeProcessStartIdentity(pid: number): string | undefined {
|
|
324
333
|
if (process.platform === "linux") {
|
|
325
334
|
try {
|
|
326
335
|
const stat = fs.readFileSync(`/proc/${pid}/stat`).toString("utf8");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AsyncJobState, SubagentState } from "../../shared/types.ts";
|
|
2
2
|
import {
|
|
3
|
-
projectAsyncStatusSnapshot,
|
|
3
|
+
projectAsyncStatusSnapshot as buildAsyncStatusSnapshot,
|
|
4
4
|
type AsyncStatusSnapshotOptions,
|
|
5
|
-
type
|
|
5
|
+
type AsyncStatusSnapshot,
|
|
6
6
|
} from "../shared/async-status-projection.ts";
|
|
7
7
|
|
|
8
8
|
export {
|
|
@@ -10,22 +10,20 @@ export {
|
|
|
10
10
|
ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
11
11
|
} from "../shared/async-status-projection.ts";
|
|
12
12
|
export type {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
AsyncStatusSnapshotActivity,
|
|
14
|
+
AsyncStatusSnapshotCaps,
|
|
15
|
+
AsyncStatusSnapshotHostStep,
|
|
16
16
|
AsyncStatusSnapshotKind,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
AsyncStatusSnapshotNode,
|
|
18
|
+
AsyncStatusSnapshotOmitted,
|
|
19
19
|
AsyncStatusSnapshotOptions,
|
|
20
20
|
AsyncStatusSnapshotState,
|
|
21
|
-
|
|
21
|
+
AsyncStatusSnapshot,
|
|
22
22
|
} from "../shared/async-status-projection.ts";
|
|
23
23
|
|
|
24
24
|
export const ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX = "PI_SUBAGENT_ASYNC_JSON:";
|
|
25
25
|
|
|
26
|
-
export
|
|
27
|
-
return projectAsyncStatusSnapshot(jobs, options);
|
|
28
|
-
}
|
|
26
|
+
export { buildAsyncStatusSnapshot };
|
|
29
27
|
|
|
30
28
|
export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined, sessionId: string | null | undefined): AsyncJobState[] {
|
|
31
29
|
if (!state || !sessionId || state.currentSessionId !== sessionId) return [];
|
|
@@ -39,7 +37,7 @@ export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined
|
|
|
39
37
|
return [...jobs.values()];
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}):
|
|
40
|
+
export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshot {
|
|
43
41
|
return buildAsyncStatusSnapshot(asyncStatusSnapshotJobsForState(state, sessionId), options);
|
|
44
42
|
}
|
|
45
43
|
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
4
|
import { previewDisplayText } from "../../shared/display-text.ts";
|
|
5
5
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
6
|
-
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type
|
|
6
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type HostStepNode, type HostStepState, type LaunchResolvedChildExtensions, type RuntimeAcknowledgedChildExtensions, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TimeoutRecoveryProjection, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type WorktreeNaming, type WorkflowPreflight, type WorkflowGraphSnapshot } from "../../shared/types.ts";
|
|
7
7
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
|
|
8
8
|
import { readStatus } from "../../shared/utils.ts";
|
|
9
9
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
@@ -25,6 +25,7 @@ import { formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSumma
|
|
|
25
25
|
import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
|
|
26
26
|
import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
|
|
27
27
|
import { formatWorkflowChecklistText, projectWorkflowChecklist } from "../../workflows/workflow-checklist.ts";
|
|
28
|
+
import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
|
|
28
29
|
|
|
29
30
|
interface AsyncRunStepSummary {
|
|
30
31
|
index: number;
|
|
@@ -82,8 +83,8 @@ interface AsyncRunStepSummary {
|
|
|
82
83
|
effects?: AsyncJobStep["effects"];
|
|
83
84
|
processTerminal?: AsyncJobStep["processTerminal"];
|
|
84
85
|
timeoutRecovery?: TimeoutRecoveryProjection;
|
|
85
|
-
launchResolvedExtensions?:
|
|
86
|
-
runtimeAcknowledgedExtensions?:
|
|
86
|
+
launchResolvedExtensions?: LaunchResolvedChildExtensions;
|
|
87
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
|
|
87
88
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
88
89
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
89
90
|
children?: NestedRunSummary[];
|
|
@@ -122,7 +123,7 @@ export interface AsyncRunSummary {
|
|
|
122
123
|
chainStepCount?: number;
|
|
123
124
|
pendingAppends?: number;
|
|
124
125
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
125
|
-
hostSteps?:
|
|
126
|
+
hostSteps?: HostStepNode[];
|
|
126
127
|
workflowGraph?: AsyncStatus["workflowGraph"];
|
|
127
128
|
steps: AsyncRunStepSummary[];
|
|
128
129
|
sessionDir?: string;
|
|
@@ -135,8 +136,8 @@ export interface AsyncRunSummary {
|
|
|
135
136
|
nestedWarnings?: string[];
|
|
136
137
|
processTerminal?: AsyncStatus["processTerminal"];
|
|
137
138
|
runFanoutBudget?: AsyncStatus["runFanoutBudget"];
|
|
138
|
-
launchResolvedExtensions?:
|
|
139
|
-
runtimeAcknowledgedExtensions?:
|
|
139
|
+
launchResolvedExtensions?: LaunchResolvedChildExtensions;
|
|
140
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
|
|
140
141
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
141
142
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
142
143
|
parentWorkflowRunId?: string;
|
|
@@ -144,7 +145,7 @@ export interface AsyncRunSummary {
|
|
|
144
145
|
lane?: AsyncStatus["lane"];
|
|
145
146
|
workflow?: Details["workflow"];
|
|
146
147
|
workflowChildren?: Details["workflowChildren"];
|
|
147
|
-
preflight?:
|
|
148
|
+
preflight?: WorkflowPreflight;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
interface AsyncRunListOptions {
|
|
@@ -481,7 +482,7 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
481
482
|
});
|
|
482
483
|
}
|
|
483
484
|
|
|
484
|
-
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
485
|
+
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}, observeStatus?: RawDrainStatusObserver): AsyncRunSummary[] {
|
|
485
486
|
let entries: string[];
|
|
486
487
|
const activeEntries = new Set<string>();
|
|
487
488
|
const wantsActive = options.states === undefined || options.states.some(isActiveAsyncState);
|
|
@@ -508,6 +509,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
508
509
|
indexed.add(entry);
|
|
509
510
|
activeEntries.add(entry);
|
|
510
511
|
} else {
|
|
512
|
+
observeStatus?.(null);
|
|
511
513
|
updateActiveRunIndex(path.join(asyncDirRoot, entry), "failed");
|
|
512
514
|
}
|
|
513
515
|
}
|
|
@@ -519,6 +521,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
519
521
|
}
|
|
520
522
|
} catch (error) {
|
|
521
523
|
if (isNotFoundError(error)) return [];
|
|
524
|
+
observeStatus?.(null);
|
|
522
525
|
throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
|
|
523
526
|
cause: error instanceof Error ? error : undefined,
|
|
524
527
|
});
|
|
@@ -544,14 +547,17 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
544
547
|
try {
|
|
545
548
|
const reconciliation = options.reconcile === false
|
|
546
549
|
? undefined
|
|
547
|
-
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
550
|
+
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now }, observeStatus);
|
|
548
551
|
status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
552
|
+
if (options.reconcile === false) observeStatus?.(status);
|
|
549
553
|
} catch (error) {
|
|
554
|
+
observeStatus?.(null);
|
|
550
555
|
if (!activeEntries.has(entry) || !isAsyncStatusIsolationError(asyncDir, error)) throw error;
|
|
551
556
|
isolateCorruptActiveRun(asyncDir, entry, error, options.now);
|
|
552
557
|
continue;
|
|
553
558
|
}
|
|
554
559
|
if (!status) {
|
|
560
|
+
observeStatus?.(null);
|
|
555
561
|
if (activeEntries.has(entry)) updateActiveRunIndex(asyncDir, "failed");
|
|
556
562
|
continue;
|
|
557
563
|
}
|
|
@@ -573,6 +579,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
573
579
|
nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
|
|
574
580
|
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
575
581
|
} catch (error) {
|
|
582
|
+
observeStatus?.(null);
|
|
576
583
|
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
577
584
|
}
|
|
578
585
|
}
|
|
@@ -580,6 +587,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
580
587
|
try {
|
|
581
588
|
summary = statusToSummary(asyncDir, status, nestedWarnings, nestedRoute);
|
|
582
589
|
} catch (error) {
|
|
590
|
+
observeStatus?.(null);
|
|
583
591
|
if (!activeEntries.has(entry) || !isAsyncStatusIsolationError(asyncDir, error)) throw error;
|
|
584
592
|
isolateCorruptActiveRun(asyncDir, entry, error, options.now);
|
|
585
593
|
continue;
|
|
@@ -2,6 +2,7 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import { snapshotBackgroundWork } from "../../api/background-work.ts";
|
|
3
3
|
import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { listAsyncRuns } from "./async-status.ts";
|
|
5
|
+
import type { ReadonlyDrainObservation } from "../shared/readonly-drain-observation.ts";
|
|
5
6
|
import { waitForSubagents, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_AUTO_DRAIN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -23,46 +24,56 @@ function resultText(value: AgentToolResult<Details>): string {
|
|
|
23
24
|
return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function hasOutstandingWork(sessionId: string, nowMs: number): boolean {
|
|
27
|
+
function hasOutstandingWork(sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
|
|
27
28
|
const asyncRuns = listAsyncRuns(DIRS.async, {
|
|
28
29
|
states: ["queued", "running"],
|
|
29
30
|
sessionId,
|
|
30
31
|
resultsDir: DIRS.results,
|
|
31
32
|
now: () => nowMs,
|
|
32
|
-
});
|
|
33
|
+
}, observation?.status);
|
|
33
34
|
return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** Drain all work owned by the current headless session, including work added while draining. */
|
|
37
|
-
export async function drainOutstandingWork(deps: AutoDrainDeps): Promise<void> {
|
|
38
|
+
export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: ReadonlyDrainObservation): Promise<void> {
|
|
38
39
|
const sessionId = deps.state.currentSessionId;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
observation?.begin(sessionId, !deps.hasWork && !deps.wait && !deps.now);
|
|
41
|
+
try {
|
|
42
|
+
if (!sessionId) throw new Error("Cannot auto-drain background work without an active session identity.");
|
|
43
|
+
const now = deps.now ?? Date.now;
|
|
44
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_AUTO_DRAIN_TIMEOUT_MS;
|
|
45
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Auto-drain timeoutMs must be a positive finite number.");
|
|
46
|
+
const deadlineAt = now() + timeoutMs;
|
|
47
|
+
const hasWork = deps.hasWork ?? (observation ? (id: string, time: number) => hasOutstandingWork(id, time, observation) : hasOutstandingWork);
|
|
48
|
+
const wait = deps.wait ?? waitForSubagents;
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
50
|
+
while (true) {
|
|
51
|
+
const work = hasWork(sessionId, now());
|
|
52
|
+
observation?.predicate(work);
|
|
53
|
+
if (!work) break;
|
|
54
|
+
const remainingMs = deadlineAt - now();
|
|
55
|
+
if (remainingMs <= 0) {
|
|
56
|
+
throw new Error(`Auto-drain timed out after ${timeoutMs}ms with background work still active in session '${sessionId}'.`);
|
|
57
|
+
}
|
|
58
|
+
const waitResult = await wait(
|
|
59
|
+
{ all: true, timeoutMs: remainingMs },
|
|
60
|
+
undefined,
|
|
61
|
+
{
|
|
62
|
+
state: deps.state,
|
|
63
|
+
events: deps.events,
|
|
64
|
+
now,
|
|
65
|
+
stopOnAttention: false,
|
|
66
|
+
failOnFailedRuns: true,
|
|
67
|
+
failOnAttention: true,
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
if (waitResult.isError) {
|
|
71
|
+
throw new Error(`Auto-drain failed for session '${sessionId}': ${resultText(waitResult) || "bg_wait returned an error without details"}.`);
|
|
72
|
+
}
|
|
66
73
|
}
|
|
74
|
+
observation?.complete();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
observation?.deny();
|
|
77
|
+
throw error;
|
|
67
78
|
}
|
|
68
79
|
}
|
|
@@ -32,6 +32,7 @@ export interface ImportedAsyncRootResult {
|
|
|
32
32
|
structuredOutputSchemaPath?: string;
|
|
33
33
|
acceptance?: AcceptanceLedger;
|
|
34
34
|
artifactPaths?: ArtifactPaths;
|
|
35
|
+
savedOutputPath?: string;
|
|
35
36
|
outputSaveError?: string;
|
|
36
37
|
transcriptPath?: string;
|
|
37
38
|
transcriptError?: string;
|
|
@@ -71,6 +72,7 @@ interface AsyncResultFile {
|
|
|
71
72
|
structuredOutputSchemaPath?: string;
|
|
72
73
|
acceptance?: AcceptanceLedger;
|
|
73
74
|
artifactPaths?: ArtifactPaths;
|
|
75
|
+
savedOutputPath?: string;
|
|
74
76
|
outputSaveError?: string;
|
|
75
77
|
transcriptPath?: string;
|
|
76
78
|
transcriptError?: string;
|
|
@@ -121,6 +123,11 @@ function selectedStatusStep(status: AsyncStatus | null, index: number): NonNulla
|
|
|
121
123
|
|
|
122
124
|
function isTerminalStatus(status: AsyncStatus | null, index: number): boolean {
|
|
123
125
|
if (!status) return false;
|
|
126
|
+
// A workflow-owned single runner publishes its result after completing its
|
|
127
|
+
// step. Keep that window open while root process proof is absent or pending;
|
|
128
|
+
// explicit terminal/unavailable proof retains the existing step fallback.
|
|
129
|
+
if (status.mode === "single" && status.parentWorkflowRunId
|
|
130
|
+
&& (!status.processTerminal || status.processTerminal.state === "pending")) return TERMINAL_STATES.has(status.state);
|
|
124
131
|
const step = selectedStatusStep(status, index);
|
|
125
132
|
if (step && TERMINAL_STEP_STATUSES.has(step.status)) return true;
|
|
126
133
|
return TERMINAL_STATES.has(status.state);
|
|
@@ -229,6 +236,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
229
236
|
...(child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath } : {}),
|
|
230
237
|
...(child?.acceptance ?? step?.acceptance ? { acceptance: child?.acceptance ?? step?.acceptance } : {}),
|
|
231
238
|
...(child?.artifactPaths ? { artifactPaths: child.artifactPaths } : {}),
|
|
239
|
+
...(child?.savedOutputPath ? { savedOutputPath: child.savedOutputPath } : {}),
|
|
232
240
|
...(child?.outputSaveError ? { outputSaveError: child.outputSaveError } : {}),
|
|
233
241
|
...(child?.transcriptPath ?? step?.transcriptPath ? { transcriptPath: child?.transcriptPath ?? step?.transcriptPath } : {}),
|
|
234
242
|
...(child?.transcriptError ? { transcriptError: child.transcriptError } : {}),
|