@selesai/code 0.8.4 → 0.8.6
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 +22 -0
- package/README.md +7 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/defaults/models.json +46 -55
- package/dist/defaults/settings.json +7 -8
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
- package/dist/extensions/pi-subagents/README.md +2 -0
- package/dist/extensions/pi-subagents/agents/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
- package/dist/extensions/pi-subagents/docs/observability.md +1 -1
- package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
- package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
- package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
- package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
- package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
- package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
- package/dist/extensions/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +18 -0
- package/dist/extensions/question/question-list.ts +22 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- package/dist/extensions/question/tests/helpers.test.ts +68 -35
- package/dist/extensions/question/tests/question-list.test.ts +506 -204
- package/dist/extensions/question/tests/row-layout.test.ts +180 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +48 -1
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/docs/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/quickstart.md +27 -43
- package/docs/settings.md +4 -0
- package/docs/usage.md +1 -0
- package/package.json +5 -1
|
@@ -21,7 +21,7 @@ import type { ContextMode } from "../shared/context-mode.ts";
|
|
|
21
21
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
22
22
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
23
23
|
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
24
|
-
import { SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV, resolveChildCwd } from "../../shared/utils.ts";
|
|
24
|
+
import { SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
25
25
|
import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
26
26
|
import type { ModelScopeConfig } from "../shared/model-scope.ts";
|
|
27
27
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
@@ -30,9 +30,11 @@ import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
|
30
30
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
|
|
31
31
|
import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
32
32
|
import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
33
|
+
import { createRunFanoutBudget, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
33
34
|
import {
|
|
34
35
|
type AcceptanceInput,
|
|
35
36
|
type AgentContract,
|
|
37
|
+
type AsyncStatus,
|
|
36
38
|
type ArtifactConfig,
|
|
37
39
|
type Details,
|
|
38
40
|
type JsonSchemaObject,
|
|
@@ -41,6 +43,7 @@ import {
|
|
|
41
43
|
type ResolvedControlConfig,
|
|
42
44
|
type ResolvedTurnBudget,
|
|
43
45
|
type ResolvedToolBudget,
|
|
46
|
+
type RunFanoutBudgetDescriptor,
|
|
44
47
|
type ToolBudgetConfig,
|
|
45
48
|
type SubagentRunMode,
|
|
46
49
|
type SteeringRecoveryDescriptor,
|
|
@@ -59,6 +62,7 @@ import { usageBudgetState } from "../shared/usage-budget.ts";
|
|
|
59
62
|
import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
60
63
|
import type { SessionLeaseRequest } from "../shared/session-lease.ts";
|
|
61
64
|
import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
|
|
65
|
+
import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
|
|
62
66
|
import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
|
|
63
67
|
import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
64
68
|
import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
@@ -171,8 +175,10 @@ interface AsyncChainParams {
|
|
|
171
175
|
/** Global cap on simultaneously-running subagent tasks within the async run. */
|
|
172
176
|
globalConcurrencyLimit?: number;
|
|
173
177
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
178
|
+
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
174
179
|
parentWorkflowRunId?: string;
|
|
175
180
|
workflowKey?: string;
|
|
181
|
+
activeAsyncCapacity?: ActiveAsyncCapacityHandle;
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
interface AsyncSingleParams {
|
|
@@ -220,8 +226,10 @@ interface AsyncSingleParams {
|
|
|
220
226
|
configToolBudget?: ResolvedToolBudget;
|
|
221
227
|
allowZeroToolBudget?: boolean;
|
|
222
228
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
229
|
+
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
223
230
|
parentWorkflowRunId?: string;
|
|
224
231
|
workflowKey?: string;
|
|
232
|
+
activeAsyncCapacity?: ActiveAsyncCapacityHandle;
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
interface AsyncExecutionResult {
|
|
@@ -343,7 +351,7 @@ type RunnerStartupState = "ready" | "acknowledged";
|
|
|
343
351
|
|
|
344
352
|
type RunnerStartupWaitResult =
|
|
345
353
|
| { ok: true; token: string }
|
|
346
|
-
| { ok: false; error: string };
|
|
354
|
+
| { ok: false; error: string; startupDidNotProceed?: boolean };
|
|
347
355
|
|
|
348
356
|
function waitForStartupInterval(delayMs = 20): void {
|
|
349
357
|
if (RUNNER_STARTUP_WAIT_VIEW) {
|
|
@@ -360,14 +368,14 @@ function readRunnerStartup(startupPath: string, expectedState: RunnerStartupStat
|
|
|
360
368
|
if (!fs.existsSync(startupPath)) return undefined;
|
|
361
369
|
try {
|
|
362
370
|
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 };
|
|
371
|
+
if (payload.state === "error" && typeof payload.error === "string") return { ok: false, error: payload.error, startupDidNotProceed: true };
|
|
364
372
|
if (payload.state !== expectedState) return undefined;
|
|
365
373
|
if (typeof payload.token !== "string" || (expectedToken !== undefined && payload.token !== expectedToken)) {
|
|
366
|
-
return { ok: false, error: `Async runner wrote an invalid ${expectedState} startup handshake: ${startupPath}
|
|
374
|
+
return { ok: false, error: `Async runner wrote an invalid ${expectedState} startup handshake: ${startupPath}`, startupDidNotProceed: true };
|
|
367
375
|
}
|
|
368
376
|
return { ok: true, token: payload.token };
|
|
369
377
|
} catch (error) {
|
|
370
|
-
return { ok: false, error: `Failed to read async runner startup handshake '${startupPath}': ${error instanceof Error ? error.message : String(error)}
|
|
378
|
+
return { ok: false, error: `Failed to read async runner startup handshake '${startupPath}': ${error instanceof Error ? error.message : String(error)}`, startupDidNotProceed: true };
|
|
371
379
|
}
|
|
372
380
|
}
|
|
373
381
|
|
|
@@ -381,7 +389,7 @@ function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupS
|
|
|
381
389
|
}
|
|
382
390
|
const finalResult = readRunnerStartup(startupPath, expectedState, expectedToken);
|
|
383
391
|
if (finalResult) return finalResult;
|
|
384
|
-
return { ok: false, error: `Timed out after ${timeoutMs}ms waiting for the async runner startup state '${expectedState}'
|
|
392
|
+
return { ok: false, error: `Timed out after ${timeoutMs}ms waiting for the async runner startup state '${expectedState}'.`, startupDidNotProceed: true };
|
|
385
393
|
}
|
|
386
394
|
|
|
387
395
|
function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "proceed"; token: string }): void {
|
|
@@ -402,20 +410,63 @@ function runnerIsAlive(pid: number): boolean {
|
|
|
402
410
|
}
|
|
403
411
|
}
|
|
404
412
|
|
|
405
|
-
function terminateRunnerBeforeProceed(pid: number):
|
|
413
|
+
function terminateRunnerBeforeProceed(pid: number): boolean {
|
|
406
414
|
for (const signal of ["SIGTERM", "SIGKILL"] as const) {
|
|
407
|
-
if (!runnerIsAlive(pid)) return;
|
|
415
|
+
if (!runnerIsAlive(pid)) return true;
|
|
408
416
|
try {
|
|
409
417
|
process.kill(pid, signal);
|
|
410
418
|
} catch {
|
|
411
|
-
if (!runnerIsAlive(pid)) return;
|
|
419
|
+
if (!runnerIsAlive(pid)) return true;
|
|
412
420
|
}
|
|
413
421
|
const deadline = Date.now() + 1000;
|
|
414
422
|
while (runnerIsAlive(pid) && Date.now() < deadline) waitForStartupInterval();
|
|
415
423
|
}
|
|
424
|
+
return !runnerIsAlive(pid);
|
|
416
425
|
}
|
|
417
426
|
|
|
418
|
-
function
|
|
427
|
+
function persistPreProceedStartupFailure(asyncDir: string, runId: string, runnerProcessInstanceId: string, sessionId: string | undefined, message: string): void {
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
try {
|
|
430
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
431
|
+
let status: Partial<AsyncStatus> = {};
|
|
432
|
+
try {
|
|
433
|
+
status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as Partial<AsyncStatus>;
|
|
434
|
+
} catch {}
|
|
435
|
+
writePrivateAtomicJson(statusPath, {
|
|
436
|
+
...status,
|
|
437
|
+
runId,
|
|
438
|
+
...(sessionId ? { sessionId } : {}),
|
|
439
|
+
state: "failed",
|
|
440
|
+
lastUpdate: now,
|
|
441
|
+
error: message,
|
|
442
|
+
processTerminal: {
|
|
443
|
+
version: 1,
|
|
444
|
+
state: "not-started",
|
|
445
|
+
runId,
|
|
446
|
+
runnerProcessInstanceId,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
writePrivateAtomicJson(path.join(asyncDir, "process-terminal-candidate.json"), {
|
|
450
|
+
version: 1,
|
|
451
|
+
runId,
|
|
452
|
+
runnerProcessInstanceId,
|
|
453
|
+
writers: {},
|
|
454
|
+
expectedWriters: { 0: 0 },
|
|
455
|
+
});
|
|
456
|
+
} catch {
|
|
457
|
+
// Startup failures must still return the original launch error.
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
interface SpawnRunnerResult {
|
|
462
|
+
pid?: number;
|
|
463
|
+
runnerProcessInstanceId?: string;
|
|
464
|
+
error?: string;
|
|
465
|
+
terminationObserved?: boolean;
|
|
466
|
+
startupDidNotProceed?: boolean;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal?: (proof: unknown) => void): SpawnRunnerResult {
|
|
419
470
|
if (!jitiCliPath) {
|
|
420
471
|
return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
|
|
421
472
|
}
|
|
@@ -436,8 +487,10 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
436
487
|
fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
|
|
437
488
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
438
489
|
const nodeCommand = resolveAsyncRunnerNodeCommand();
|
|
439
|
-
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; revivalLease?: unknown };
|
|
490
|
+
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; revivalLease?: unknown };
|
|
440
491
|
const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
|
|
492
|
+
const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
|
|
493
|
+
const launchSessionId = typeof launchForStartup.sessionId === "string" ? launchForStartup.sessionId : undefined;
|
|
441
494
|
const startupPath = typeof launchForStartup.revivalLease === "object" && launchAsyncDir
|
|
442
495
|
? path.join(launchAsyncDir, "runner-startup.json")
|
|
443
496
|
: undefined;
|
|
@@ -526,27 +579,36 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
526
579
|
}
|
|
527
580
|
proc.unref();
|
|
528
581
|
if (startupPath && startupAckPath && startupProceedPath) {
|
|
582
|
+
const persistStartupFailure = (message: string) => {
|
|
583
|
+
if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, message);
|
|
584
|
+
};
|
|
529
585
|
const ready = waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS);
|
|
530
586
|
if (ready.ok === false) {
|
|
531
|
-
|
|
532
|
-
|
|
587
|
+
persistStartupFailure(ready.error);
|
|
588
|
+
const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
|
|
589
|
+
return { pid: proc.pid, runnerProcessInstanceId, error: ready.error, terminationObserved, startupDidNotProceed: ready.startupDidNotProceed };
|
|
533
590
|
}
|
|
534
591
|
try {
|
|
535
592
|
writeRunnerStartupControl(startupAckPath, { action: "ack", token: ready.token });
|
|
536
593
|
} catch (error) {
|
|
537
|
-
|
|
538
|
-
|
|
594
|
+
const message = `Failed to acknowledge async runner startup: ${error instanceof Error ? error.message : String(error)}`;
|
|
595
|
+
persistStartupFailure(message);
|
|
596
|
+
const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
|
|
597
|
+
return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
|
|
539
598
|
}
|
|
540
599
|
const acknowledged = waitForRunnerStartup(startupPath, "acknowledged", RUNNER_STARTUP_TIMEOUT_MS, ready.token);
|
|
541
600
|
if (acknowledged.ok === false) {
|
|
542
|
-
|
|
543
|
-
|
|
601
|
+
persistStartupFailure(acknowledged.error);
|
|
602
|
+
const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
|
|
603
|
+
return { pid: proc.pid, runnerProcessInstanceId, error: acknowledged.error, terminationObserved, startupDidNotProceed: acknowledged.startupDidNotProceed };
|
|
544
604
|
}
|
|
545
605
|
try {
|
|
546
606
|
writeRunnerStartupControl(startupProceedPath, { action: "proceed", token: ready.token });
|
|
547
607
|
} catch (error) {
|
|
548
|
-
|
|
549
|
-
|
|
608
|
+
const message = `Failed to authorize async runner startup: ${error instanceof Error ? error.message : String(error)}`;
|
|
609
|
+
persistStartupFailure(message);
|
|
610
|
+
const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
|
|
611
|
+
return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
|
|
550
612
|
}
|
|
551
613
|
try {
|
|
552
614
|
fs.rmSync(startupPath, { force: true });
|
|
@@ -554,7 +616,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
554
616
|
// Proceed is the commit point; handshake cleanup cannot turn a running revival into a start error.
|
|
555
617
|
}
|
|
556
618
|
}
|
|
557
|
-
return { pid: proc.pid };
|
|
619
|
+
return { pid: proc.pid, runnerProcessInstanceId };
|
|
558
620
|
} catch (error) {
|
|
559
621
|
closeFd(stdoutFd);
|
|
560
622
|
closeFd(stderrFd);
|
|
@@ -648,7 +710,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
648
710
|
...(s.model !== undefined ? { model: s.model } : {}),
|
|
649
711
|
};
|
|
650
712
|
};
|
|
651
|
-
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }) => {
|
|
713
|
+
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }, runFanoutPath?: string) => {
|
|
652
714
|
const a = agents.find((x) => x.name === s.agent)!;
|
|
653
715
|
const externalRunner = a.runner?.type === "external-cli";
|
|
654
716
|
if (externalRunner) {
|
|
@@ -748,6 +810,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
748
810
|
parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
|
|
749
811
|
permissionRules,
|
|
750
812
|
...(params.capabilityCeiling ? { capabilityCeiling: params.capabilityCeiling } : {}),
|
|
813
|
+
...(runFanoutPath ? { runFanoutPath } : {}),
|
|
751
814
|
agent: s.agent,
|
|
752
815
|
task,
|
|
753
816
|
...(a.runner ? { runner: a.runner } : {}),
|
|
@@ -839,7 +902,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
839
902
|
}
|
|
840
903
|
}
|
|
841
904
|
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 });
|
|
905
|
+
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
906
|
}),
|
|
844
907
|
concurrency: s.concurrency,
|
|
845
908
|
failFast: s.failFast,
|
|
@@ -884,7 +947,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
884
947
|
};
|
|
885
948
|
}
|
|
886
949
|
const staticStep = nextFlatStep();
|
|
887
|
-
return buildSeqStep(s as SequentialStep, staticStep.sessionFile, undefined, false, undefined, staticStep.index);
|
|
950
|
+
return buildSeqStep(s as SequentialStep, staticStep.sessionFile, undefined, false, undefined, staticStep.index, undefined, `chain[${stepIndex}]`);
|
|
888
951
|
});
|
|
889
952
|
const steps = params.attachRoot
|
|
890
953
|
? [{
|
|
@@ -966,8 +1029,11 @@ export function executeAsyncChain(
|
|
|
966
1029
|
const asyncDir = inheritedNestedRoute
|
|
967
1030
|
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
968
1031
|
: path.join(DIRS.async, id);
|
|
1032
|
+
let runFanoutBudget: RunFanoutBudgetDescriptor;
|
|
969
1033
|
try {
|
|
1034
|
+
runFanoutBudget = params.runFanoutBudget ?? createRunFanoutBudget(id, 64);
|
|
970
1035
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
1036
|
+
writeRunFanoutBudgetDescriptor(asyncDir, runFanoutBudget);
|
|
971
1037
|
} catch (error) {
|
|
972
1038
|
const message = error instanceof Error ? error.message : String(error);
|
|
973
1039
|
return {
|
|
@@ -1030,7 +1096,7 @@ export function executeAsyncChain(
|
|
|
1030
1096
|
return "agent" in step ? [childIntercomTarget(step.agent, childTargetIndex++)] : [undefined];
|
|
1031
1097
|
}) : undefined;
|
|
1032
1098
|
|
|
1033
|
-
let spawnResult:
|
|
1099
|
+
let spawnResult: SpawnRunnerResult = {};
|
|
1034
1100
|
try {
|
|
1035
1101
|
spawnResult = spawnRunner(
|
|
1036
1102
|
{
|
|
@@ -1063,6 +1129,7 @@ export function executeAsyncChain(
|
|
|
1063
1129
|
timeoutMs: params.timeoutMs,
|
|
1064
1130
|
deadlineAt,
|
|
1065
1131
|
globalConcurrencyLimit: params.globalConcurrencyLimit,
|
|
1132
|
+
runFanoutBudget,
|
|
1066
1133
|
workflowGraph,
|
|
1067
1134
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
1068
1135
|
...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
|
|
@@ -1079,13 +1146,21 @@ export function executeAsyncChain(
|
|
|
1079
1146
|
(proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
|
|
1080
1147
|
);
|
|
1081
1148
|
} catch (error) {
|
|
1149
|
+
params.activeAsyncCapacity?.rollback();
|
|
1082
1150
|
const message = error instanceof Error ? error.message : String(error);
|
|
1083
1151
|
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${message}`);
|
|
1084
1152
|
}
|
|
1085
1153
|
|
|
1086
1154
|
if (spawnResult.error) {
|
|
1155
|
+
if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
|
|
1156
|
+
else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
|
|
1087
1157
|
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${spawnResult.error}`);
|
|
1088
1158
|
}
|
|
1159
|
+
if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) {
|
|
1160
|
+
params.activeAsyncCapacity?.rollback();
|
|
1161
|
+
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': runner identity unavailable`);
|
|
1162
|
+
}
|
|
1163
|
+
params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
|
|
1089
1164
|
|
|
1090
1165
|
if (spawnResult.pid) {
|
|
1091
1166
|
const eventFirstStep = eventChain[0];
|
|
@@ -1166,8 +1241,8 @@ export function executeAsyncChain(
|
|
|
1166
1241
|
mode: resultMode,
|
|
1167
1242
|
agent: firstAgents[0],
|
|
1168
1243
|
agents: flatAgents,
|
|
1169
|
-
task: firstTask?.
|
|
1170
|
-
goal: workflowGoal?.
|
|
1244
|
+
task: firstTask?.trim() ? PROMPT_REDACTED : undefined,
|
|
1245
|
+
goal: workflowGoal?.trim() ? PROMPT_REDACTED : undefined,
|
|
1171
1246
|
chain: eventChain.map((s) =>
|
|
1172
1247
|
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
|
|
1173
1248
|
),
|
|
@@ -1279,8 +1354,11 @@ export function executeAsyncSingle(
|
|
|
1279
1354
|
const asyncDir = inheritedNestedRoute
|
|
1280
1355
|
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
1281
1356
|
: path.join(DIRS.async, id);
|
|
1357
|
+
let runFanoutBudget: RunFanoutBudgetDescriptor;
|
|
1282
1358
|
try {
|
|
1359
|
+
runFanoutBudget = params.runFanoutBudget ?? createRunFanoutBudget(id, 64);
|
|
1283
1360
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
1361
|
+
writeRunFanoutBudgetDescriptor(asyncDir, runFanoutBudget);
|
|
1284
1362
|
} catch (error) {
|
|
1285
1363
|
const message = error instanceof Error ? error.message : String(error);
|
|
1286
1364
|
return {
|
|
@@ -1376,6 +1454,7 @@ export function executeAsyncSingle(
|
|
|
1376
1454
|
const recoveryDescriptor: SteeringRecoveryDescriptor = {
|
|
1377
1455
|
version: 1,
|
|
1378
1456
|
launchContractDigest,
|
|
1457
|
+
runFanoutBudget,
|
|
1379
1458
|
sourceRunId: id,
|
|
1380
1459
|
...(params.agentContract ? { agentContract: params.agentContract } : {}),
|
|
1381
1460
|
agent,
|
|
@@ -1421,7 +1500,7 @@ export function executeAsyncSingle(
|
|
|
1421
1500
|
return formatAsyncStartError("single", `Failed to persist async recovery descriptor for '${id}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1422
1501
|
}
|
|
1423
1502
|
}
|
|
1424
|
-
let spawnResult:
|
|
1503
|
+
let spawnResult: SpawnRunnerResult = {};
|
|
1425
1504
|
try {
|
|
1426
1505
|
spawnResult = spawnRunner(
|
|
1427
1506
|
{
|
|
@@ -1494,6 +1573,7 @@ export function executeAsyncSingle(
|
|
|
1494
1573
|
resultMode: "single",
|
|
1495
1574
|
launchContractDigest,
|
|
1496
1575
|
launchResolvedExtensions,
|
|
1576
|
+
runFanoutBudget,
|
|
1497
1577
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
1498
1578
|
...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
|
|
1499
1579
|
...(params.revivalLease ? { revivalLease: params.revivalLease } : {}),
|
|
@@ -1510,13 +1590,21 @@ export function executeAsyncSingle(
|
|
|
1510
1590
|
(proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
|
|
1511
1591
|
);
|
|
1512
1592
|
} catch (error) {
|
|
1593
|
+
params.activeAsyncCapacity?.rollback();
|
|
1513
1594
|
const message = error instanceof Error ? error.message : String(error);
|
|
1514
1595
|
return formatAsyncStartError("single", `Failed to start async run '${id}': ${message}`);
|
|
1515
1596
|
}
|
|
1516
1597
|
|
|
1517
1598
|
if (spawnResult.error) {
|
|
1599
|
+
if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId || (spawnResult.startupDidNotProceed && spawnResult.terminationObserved)) params.activeAsyncCapacity?.rollback();
|
|
1600
|
+
else params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
|
|
1518
1601
|
return formatAsyncStartError("single", `Failed to start async run '${id}': ${spawnResult.error}`);
|
|
1519
1602
|
}
|
|
1603
|
+
if (!spawnResult.pid || !spawnResult.runnerProcessInstanceId) {
|
|
1604
|
+
params.activeAsyncCapacity?.rollback();
|
|
1605
|
+
return formatAsyncStartError("single", `Failed to start async run '${id}': runner identity unavailable`);
|
|
1606
|
+
}
|
|
1607
|
+
params.activeAsyncCapacity?.markStarted(spawnResult.runnerProcessInstanceId);
|
|
1520
1608
|
|
|
1521
1609
|
if (spawnResult.pid) {
|
|
1522
1610
|
if (inheritedNestedRoute && nestedAddress) {
|
|
@@ -1562,8 +1650,8 @@ export function executeAsyncSingle(
|
|
|
1562
1650
|
sessionId: ctx.currentSessionId,
|
|
1563
1651
|
mode: "single",
|
|
1564
1652
|
agent,
|
|
1565
|
-
task: task?.
|
|
1566
|
-
goal: (params.goal ?? task).
|
|
1653
|
+
task: task?.trim() ? PROMPT_REDACTED : undefined,
|
|
1654
|
+
goal: (params.goal ?? task).trim() ? PROMPT_REDACTED : undefined,
|
|
1567
1655
|
cwd: runnerCwd,
|
|
1568
1656
|
asyncDir,
|
|
1569
1657
|
launchContractDigest,
|
|
@@ -54,7 +54,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
54
54
|
handleStarted: (data: unknown) => void;
|
|
55
55
|
handleComplete: (data: unknown) => void;
|
|
56
56
|
resetJobs: (ctx?: ExtensionContext) => void;
|
|
57
|
-
restoreActiveJobs: (ctx?: ExtensionContext) =>
|
|
57
|
+
restoreActiveJobs: (ctx?: ExtensionContext) => number;
|
|
58
58
|
dispose: () => void;
|
|
59
59
|
} {
|
|
60
60
|
const completionRetentionMs = options.completionRetentionMs ?? 10000;
|
|
@@ -645,26 +645,32 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
645
645
|
}
|
|
646
646
|
};
|
|
647
647
|
|
|
648
|
-
const restoreActiveJobs = (ctx?: ExtensionContext) => {
|
|
648
|
+
const restoreActiveJobs = (ctx?: ExtensionContext): number => {
|
|
649
649
|
if (ctx?.hasUI) state.lastUiContext = ctx;
|
|
650
|
-
if (!state.currentSessionId) return;
|
|
650
|
+
if (!state.currentSessionId) return 0;
|
|
651
|
+
const sessionIds = state.sessionLineage?.length ? state.sessionLineage : [state.currentSessionId];
|
|
651
652
|
let runs: AsyncRunSummary[];
|
|
652
653
|
try {
|
|
653
|
-
runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"],
|
|
654
|
+
runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionIds, resultsDir, kill: options.kill, now: options.now });
|
|
654
655
|
} catch (error) {
|
|
655
656
|
console.error(`Failed to restore active async jobs from '${asyncDirRoot}':`, error);
|
|
656
|
-
return;
|
|
657
|
+
return 0;
|
|
657
658
|
}
|
|
659
|
+
let adopted = 0;
|
|
658
660
|
for (const run of runs) {
|
|
661
|
+
if (state.asyncJobs.has(run.id)) continue;
|
|
659
662
|
const job = summaryToJob(run);
|
|
663
|
+
job.adopted = run.sessionId !== undefined && run.sessionId !== state.currentSessionId;
|
|
660
664
|
state.asyncJobs.set(run.id, job);
|
|
661
665
|
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
662
666
|
rememberFleetJob(state, job);
|
|
663
667
|
watchJob(job);
|
|
668
|
+
if (job.adopted) adopted += 1;
|
|
664
669
|
}
|
|
665
|
-
if (runs.length === 0) return;
|
|
670
|
+
if (runs.length === 0) return 0;
|
|
666
671
|
ensurePoller();
|
|
667
672
|
rerenderLastWidget();
|
|
673
|
+
return adopted;
|
|
668
674
|
};
|
|
669
675
|
|
|
670
676
|
return { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose };
|
|
@@ -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
|
|
|
@@ -307,7 +308,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
307
308
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
308
309
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
309
310
|
"artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
310
|
-
"launchResolvedExtensions",
|
|
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.`);
|
|
@@ -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;
|
|
@@ -118,6 +120,8 @@ export interface AsyncRunSummary {
|
|
|
118
120
|
interface AsyncRunListOptions {
|
|
119
121
|
states?: Array<AsyncRunSummary["state"]>;
|
|
120
122
|
sessionId?: string;
|
|
123
|
+
/** Accept runs owned by any of these session identities (union). */
|
|
124
|
+
sessionIds?: string[];
|
|
121
125
|
limit?: number;
|
|
122
126
|
/** Limits status-file reads after candidates are ordered by status mtime. */
|
|
123
127
|
entryLimit?: number;
|
|
@@ -222,6 +226,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
222
226
|
const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
|
|
223
227
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
|
|
224
228
|
?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
|
|
229
|
+
const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
|
|
230
|
+
const runFanoutBudget = runFanoutBudgetDescriptor ? getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor) : status.runFanoutBudget;
|
|
225
231
|
const steps = status.steps ?? [];
|
|
226
232
|
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
227
233
|
const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
|
|
@@ -327,6 +333,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
327
333
|
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
328
334
|
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
329
335
|
...(processTerminal ? { processTerminal } : {}),
|
|
336
|
+
...(runFanoutBudget ? { runFanoutBudget } : {}),
|
|
330
337
|
...(status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {}),
|
|
331
338
|
...(status.launchResolvedExtensions ? { launchResolvedExtensions: status.launchResolvedExtensions } : {}),
|
|
332
339
|
...(status.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: status.runtimeAcknowledgedExtensions } : {}),
|
|
@@ -452,17 +459,18 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
452
459
|
if (usedActiveIndex) updateActiveRunIndex(asyncDir, "failed");
|
|
453
460
|
continue;
|
|
454
461
|
}
|
|
455
|
-
if (status.
|
|
456
|
-
|
|
457
|
-
|
|
462
|
+
if (usedActiveIndex && !isActiveAsyncState(status.state)) {
|
|
463
|
+
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
|
|
464
|
+
if (processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
458
465
|
}
|
|
459
|
-
if (
|
|
466
|
+
if (status.displayDismissedAt !== undefined) continue;
|
|
460
467
|
// Filter before the nested-route lookup: the lookup builds an index over
|
|
461
468
|
// the nested-events directory, so deferring it for filtered-out runs keeps
|
|
462
469
|
// restoration at load from scanning that directory when no active runs
|
|
463
470
|
// match.
|
|
464
471
|
if (allowedStates && !allowedStates.has(status.state)) continue;
|
|
465
472
|
if (options.sessionId && status.sessionId !== options.sessionId) continue;
|
|
473
|
+
if (options.sessionIds && (status.sessionId === undefined || !options.sessionIds.includes(status.sessionId))) continue;
|
|
466
474
|
const nestedWarnings: string[] = [];
|
|
467
475
|
let nestedRoute: NestedRoute | undefined;
|
|
468
476
|
if (options.reconcile !== false) {
|
|
@@ -559,6 +567,7 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
|
|
|
559
567
|
const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
560
568
|
const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
561
569
|
lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
|
|
570
|
+
if (run.runFanoutBudget) lines.push(` ${formatRunFanoutBudget(run.runFanoutBudget)}`);
|
|
562
571
|
if (run.error) lines.push(` Error: ${run.error}`);
|
|
563
572
|
for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
|
|
564
573
|
const outputPath = formatAsyncRunOutputPath(run);
|
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
5
5
|
import { appendJsonl } from "../../shared/artifacts.ts";
|
|
6
6
|
import type { AsyncParallelGroupStatus, AsyncStatus, WorkflowGraphNode, WorkflowGraphSnapshot } from "../../shared/types.ts";
|
|
7
|
-
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
+
import { PROMPT_REDACTED, readStatus } from "../../shared/utils.ts";
|
|
8
8
|
import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep, RunnerCheckpointStep } from "../shared/parallel-utils.ts";
|
|
9
9
|
import { isCheckpointRunnerStep, isDynamicRunnerGroup, isParallelGroup } from "../shared/parallel-utils.ts";
|
|
10
10
|
|
|
@@ -19,6 +19,7 @@ export interface ChainAppendRequest {
|
|
|
19
19
|
export interface ChainAppendResult {
|
|
20
20
|
request: ChainAppendRequest;
|
|
21
21
|
pendingCount: number;
|
|
22
|
+
bookkeepingError?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
type StatusStep = NonNullable<AsyncStatus["steps"]>[number];
|
|
@@ -69,6 +70,7 @@ export function enqueueChainAppendRequest(input: {
|
|
|
69
70
|
runId: string;
|
|
70
71
|
steps: RunnerStep[];
|
|
71
72
|
now?: number;
|
|
73
|
+
admit?: (persist: () => void) => void;
|
|
72
74
|
}): ChainAppendResult {
|
|
73
75
|
const status = readStatus(input.asyncDir);
|
|
74
76
|
if (!status) throw new Error(`No async run status found for '${input.runId}'.`);
|
|
@@ -85,20 +87,36 @@ export function enqueueChainAppendRequest(input: {
|
|
|
85
87
|
steps: input.steps,
|
|
86
88
|
};
|
|
87
89
|
fs.mkdirSync(appendDir(input.asyncDir), { recursive: true });
|
|
88
|
-
writeAtomicJson(appendRequestPath(input.asyncDir, request), request);
|
|
89
|
-
|
|
90
|
+
const persist = () => writeAtomicJson(appendRequestPath(input.asyncDir, request), request);
|
|
91
|
+
if (input.admit) input.admit(persist);
|
|
92
|
+
else persist();
|
|
93
|
+
let pendingCount = 1;
|
|
94
|
+
const bookkeepingErrors: string[] = [];
|
|
95
|
+
try {
|
|
96
|
+
pendingCount = countPendingChainAppendRequests(input.asyncDir);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
bookkeepingErrors.push(`pending append count failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
99
|
+
}
|
|
90
100
|
const statusPath = path.join(input.asyncDir, "status.json");
|
|
91
101
|
const updatedStatus = { ...status, pendingAppends: pendingCount, lastUpdate: request.createdAt };
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
try {
|
|
103
|
+
writeAtomicJson(statusPath, updatedStatus);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
bookkeepingErrors.push(`status update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
appendJsonl(path.join(input.asyncDir, "events.jsonl"), JSON.stringify({
|
|
109
|
+
type: "subagent.chain.append.requested",
|
|
110
|
+
ts: request.createdAt,
|
|
111
|
+
runId: input.runId,
|
|
112
|
+
requestId: request.id,
|
|
113
|
+
stepCount: input.steps.length,
|
|
114
|
+
pendingAppends: pendingCount,
|
|
115
|
+
}));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
bookkeepingErrors.push(`event append failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
}
|
|
119
|
+
return { request, pendingCount, ...(bookkeepingErrors.length > 0 ? { bookkeepingError: bookkeepingErrors.join("; ") } : {}) };
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
function readAppendRequest(filePath: string): ChainAppendRequest | undefined {
|
|
@@ -135,8 +153,8 @@ const MAX_STATUS_STEP_DESCRIPTION_CHARS = 160;
|
|
|
135
153
|
|
|
136
154
|
/** Bounded one-line per-step task description persisted into status.json for fleet display. */
|
|
137
155
|
export function statusStepDescription(task: string | undefined): string | undefined {
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
if (!task?.trim()) return undefined;
|
|
157
|
+
const description = PROMPT_REDACTED;
|
|
140
158
|
return description.length > MAX_STATUS_STEP_DESCRIPTION_CHARS
|
|
141
159
|
? `${description.slice(0, MAX_STATUS_STEP_DESCRIPTION_CHARS - 1)}…`
|
|
142
160
|
: description;
|
|
@@ -239,7 +239,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
|
|
|
239
239
|
|
|
240
240
|
export default function registerSubagentNotify(
|
|
241
241
|
pi: ExtensionAPI,
|
|
242
|
-
state: Pick<SubagentState, "currentSessionId">,
|
|
242
|
+
state: Pick<SubagentState, "currentSessionId" | "sessionLineage">,
|
|
243
243
|
options: RegisterSubagentNotifyOptions = {},
|
|
244
244
|
): CompletionNotifier {
|
|
245
245
|
const seen = new Map<string, number>();
|
|
@@ -250,6 +250,9 @@ export default function registerSubagentNotify(
|
|
|
250
250
|
const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
|
|
251
251
|
let disposed = false;
|
|
252
252
|
|
|
253
|
+
const ownsSession = (sessionId: string) =>
|
|
254
|
+
sessionId === state.currentSessionId || state.sessionLineage?.includes(sessionId) === true;
|
|
255
|
+
|
|
253
256
|
const settle = (items: PendingCompletion[], accepted: boolean) => {
|
|
254
257
|
for (const item of items) {
|
|
255
258
|
pending.delete(item.key);
|
|
@@ -274,7 +277,7 @@ export default function registerSubagentNotify(
|
|
|
274
277
|
};
|
|
275
278
|
|
|
276
279
|
const deliver = (result: CompletionNotification): Promise<boolean> => {
|
|
277
|
-
if (disposed || typeof result.sessionId !== "string" || result.sessionId
|
|
280
|
+
if (disposed || typeof result.sessionId !== "string" || !ownsSession(result.sessionId)) return Promise.resolve(false);
|
|
278
281
|
if (result.intercomDelivered === true) return Promise.resolve(true);
|
|
279
282
|
const key = buildCompletionKey(result, "notify");
|
|
280
283
|
const seenAt = seen.get(key);
|