pi-subagents 0.65.1 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +16 -2
- package/docs/configuration.md +18 -0
- package/docs/extension-api.md +91 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +3 -3
- package/docs/workflows.md +14 -7
- package/package.json +2 -1
- package/skills/pi-subagents/references/execution-controls.md +14 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/src/agents/advertised-agent-prompt.ts +63 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +8 -0
- package/src/api/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/index.ts +40 -2
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +8 -6
- package/src/extension/tool-description.ts +6 -5
- package/src/intercom/native-supervisor-channel.ts +82 -54
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +40 -29
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/notify.ts +86 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +60 -32
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +18 -7
- package/src/runs/background/runner-child-launch.ts +86 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/subagent-runner.ts +412 -209
- package/src/runs/background/subagent-wait.ts +3 -0
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/async-steering-action.ts +19 -0
- package/src/runs/foreground/execution.ts +101 -25
- package/src/runs/foreground/subagent-executor.ts +474 -197
- package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
- package/src/runs/shared/acceptance.ts +2 -2
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +47 -47
- package/src/runs/shared/child-hooks.ts +151 -2
- package/src/runs/shared/child-launch.ts +18 -13
- package/src/runs/shared/child-session.ts +42 -6
- package/src/runs/shared/child-tool-plan.ts +2 -2
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +1 -0
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +10 -9
- package/src/runs/shared/model-fallback.ts +10 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +6 -0
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +332 -204
- package/src/shared/types.ts +81 -60
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +23 -5
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +38 -10
- package/src/workflows/workflow-checklist.ts +11 -15
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
|
@@ -5,6 +5,7 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import type { Message } from "@earendil-works/pi-ai";
|
|
8
|
+
import { arbitrateCompletionGuardRescue, createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
|
|
8
9
|
|
|
9
10
|
// Detached runners skip Pi's CLI proxy setup. Keep fetch on the same Undici dispatcher.
|
|
10
11
|
function ensureProxyAwareHttpDispatcher(): void {
|
|
@@ -20,7 +21,8 @@ function ensureProxyAwareHttpDispatcher(): void {
|
|
|
20
21
|
console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
const isRunnerEntrypoint = Boolean(process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href);
|
|
25
|
+
if (isRunnerEntrypoint) ensureProxyAwareHttpDispatcher();
|
|
24
26
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
25
27
|
import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
|
|
26
28
|
import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
@@ -45,10 +47,10 @@ import {
|
|
|
45
47
|
type AsyncStatus,
|
|
46
48
|
type ChainOutputMap,
|
|
47
49
|
type CostSummary,
|
|
48
|
-
type
|
|
49
|
-
type
|
|
50
|
+
type LaunchResolvedChildExtensions,
|
|
51
|
+
type RuntimeAcknowledgedChildExtensions,
|
|
50
52
|
type ModelAttempt,
|
|
51
|
-
type
|
|
53
|
+
type PiWriterProcessInstanceExit,
|
|
52
54
|
type NestedRouteInfo,
|
|
53
55
|
type NestedRunSummary,
|
|
54
56
|
type ResolvedControlConfig,
|
|
@@ -90,10 +92,14 @@ import {
|
|
|
90
92
|
DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
|
|
91
93
|
Semaphore,
|
|
92
94
|
} from "../shared/parallel-utils.ts";
|
|
93
|
-
import { applyThinkingSuffix,
|
|
94
|
-
import {
|
|
95
|
+
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
|
|
96
|
+
import type { InheritedChildRuntime } from "../shared/child-launch.ts";
|
|
97
|
+
import { buildRunnerChildLaunch } from "./runner-child-launch.ts";
|
|
98
|
+
import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
|
|
95
99
|
import type { ChildSessionFactory } from "../shared/child-session.ts";
|
|
96
|
-
import { runChildSession, type ChildEvent, type RunChildSessionResult, type StepSteerHandler } from "./run-child-session.ts";
|
|
100
|
+
import { getSettledReadonlyChild, runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type StepSteerHandler } from "./run-child-session.ts";
|
|
101
|
+
import { planReadonlyModelContinuation, READONLY_CONTINUATION_PROMPT, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
|
|
102
|
+
import { getReadonlySessionEvidence } from "../shared/readonly-session-evidence.ts";
|
|
97
103
|
import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
|
|
98
104
|
import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
|
|
99
105
|
import { deriveChildSessionName } from "../../shared/child-session-name.ts";
|
|
@@ -107,7 +113,7 @@ import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelSt
|
|
|
107
113
|
import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
|
|
108
114
|
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
109
115
|
import { formatModelAttemptNote, formatSubagentModelVerificationError, isContextOverflow, isRetryableModelFailureAttempt, recordRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
110
|
-
import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
|
|
116
|
+
import { markProcessTerminalCandidateLeaseRelease, processTerminalPath, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
|
|
111
117
|
import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
|
|
112
118
|
import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
|
|
113
119
|
import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
|
|
@@ -129,6 +135,9 @@ import type { TokenUsage } from "../../shared/types.ts";
|
|
|
129
135
|
import {
|
|
130
136
|
cleanupWorktrees,
|
|
131
137
|
createWorktrees,
|
|
138
|
+
withWorktreeTransaction,
|
|
139
|
+
WorktreeSetupError,
|
|
140
|
+
type WorktreeSetupProgress,
|
|
132
141
|
diffWorktrees,
|
|
133
142
|
findWorktreeTaskCwdConflict,
|
|
134
143
|
formatWorktreeDiffSummary,
|
|
@@ -136,21 +145,20 @@ import {
|
|
|
136
145
|
WORKTREE_AGENT_CWD_PLACEHOLDER,
|
|
137
146
|
type WorktreeSetup,
|
|
138
147
|
} from "../shared/worktree.ts";
|
|
139
|
-
import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
148
|
+
import { findModelInfo, resolveEffectiveThinking, splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
140
149
|
import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
|
|
141
150
|
import { launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
142
151
|
import { writeInitialProgressFile } from "../../shared/settings.ts";
|
|
143
152
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
144
153
|
import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
|
|
145
|
-
import { attachContractProjections,
|
|
154
|
+
import { attachContractProjections, isAgentContract } from "../shared/agent-contract.ts";
|
|
146
155
|
import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
147
|
-
import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
|
|
148
156
|
import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
|
|
149
157
|
import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
|
|
150
158
|
import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
|
|
151
159
|
import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
|
|
152
160
|
import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
|
|
153
|
-
import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup,
|
|
161
|
+
import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writeWorktreeSetupHandoff } from "../shared/parallel-handoff.ts";
|
|
154
162
|
import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
|
|
155
163
|
import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
|
|
156
164
|
import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
|
|
@@ -225,8 +233,8 @@ interface SubagentRunConfig {
|
|
|
225
233
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
226
234
|
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
227
235
|
launchContractDigest?: string;
|
|
228
|
-
launchResolvedExtensions?:
|
|
229
|
-
runtimeAcknowledgedExtensions?:
|
|
236
|
+
launchResolvedExtensions?: LaunchResolvedChildExtensions;
|
|
237
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
|
|
230
238
|
runnerProcessInstanceId?: string;
|
|
231
239
|
launchBarrierToken?: string;
|
|
232
240
|
parentWorkflowRunId?: string;
|
|
@@ -241,8 +249,8 @@ interface StepResult {
|
|
|
241
249
|
context?: "fresh" | "fork";
|
|
242
250
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
243
251
|
capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
|
|
244
|
-
launchResolvedExtensions?:
|
|
245
|
-
runtimeAcknowledgedExtensions?:
|
|
252
|
+
launchResolvedExtensions?: LaunchResolvedChildExtensions;
|
|
253
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
|
|
246
254
|
output: string;
|
|
247
255
|
outputState?: SubagentOutputState;
|
|
248
256
|
error?: string;
|
|
@@ -692,14 +700,17 @@ interface SingleStepContext {
|
|
|
692
700
|
onExternalProcess?: (process: ExternalProcessStatus) => void;
|
|
693
701
|
onExternalJob?: (status: ExternalJobStatus) => void;
|
|
694
702
|
skipAcceptance?: () => boolean;
|
|
695
|
-
|
|
703
|
+
/** Authoritative owner decision after event delivery; undefined includes incomplete run-wide usage. */
|
|
704
|
+
usageBudgetExhausted?: () => boolean | undefined;
|
|
705
|
+
/** Existing run-owned budget configuration; cost allowance is not settled by the live token ledger. */
|
|
706
|
+
usageBudget?: UsageBudgetConfig;
|
|
696
707
|
/** False when sibling work in the same Git worktree could have caused the tracked diff. */
|
|
697
708
|
trackedMutationEvidenceForCompletionGuard?: boolean;
|
|
698
709
|
orcaProgressTab?: OrcaProgressTab;
|
|
699
710
|
}
|
|
700
711
|
|
|
701
712
|
/** Run a single pi agent step, returning output and metadata */
|
|
702
|
-
async function runSingleStepInner(
|
|
713
|
+
export async function runSingleStepInner(
|
|
703
714
|
step: SubagentStep,
|
|
704
715
|
ctx: SingleStepContext,
|
|
705
716
|
): Promise<StepResult & { completionGuardTriggered?: boolean }> {
|
|
@@ -827,8 +838,8 @@ async function runSingleStepInner(
|
|
|
827
838
|
// Derive from the pre-acceptance task so internal acceptance/recovery
|
|
828
839
|
// instructions never leak into the display name.
|
|
829
840
|
const childSessionName = step.sessionName ?? deriveChildSessionName({ agent: step.agent, task, label: step.label });
|
|
830
|
-
if (step.effectiveAcceptance) {
|
|
831
|
-
const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional:
|
|
841
|
+
if (step.effectiveAcceptance && (step.runner?.type === "external-cli" || step.runner?.type === "external-job")) {
|
|
842
|
+
const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContract(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
|
|
832
843
|
if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
|
|
833
844
|
}
|
|
834
845
|
const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
|
|
@@ -1035,11 +1046,31 @@ async function runSingleStepInner(
|
|
|
1035
1046
|
let modelIndex = 0;
|
|
1036
1047
|
let contextOverflow = false;
|
|
1037
1048
|
let launchWarningsEmitted = false;
|
|
1038
|
-
let
|
|
1049
|
+
let recoveryState: LogicalRecoveryState = "unused";
|
|
1050
|
+
let readonlyContinuation: RunChildSessionInput["readonlyContinuation"];
|
|
1051
|
+
const continuationBudget = () => {
|
|
1052
|
+
if (step.toolBudget) return "tool-budget-configured" as const;
|
|
1053
|
+
// Refresh the authoritative run ledger, already fed synchronously by onChildEvent.
|
|
1054
|
+
const exhausted = ctx.usageBudgetExhausted?.();
|
|
1055
|
+
if (exhausted === true) return "exhausted" as const;
|
|
1056
|
+
if (!ctx.usageBudget) return "unconfigured" as const;
|
|
1057
|
+
if (ctx.usageBudget.costUsd || !ctx.onChildEvent || exhausted !== false) return "unknown" as const;
|
|
1058
|
+
return "available" as const;
|
|
1059
|
+
};
|
|
1060
|
+
const lifecycleAllowsContinuation = () => !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted
|
|
1061
|
+
&& !ctx.skipAcceptance?.() && (ctx.deadlineAt === undefined || Date.now() < ctx.deadlineAt);
|
|
1062
|
+
const canContinue = () => lifecycleAllowsContinuation() && ["available", "unconfigured"].includes(continuationBudget());
|
|
1063
|
+
const failContinuationLaunch = (candidate: string | undefined, error: unknown) => {
|
|
1064
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1065
|
+
modelAttempts.push({ model: candidate ?? "default", success: false, exitCode: 1, error: message });
|
|
1066
|
+
if (candidate) attemptedModels.push(candidate);
|
|
1067
|
+
if (finalResult) finalResult = { ...finalResult, exitCode: 1, error: message };
|
|
1068
|
+
};
|
|
1039
1069
|
let nextAttemptTask = task;
|
|
1040
1070
|
modelAttemptsLoop: while (modelIndex < candidates.length) {
|
|
1041
1071
|
if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
1042
|
-
|
|
1072
|
+
if (readonlyContinuation && !canContinue()) break;
|
|
1073
|
+
const recoveringAbort = recoveryState === "abort-recovery";
|
|
1043
1074
|
const attemptTask = nextAttemptTask;
|
|
1044
1075
|
const candidate = candidates[modelIndex];
|
|
1045
1076
|
const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
|
|
@@ -1047,6 +1078,10 @@ async function runSingleStepInner(
|
|
|
1047
1078
|
assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
|
|
1048
1079
|
} catch (error) {
|
|
1049
1080
|
const message = error instanceof Error ? error.message : String(error);
|
|
1081
|
+
if (readonlyContinuation) {
|
|
1082
|
+
failContinuationLaunch(candidate, error);
|
|
1083
|
+
break modelAttemptsLoop;
|
|
1084
|
+
}
|
|
1050
1085
|
return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
|
|
1051
1086
|
}
|
|
1052
1087
|
ctx.onAttemptStart?.(omitUndefinedProperties({
|
|
@@ -1072,56 +1107,22 @@ async function runSingleStepInner(
|
|
|
1072
1107
|
})
|
|
1073
1108
|
: undefined;
|
|
1074
1109
|
let watchdogSink: ((event: ChildWatchdogStatusEvent) => void) | undefined;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
fast: step.fast,
|
|
1092
|
-
modelCandidates: step.modelCandidates,
|
|
1093
|
-
systemPrompt: step.systemPrompt ?? "",
|
|
1094
|
-
systemPromptMode: step.systemPromptMode,
|
|
1095
|
-
mcpDirectTools: step.mcpDirectTools,
|
|
1096
|
-
extensionBindings,
|
|
1097
|
-
capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
|
|
1098
|
-
cwd: step.cwd ?? ctx.cwd,
|
|
1099
|
-
intercomSessionName: ctx.childIntercomTarget,
|
|
1100
|
-
sessionName: childSessionName,
|
|
1101
|
-
orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
|
|
1102
|
-
runId: ctx.id,
|
|
1103
|
-
childAgentName: step.agent,
|
|
1104
|
-
childIndex: ctx.flatIndex,
|
|
1105
|
-
nestedRoute: ctx.nestedRoute,
|
|
1106
|
-
runFanoutBudget: ctx.runFanoutBudget ? {
|
|
1107
|
-
...ctx.runFanoutBudget,
|
|
1108
|
-
...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
|
|
1109
|
-
} : undefined,
|
|
1110
|
-
structuredOutput: effectiveStructuredOutput,
|
|
1111
|
-
toolBudget: step.toolBudget,
|
|
1112
|
-
permissionRules: step.permissionRules,
|
|
1113
|
-
permissionAuditPath: step.permissionRules && ctx.artifactsDir
|
|
1114
|
-
? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`)
|
|
1115
|
-
: undefined,
|
|
1116
|
-
childWatchdog,
|
|
1117
|
-
watchdogStatus: (event) => watchdogSink?.(event),
|
|
1118
|
-
waitToolEnabled: step.waitToolEnabled,
|
|
1119
|
-
waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
|
|
1120
|
-
thinkingCeiling: step.thinkingCeiling,
|
|
1121
|
-
maxSubagentDepth: step.maxSubagentDepth,
|
|
1122
|
-
inherited: ctx.inheritedChildRuntime,
|
|
1123
|
-
host: "runner",
|
|
1124
|
-
}));
|
|
1110
|
+
let launch: ReturnType<typeof buildRunnerChildLaunch>;
|
|
1111
|
+
try {
|
|
1112
|
+
launch = buildRunnerChildLaunch(step, ctx, {
|
|
1113
|
+
sessionEnabled,
|
|
1114
|
+
sessionDir,
|
|
1115
|
+
model: candidate,
|
|
1116
|
+
sessionName: childSessionName,
|
|
1117
|
+
structuredOutput: effectiveStructuredOutput,
|
|
1118
|
+
childWatchdog,
|
|
1119
|
+
watchdogStatus: (event) => watchdogSink?.(event),
|
|
1120
|
+
});
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (!readonlyContinuation) throw error;
|
|
1123
|
+
failContinuationLaunch(candidate, error);
|
|
1124
|
+
break modelAttemptsLoop;
|
|
1125
|
+
}
|
|
1125
1126
|
if (effectiveStructuredOutput && launch.config.structuredOutput) {
|
|
1126
1127
|
// The runner reads the value back from the runtime's files after the run.
|
|
1127
1128
|
launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
|
|
@@ -1181,6 +1182,9 @@ async function runSingleStepInner(
|
|
|
1181
1182
|
const run = await runChildSession(omitUndefinedProperties({
|
|
1182
1183
|
factory: ctx.childSessions,
|
|
1183
1184
|
launch,
|
|
1185
|
+
collectReadonlyEvidence: true,
|
|
1186
|
+
readonlyContinuation,
|
|
1187
|
+
canContinue,
|
|
1184
1188
|
prompt: `Task: ${attemptTask}`,
|
|
1185
1189
|
childWatchdog,
|
|
1186
1190
|
childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
@@ -1261,7 +1265,7 @@ async function runSingleStepInner(
|
|
|
1261
1265
|
&& (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
|
|
1262
1266
|
? formatEmptyTerminalAssistantResponseError(run.messages)
|
|
1263
1267
|
: undefined;
|
|
1264
|
-
const completionGuardEnabled =
|
|
1268
|
+
const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
|
|
1265
1269
|
const completionToolPlan = resolvedTaskToolPlan;
|
|
1266
1270
|
const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
|
|
1267
1271
|
const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
|
|
@@ -1280,14 +1284,27 @@ async function runSingleStepInner(
|
|
|
1280
1284
|
}))
|
|
1281
1285
|
: undefined;
|
|
1282
1286
|
const mutationAttemptObserved = run.observedMutationAttempt === true || completionMutationEvidence?.attemptedMutation === true;
|
|
1287
|
+
let arbitration = { triggered: completionGuard?.triggered === true && !mutationAttemptObserved, rescued: false };
|
|
1288
|
+
if (arbitration.triggered) {
|
|
1289
|
+
const modelContext = launch.capture.completionIntentContext?.();
|
|
1290
|
+
arbitration = await arbitrateCompletionGuardRescue({
|
|
1291
|
+
guardTriggered: true,
|
|
1292
|
+
task: taskForCompletionGuard,
|
|
1293
|
+
// Construct lazily too: the shared gate refuses overlength tasks before
|
|
1294
|
+
// registry/auth/model work. The child has already shut down normally.
|
|
1295
|
+
arbiter: modelContext ? async (task) => createTaskMutationArbiter(modelContext)?.(task) ?? "unavailable" : undefined,
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1283
1298
|
const completionEvidence = planCompletionEvidence({
|
|
1284
1299
|
guard: completionGuard,
|
|
1300
|
+
guardTriggered: arbitration.triggered,
|
|
1301
|
+
arbiterRescued: arbitration.rescued,
|
|
1285
1302
|
completionGuardEnabled,
|
|
1286
1303
|
mutationCapable: hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools),
|
|
1287
1304
|
implementationMutationExpected: expectsImplementationMutation(step.agent, taskForCompletionGuard),
|
|
1288
1305
|
mutationAttemptObserved,
|
|
1289
1306
|
mutationEvidence: completionMutationEvidence,
|
|
1290
|
-
|
|
1307
|
+
agentContractEnabled: isAgentContract(step.agentContract),
|
|
1291
1308
|
});
|
|
1292
1309
|
const finalOutputHasPersistableFileContent = run.exitCode === 0 && !run.error && !emptyOutputError && Boolean(stripAcceptanceReport(run.finalOutput).trim());
|
|
1293
1310
|
const requiredOutput = step.outputMode === "file-only" && step.outputPath
|
|
@@ -1347,7 +1364,7 @@ async function runSingleStepInner(
|
|
|
1347
1364
|
messages: run.messages,
|
|
1348
1365
|
error,
|
|
1349
1366
|
sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
|
|
1350
|
-
alreadyResumed:
|
|
1367
|
+
alreadyResumed: recoveryState !== "unused",
|
|
1351
1368
|
stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
|
|
1352
1369
|
interrupted: run.interrupted,
|
|
1353
1370
|
timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
|
|
@@ -1367,15 +1384,59 @@ async function runSingleStepInner(
|
|
|
1367
1384
|
if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
|
|
1368
1385
|
if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
|
|
1369
1386
|
if (attempt.success) break modelAttemptsLoop;
|
|
1387
|
+
// The shared token is consumed before sibling creation; no third dispatch of any kind.
|
|
1388
|
+
if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
|
|
1370
1389
|
if (recoveringAbort) break modelAttemptsLoop;
|
|
1371
1390
|
if (abortRecovery?.action === "resume") {
|
|
1372
|
-
|
|
1391
|
+
recoveryState = "abort-recovery";
|
|
1373
1392
|
nextAttemptTask = abortRecovery.prompt;
|
|
1374
1393
|
attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
|
|
1375
1394
|
continue;
|
|
1376
1395
|
}
|
|
1377
1396
|
if (completionEvidence.guardTriggered) break modelAttemptsLoop;
|
|
1378
1397
|
|
|
1398
|
+
const source = getSettledReadonlyChild(run);
|
|
1399
|
+
if (source) {
|
|
1400
|
+
const captured = launch.capture.completionIntentContext?.();
|
|
1401
|
+
const sourceModel = captured?.model;
|
|
1402
|
+
const retained = getReadonlySessionEvidence(source)!;
|
|
1403
|
+
// Modality/capacity assessment, not another history/provenance validator.
|
|
1404
|
+
// Images and unknown content have no certified token bound in this host slice.
|
|
1405
|
+
const textOnly = (JSON.parse(retained.contextJson) as Message[]).every((message) => typeof message.content === "string"
|
|
1406
|
+
|| (Array.isArray(message.content) && message.content.every((block) => ["text", "thinking", "toolCall"].includes(block.type))));
|
|
1407
|
+
const retainedBytes = Buffer.byteLength(retained.contextJson, "utf8");
|
|
1408
|
+
const resolvedCandidates = candidates.map((reference, index) => {
|
|
1409
|
+
// Only exact registry identities qualify; aliases/default guesses remain ineligible.
|
|
1410
|
+
const base = reference ? splitKnownThinkingSuffix(reference).baseModel : "";
|
|
1411
|
+
const slash = base.indexOf("/");
|
|
1412
|
+
const model = slash > 0 ? captured?.modelRegistry.find(base.slice(0, slash), base.slice(slash + 1)) : undefined;
|
|
1413
|
+
// Reserve the source window for runtime/system/tool overhead, plus a conservative
|
|
1414
|
+
// byte bound for the complete retained text and new prompt; never assume a fit
|
|
1415
|
+
// merely because a 429 request was sent. Equal/smaller windows remain denied.
|
|
1416
|
+
const compatible = textOnly && model && sourceModel && model.input?.includes("text")
|
|
1417
|
+
&& Number.isFinite(sourceModel.contextWindow) && sourceModel.contextWindow > 0
|
|
1418
|
+
&& model.contextWindow >= sourceModel.contextWindow + retainedBytes + Buffer.byteLength(READONLY_CONTINUATION_PROMPT, "utf8")
|
|
1419
|
+
&& model.maxTokens > 0 && model.maxTokens <= sourceModel.maxTokens;
|
|
1420
|
+
return { resolved: model ? { provider: model.provider, model: model.id, api: model.api } : undefined,
|
|
1421
|
+
tried: index <= modelIndex, compatibility: compatible ? "compatible" as const : "unknown" as const };
|
|
1422
|
+
});
|
|
1423
|
+
const plan = planReadonlyModelContinuation({ source, recoveryState, candidates: resolvedCandidates, currentIndex: modelIndex,
|
|
1424
|
+
lifecycleAllowsContinuation: lifecycleAllowsContinuation() && !run.interrupted && !run.stopped && !run.timedOut,
|
|
1425
|
+
effectsAllowContinuation: !run.currentTool && !run.observedMutationAttempt && !mutationEvidence.attemptedMutation
|
|
1426
|
+
&& !structuredError && !effectiveStructuredOutput && !missingRequiredOutputError && !toolAvailabilityError
|
|
1427
|
+
&& !completionEvidence.guardTriggered && !midToolExitError && !hiddenError?.hasError,
|
|
1428
|
+
budget: continuationBudget(), knownContextOverflow: isContextOverflow(error) });
|
|
1429
|
+
if (plan.kind === "continue" && canContinue()) {
|
|
1430
|
+
recoveryState = plan.recoveryState;
|
|
1431
|
+
const selected = resolvedCandidates[plan.candidateIndex]!.resolved!;
|
|
1432
|
+
readonlyContinuation = { source, expected: plan.expected, modelId: `${selected.provider}/${selected.model}` };
|
|
1433
|
+
nextAttemptTask = plan.prompt;
|
|
1434
|
+
modelIndex = plan.candidateIndex;
|
|
1435
|
+
attemptNotes.push(`[readonly-continuation] ${attempt.model} returned HTTP 429 after read-only progress; continuing the retained session once with ${candidates[modelIndex]}.`);
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1379
1440
|
const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
|
|
1380
1441
|
if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
|
|
1381
1442
|
if (isContextOverflow(error)) {
|
|
@@ -1457,7 +1518,7 @@ async function runSingleStepInner(
|
|
|
1457
1518
|
cwd: step.cwd ?? ctx.cwd,
|
|
1458
1519
|
signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
|
|
1459
1520
|
abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
|
|
1460
|
-
reportOptional:
|
|
1521
|
+
reportOptional: isAgentContract(step.agentContract),
|
|
1461
1522
|
artifactsDir: ctx.artifactsDir,
|
|
1462
1523
|
runId: ctx.id,
|
|
1463
1524
|
watchdog: finalResult?.watchdog,
|
|
@@ -1473,7 +1534,7 @@ async function runSingleStepInner(
|
|
|
1473
1534
|
: acceptance
|
|
1474
1535
|
: undefined;
|
|
1475
1536
|
const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
|
|
1476
|
-
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !
|
|
1537
|
+
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContract(step.agentContract);
|
|
1477
1538
|
const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
1478
1539
|
const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
|
|
1479
1540
|
const baseFinalError = stoppedAfterAcceptance
|
|
@@ -1515,7 +1576,7 @@ async function runSingleStepInner(
|
|
|
1515
1576
|
...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
|
|
1516
1577
|
launchContractDigest: actualLaunchContractDigest,
|
|
1517
1578
|
launchResolvedExtensions,
|
|
1518
|
-
...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?:
|
|
1579
|
+
...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
|
|
1519
1580
|
...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
|
|
1520
1581
|
transcriptError: transcriptWriter?.getError(),
|
|
1521
1582
|
skills: step.skills,
|
|
@@ -1544,6 +1605,7 @@ async function runSingleStepInner(
|
|
|
1544
1605
|
totalCost: costSummaryFromAttempts(modelAttempts),
|
|
1545
1606
|
usage,
|
|
1546
1607
|
artifactPaths,
|
|
1608
|
+
savedOutputPath: finalizedOutput.savedPath,
|
|
1547
1609
|
outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
|
|
1548
1610
|
metadataSaveError: artifactErrors.metadataSaveError,
|
|
1549
1611
|
transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
|
|
@@ -1563,9 +1625,9 @@ async function runSingleStepInner(
|
|
|
1563
1625
|
watchdog: finalResult?.watchdog,
|
|
1564
1626
|
...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
|
|
1565
1627
|
launchResolvedExtensions,
|
|
1566
|
-
...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?:
|
|
1628
|
+
...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
|
|
1567
1629
|
});
|
|
1568
|
-
return
|
|
1630
|
+
return isAgentContract(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
|
|
1569
1631
|
}
|
|
1570
1632
|
|
|
1571
1633
|
async function runSingleStep(
|
|
@@ -1656,12 +1718,15 @@ function markParallelGroupSetupFailure(input: {
|
|
|
1656
1718
|
const statusStep = requiredStatusStep(input.statusPayload, flatTaskIndex);
|
|
1657
1719
|
const task = input.group.parallel[taskIndex];
|
|
1658
1720
|
if (!task) throw new Error(`Missing parallel task at index ${taskIndex}`);
|
|
1659
|
-
statusStep.
|
|
1721
|
+
const stopped = statusStep.stopped || statusStep.stopRequested || input.statusPayload.stopped;
|
|
1722
|
+
const paused = !stopped && input.statusPayload.state === "paused";
|
|
1723
|
+
statusStep.status = stopped ? "stopped" : paused ? "paused" : "failed";
|
|
1660
1724
|
statusStep.startedAt = input.failedAt;
|
|
1661
1725
|
statusStep.endedAt = input.failedAt;
|
|
1662
1726
|
statusStep.durationMs = 0;
|
|
1663
|
-
statusStep.exitCode = 1;
|
|
1664
|
-
|
|
1727
|
+
statusStep.exitCode = paused ? 0 : 1;
|
|
1728
|
+
statusStep.error ??= input.setupError;
|
|
1729
|
+
input.results.push(omitUndefinedProperties({ agent: task.agent, context: task.context, output: input.setupError, error: input.setupError, success: false, exitCode: paused ? 0 : 1, sessionFile: task.sessionFile, stopped: stopped || undefined, interrupted: paused || undefined, timedOut: input.statusPayload.timedOut || undefined }));
|
|
1665
1730
|
}
|
|
1666
1731
|
input.statusPayload.currentStep = input.groupStartFlatIndex;
|
|
1667
1732
|
input.statusPayload.lastUpdate = input.failedAt;
|
|
@@ -1818,7 +1883,10 @@ async function runSingleStepWithTimeout(
|
|
|
1818
1883
|
ctx: SingleStepContext,
|
|
1819
1884
|
parentDeadlineAt?: number,
|
|
1820
1885
|
): Promise<SingleStepResult> {
|
|
1821
|
-
if (step.timeoutMs === undefined) return runSingleStep(step, ctx
|
|
1886
|
+
if (step.timeoutMs === undefined) return runSingleStep(step, parentDeadlineAt === undefined ? ctx : {
|
|
1887
|
+
...ctx,
|
|
1888
|
+
deadlineAt: ctx.deadlineAt === undefined ? parentDeadlineAt : Math.min(ctx.deadlineAt, parentDeadlineAt),
|
|
1889
|
+
});
|
|
1822
1890
|
|
|
1823
1891
|
const parentRemainingMs = parentDeadlineAt === undefined ? undefined : Math.max(0, parentDeadlineAt - Date.now());
|
|
1824
1892
|
const timeoutMs = parentRemainingMs === undefined ? step.timeoutMs : Math.min(step.timeoutMs, parentRemainingMs);
|
|
@@ -1855,7 +1923,7 @@ async function runSingleStepWithTimeout(
|
|
|
1855
1923
|
}
|
|
1856
1924
|
}
|
|
1857
1925
|
|
|
1858
|
-
async function runSubagent(
|
|
1926
|
+
export async function runSubagent(
|
|
1859
1927
|
config: SubagentRunConfig,
|
|
1860
1928
|
childSessions: ChildSessionFactory,
|
|
1861
1929
|
): Promise<void> {
|
|
@@ -1893,6 +1961,8 @@ async function runSubagent(
|
|
|
1893
1961
|
const stopMessage = "Subagent stopped by user.";
|
|
1894
1962
|
const timeoutAbortController = new AbortController();
|
|
1895
1963
|
const stopAbortController = new AbortController();
|
|
1964
|
+
const setupInterruptController = new AbortController();
|
|
1965
|
+
const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
|
|
1896
1966
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
1897
1967
|
let latestSessionFile: string | undefined;
|
|
1898
1968
|
|
|
@@ -2059,20 +2129,29 @@ async function runSubagent(
|
|
|
2059
2129
|
},
|
|
2060
2130
|
onError: (error, filePath) => console.error(`Failed to update async run index '${filePath}':`, error),
|
|
2061
2131
|
});
|
|
2062
|
-
const queueActiveRunIndex = (): void => {
|
|
2063
|
-
const state =
|
|
2132
|
+
const queueActiveRunIndex = (status: AsyncStatus): void => {
|
|
2133
|
+
const state = status.state;
|
|
2064
2134
|
if (state === lastIndexedStatusState && indexPersistence.pendingCount() === 0) return;
|
|
2065
|
-
indexPersistence.write(asyncDir, { state, toolCallId:
|
|
2135
|
+
indexPersistence.write(asyncDir, { state, toolCallId: status.toolCallId }, (_filePath, payload) => {
|
|
2066
2136
|
const indexPayload = payload as { state: AsyncStatus["state"]; toolCallId?: string };
|
|
2067
2137
|
updateActiveRunIndex(asyncDir, indexPayload.state, indexPayload.toolCallId, { retryCapacityErrors: true });
|
|
2068
2138
|
});
|
|
2069
2139
|
};
|
|
2140
|
+
let finalResultCommitted = false;
|
|
2141
|
+
let finalResultPublication: { resolve(): void; reject(error: unknown): void } | undefined;
|
|
2070
2142
|
const runPersistence = createCapacityResilientJsonWriter({
|
|
2071
2143
|
keepAlive: true,
|
|
2072
|
-
onSuccess: (filePath) => {
|
|
2073
|
-
if (filePath === statusPath) queueActiveRunIndex();
|
|
2144
|
+
onSuccess: (filePath, payload) => {
|
|
2145
|
+
if (filePath === statusPath) queueActiveRunIndex(payload as AsyncStatus);
|
|
2146
|
+
if (filePath === resultPath && finalResultPublication) {
|
|
2147
|
+
finalResultCommitted = true;
|
|
2148
|
+
finalResultPublication.resolve();
|
|
2149
|
+
}
|
|
2150
|
+
},
|
|
2151
|
+
onError: (error, filePath) => {
|
|
2152
|
+
console.error(`Failed to persist async run state '${filePath}':`, error);
|
|
2153
|
+
if (filePath === resultPath) finalResultPublication?.reject(error);
|
|
2074
2154
|
},
|
|
2075
|
-
onError: (error, filePath) => console.error(`Failed to persist async run state '${filePath}':`, error),
|
|
2076
2155
|
});
|
|
2077
2156
|
try {
|
|
2078
2157
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
@@ -2080,7 +2159,7 @@ async function runSubagent(
|
|
|
2080
2159
|
if (!isStorageCapacityError(error)) throw error;
|
|
2081
2160
|
console.error(`Failed to prepare async run storage '${asyncDir}' while storage is full:`, error);
|
|
2082
2161
|
}
|
|
2083
|
-
runPersistence.write(statusPath, statusPayload);
|
|
2162
|
+
runPersistence.write(statusPath, { ...statusPayload });
|
|
2084
2163
|
|
|
2085
2164
|
let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
2086
2165
|
const currentUsageTotals = (): CostSummary => {
|
|
@@ -2099,6 +2178,13 @@ async function runSubagent(
|
|
|
2099
2178
|
setOptionalProperty(statusPayload, "usageBudget", usageBudgetState(config.usageBudget, currentUsageTotals()));
|
|
2100
2179
|
return statusPayload.usageBudget;
|
|
2101
2180
|
};
|
|
2181
|
+
// Continuation admission only: the existing ledger has no in-flight cost or
|
|
2182
|
+
// external/import usage coverage. Never change ordinary budget enforcement.
|
|
2183
|
+
let continuationUsageUncertain = config.steps.some(isDynamicRunnerGroup) || flatSteps.some((step) => Boolean(step.runner || step.importAsyncRoot));
|
|
2184
|
+
const continuationUsageBudgetExhausted = (): boolean | undefined => {
|
|
2185
|
+
const exhausted = refreshUsageBudget()?.exhausted === true;
|
|
2186
|
+
return exhausted ? true : config.usageBudget && continuationUsageUncertain ? undefined : false;
|
|
2187
|
+
};
|
|
2102
2188
|
const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
|
|
2103
2189
|
if (!config.nestedRoute || !config.nestedSelf) return;
|
|
2104
2190
|
try {
|
|
@@ -2153,7 +2239,6 @@ async function runSubagent(
|
|
|
2153
2239
|
for (const node of graph.nodes) updateNode(node);
|
|
2154
2240
|
statusPayload.workflowGraph = graph;
|
|
2155
2241
|
};
|
|
2156
|
-
let finalResultCommitted = false;
|
|
2157
2242
|
const statusResultState = (): AsyncStatus["state"] | undefined => {
|
|
2158
2243
|
if (statusPayload.state === "running" || statusPayload.state === "queued") return undefined;
|
|
2159
2244
|
return statusPayload.state;
|
|
@@ -2215,14 +2300,15 @@ async function runSubagent(
|
|
|
2215
2300
|
}), (filePath, payload) => writePendingAsyncResultFile(filePath, payload as Record<string, unknown>));
|
|
2216
2301
|
};
|
|
2217
2302
|
const writeStatusPayloadNow = (): void => {
|
|
2303
|
+
if (finalResultPublication) return;
|
|
2218
2304
|
refreshWorkflowGraph();
|
|
2219
2305
|
writeRecoverableStatusResult();
|
|
2220
|
-
runPersistence.write(statusPath, statusPayload);
|
|
2306
|
+
runPersistence.write(statusPath, { ...statusPayload });
|
|
2221
2307
|
emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
|
|
2222
2308
|
};
|
|
2223
2309
|
const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
|
|
2224
2310
|
const writeStatusPayload = (immediate = true): void => {
|
|
2225
|
-
if (immediate || statusPayload.state !== "running"
|
|
2311
|
+
if (immediate || statusPayload.state !== "running") {
|
|
2226
2312
|
if (!statusWriteCoalescer.flush(statusPath)) writeStatusPayloadNow();
|
|
2227
2313
|
return;
|
|
2228
2314
|
}
|
|
@@ -2878,6 +2964,7 @@ async function runSubagent(
|
|
|
2878
2964
|
const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
|
|
2879
2965
|
const step = statusPayload.steps[flatIndex];
|
|
2880
2966
|
if (!step) return;
|
|
2967
|
+
const previousActivityState = step.activityState;
|
|
2881
2968
|
const now = Date.now();
|
|
2882
2969
|
statusPayload.currentStep = flatIndex;
|
|
2883
2970
|
if (isChildWatchdogStatusEvent(event)) {
|
|
@@ -3012,6 +3099,10 @@ async function runSubagent(
|
|
|
3012
3099
|
appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
|
|
3013
3100
|
step.turnCount = (step.turnCount ?? 0) + 1;
|
|
3014
3101
|
const usage = event.message.usage;
|
|
3102
|
+
if (config.usageBudget) {
|
|
3103
|
+
const known = (value: unknown) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
3104
|
+
if (!known(usage?.input ?? usage?.inputTokens) || !known(usage?.output ?? usage?.outputTokens)) continuationUsageUncertain = true;
|
|
3105
|
+
}
|
|
3015
3106
|
if (usage) {
|
|
3016
3107
|
const input = usage.input ?? usage.inputTokens ?? 0;
|
|
3017
3108
|
const output = usage.output ?? usage.outputTokens ?? 0;
|
|
@@ -3031,7 +3122,8 @@ async function runSubagent(
|
|
|
3031
3122
|
statusPayload.lastActivityAt = now;
|
|
3032
3123
|
statusPayload.lastUpdate = now;
|
|
3033
3124
|
maybeEmitActiveLongRunning(flatIndex, now);
|
|
3034
|
-
|
|
3125
|
+
// A sibling may keep aggregate attention unchanged; publish this step's transition.
|
|
3126
|
+
writeStatusPayload(step.activityState !== previousActivityState);
|
|
3035
3127
|
};
|
|
3036
3128
|
const updateRunnerActivityState = (now: number): boolean => {
|
|
3037
3129
|
if (!controlConfig.enabled) return false;
|
|
@@ -3103,10 +3195,55 @@ async function runSubagent(
|
|
|
3103
3195
|
activityTimer.unref?.();
|
|
3104
3196
|
}
|
|
3105
3197
|
|
|
3198
|
+
const publishSetupUnknown = (progress: WorktreeSetupProgress): void => {
|
|
3199
|
+
if (!progress.unknown) return;
|
|
3200
|
+
const proof = {
|
|
3201
|
+
version: 1 as const, state: "unknown" as const, runId: id,
|
|
3202
|
+
runnerProcessInstanceId: config.runnerProcessInstanceId ?? "unknown",
|
|
3203
|
+
reason: "process-tree-unverified" as const,
|
|
3204
|
+
diagnostic: `Worktree setup settlement unknown: ${progress.unknown}; manual reconciliation required; ${parallelHandoffPath(asyncDir)}`,
|
|
3205
|
+
};
|
|
3206
|
+
statusPayload.processTerminal = proof;
|
|
3207
|
+
// Publish the sticky proof independently of in-process child writer counts.
|
|
3208
|
+
writeAtomicJson(processTerminalPath(asyncDir), proof);
|
|
3209
|
+
};
|
|
3210
|
+
const finalizeWorktree = async (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number, action: () => void, deadlineAt?: number): Promise<void> => {
|
|
3211
|
+
let admitted = false;
|
|
3212
|
+
try { await withWorktreeTransaction(() => {
|
|
3213
|
+
if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
|
|
3214
|
+
admitted = true;
|
|
3215
|
+
action();
|
|
3216
|
+
}); }
|
|
3217
|
+
catch (error) {
|
|
3218
|
+
if (admitted) throw error;
|
|
3219
|
+
const reason = `Worktree finalization retained; manual reconciliation required: ${error instanceof Error ? error.message : String(error)}`;
|
|
3220
|
+
statusPayload.parallelHandoff = writeParallelHandoffGroup({
|
|
3221
|
+
manifestPath: parallelHandoffPath(asyncDir), runId: id,
|
|
3222
|
+
mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
|
|
3223
|
+
source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [],
|
|
3224
|
+
cleanup: { state: "partial", pruned: false, errors: [reason], tasks: setup.worktrees.map((worktree) => ({
|
|
3225
|
+
index: worktree.index, path: worktree.path, branch: worktree.branch, provider: worktree.provider, naming: worktree.naming,
|
|
3226
|
+
worktreeRemoved: false, branchRemoved: false, preserved: true, reason,
|
|
3227
|
+
})) },
|
|
3228
|
+
});
|
|
3229
|
+
previousOutput = [previousOutput, reason, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
|
|
3230
|
+
writeStatusPayload();
|
|
3231
|
+
}
|
|
3232
|
+
};
|
|
3233
|
+
const cleanupRemainingWorktree = (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number) => finalizeWorktree(setup, stepIndex, flatStartIndex, () => {
|
|
3234
|
+
const cleanup = cleanupWorktrees(setup);
|
|
3235
|
+
statusPayload.parallelHandoff = writeParallelHandoffGroup({
|
|
3236
|
+
manifestPath: parallelHandoffPath(asyncDir), runId: id,
|
|
3237
|
+
mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
|
|
3238
|
+
source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [], cleanup,
|
|
3239
|
+
});
|
|
3240
|
+
writeStatusPayload();
|
|
3241
|
+
}, config.deadlineAt);
|
|
3106
3242
|
const interruptRunner = () => {
|
|
3107
3243
|
consumeInterruptRequest(asyncDir);
|
|
3108
3244
|
if (interrupted || statusPayload.state !== "running") return;
|
|
3109
3245
|
interrupted = true;
|
|
3246
|
+
setupInterruptController.abort();
|
|
3110
3247
|
const now = Date.now();
|
|
3111
3248
|
statusPayload.state = "paused";
|
|
3112
3249
|
currentActivityState = undefined;
|
|
@@ -3340,14 +3477,14 @@ async function runSubagent(
|
|
|
3340
3477
|
cwd,
|
|
3341
3478
|
signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
|
|
3342
3479
|
abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
|
|
3343
|
-
reportOptional:
|
|
3480
|
+
reportOptional: isAgentContract(step.agentContract),
|
|
3344
3481
|
}))
|
|
3345
3482
|
: undefined;
|
|
3346
3483
|
const groupStopped = stopped || stopAbortController.signal.aborted;
|
|
3347
3484
|
const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
|
|
3348
3485
|
const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
|
|
3349
3486
|
if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
|
|
3350
|
-
const groupAcceptanceFailure = effectiveGroupAcceptance && (!
|
|
3487
|
+
const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
3351
3488
|
if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
|
|
3352
3489
|
const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
|
|
3353
3490
|
statusPayload.state = groupStopped ? "stopped" : "failed";
|
|
@@ -3564,7 +3701,8 @@ async function runSubagent(
|
|
|
3564
3701
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
3565
3702
|
onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
|
|
3566
3703
|
skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
|
|
3567
|
-
usageBudgetExhausted:
|
|
3704
|
+
usageBudgetExhausted: continuationUsageBudgetExhausted,
|
|
3705
|
+
usageBudget: config.usageBudget,
|
|
3568
3706
|
orcaProgressTab,
|
|
3569
3707
|
}), config.deadlineAt);
|
|
3570
3708
|
const taskEndTime = Date.now();
|
|
@@ -3680,7 +3818,7 @@ async function runSubagent(
|
|
|
3680
3818
|
const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
|
|
3681
3819
|
const acceptanceFailures = parallelResults
|
|
3682
3820
|
.map((result, originalIndex) => ({ result, originalIndex, task: dynamicSteps[originalIndex] }))
|
|
3683
|
-
.filter(({ result, task }) =>
|
|
3821
|
+
.filter(({ result, task }) => isAgentContract(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
|
|
3684
3822
|
if (acceptanceFailures.length > 0) {
|
|
3685
3823
|
const message = acceptanceFailures
|
|
3686
3824
|
.map(({ result, originalIndex }) => `Dynamic item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}) acceptance rejected: ${(result.acceptance ? acceptanceFailureMessage(result.acceptance) : undefined) ?? "acceptance rejected"}`)
|
|
@@ -3710,13 +3848,13 @@ async function runSubagent(
|
|
|
3710
3848
|
cwd,
|
|
3711
3849
|
signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
|
|
3712
3850
|
abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
|
|
3713
|
-
reportOptional:
|
|
3851
|
+
reportOptional: isAgentContract(step.agentContract),
|
|
3714
3852
|
}))
|
|
3715
3853
|
: undefined;
|
|
3716
3854
|
const groupStopped = stopped || stopAbortController.signal.aborted;
|
|
3717
3855
|
const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
|
|
3718
3856
|
const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
|
|
3719
|
-
const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!
|
|
3857
|
+
const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
3720
3858
|
const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
|
|
3721
3859
|
markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
|
|
3722
3860
|
if (groupError) {
|
|
@@ -3795,7 +3933,9 @@ async function runSubagent(
|
|
|
3795
3933
|
break;
|
|
3796
3934
|
}
|
|
3797
3935
|
try {
|
|
3798
|
-
worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
|
|
3936
|
+
worktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
|
|
3937
|
+
signal: setupSignal,
|
|
3938
|
+
deadlineAt: config.deadlineAt,
|
|
3799
3939
|
agents: group.parallel.map((task) => task.agent),
|
|
3800
3940
|
labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
|
|
3801
3941
|
tasks: group.parallel.map((task) => task.task),
|
|
@@ -3806,9 +3946,10 @@ async function runSubagent(
|
|
|
3806
3946
|
? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
|
|
3807
3947
|
: undefined,
|
|
3808
3948
|
baseDir: config.worktreeBaseDir,
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
const
|
|
3949
|
+
onProgress: (progress) => {
|
|
3950
|
+
publishSetupUnknown(progress);
|
|
3951
|
+
for (const worktree of progress.setup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
|
|
3952
|
+
const pendingHandoff = writeWorktreeSetupHandoff({
|
|
3812
3953
|
manifestPath: parallelHandoffPath(asyncDir),
|
|
3813
3954
|
runId: id,
|
|
3814
3955
|
mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
|
|
@@ -3816,16 +3957,25 @@ async function runSubagent(
|
|
|
3816
3957
|
cwd,
|
|
3817
3958
|
stepIndex,
|
|
3818
3959
|
flatStartIndex: groupStartFlatIndex,
|
|
3819
|
-
|
|
3960
|
+
progress,
|
|
3820
3961
|
laneBindings: handoffWorkflowKey || config.lane ? [{ index: groupStartFlatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
|
|
3821
3962
|
});
|
|
3963
|
+
if (!pendingHandoff) return;
|
|
3822
3964
|
statusPayload.parallelHandoff = pendingHandoff;
|
|
3823
3965
|
statusPayload.lastUpdate = Date.now();
|
|
3824
3966
|
writeStatusPayload();
|
|
3825
3967
|
},
|
|
3826
3968
|
}));
|
|
3969
|
+
if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
|
|
3970
|
+
if (setupSignal.aborted) throw new Error(stopped ? stopMessage : timedOut ? timeoutMessage ?? "Subagent timed out." : "Subagent paused during worktree setup.");
|
|
3827
3971
|
} catch (error) {
|
|
3972
|
+
if (worktreeSetup) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
|
|
3973
|
+
if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
|
|
3828
3974
|
const setupError = error instanceof Error ? error.message : String(error);
|
|
3975
|
+
if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
|
|
3976
|
+
for (let index = groupStartFlatIndex; index < groupStartFlatIndex + group.parallel.length; index++) {
|
|
3977
|
+
if (childStopRequests.has(index)) markChildStopped(index);
|
|
3978
|
+
}
|
|
3829
3979
|
const failedAt = Date.now();
|
|
3830
3980
|
markParallelGroupSetupFailure({
|
|
3831
3981
|
statusPayload,
|
|
@@ -3961,7 +4111,8 @@ async function runSubagent(
|
|
|
3961
4111
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
3962
4112
|
onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
|
|
3963
4113
|
skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
|
|
3964
|
-
usageBudgetExhausted:
|
|
4114
|
+
usageBudgetExhausted: continuationUsageBudgetExhausted,
|
|
4115
|
+
usageBudget: config.usageBudget,
|
|
3965
4116
|
orcaProgressTab,
|
|
3966
4117
|
}), config.deadlineAt);
|
|
3967
4118
|
if (task.sessionFile) {
|
|
@@ -4139,47 +4290,50 @@ async function runSubagent(
|
|
|
4139
4290
|
})),
|
|
4140
4291
|
);
|
|
4141
4292
|
if (worktreeSetup) {
|
|
4142
|
-
const
|
|
4143
|
-
if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
|
|
4293
|
+
const setup = worktreeSetup;
|
|
4144
4294
|
worktreeFinalized = true;
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4295
|
+
await finalizeWorktree(setup, stepIndex, groupStartFlatIndex, () => {
|
|
4296
|
+
const captured = captureParallelWorktreeDiffs(setup, asyncDir, stepIndex, group);
|
|
4297
|
+
if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
|
|
4298
|
+
const manifestPath = parallelHandoffPath(asyncDir);
|
|
4299
|
+
const handoff = {
|
|
4300
|
+
manifestPath,
|
|
4301
|
+
runId: id,
|
|
4302
|
+
mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" as const : "chain" as const,
|
|
4303
|
+
source: "async" as const,
|
|
4304
|
+
cwd,
|
|
4305
|
+
stepIndex,
|
|
4306
|
+
flatStartIndex: groupStartFlatIndex,
|
|
4307
|
+
setup,
|
|
4308
|
+
diffs: captured.diffs,
|
|
4309
|
+
results: parallelResults.map((result) => ({
|
|
4310
|
+
agent: result.agent,
|
|
4311
|
+
...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
|
|
4312
|
+
...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
|
|
4313
|
+
...(config.lane ? { lane: config.lane } : {}),
|
|
4314
|
+
status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
|
|
4315
|
+
processSignal: result.processSignal,
|
|
4316
|
+
interrupted: result.interrupted,
|
|
4317
|
+
timedOut: result.timedOut,
|
|
4318
|
+
stopped: result.stopped,
|
|
4319
|
+
}))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
|
|
4320
|
+
summary: result.output || result.error || "(no output)",
|
|
4321
|
+
...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
|
|
4322
|
+
...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
|
|
4323
|
+
...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
|
|
4324
|
+
...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
|
|
4325
|
+
})),
|
|
4326
|
+
};
|
|
4327
|
+
try {
|
|
4328
|
+
writeParallelHandoffGroup(handoff);
|
|
4329
|
+
const cleanup = cleanupWorktrees(setup, { kind: "preserve", capturedDiffs: captured.diffs, handoffManifestPath: manifestPath });
|
|
4330
|
+
statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
|
|
4331
|
+
previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
|
|
4332
|
+
} catch (error) {
|
|
4333
|
+
previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
|
|
4334
|
+
}
|
|
4335
|
+
writeStatusPayload();
|
|
4336
|
+
});
|
|
4183
4337
|
}
|
|
4184
4338
|
|
|
4185
4339
|
appendJsonl(eventsPath, JSON.stringify({
|
|
@@ -4188,12 +4342,12 @@ async function runSubagent(
|
|
|
4188
4342
|
runId: id,
|
|
4189
4343
|
stepIndex,
|
|
4190
4344
|
success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1)
|
|
4191
|
-
&& parallelResults.every((result, index) => !(
|
|
4345
|
+
&& parallelResults.every((result, index) => !(isAgentContract(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
|
|
4192
4346
|
}));
|
|
4193
4347
|
|
|
4194
4348
|
const acceptanceGateFailure = parallelResults
|
|
4195
4349
|
.map((result, index) => ({ result, index, task: group.parallel[index] }))
|
|
4196
|
-
.find(({ result, task }) =>
|
|
4350
|
+
.find(({ result, task }) => isAgentContract(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
|
|
4197
4351
|
if (acceptanceGateFailure) {
|
|
4198
4352
|
statusPayload.error = (acceptanceGateFailure.result.acceptance ? acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) : undefined) ?? "Parallel acceptance gate rejected the step.";
|
|
4199
4353
|
writeStatusPayload();
|
|
@@ -4203,7 +4357,7 @@ async function runSubagent(
|
|
|
4203
4357
|
break;
|
|
4204
4358
|
}
|
|
4205
4359
|
} finally {
|
|
4206
|
-
if (worktreeSetup && !worktreeFinalized)
|
|
4360
|
+
if (worktreeSetup && !worktreeFinalized) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
|
|
4207
4361
|
}
|
|
4208
4362
|
} else {
|
|
4209
4363
|
const seqStep = step as SubagentStep;
|
|
@@ -4230,7 +4384,9 @@ async function runSubagent(
|
|
|
4230
4384
|
let singleWorktreeSetup: WorktreeSetup | undefined;
|
|
4231
4385
|
if (seqStep.worktree) {
|
|
4232
4386
|
try {
|
|
4233
|
-
singleWorktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
|
|
4387
|
+
singleWorktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
|
|
4388
|
+
signal: setupSignal,
|
|
4389
|
+
deadlineAt: config.deadlineAt,
|
|
4234
4390
|
agents: [seqStep.agent],
|
|
4235
4391
|
labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
|
|
4236
4392
|
tasks: [seqStep.task],
|
|
@@ -4241,12 +4397,13 @@ async function runSubagent(
|
|
|
4241
4397
|
? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
|
|
4242
4398
|
: undefined,
|
|
4243
4399
|
baseDir: config.worktreeBaseDir,
|
|
4244
|
-
|
|
4245
|
-
|
|
4400
|
+
onProgress: (progress) => {
|
|
4401
|
+
publishSetupUnknown(progress);
|
|
4402
|
+
const worktree = progress.setup.worktrees[0];
|
|
4246
4403
|
if (worktree) {
|
|
4247
4404
|
setStatusWorktreeReference(requiredStatusStep(statusPayload, flatIndex), worktree);
|
|
4248
4405
|
}
|
|
4249
|
-
const pendingHandoff =
|
|
4406
|
+
const pendingHandoff = writeWorktreeSetupHandoff({
|
|
4250
4407
|
manifestPath: parallelHandoffPath(asyncDir),
|
|
4251
4408
|
runId: id,
|
|
4252
4409
|
mode: "single",
|
|
@@ -4254,19 +4411,47 @@ async function runSubagent(
|
|
|
4254
4411
|
cwd,
|
|
4255
4412
|
stepIndex,
|
|
4256
4413
|
flatStartIndex: flatIndex,
|
|
4257
|
-
|
|
4414
|
+
progress,
|
|
4258
4415
|
laneBindings: handoffWorkflowKey || config.lane ? [{ index: flatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
|
|
4259
4416
|
});
|
|
4417
|
+
if (!pendingHandoff) return;
|
|
4260
4418
|
statusPayload.parallelHandoff = pendingHandoff;
|
|
4261
4419
|
statusPayload.lastUpdate = Date.now();
|
|
4262
4420
|
writeStatusPayload();
|
|
4263
4421
|
},
|
|
4264
4422
|
}));
|
|
4265
4423
|
} catch (error) {
|
|
4266
|
-
if (
|
|
4267
|
-
|
|
4424
|
+
if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
|
|
4425
|
+
if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
|
|
4426
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4427
|
+
if (childStopRequests.has(flatIndex)) markChildStopped(flatIndex);
|
|
4428
|
+
const statusStep = requiredStatusStep(statusPayload, flatIndex);
|
|
4429
|
+
statusStep.status = stopped || childStopRequests.has(flatIndex) ? "stopped" : interrupted ? "paused" : "failed";
|
|
4430
|
+
statusStep.error ??= message;
|
|
4431
|
+
statusStep.exitCode = interrupted ? 0 : 1;
|
|
4432
|
+
statusStep.endedAt = Date.now();
|
|
4433
|
+
results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4434
|
+
: childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
|
|
4435
|
+
: timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4436
|
+
: interrupted ? pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4437
|
+
: { agent: seqStep.agent, output: message, error: message, exitCode: 1, success: false });
|
|
4438
|
+
statusPayload.error ??= message;
|
|
4439
|
+
writeStatusPayload();
|
|
4440
|
+
flatIndex++;
|
|
4441
|
+
break;
|
|
4268
4442
|
}
|
|
4269
4443
|
}
|
|
4444
|
+
if (singleWorktreeSetup && config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
|
|
4445
|
+
if (singleWorktreeSetup && (timedOut || stopped || interrupted || childStopRequests.has(flatIndex))) {
|
|
4446
|
+
await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
|
|
4447
|
+
results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4448
|
+
: childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
|
|
4449
|
+
: timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4450
|
+
: pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
|
|
4451
|
+
if (interrupted) requiredStatusStep(statusPayload, flatIndex).status = "paused";
|
|
4452
|
+
flatIndex++;
|
|
4453
|
+
continue;
|
|
4454
|
+
}
|
|
4270
4455
|
const singleCwd = singleWorktreeSetup?.worktrees[0]?.agentCwd ?? cwd;
|
|
4271
4456
|
const stepStartTime = Date.now();
|
|
4272
4457
|
statusPayload.currentStep = flatIndex;
|
|
@@ -4325,11 +4510,12 @@ async function runSubagent(
|
|
|
4325
4510
|
onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
|
|
4326
4511
|
onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
|
|
4327
4512
|
skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
|
|
4328
|
-
usageBudgetExhausted:
|
|
4513
|
+
usageBudgetExhausted: continuationUsageBudgetExhausted,
|
|
4514
|
+
usageBudget: config.usageBudget,
|
|
4329
4515
|
orcaProgressTab,
|
|
4330
4516
|
}), config.deadlineAt);
|
|
4331
4517
|
} catch (error) {
|
|
4332
|
-
if (singleWorktreeSetup)
|
|
4518
|
+
if (singleWorktreeSetup) await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
|
|
4333
4519
|
throw error;
|
|
4334
4520
|
}
|
|
4335
4521
|
if (seqStep.sessionFile) {
|
|
@@ -4361,6 +4547,7 @@ async function runSubagent(
|
|
|
4361
4547
|
totalCost: singleResult.totalCost,
|
|
4362
4548
|
usage: singleResult.usage,
|
|
4363
4549
|
artifactPaths: singleResult.artifactPaths,
|
|
4550
|
+
savedOutputPath: singleResult.savedOutputPath,
|
|
4364
4551
|
transcriptPath: singleResult.transcriptPath,
|
|
4365
4552
|
transcriptError: singleResult.transcriptError,
|
|
4366
4553
|
effects: singleResult.effects,
|
|
@@ -4481,48 +4668,51 @@ async function runSubagent(
|
|
|
4481
4668
|
}));
|
|
4482
4669
|
if (stopped || childStopped) appendTerminalChildStatusEvent(flatIndex, stepEndTime);
|
|
4483
4670
|
if (singleWorktreeSetup && !singleResult.detached) {
|
|
4484
|
-
const
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
manifestPath
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4671
|
+
const setup = singleWorktreeSetup;
|
|
4672
|
+
await finalizeWorktree(setup, stepIndex, flatIndex, () => {
|
|
4673
|
+
const diffs = diffWorktrees(setup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
|
|
4674
|
+
const diffSummary = formatWorktreeDiffSummary(diffs);
|
|
4675
|
+
const manifestPath = parallelHandoffPath(asyncDir);
|
|
4676
|
+
const handoff = {
|
|
4677
|
+
manifestPath,
|
|
4678
|
+
runId: id,
|
|
4679
|
+
mode: "single" as const,
|
|
4680
|
+
source: "async" as const,
|
|
4681
|
+
cwd,
|
|
4682
|
+
stepIndex,
|
|
4683
|
+
flatStartIndex: flatIndex,
|
|
4684
|
+
setup,
|
|
4685
|
+
diffs,
|
|
4686
|
+
results: [{
|
|
4687
|
+
agent: singleResult.agent,
|
|
4688
|
+
...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
|
|
4689
|
+
...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
|
|
4690
|
+
...(config.lane ? { lane: config.lane } : {}),
|
|
4691
|
+
status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
|
|
4692
|
+
summary: singleResult.output || singleResult.error || "(no output)",
|
|
4693
|
+
...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
|
|
4694
|
+
...(singleResult.structuredOutput !== undefined ? { structuredOutput: singleResult.structuredOutput } : {}),
|
|
4695
|
+
...(singleResult.structuredOutputPath ? { structuredOutputPath: singleResult.structuredOutputPath } : {}),
|
|
4696
|
+
...(singleResult.sessionFile ? { sessionPath: singleResult.sessionFile } : {}),
|
|
4697
|
+
}],
|
|
4698
|
+
};
|
|
4699
|
+
try {
|
|
4700
|
+
writeParallelHandoffGroup(handoff);
|
|
4701
|
+
const cleanup = cleanupWorktrees(setup, {
|
|
4702
|
+
kind: "preserve",
|
|
4703
|
+
capturedDiffs: diffs,
|
|
4704
|
+
handoffManifestPath: manifestPath,
|
|
4705
|
+
...(config.parentWorkflowRunId && singleResult.sessionFile && fs.existsSync(singleResult.sessionFile) && !singleResult.stopped
|
|
4706
|
+
? { cleanupBlocker: "retained child resume requires managed worktree cwd" }
|
|
4707
|
+
: {}),
|
|
4708
|
+
});
|
|
4709
|
+
statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
|
|
4710
|
+
previousOutput = [previousOutput, diffSummary, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
|
|
4711
|
+
} catch (error) {
|
|
4712
|
+
previousOutput = [previousOutput, diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
|
|
4713
|
+
}
|
|
4714
|
+
writeStatusPayload();
|
|
4715
|
+
});
|
|
4526
4716
|
}
|
|
4527
4717
|
|
|
4528
4718
|
if (singleResult.completionGuardTriggered) {
|
|
@@ -4540,7 +4730,7 @@ async function runSubagent(
|
|
|
4540
4730
|
}
|
|
4541
4731
|
|
|
4542
4732
|
flatIndex++;
|
|
4543
|
-
if (
|
|
4733
|
+
if (isAgentContract(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
|
|
4544
4734
|
statusPayload.error = acceptanceFailureMessage(singleResult.acceptance) ?? "Chain acceptance gate rejected the step.";
|
|
4545
4735
|
writeStatusPayload();
|
|
4546
4736
|
break;
|
|
@@ -4626,6 +4816,11 @@ async function runSubagent(
|
|
|
4626
4816
|
stopped: result.stopped,
|
|
4627
4817
|
})));
|
|
4628
4818
|
const partialWithEvidence = !stopped && !signalTerminated && !timedOut && !usageBudgetExceeded && !interrupted && results.some(partialEvidenceResult) && !results.some(concreteFailureResult);
|
|
4819
|
+
// Flush while still nonterminal; deferred status retries retain that state snapshot.
|
|
4820
|
+
statusWriteCoalescer.flush(statusPath);
|
|
4821
|
+
const publication = new Promise<void>((resolve, reject) => {
|
|
4822
|
+
finalResultPublication = { resolve, reject };
|
|
4823
|
+
});
|
|
4629
4824
|
statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
|
|
4630
4825
|
closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
|
|
4631
4826
|
disposeControlInbox();
|
|
@@ -4684,6 +4879,9 @@ async function runSubagent(
|
|
|
4684
4879
|
statusPayload.error = `Step failed: ${failedStep.agent}`;
|
|
4685
4880
|
}
|
|
4686
4881
|
}
|
|
4882
|
+
let childSessionDisposal: Promise<void> | undefined;
|
|
4883
|
+
const disposeChildSessions = (): Promise<void> => childSessionDisposal ??= childSessions.dispose()
|
|
4884
|
+
.catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
|
|
4687
4885
|
try {
|
|
4688
4886
|
runPersistence.write(resultPath, {
|
|
4689
4887
|
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -4724,6 +4922,7 @@ async function runSubagent(
|
|
|
4724
4922
|
totalCost: r.totalCost,
|
|
4725
4923
|
usage: r.usage,
|
|
4726
4924
|
artifactPaths: r.artifactPaths,
|
|
4925
|
+
savedOutputPath: r.savedOutputPath,
|
|
4727
4926
|
outputSaveError: r.outputSaveError,
|
|
4728
4927
|
metadataSaveError: r.metadataSaveError,
|
|
4729
4928
|
truncated: r.truncated,
|
|
@@ -4778,13 +4977,16 @@ async function runSubagent(
|
|
|
4778
4977
|
...(taskIndex !== undefined && { taskIndex }),
|
|
4779
4978
|
...(totalTasks !== undefined && { totalTasks }),
|
|
4780
4979
|
}, (filePath, payload) => { writeAsyncResultFile(filePath, payload as Record<string, unknown>); });
|
|
4781
|
-
|
|
4980
|
+
// Only capacity deferral releases settled sessions before terminal publication.
|
|
4981
|
+
if (!finalResultCommitted) await Promise.all([publication, disposeChildSessions()]);
|
|
4782
4982
|
} catch (err) {
|
|
4783
4983
|
const message = `Failed to write result file ${resultPath}: ${err instanceof Error ? err.message : String(err)}`;
|
|
4784
4984
|
console.error(message, err);
|
|
4785
4985
|
statusPayload.state = "failed";
|
|
4786
4986
|
statusPayload.error = message;
|
|
4787
4987
|
statusPayload.lastUpdate = Date.now();
|
|
4988
|
+
} finally {
|
|
4989
|
+
finalResultPublication = undefined;
|
|
4788
4990
|
}
|
|
4789
4991
|
writeStatusPayload();
|
|
4790
4992
|
await orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
|
|
@@ -4822,9 +5024,8 @@ async function runSubagent(
|
|
|
4822
5024
|
}), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
|
|
4823
5025
|
fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
|
|
4824
5026
|
}));
|
|
4825
|
-
//
|
|
4826
|
-
|
|
4827
|
-
await childSessions.dispose().catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
|
|
5027
|
+
// Preserve normal-success disposal ordering, then drain remaining persistence retries.
|
|
5028
|
+
await disposeChildSessions();
|
|
4828
5029
|
while (runPersistence.pendingCount() + indexPersistence.pendingCount() > 0) {
|
|
4829
5030
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
4830
5031
|
}
|
|
@@ -4832,7 +5033,7 @@ async function runSubagent(
|
|
|
4832
5033
|
indexPersistence.dispose();
|
|
4833
5034
|
if (config.runnerProcessInstanceId) {
|
|
4834
5035
|
// Children run inside this process, so no step has writer processes to prove terminal.
|
|
4835
|
-
const writers: Record<string,
|
|
5036
|
+
const writers: Record<string, PiWriterProcessInstanceExit[]> = {};
|
|
4836
5037
|
const expectedWriters: Record<string, number> = {};
|
|
4837
5038
|
for (const index of results.keys()) {
|
|
4838
5039
|
writers[String(index)] = [];
|
|
@@ -4968,6 +5169,7 @@ function startConfiguredSubagent(config: SubagentRunConfig): void {
|
|
|
4968
5169
|
);
|
|
4969
5170
|
}
|
|
4970
5171
|
|
|
5172
|
+
if (isRunnerEntrypoint) {
|
|
4971
5173
|
const configArg = process.argv[2];
|
|
4972
5174
|
if (configArg) {
|
|
4973
5175
|
try {
|
|
@@ -4999,3 +5201,4 @@ if (configArg) {
|
|
|
4999
5201
|
}
|
|
5000
5202
|
});
|
|
5001
5203
|
}
|
|
5204
|
+
}
|