pi-subagents 0.47.1 → 0.49.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 +51 -0
- package/README.md +2 -0
- package/docs/agents.md +1 -0
- package/docs/configuration.md +74 -7
- package/docs/missions.md +4 -2
- package/docs/observability.md +27 -3
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agents.ts +36 -15
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +26 -0
- package/src/extension/doctor.ts +40 -0
- package/src/extension/index.ts +39 -8
- package/src/extension/public-execution.ts +35 -4
- package/src/extension/rpc.ts +3 -9
- package/src/extension/schemas.ts +10 -9
- package/src/extension/tool-description.ts +10 -8
- package/src/inspectors/herdr/actions.ts +11 -2
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/intercom/intercom-bridge.ts +7 -2
- package/src/missions/lifecycle.ts +4 -7
- package/src/missions/store.ts +12 -7
- package/src/missions/workflow-state.ts +6 -2
- package/src/runs/background/active-async-capacity.ts +431 -0
- package/src/runs/background/active-run-index.ts +9 -5
- package/src/runs/background/async-execution.ts +141 -65
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +18 -2
- package/src/runs/background/async-status.ts +11 -5
- package/src/runs/background/chain-append.ts +33 -15
- package/src/runs/background/fleet-view.ts +18 -5
- package/src/runs/background/owned-process-tree.ts +104 -0
- package/src/runs/background/process-terminal.ts +17 -3
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +14 -6
- package/src/runs/background/run-status.ts +101 -5
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +60 -32
- package/src/runs/foreground/chain-execution.ts +37 -2
- package/src/runs/foreground/execution.ts +90 -19
- package/src/runs/foreground/foreground-control.ts +12 -0
- package/src/runs/foreground/prompt-audit.ts +172 -0
- package/src/runs/foreground/subagent-executor.ts +829 -203
- package/src/runs/shared/acceptance.ts +13 -4
- package/src/runs/shared/completion-guard.ts +17 -1
- package/src/runs/shared/llm-intent-arbiter.ts +302 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/pi-args.ts +44 -1
- package/src/runs/shared/run-fanout-budget.ts +280 -0
- package/src/runs/shared/single-output.ts +4 -2
- package/src/runs/shared/task-intent.ts +19 -3
- package/src/runs/shared/worktree.ts +17 -5
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +115 -4
- package/src/shared/utils.ts +3 -1
- package/src/tui/fleet-status.ts +7 -5
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +228 -13
- package/src/tui/render.ts +86 -39
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +8 -2
- package/src/workflows/scripted-workflow.ts +66 -10
|
@@ -19,9 +19,10 @@ import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isPara
|
|
|
19
19
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
20
20
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
21
21
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
22
|
+
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
22
23
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
23
24
|
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
24
|
-
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, resolveChildCwd } from "../../shared/utils.ts";
|
|
25
|
+
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
25
26
|
import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
26
27
|
import type { ModelScopeConfig } from "../shared/model-scope.ts";
|
|
27
28
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
@@ -30,17 +31,21 @@ import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
|
30
31
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
|
|
31
32
|
import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
32
33
|
import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
34
|
+
import { createRunFanoutBudget, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
33
35
|
import {
|
|
34
36
|
type AcceptanceInput,
|
|
35
37
|
type AgentContract,
|
|
38
|
+
type AsyncStatus,
|
|
36
39
|
type ArtifactConfig,
|
|
37
40
|
type Details,
|
|
41
|
+
type IntercomBridgeConfig,
|
|
38
42
|
type JsonSchemaObject,
|
|
39
43
|
type MaxOutputConfig,
|
|
40
44
|
type NestedRouteInfo,
|
|
41
45
|
type ResolvedControlConfig,
|
|
42
46
|
type ResolvedTurnBudget,
|
|
43
47
|
type ResolvedToolBudget,
|
|
48
|
+
type RunFanoutBudgetDescriptor,
|
|
44
49
|
type ToolBudgetConfig,
|
|
45
50
|
type SubagentRunMode,
|
|
46
51
|
type SteeringRecoveryDescriptor,
|
|
@@ -59,6 +64,7 @@ import { usageBudgetState } from "../shared/usage-budget.ts";
|
|
|
59
64
|
import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
60
65
|
import type { SessionLeaseRequest } from "../shared/session-lease.ts";
|
|
61
66
|
import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
|
|
67
|
+
import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
|
|
62
68
|
import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
|
|
63
69
|
import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
64
70
|
import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
@@ -171,8 +177,10 @@ interface AsyncChainParams {
|
|
|
171
177
|
/** Global cap on simultaneously-running subagent tasks within the async run. */
|
|
172
178
|
globalConcurrencyLimit?: number;
|
|
173
179
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
180
|
+
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
174
181
|
parentWorkflowRunId?: string;
|
|
175
182
|
workflowKey?: string;
|
|
183
|
+
activeAsyncCapacity?: ActiveAsyncCapacityHandle;
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
interface AsyncSingleParams {
|
|
@@ -181,6 +189,8 @@ interface AsyncSingleParams {
|
|
|
181
189
|
/** Raw caller-facing goal used only by the started event. */
|
|
182
190
|
goal?: string;
|
|
183
191
|
agentConfig: AgentConfig;
|
|
192
|
+
/** Agent contract before per-run bridge injection, used only for recovery persistence. */
|
|
193
|
+
recoveryAgentConfig?: AgentConfig;
|
|
184
194
|
ctx: AsyncExecutionContext;
|
|
185
195
|
cwd?: string;
|
|
186
196
|
maxOutput?: MaxOutputConfig;
|
|
@@ -208,6 +218,7 @@ interface AsyncSingleParams {
|
|
|
208
218
|
worktreeSetupHookTimeoutMs?: number;
|
|
209
219
|
worktreeBaseDir?: string;
|
|
210
220
|
controlConfig?: ResolvedControlConfig;
|
|
221
|
+
intercomBridge?: IntercomBridgeConfig;
|
|
211
222
|
controlIntercomTarget?: string;
|
|
212
223
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
213
224
|
nestedRoute?: NestedRouteInfo;
|
|
@@ -220,8 +231,10 @@ interface AsyncSingleParams {
|
|
|
220
231
|
configToolBudget?: ResolvedToolBudget;
|
|
221
232
|
allowZeroToolBudget?: boolean;
|
|
222
233
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
234
|
+
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
223
235
|
parentWorkflowRunId?: string;
|
|
224
236
|
workflowKey?: string;
|
|
237
|
+
activeAsyncCapacity?: ActiveAsyncCapacityHandle;
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
interface AsyncExecutionResult {
|
|
@@ -291,27 +304,6 @@ export function isAsyncAvailable(): boolean {
|
|
|
291
304
|
return jitiCliPath !== undefined;
|
|
292
305
|
}
|
|
293
306
|
|
|
294
|
-
function isNodeExecutableName(execPath: string): boolean {
|
|
295
|
-
const basename = path.basename(execPath).toLowerCase();
|
|
296
|
-
return basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe";
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function canUseCurrentNodeExecutable(execPath: string): boolean {
|
|
300
|
-
try {
|
|
301
|
-
fs.accessSync(execPath, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
|
|
302
|
-
return true;
|
|
303
|
-
} catch {
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function resolveAsyncRunnerNodeCommand(): string {
|
|
309
|
-
if (isNodeExecutableName(process.execPath) && canUseCurrentNodeExecutable(process.execPath)) {
|
|
310
|
-
return process.execPath;
|
|
311
|
-
}
|
|
312
|
-
return process.platform === "win32" ? "node.exe" : "node";
|
|
313
|
-
}
|
|
314
|
-
|
|
315
307
|
export function resolveAsyncRunnerLogPaths(cfg: object): { stdoutPath: string; stderrPath: string } | undefined {
|
|
316
308
|
const asyncDir = typeof (cfg as { asyncDir?: unknown }).asyncDir === "string"
|
|
317
309
|
? (cfg as { asyncDir: string }).asyncDir
|
|
@@ -343,7 +335,7 @@ type RunnerStartupState = "ready" | "acknowledged";
|
|
|
343
335
|
|
|
344
336
|
type RunnerStartupWaitResult =
|
|
345
337
|
| { ok: true; token: string }
|
|
346
|
-
| { ok: false; error: string };
|
|
338
|
+
| { ok: false; error: string; startupDidNotProceed?: boolean };
|
|
347
339
|
|
|
348
340
|
function waitForStartupInterval(delayMs = 20): void {
|
|
349
341
|
if (RUNNER_STARTUP_WAIT_VIEW) {
|
|
@@ -360,14 +352,14 @@ function readRunnerStartup(startupPath: string, expectedState: RunnerStartupStat
|
|
|
360
352
|
if (!fs.existsSync(startupPath)) return undefined;
|
|
361
353
|
try {
|
|
362
354
|
const payload = JSON.parse(fs.readFileSync(startupPath, "utf-8")) as { state?: unknown; token?: unknown; error?: unknown };
|
|
363
|
-
if (payload.state === "error" && typeof payload.error === "string") return { ok: false, error: payload.error };
|
|
355
|
+
if (payload.state === "error" && typeof payload.error === "string") return { ok: false, error: payload.error, startupDidNotProceed: true };
|
|
364
356
|
if (payload.state !== expectedState) return undefined;
|
|
365
357
|
if (typeof payload.token !== "string" || (expectedToken !== undefined && payload.token !== expectedToken)) {
|
|
366
|
-
return { ok: false, error: `Async runner wrote an invalid ${expectedState} startup handshake: ${startupPath}
|
|
358
|
+
return { ok: false, error: `Async runner wrote an invalid ${expectedState} startup handshake: ${startupPath}`, startupDidNotProceed: true };
|
|
367
359
|
}
|
|
368
360
|
return { ok: true, token: payload.token };
|
|
369
361
|
} catch (error) {
|
|
370
|
-
return { ok: false, error: `Failed to read async runner startup handshake '${startupPath}': ${error instanceof Error ? error.message : String(error)}
|
|
362
|
+
return { ok: false, error: `Failed to read async runner startup handshake '${startupPath}': ${error instanceof Error ? error.message : String(error)}`, startupDidNotProceed: true };
|
|
371
363
|
}
|
|
372
364
|
}
|
|
373
365
|
|
|
@@ -381,7 +373,7 @@ function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupS
|
|
|
381
373
|
}
|
|
382
374
|
const finalResult = readRunnerStartup(startupPath, expectedState, expectedToken);
|
|
383
375
|
if (finalResult) return finalResult;
|
|
384
|
-
return { ok: false, error: `Timed out after ${timeoutMs}ms waiting for the async runner startup state '${expectedState}'
|
|
376
|
+
return { ok: false, error: `Timed out after ${timeoutMs}ms waiting for the async runner startup state '${expectedState}'.`, startupDidNotProceed: true };
|
|
385
377
|
}
|
|
386
378
|
|
|
387
379
|
function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "proceed"; token: string }): void {
|
|
@@ -402,20 +394,63 @@ function runnerIsAlive(pid: number): boolean {
|
|
|
402
394
|
}
|
|
403
395
|
}
|
|
404
396
|
|
|
405
|
-
function terminateRunnerBeforeProceed(pid: number):
|
|
397
|
+
function terminateRunnerBeforeProceed(pid: number): boolean {
|
|
406
398
|
for (const signal of ["SIGTERM", "SIGKILL"] as const) {
|
|
407
|
-
if (!runnerIsAlive(pid)) return;
|
|
399
|
+
if (!runnerIsAlive(pid)) return true;
|
|
408
400
|
try {
|
|
409
401
|
process.kill(pid, signal);
|
|
410
402
|
} catch {
|
|
411
|
-
if (!runnerIsAlive(pid)) return;
|
|
403
|
+
if (!runnerIsAlive(pid)) return true;
|
|
412
404
|
}
|
|
413
405
|
const deadline = Date.now() + 1000;
|
|
414
406
|
while (runnerIsAlive(pid) && Date.now() < deadline) waitForStartupInterval();
|
|
415
407
|
}
|
|
408
|
+
return !runnerIsAlive(pid);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function persistPreProceedStartupFailure(asyncDir: string, runId: string, runnerProcessInstanceId: string, sessionId: string | undefined, message: string): void {
|
|
412
|
+
const now = Date.now();
|
|
413
|
+
try {
|
|
414
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
415
|
+
let status: Partial<AsyncStatus> = {};
|
|
416
|
+
try {
|
|
417
|
+
status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as Partial<AsyncStatus>;
|
|
418
|
+
} catch {}
|
|
419
|
+
writePrivateAtomicJson(statusPath, {
|
|
420
|
+
...status,
|
|
421
|
+
runId,
|
|
422
|
+
...(sessionId ? { sessionId } : {}),
|
|
423
|
+
state: "failed",
|
|
424
|
+
lastUpdate: now,
|
|
425
|
+
error: message,
|
|
426
|
+
processTerminal: {
|
|
427
|
+
version: 1,
|
|
428
|
+
state: "not-started",
|
|
429
|
+
runId,
|
|
430
|
+
runnerProcessInstanceId,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
writePrivateAtomicJson(path.join(asyncDir, "process-terminal-candidate.json"), {
|
|
434
|
+
version: 1,
|
|
435
|
+
runId,
|
|
436
|
+
runnerProcessInstanceId,
|
|
437
|
+
writers: {},
|
|
438
|
+
expectedWriters: { 0: 0 },
|
|
439
|
+
});
|
|
440
|
+
} catch {
|
|
441
|
+
// Startup failures must still return the original launch error.
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
interface SpawnRunnerResult {
|
|
446
|
+
pid?: number;
|
|
447
|
+
runnerProcessInstanceId?: string;
|
|
448
|
+
error?: string;
|
|
449
|
+
terminationObserved?: boolean;
|
|
450
|
+
startupDidNotProceed?: boolean;
|
|
416
451
|
}
|
|
417
452
|
|
|
418
|
-
function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal?: (proof: unknown) => void):
|
|
453
|
+
function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal?: (proof: unknown) => void): SpawnRunnerResult {
|
|
419
454
|
if (!jitiCliPath) {
|
|
420
455
|
return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
|
|
421
456
|
}
|
|
@@ -435,9 +470,11 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
435
470
|
const launchConfig = { ...cfg, runnerProcessInstanceId };
|
|
436
471
|
fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
|
|
437
472
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
438
|
-
const nodeCommand =
|
|
439
|
-
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; revivalLease?: unknown };
|
|
473
|
+
const nodeCommand = resolveNodeExecutable();
|
|
474
|
+
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; revivalLease?: unknown };
|
|
440
475
|
const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
|
|
476
|
+
const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
|
|
477
|
+
const launchSessionId = typeof launchForStartup.sessionId === "string" ? launchForStartup.sessionId : undefined;
|
|
441
478
|
const startupPath = typeof launchForStartup.revivalLease === "object" && launchAsyncDir
|
|
442
479
|
? path.join(launchAsyncDir, "runner-startup.json")
|
|
443
480
|
: undefined;
|
|
@@ -526,27 +563,36 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
526
563
|
}
|
|
527
564
|
proc.unref();
|
|
528
565
|
if (startupPath && startupAckPath && startupProceedPath) {
|
|
566
|
+
const persistStartupFailure = (message: string) => {
|
|
567
|
+
if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, message);
|
|
568
|
+
};
|
|
529
569
|
const ready = waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS);
|
|
530
570
|
if (ready.ok === false) {
|
|
531
|
-
|
|
532
|
-
|
|
571
|
+
persistStartupFailure(ready.error);
|
|
572
|
+
const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
|
|
573
|
+
return { pid: proc.pid, runnerProcessInstanceId, error: ready.error, terminationObserved, startupDidNotProceed: ready.startupDidNotProceed };
|
|
533
574
|
}
|
|
534
575
|
try {
|
|
535
576
|
writeRunnerStartupControl(startupAckPath, { action: "ack", token: ready.token });
|
|
536
577
|
} catch (error) {
|
|
537
|
-
|
|
538
|
-
|
|
578
|
+
const message = `Failed to acknowledge async runner startup: ${error instanceof Error ? error.message : String(error)}`;
|
|
579
|
+
persistStartupFailure(message);
|
|
580
|
+
const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
|
|
581
|
+
return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
|
|
539
582
|
}
|
|
540
583
|
const acknowledged = waitForRunnerStartup(startupPath, "acknowledged", RUNNER_STARTUP_TIMEOUT_MS, ready.token);
|
|
541
584
|
if (acknowledged.ok === false) {
|
|
542
|
-
|
|
543
|
-
|
|
585
|
+
persistStartupFailure(acknowledged.error);
|
|
586
|
+
const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
|
|
587
|
+
return { pid: proc.pid, runnerProcessInstanceId, error: acknowledged.error, terminationObserved, startupDidNotProceed: acknowledged.startupDidNotProceed };
|
|
544
588
|
}
|
|
545
589
|
try {
|
|
546
590
|
writeRunnerStartupControl(startupProceedPath, { action: "proceed", token: ready.token });
|
|
547
591
|
} catch (error) {
|
|
548
|
-
|
|
549
|
-
|
|
592
|
+
const message = `Failed to authorize async runner startup: ${error instanceof Error ? error.message : String(error)}`;
|
|
593
|
+
persistStartupFailure(message);
|
|
594
|
+
const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
|
|
595
|
+
return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
|
|
550
596
|
}
|
|
551
597
|
try {
|
|
552
598
|
fs.rmSync(startupPath, { force: true });
|
|
@@ -554,7 +600,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
554
600
|
// Proceed is the commit point; handshake cleanup cannot turn a running revival into a start error.
|
|
555
601
|
}
|
|
556
602
|
}
|
|
557
|
-
return { pid: proc.pid };
|
|
603
|
+
return { pid: proc.pid, runnerProcessInstanceId };
|
|
558
604
|
} catch (error) {
|
|
559
605
|
closeFd(stdoutFd);
|
|
560
606
|
closeFd(stderrFd);
|
|
@@ -648,7 +694,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
648
694
|
...(s.model !== undefined ? { model: s.model } : {}),
|
|
649
695
|
};
|
|
650
696
|
};
|
|
651
|
-
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }) => {
|
|
697
|
+
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }, runFanoutPath?: string) => {
|
|
652
698
|
const a = agents.find((x) => x.name === s.agent)!;
|
|
653
699
|
const externalRunner = a.runner?.type === "external-cli";
|
|
654
700
|
if (externalRunner) {
|
|
@@ -748,6 +794,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
748
794
|
parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
|
|
749
795
|
permissionRules,
|
|
750
796
|
...(params.capabilityCeiling ? { capabilityCeiling: params.capabilityCeiling } : {}),
|
|
797
|
+
...(runFanoutPath ? { runFanoutPath } : {}),
|
|
751
798
|
agent: s.agent,
|
|
752
799
|
task,
|
|
753
800
|
...(a.runner ? { runner: a.runner } : {}),
|
|
@@ -839,7 +886,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
839
886
|
}
|
|
840
887
|
}
|
|
841
888
|
const staticStep = nextFlatStep();
|
|
842
|
-
return buildSeqStep({ ...t, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex });
|
|
889
|
+
return buildSeqStep({ ...t, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex }, resultMode === "parallel" ? `tasks[${taskIndex}]` : `chain[${stepIndex}].parallel[${taskIndex}]`);
|
|
843
890
|
}),
|
|
844
891
|
concurrency: s.concurrency,
|
|
845
892
|
failFast: s.failFast,
|
|
@@ -884,7 +931,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
884
931
|
};
|
|
885
932
|
}
|
|
886
933
|
const staticStep = nextFlatStep();
|
|
887
|
-
return buildSeqStep(s as SequentialStep, staticStep.sessionFile, undefined, false, undefined, staticStep.index);
|
|
934
|
+
return buildSeqStep(s as SequentialStep, staticStep.sessionFile, undefined, false, undefined, staticStep.index, undefined, `chain[${stepIndex}]`);
|
|
888
935
|
});
|
|
889
936
|
const steps = params.attachRoot
|
|
890
937
|
? [{
|
|
@@ -966,8 +1013,11 @@ export function executeAsyncChain(
|
|
|
966
1013
|
const asyncDir = inheritedNestedRoute
|
|
967
1014
|
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
968
1015
|
: path.join(DIRS.async, id);
|
|
1016
|
+
let runFanoutBudget: RunFanoutBudgetDescriptor;
|
|
969
1017
|
try {
|
|
1018
|
+
runFanoutBudget = params.runFanoutBudget ?? createRunFanoutBudget(id, 64);
|
|
970
1019
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
1020
|
+
writeRunFanoutBudgetDescriptor(asyncDir, runFanoutBudget);
|
|
971
1021
|
} catch (error) {
|
|
972
1022
|
const message = error instanceof Error ? error.message : String(error);
|
|
973
1023
|
return {
|
|
@@ -1030,7 +1080,7 @@ export function executeAsyncChain(
|
|
|
1030
1080
|
return "agent" in step ? [childIntercomTarget(step.agent, childTargetIndex++)] : [undefined];
|
|
1031
1081
|
}) : undefined;
|
|
1032
1082
|
|
|
1033
|
-
let spawnResult:
|
|
1083
|
+
let spawnResult: SpawnRunnerResult = {};
|
|
1034
1084
|
try {
|
|
1035
1085
|
spawnResult = spawnRunner(
|
|
1036
1086
|
{
|
|
@@ -1063,6 +1113,7 @@ export function executeAsyncChain(
|
|
|
1063
1113
|
timeoutMs: params.timeoutMs,
|
|
1064
1114
|
deadlineAt,
|
|
1065
1115
|
globalConcurrencyLimit: params.globalConcurrencyLimit,
|
|
1116
|
+
runFanoutBudget,
|
|
1066
1117
|
workflowGraph,
|
|
1067
1118
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
1068
1119
|
...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
|
|
@@ -1079,13 +1130,21 @@ export function executeAsyncChain(
|
|
|
1079
1130
|
(proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
|
|
1080
1131
|
);
|
|
1081
1132
|
} catch (error) {
|
|
1133
|
+
params.activeAsyncCapacity?.rollback();
|
|
1082
1134
|
const message = error instanceof Error ? error.message : String(error);
|
|
1083
1135
|
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${message}`);
|
|
1084
1136
|
}
|
|
1085
1137
|
|
|
1086
1138
|
if (spawnResult.error) {
|
|
1139
|
+
if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
|
|
1140
|
+
else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
|
|
1087
1141
|
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${spawnResult.error}`);
|
|
1088
1142
|
}
|
|
1143
|
+
if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) {
|
|
1144
|
+
params.activeAsyncCapacity?.rollback();
|
|
1145
|
+
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': runner identity unavailable`);
|
|
1146
|
+
}
|
|
1147
|
+
params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
|
|
1089
1148
|
|
|
1090
1149
|
if (spawnResult.pid) {
|
|
1091
1150
|
const eventFirstStep = eventChain[0];
|
|
@@ -1166,8 +1225,8 @@ export function executeAsyncChain(
|
|
|
1166
1225
|
mode: resultMode,
|
|
1167
1226
|
agent: firstAgents[0],
|
|
1168
1227
|
agents: flatAgents,
|
|
1169
|
-
task: firstTask?.
|
|
1170
|
-
goal: workflowGoal?.
|
|
1228
|
+
task: firstTask?.trim() ? PROMPT_REDACTED : undefined,
|
|
1229
|
+
goal: workflowGoal?.trim() ? PROMPT_REDACTED : undefined,
|
|
1171
1230
|
chain: eventChain.map((s) =>
|
|
1172
1231
|
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
|
|
1173
1232
|
),
|
|
@@ -1176,6 +1235,7 @@ export function executeAsyncChain(
|
|
|
1176
1235
|
workflowGraph,
|
|
1177
1236
|
cwd: runnerCwd,
|
|
1178
1237
|
asyncDir,
|
|
1238
|
+
...(sessionRoot ? { sessionRoot } : {}),
|
|
1179
1239
|
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
1180
1240
|
...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
|
|
1181
1241
|
...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
|
|
@@ -1279,8 +1339,11 @@ export function executeAsyncSingle(
|
|
|
1279
1339
|
const asyncDir = inheritedNestedRoute
|
|
1280
1340
|
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
1281
1341
|
: path.join(DIRS.async, id);
|
|
1342
|
+
let runFanoutBudget: RunFanoutBudgetDescriptor;
|
|
1282
1343
|
try {
|
|
1344
|
+
runFanoutBudget = params.runFanoutBudget ?? createRunFanoutBudget(id, 64);
|
|
1283
1345
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
1346
|
+
writeRunFanoutBudgetDescriptor(asyncDir, runFanoutBudget);
|
|
1284
1347
|
} catch (error) {
|
|
1285
1348
|
const message = error instanceof Error ? error.message : String(error);
|
|
1286
1349
|
return {
|
|
@@ -1373,9 +1436,11 @@ export function executeAsyncSingle(
|
|
|
1373
1436
|
async: true,
|
|
1374
1437
|
agentContract: params.agentContract,
|
|
1375
1438
|
});
|
|
1439
|
+
const recoveryAgentConfig = params.recoveryAgentConfig ?? agentConfig;
|
|
1376
1440
|
const recoveryDescriptor: SteeringRecoveryDescriptor = {
|
|
1377
1441
|
version: 1,
|
|
1378
1442
|
launchContractDigest,
|
|
1443
|
+
runFanoutBudget,
|
|
1379
1444
|
sourceRunId: id,
|
|
1380
1445
|
...(params.agentContract ? { agentContract: params.agentContract } : {}),
|
|
1381
1446
|
agent,
|
|
@@ -1383,30 +1448,31 @@ export function executeAsyncSingle(
|
|
|
1383
1448
|
...(sessionFile ? { sessionFile } : {}),
|
|
1384
1449
|
cwd: runnerCwd,
|
|
1385
1450
|
...(model ? { model } : {}),
|
|
1386
|
-
...(
|
|
1451
|
+
...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
|
|
1387
1452
|
...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
|
|
1388
|
-
...(
|
|
1389
|
-
...(
|
|
1390
|
-
...(
|
|
1391
|
-
...(
|
|
1392
|
-
...(
|
|
1393
|
-
systemPromptMode:
|
|
1394
|
-
inheritProjectContext:
|
|
1395
|
-
inheritSkills:
|
|
1453
|
+
...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
|
|
1454
|
+
...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
|
|
1455
|
+
...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
|
|
1456
|
+
...(recoveryAgentConfig.mcpDirectTools ? { mcpDirectTools: [...recoveryAgentConfig.mcpDirectTools] } : {}),
|
|
1457
|
+
...(recoveryAgentConfig.systemPrompt ? { systemPrompt: recoveryAgentConfig.systemPrompt } : {}),
|
|
1458
|
+
systemPromptMode: recoveryAgentConfig.systemPromptMode,
|
|
1459
|
+
inheritProjectContext: recoveryAgentConfig.inheritProjectContext,
|
|
1460
|
+
inheritSkills: recoveryAgentConfig.inheritSkills,
|
|
1396
1461
|
...(resolvedSkills.length ? { skills: resolvedSkills.map((skill) => skill.name) } : {}),
|
|
1397
|
-
...(
|
|
1398
|
-
...(
|
|
1399
|
-
...(
|
|
1400
|
-
...(
|
|
1462
|
+
...(recoveryAgentConfig.skillPath ? { skillPath: [...recoveryAgentConfig.skillPath] } : {}),
|
|
1463
|
+
...(recoveryAgentConfig.filePath ? { agentFilePath: recoveryAgentConfig.filePath } : {}),
|
|
1464
|
+
...(recoveryAgentConfig.completionGuard !== undefined ? { completionGuard: recoveryAgentConfig.completionGuard } : {}),
|
|
1465
|
+
...(recoveryAgentConfig.memory ? { memory: { ...recoveryAgentConfig.memory } } : {}),
|
|
1401
1466
|
...(outputPath ? { outputPath } : {}),
|
|
1402
1467
|
outputMode,
|
|
1403
1468
|
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
1404
1469
|
...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
|
|
1405
1470
|
...(controlConfig ? { controlConfig } : {}),
|
|
1471
|
+
...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
|
|
1406
1472
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1407
1473
|
...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
|
|
1408
1474
|
...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
|
|
1409
|
-
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth,
|
|
1475
|
+
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, recoveryAgentConfig.maxSubagentDepth),
|
|
1410
1476
|
...(maxOutput ? { maxOutput } : {}),
|
|
1411
1477
|
share: shareEnabled,
|
|
1412
1478
|
...(resolvedSessionDir ? { sessionDir: resolvedSessionDir } : {}),
|
|
@@ -1421,7 +1487,7 @@ export function executeAsyncSingle(
|
|
|
1421
1487
|
return formatAsyncStartError("single", `Failed to persist async recovery descriptor for '${id}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1422
1488
|
}
|
|
1423
1489
|
}
|
|
1424
|
-
let spawnResult:
|
|
1490
|
+
let spawnResult: SpawnRunnerResult = {};
|
|
1425
1491
|
try {
|
|
1426
1492
|
spawnResult = spawnRunner(
|
|
1427
1493
|
{
|
|
@@ -1494,6 +1560,7 @@ export function executeAsyncSingle(
|
|
|
1494
1560
|
resultMode: "single",
|
|
1495
1561
|
launchContractDigest,
|
|
1496
1562
|
launchResolvedExtensions,
|
|
1563
|
+
runFanoutBudget,
|
|
1497
1564
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
1498
1565
|
...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
|
|
1499
1566
|
...(params.revivalLease ? { revivalLease: params.revivalLease } : {}),
|
|
@@ -1510,13 +1577,21 @@ export function executeAsyncSingle(
|
|
|
1510
1577
|
(proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
|
|
1511
1578
|
);
|
|
1512
1579
|
} catch (error) {
|
|
1580
|
+
params.activeAsyncCapacity?.rollback();
|
|
1513
1581
|
const message = error instanceof Error ? error.message : String(error);
|
|
1514
1582
|
return formatAsyncStartError("single", `Failed to start async run '${id}': ${message}`);
|
|
1515
1583
|
}
|
|
1516
1584
|
|
|
1517
1585
|
if (spawnResult.error) {
|
|
1586
|
+
if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
|
|
1587
|
+
else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
|
|
1518
1588
|
return formatAsyncStartError("single", `Failed to start async run '${id}': ${spawnResult.error}`);
|
|
1519
1589
|
}
|
|
1590
|
+
if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) {
|
|
1591
|
+
params.activeAsyncCapacity?.rollback();
|
|
1592
|
+
return formatAsyncStartError("single", `Failed to start async run '${id}': runner identity unavailable`);
|
|
1593
|
+
}
|
|
1594
|
+
params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
|
|
1520
1595
|
|
|
1521
1596
|
if (spawnResult.pid) {
|
|
1522
1597
|
if (inheritedNestedRoute && nestedAddress) {
|
|
@@ -1562,10 +1637,11 @@ export function executeAsyncSingle(
|
|
|
1562
1637
|
sessionId: ctx.currentSessionId,
|
|
1563
1638
|
mode: "single",
|
|
1564
1639
|
agent,
|
|
1565
|
-
task: task?.
|
|
1566
|
-
goal: (params.goal ?? task).
|
|
1640
|
+
task: task?.trim() ? PROMPT_REDACTED : undefined,
|
|
1641
|
+
goal: (params.goal ?? task).trim() ? PROMPT_REDACTED : undefined,
|
|
1567
1642
|
cwd: runnerCwd,
|
|
1568
1643
|
asyncDir,
|
|
1644
|
+
...(sessionRoot ? { sessionRoot } : {}),
|
|
1569
1645
|
launchContractDigest,
|
|
1570
1646
|
launchResolvedExtensions,
|
|
1571
1647
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
@@ -557,10 +557,13 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
557
557
|
const agents = firstGroupCount && firstGroupCount > 0
|
|
558
558
|
? rawAgents?.slice(0, firstGroupCount)
|
|
559
559
|
: rawAgents;
|
|
560
|
+
const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
|
|
561
|
+
state.liveAsyncSessionRoots?.delete(info.id);
|
|
560
562
|
state.asyncJobs.set(info.id, {
|
|
561
563
|
asyncId: info.id,
|
|
562
564
|
asyncDir,
|
|
563
565
|
...(typeof info.cwd === "string" ? { cwd: path.resolve(info.cwd) } : {}),
|
|
566
|
+
...(sessionRoot ? { sessionRoot } : {}),
|
|
564
567
|
status: "queued",
|
|
565
568
|
pid: typeof info.pid === "number" ? info.pid : undefined,
|
|
566
569
|
...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
|
|
@@ -637,6 +640,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
637
640
|
state.asyncJobs.clear();
|
|
638
641
|
state.fleetJobs?.clear();
|
|
639
642
|
state.foregroundControls?.clear();
|
|
643
|
+
state.liveAsyncSessionRoots?.clear();
|
|
640
644
|
state.lastForegroundControlId = null;
|
|
641
645
|
state.resultFileCoalescer.clear();
|
|
642
646
|
if (ctx?.hasUI) {
|
|
@@ -5,6 +5,7 @@ import type { AgentConfig } from "../../agents/agents.ts";
|
|
|
5
5
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
6
6
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
7
7
|
import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
8
|
+
import { validateRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
8
9
|
import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
|
|
9
10
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
10
11
|
|
|
@@ -306,8 +307,8 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
306
307
|
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
|
|
307
308
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
308
309
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
309
|
-
"artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
310
|
-
"launchResolvedExtensions",
|
|
310
|
+
"artifactsDir", "maxOutput", "controlConfig", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
311
|
+
"launchResolvedExtensions", "runFanoutBudget",
|
|
311
312
|
]);
|
|
312
313
|
for (const field of Object.keys(parsed)) {
|
|
313
314
|
if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
|
|
@@ -317,6 +318,11 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
317
318
|
if (typeof parsed[field] !== "string" || !(parsed[field] as string).trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
|
|
318
319
|
}
|
|
319
320
|
if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
|
|
321
|
+
try {
|
|
322
|
+
parsed.runFanoutBudget = validateRunFanoutBudgetDescriptor(parsed.runFanoutBudget);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
}
|
|
320
326
|
if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
|
|
321
327
|
if (parsed.agentContract !== undefined) {
|
|
322
328
|
if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
|
|
@@ -368,6 +374,16 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
368
374
|
if (artifact.includeTranscript !== undefined && typeof artifact.includeTranscript !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.includeTranscript must be a boolean.`);
|
|
369
375
|
if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
|
|
370
376
|
}
|
|
377
|
+
if (parsed.intercomBridge !== undefined) {
|
|
378
|
+
if (!parsed.intercomBridge || typeof parsed.intercomBridge !== "object" || Array.isArray(parsed.intercomBridge)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge must be an object.`);
|
|
379
|
+
const bridge = parsed.intercomBridge as Record<string, unknown>;
|
|
380
|
+
for (const field of Object.keys(bridge)) {
|
|
381
|
+
if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
|
|
382
|
+
}
|
|
383
|
+
if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.mode is invalid.`);
|
|
384
|
+
if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
|
|
385
|
+
if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
|
|
386
|
+
}
|
|
371
387
|
if (parsed.controlConfig !== undefined) {
|
|
372
388
|
if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
|
|
373
389
|
const control = parsed.controlConfig as Record<string, unknown>;
|
|
@@ -7,11 +7,12 @@ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from
|
|
|
7
7
|
import { pruneStatusCacheForAsyncRoot, readStatus } from "../../shared/utils.ts";
|
|
8
8
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
9
9
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
10
|
+
import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
10
11
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
11
12
|
import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
|
|
12
13
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
13
14
|
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
14
|
-
import { ACTIVE_RUN_INDEX_DIR, isActiveAsyncState, readActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
15
|
+
import { ACTIVE_RUN_INDEX_DIR, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
15
16
|
|
|
16
17
|
interface AsyncRunStepSummary {
|
|
17
18
|
index: number;
|
|
@@ -106,6 +107,7 @@ export interface AsyncRunSummary {
|
|
|
106
107
|
nestedChildren?: NestedRunSummary[];
|
|
107
108
|
nestedWarnings?: string[];
|
|
108
109
|
processTerminal?: AsyncStatus["processTerminal"];
|
|
110
|
+
runFanoutBudget?: AsyncStatus["runFanoutBudget"];
|
|
109
111
|
launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
|
|
110
112
|
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
|
|
111
113
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
@@ -222,6 +224,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
222
224
|
const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
|
|
223
225
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
|
|
224
226
|
?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
|
|
227
|
+
const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
|
|
228
|
+
const runFanoutBudget = runFanoutBudgetDescriptor ? getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor) : status.runFanoutBudget;
|
|
225
229
|
const steps = status.steps ?? [];
|
|
226
230
|
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
227
231
|
const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
|
|
@@ -327,6 +331,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
327
331
|
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
328
332
|
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
329
333
|
...(processTerminal ? { processTerminal } : {}),
|
|
334
|
+
...(runFanoutBudget ? { runFanoutBudget } : {}),
|
|
330
335
|
...(status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {}),
|
|
331
336
|
...(status.launchResolvedExtensions ? { launchResolvedExtensions: status.launchResolvedExtensions } : {}),
|
|
332
337
|
...(status.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: status.runtimeAcknowledgedExtensions } : {}),
|
|
@@ -452,11 +457,11 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
452
457
|
if (usedActiveIndex) updateActiveRunIndex(asyncDir, "failed");
|
|
453
458
|
continue;
|
|
454
459
|
}
|
|
455
|
-
if (status.
|
|
456
|
-
|
|
457
|
-
|
|
460
|
+
if (usedActiveIndex && !isActiveAsyncState(status.state)) {
|
|
461
|
+
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
|
|
462
|
+
if (processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
458
463
|
}
|
|
459
|
-
if (
|
|
464
|
+
if (status.displayDismissedAt !== undefined) continue;
|
|
460
465
|
// Filter before the nested-route lookup: the lookup builds an index over
|
|
461
466
|
// the nested-events directory, so deferring it for filtered-out runs keeps
|
|
462
467
|
// restoration at load from scanning that directory when no active runs
|
|
@@ -559,6 +564,7 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
|
|
|
559
564
|
const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
560
565
|
const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
561
566
|
lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
|
|
567
|
+
if (run.runFanoutBudget) lines.push(` ${formatRunFanoutBudget(run.runFanoutBudget)}`);
|
|
562
568
|
if (run.error) lines.push(` Error: ${run.error}`);
|
|
563
569
|
for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
|
|
564
570
|
const outputPath = formatAsyncRunOutputPath(run);
|