pi-subagents 0.67.0 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +1 -1
- package/docs/agents.md +37 -12
- package/docs/configuration.md +61 -19
- package/docs/extension-api.md +5 -1
- package/docs/missions.md +2 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +18 -8
- package/docs/standalone-background.md +13 -3
- package/docs/tool-reference.md +15 -12
- package/docs/watchdog.md +10 -12
- package/docs/workflows.md +11 -1
- package/index.ts +5 -2
- package/package.json +4 -2
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +25 -11
- package/skills/pi-subagents/SKILL.md +18 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +4 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +184 -71
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/preflight.ts +20 -16
- package/src/api/required-child-extensions.ts +6 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +3 -0
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +42 -31
- package/src/extension/public-execution.ts +3 -3
- package/src/extension/schemas.ts +16 -5
- package/src/extension/tool-description.ts +8 -7
- package/src/intercom/native-supervisor-channel.ts +22 -18
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +309 -126
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +13 -4
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +20 -10
- package/src/runs/background/binary-bootstrap.ts +5 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/notify.ts +74 -6
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +20 -29
- package/src/runs/background/runner-aliases.ts +4 -33
- package/src/runs/background/runner-child-launch.ts +4 -1
- package/src/runs/background/runner-child-sessions.ts +2 -2
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +11 -5
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/subagent-runner.ts +396 -275
- package/src/runs/background/subagent-wait.ts +128 -23
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +4 -2
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +91 -218
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/subagent-executor.ts +266 -80
- package/src/runs/shared/acceptance.ts +34 -10
- package/src/runs/shared/async-status-projection.ts +123 -33
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +19 -6
- package/src/runs/shared/child-runtime-config.ts +5 -0
- package/src/runs/shared/child-session.ts +94 -50
- package/src/runs/shared/child-tool-plan.ts +28 -16
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +3 -3
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/launch-contract.ts +6 -9
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +70 -29
- package/src/slash/slash-commands.ts +0 -6
- package/src/slash/subagents-admin.ts +13 -9
- package/src/tui/render.ts +20 -10
- package/src/watchdog/child-status.ts +28 -36
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +1 -1
- package/src/watchdog/register-child.ts +10 -3
- package/src/watchdog/register-main.ts +20 -20
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +14 -30
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +23 -12
- package/src/watchdog/settings.ts +3 -6
- package/src/watchdog/types.ts +3 -5
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +42 -3
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
|
@@ -1,28 +1,15 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
2
|
import * as fs from "node:fs";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
3
|
import * as path from "node:path";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
6
5
|
import { pathToFileURL } from "node:url";
|
|
7
6
|
import type { Message } from "@earendil-works/pi-ai";
|
|
8
7
|
import { arbitrateCompletionGuardRescue, createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
|
|
8
|
+
import { installRunnerHttpDispatcher } from "./runner-http-dispatcher.ts";
|
|
9
9
|
|
|
10
|
-
// Detached runners skip Pi's CLI proxy setup. Keep fetch on the same Undici dispatcher.
|
|
11
|
-
function ensureProxyAwareHttpDispatcher(): void {
|
|
12
|
-
try {
|
|
13
|
-
// SAFETY: require loads the pinned direct dependency described by these types.
|
|
14
|
-
const undici = createRequire(import.meta.url)("undici") as typeof import("undici");
|
|
15
|
-
const dispatcher = new undici.EnvHttpProxyAgent({ allowH2: false });
|
|
16
|
-
// Fetch rejects stream errors; the listener prevents an unhandled EventEmitter error.
|
|
17
|
-
EventEmitter.prototype.on.call(dispatcher, "error", () => {});
|
|
18
|
-
undici.setGlobalDispatcher(dispatcher);
|
|
19
|
-
undici.install();
|
|
20
|
-
} catch (error) {
|
|
21
|
-
console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
10
|
const isRunnerEntrypoint = Boolean(process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href);
|
|
25
|
-
|
|
11
|
+
// Detached Node runners skip Pi's CLI dispatcher setup; install the runner's own.
|
|
12
|
+
if (isRunnerEntrypoint) installRunnerHttpDispatcher({ agentDir: getAgentDir(), cwd: process.cwd() });
|
|
26
13
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
27
14
|
import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
|
|
28
15
|
import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
@@ -30,11 +17,12 @@ import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilie
|
|
|
30
17
|
import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
|
|
31
18
|
import { updateActiveRunIndex } from "./active-run-index.ts";
|
|
32
19
|
import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
33
|
-
import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, watchAsyncControlInbox, type SteerRequest, type StopRequest } from "./control-channel.ts";
|
|
20
|
+
import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, consumeStopRequestPayloads, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, watchAsyncControlInbox, type SteerRequest, type StopRequest } from "./control-channel.ts";
|
|
34
21
|
import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
|
|
35
22
|
import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
|
|
36
23
|
import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
|
|
37
24
|
import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
25
|
+
import { runSetupCommand } from "../shared/worktree-setup-command.ts";
|
|
38
26
|
import {
|
|
39
27
|
type ActivityState,
|
|
40
28
|
type ArtifactConfig,
|
|
@@ -49,7 +37,6 @@ import {
|
|
|
49
37
|
type CostSummary,
|
|
50
38
|
type LaunchResolvedChildExtensions,
|
|
51
39
|
type RuntimeAcknowledgedChildExtensions,
|
|
52
|
-
type ModelAttempt,
|
|
53
40
|
type PiWriterProcessInstanceExit,
|
|
54
41
|
type NestedRouteInfo,
|
|
55
42
|
type NestedRunSummary,
|
|
@@ -66,6 +53,7 @@ import {
|
|
|
66
53
|
type SteeringTargetStatus,
|
|
67
54
|
type SubagentChildStatusEvent,
|
|
68
55
|
type WorkflowLaneMetadata,
|
|
56
|
+
type HerdrMachineReference,
|
|
69
57
|
DEFAULT_MAX_OUTPUT,
|
|
70
58
|
type MaxOutputConfig,
|
|
71
59
|
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -97,9 +85,7 @@ import type { InheritedChildRuntime } from "../shared/child-launch.ts";
|
|
|
97
85
|
import { buildRunnerChildLaunch } from "./runner-child-launch.ts";
|
|
98
86
|
import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
|
|
99
87
|
import type { ChildSessionFactory, DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
|
|
100
|
-
import {
|
|
101
|
-
import { planReadonlyModelContinuation, READONLY_CONTINUATION_PROMPT, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
|
|
102
|
-
import { getReadonlySessionEvidence } from "../shared/readonly-session-evidence.ts";
|
|
88
|
+
import { runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type SteerDelivery, type StepSteerHandler } from "./run-child-session.ts";
|
|
103
89
|
import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
|
|
104
90
|
import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
|
|
105
91
|
import { deriveChildSessionName } from "../../shared/child-session-name.ts";
|
|
@@ -112,10 +98,10 @@ import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTr
|
|
|
112
98
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
113
99
|
import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
|
|
114
100
|
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
115
|
-
import {
|
|
101
|
+
import { formatSubagentModelVerificationError, isContextOverflow } from "../shared/model-resolution.ts";
|
|
116
102
|
import { markProcessTerminalCandidateLeaseRelease, processTerminalPath, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
|
|
117
103
|
import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, unconsumedSteerReason, updateSteeringTarget } from "./steering.ts";
|
|
118
|
-
import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
|
|
104
|
+
import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getAgentDir, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
|
|
119
105
|
import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
|
|
120
106
|
import { planCompletionEvidence, projectSettlementDiagnostic } from "../shared/completion-evidence.ts";
|
|
121
107
|
import { planAbortRecovery } from "../shared/abort-recovery.ts";
|
|
@@ -166,6 +152,8 @@ import { resolveClaudeCodeLaunch } from "../shared/claude-code-adapter.ts";
|
|
|
166
152
|
import { resolveCodexExecLaunch } from "../shared/codex-exec-adapter.ts";
|
|
167
153
|
import { resolveCursorAgentLaunch } from "../shared/cursor-agent-adapter.ts";
|
|
168
154
|
import { resolveExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
|
|
155
|
+
import { formatHerdrMachineHint, prepareHerdrMachineExternalCliRun } from "../shared/herdr-machine.ts";
|
|
156
|
+
import { HerdrExternalNeedsAttentionError, createHerdrExternalAdapter, prepareInternalHerdrExternalAdapter, type HerdrExternalAdapterId, type HerdrExternalResult } from "../shared/herdr-external-adapters.ts";
|
|
169
157
|
import { runExternalJob } from "../shared/external-job-runner.ts";
|
|
170
158
|
import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
|
|
171
159
|
import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
@@ -224,6 +212,8 @@ export interface SubagentRunConfig {
|
|
|
224
212
|
deadlineAt?: number;
|
|
225
213
|
/** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
|
|
226
214
|
toolTimeoutMs?: number;
|
|
215
|
+
/** Steer the running steps to checkpoint and stop this many ms before `deadlineAt`; absent = no checkpoint steer. */
|
|
216
|
+
checkpointBeforeDeadlineMs?: number;
|
|
227
217
|
toolBudget?: ResolvedToolBudget;
|
|
228
218
|
usageBudget?: UsageBudgetConfig;
|
|
229
219
|
revivalLease?: SessionLeaseRequest;
|
|
@@ -272,14 +262,15 @@ interface StepResult {
|
|
|
272
262
|
sessionFile?: string;
|
|
273
263
|
intercomTarget?: string;
|
|
274
264
|
model?: string;
|
|
265
|
+
nativeMachine?: import("../../shared/types.ts").SingleResult["nativeMachine"];
|
|
275
266
|
thinking?: string;
|
|
276
|
-
|
|
277
|
-
modelAttempts?: ModelAttempt[];
|
|
267
|
+
requestedModel?: string;
|
|
278
268
|
/** True when the dispatch failed because the input exceeded the model's context window. */
|
|
279
269
|
contextOverflow?: boolean;
|
|
280
270
|
totalCost?: CostSummary;
|
|
281
271
|
artifactPaths?: ArtifactPaths;
|
|
282
272
|
outputSaveError?: string;
|
|
273
|
+
artifactOutputSaveFailed?: true;
|
|
283
274
|
metadataSaveError?: string;
|
|
284
275
|
truncated?: boolean;
|
|
285
276
|
transcriptPath?: string;
|
|
@@ -422,50 +413,22 @@ function emptyUsage(): Usage {
|
|
|
422
413
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
423
414
|
}
|
|
424
415
|
|
|
425
|
-
function
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
let output = 0;
|
|
429
|
-
for (const attempt of attempts) {
|
|
430
|
-
input += attempt.usage?.input ?? 0;
|
|
431
|
-
output += attempt.usage?.output ?? 0;
|
|
432
|
-
}
|
|
416
|
+
function tokenUsageFromUsage(usage: Usage | undefined): TokenUsage | null {
|
|
417
|
+
const input = usage?.input ?? 0;
|
|
418
|
+
const output = usage?.output ?? 0;
|
|
433
419
|
const total = input + output;
|
|
434
420
|
return total > 0 ? { input, output, total } : null;
|
|
435
421
|
}
|
|
436
422
|
|
|
437
|
-
function
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
let costUsd = 0;
|
|
442
|
-
for (const attempt of attempts) {
|
|
443
|
-
inputTokens += attempt.usage?.input ?? 0;
|
|
444
|
-
outputTokens += attempt.usage?.output ?? 0;
|
|
445
|
-
costUsd += attempt.usage?.cost ?? 0;
|
|
446
|
-
}
|
|
423
|
+
function costSummaryFromUsage(usage: Usage | undefined): CostSummary | undefined {
|
|
424
|
+
const inputTokens = usage?.input ?? 0;
|
|
425
|
+
const outputTokens = usage?.output ?? 0;
|
|
426
|
+
const costUsd = usage?.cost ?? 0;
|
|
447
427
|
return inputTokens > 0 || outputTokens > 0 || costUsd > 0
|
|
448
428
|
? { inputTokens, outputTokens, costUsd }
|
|
449
429
|
: undefined;
|
|
450
430
|
}
|
|
451
431
|
|
|
452
|
-
function usageFromAttempts(attempts: ModelAttempt[] | undefined): Usage | undefined {
|
|
453
|
-
if (!attempts || attempts.length === 0) return undefined;
|
|
454
|
-
const usage = emptyUsage();
|
|
455
|
-
for (const attempt of attempts) {
|
|
456
|
-
if (!attempt.usage) continue;
|
|
457
|
-
usage.input += attempt.usage.input;
|
|
458
|
-
usage.output += attempt.usage.output;
|
|
459
|
-
usage.cacheRead += attempt.usage.cacheRead;
|
|
460
|
-
usage.cacheWrite += attempt.usage.cacheWrite;
|
|
461
|
-
usage.cost += attempt.usage.cost;
|
|
462
|
-
usage.turns += attempt.usage.turns;
|
|
463
|
-
}
|
|
464
|
-
return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0
|
|
465
|
-
? usage
|
|
466
|
-
: undefined;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
432
|
function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
|
|
470
433
|
const nonEmpty = lines.filter((line) => line.trim());
|
|
471
434
|
if (nonEmpty.length === 0) return;
|
|
@@ -659,6 +622,38 @@ function writeRunLog(
|
|
|
659
622
|
write(logPath, lines.join("\n"));
|
|
660
623
|
}
|
|
661
624
|
|
|
625
|
+
function expectedMissingGitEvidence(error: unknown): boolean {
|
|
626
|
+
if (typeof (error as { code?: unknown })?.code !== "number") return false;
|
|
627
|
+
const stderr = (error as { stderr?: unknown }).stderr;
|
|
628
|
+
const detail = Buffer.isBuffer(stderr) ? stderr.toString("utf-8") : String(stderr ?? "");
|
|
629
|
+
return /not a git repository|Needed a single revision/i.test(detail);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function readGitFingerprint(cwd: string, signal: AbortSignal, onError: (error: unknown) => void): Promise<string | undefined> {
|
|
633
|
+
try {
|
|
634
|
+
const runGit = async (args: string[]) => {
|
|
635
|
+
const result = await runSetupCommand("git", args, {
|
|
636
|
+
cwd, signal, maxBuffer: 16 * 1024 * 1024, deadlineAt: Date.now() + 30_000,
|
|
637
|
+
acceptedExitCodes: Array.from({ length: 256 }, (_, code) => code),
|
|
638
|
+
});
|
|
639
|
+
if (result.error) throw result.error;
|
|
640
|
+
if (result.status !== 0) {
|
|
641
|
+
throw Object.assign(new Error(result.stderr || `git exited with ${result.status}`), { code: result.status, stderr: result.stderr });
|
|
642
|
+
}
|
|
643
|
+
return result.stdoutBuffer;
|
|
644
|
+
};
|
|
645
|
+
const headOutput = await runGit(["rev-parse", "--verify", "HEAD"]);
|
|
646
|
+
const head = headOutput.toString("utf-8").trim();
|
|
647
|
+
if (!head) return undefined;
|
|
648
|
+
const status = await runGit(["status", "--porcelain=v1", "-z", "--untracked-files=normal"]);
|
|
649
|
+
return `${head}\0${status.toString("base64")}`;
|
|
650
|
+
} catch (error) {
|
|
651
|
+
if ((error as NodeJS.ErrnoException)?.code === "PROCESS_TREE_UNVERIFIED") throw error;
|
|
652
|
+
if (!signal.aborted && !expectedMissingGitEvidence(error)) onError(error);
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
662
657
|
/** Context for running a single step */
|
|
663
658
|
interface SingleStepContext {
|
|
664
659
|
previousOutput: string;
|
|
@@ -703,6 +698,8 @@ interface SingleStepContext {
|
|
|
703
698
|
onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
|
|
704
699
|
onChildEvent?: (event: ChildEvent) => void;
|
|
705
700
|
onExternalProcess?: (process: ExternalProcessStatus) => void;
|
|
701
|
+
prepareExternalActivity?: (cwd: string, signal: AbortSignal) => Promise<void>;
|
|
702
|
+
onExternalStreamActivity?: () => void;
|
|
706
703
|
onExternalJob?: (status: ExternalJobStatus) => void;
|
|
707
704
|
skipAcceptance?: () => boolean;
|
|
708
705
|
/** Authoritative owner decision after event delivery; undefined includes incomplete run-wide usage. */
|
|
@@ -714,7 +711,26 @@ interface SingleStepContext {
|
|
|
714
711
|
orcaProgressTab?: OrcaProgressTab;
|
|
715
712
|
}
|
|
716
713
|
|
|
717
|
-
/**
|
|
714
|
+
/** Machine runs: a one-line hint for predictable remote failures, and a note that writer changes live on the machine. */
|
|
715
|
+
function decorateHerdrMachineResult<T extends { output: string; exitCode: number | null; error?: string; externalProcess: { stderrPath: string } }>(result: T, machine: HerdrMachineReference, adapter: string | undefined): T {
|
|
716
|
+
let stderrTail = "";
|
|
717
|
+
try {
|
|
718
|
+
const stderr = fs.readFileSync(result.externalProcess.stderrPath, "utf-8");
|
|
719
|
+
stderrTail = stderr.slice(-4096);
|
|
720
|
+
} catch { /* stderr log is best-effort evidence. */ }
|
|
721
|
+
const hint = result.exitCode === 0 ? undefined : formatHerdrMachineHint(machine, `${result.error ?? ""}\n${stderrTail}\nexit code ${result.exitCode}`);
|
|
722
|
+
const error = hint ? `${result.error ?? `Remote command exited with code ${result.exitCode}.`}\n${hint}` : result.error;
|
|
723
|
+
const note = result.exitCode === 0 && adapter?.endsWith("-writer") ? `Changes made by this run live on ${machine.label ?? machine.id} at ${machine.cwd}; the local checkout is unchanged.` : undefined;
|
|
724
|
+
const output = note ? (result.output.trim() ? `${result.output.trimEnd()}\n\n${note}` : note) : result.output;
|
|
725
|
+
return { ...result, output, ...(error !== undefined ? { error } : {}) };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export async function settleHerdrExternalRunnerError(error: unknown, adapter: HerdrExternalAdapterId, evidence: Parameters<ReturnType<typeof createHerdrExternalAdapter>["normalize"]>[0], retain: () => Promise<void>): Promise<HerdrExternalResult> {
|
|
729
|
+
await retain();
|
|
730
|
+
if (!(error instanceof HerdrExternalNeedsAttentionError)) throw error;
|
|
731
|
+
return createHerdrExternalAdapter(adapter).normalize(evidence, error.message);
|
|
732
|
+
}
|
|
733
|
+
|
|
718
734
|
export async function runSingleStepInner(
|
|
719
735
|
step: SubagentStep,
|
|
720
736
|
ctx: SingleStepContext,
|
|
@@ -773,8 +789,7 @@ export async function runSingleStepInner(
|
|
|
773
789
|
sessionFile: imported.sessionFile,
|
|
774
790
|
intercomTarget: imported.intercomTarget,
|
|
775
791
|
model: imported.model,
|
|
776
|
-
|
|
777
|
-
modelAttempts: imported.modelAttempts,
|
|
792
|
+
requestedModel: imported.requestedModel,
|
|
778
793
|
contextOverflow: imported.contextOverflow,
|
|
779
794
|
totalCost: imported.totalCost,
|
|
780
795
|
usage: imported.usage,
|
|
@@ -808,13 +823,13 @@ export async function runSingleStepInner(
|
|
|
808
823
|
subagentOnlyExtensions: step.subagentOnlyExtensions,
|
|
809
824
|
fast: step.fast,
|
|
810
825
|
model: step.model,
|
|
811
|
-
modelCandidates: step.modelCandidates,
|
|
812
826
|
mcpDirectTools: step.mcpDirectTools,
|
|
813
827
|
cwd: step.cwd ?? ctx.cwd,
|
|
814
828
|
requireReadTool: Boolean(step.skills?.length),
|
|
815
829
|
structuredOutput: Boolean(effectiveStructuredOutput),
|
|
816
830
|
capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
|
|
817
831
|
inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
|
|
832
|
+
requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
|
|
818
833
|
permissionRules: step.permissionRules,
|
|
819
834
|
hostAvailableBuiltins: ctx.hostAvailableBuiltins,
|
|
820
835
|
}));
|
|
@@ -874,7 +889,53 @@ export async function runSingleStepInner(
|
|
|
874
889
|
transcriptWriter?.writeInitialUserMessage(`${PROMPT_REDACTED}; live Prompt Audit only.`);
|
|
875
890
|
|
|
876
891
|
if (step.runner?.type === "external-cli") {
|
|
877
|
-
const externalCwd = step.cwd ?? ctx.cwd;
|
|
892
|
+
const externalCwd = step.machine?.cwd ?? step.cwd ?? ctx.cwd;
|
|
893
|
+
const externalAbortSignal = combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]);
|
|
894
|
+
if (!step.machine && externalAbortSignal) await ctx.prepareExternalActivity?.(externalCwd, externalAbortSignal);
|
|
895
|
+
if (externalAbortSignal?.aborted) {
|
|
896
|
+
const stopped = ctx.stopSignal?.aborted === true;
|
|
897
|
+
const message = stopped ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.";
|
|
898
|
+
return omitUndefinedProperties({ agent: step.agent, context: step.context, output: message, error: message, exitCode: 1, stopped: stopped || undefined, timedOut: stopped ? undefined : true });
|
|
899
|
+
}
|
|
900
|
+
if (step.machine) {
|
|
901
|
+
const runner = resolveExternalCliRunnerStatus({ ...step.runner, machine: step.machine });
|
|
902
|
+
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
903
|
+
const placedRunId = `${ctx.id}-${ctx.flatIndex}`;
|
|
904
|
+
const session = await prepareInternalHerdrExternalAdapter({ adapter: step.runner.adapter as HerdrExternalAdapterId, machine: step.machine, runId: placedRunId }, {});
|
|
905
|
+
let timedOut = false, stopped = false, retain = false;
|
|
906
|
+
const evidenceInput = { runId: placedRunId, requestId: `prompt-${ctx.flatIndex}`, task: buildExternalCliPrompt(step.systemPrompt ?? "", task), cwd: externalCwd, nativeSessionId: session.launch.nativeSessionId };
|
|
907
|
+
let resolveInterruption!: (value: "timeout" | "stop") => void, rejectInterruption!: (error: unknown) => void;
|
|
908
|
+
const interruption = new Promise<"timeout" | "stop">((resolve, reject) => { resolveInterruption = resolve; rejectInterruption = reject; });
|
|
909
|
+
ctx.registerTimeout?.(() => { timedOut = true; retain = true; resolveInterruption("timeout"); });
|
|
910
|
+
ctx.registerStop?.(() => { stopped = true; void session.abort().then(() => resolveInterruption("stop"), rejectInterruption); });
|
|
911
|
+
try {
|
|
912
|
+
let settled: HerdrExternalResult;
|
|
913
|
+
try {
|
|
914
|
+
const raced = await Promise.race([session.promptAndSettle(evidenceInput).then((value) => ({ kind: "settled" as const, value })), interruption.then((kind) => ({ kind }))]);
|
|
915
|
+
if (raced.kind === "settled") settled = raced.value;
|
|
916
|
+
else if (raced.kind === "timeout") settled = createHerdrExternalAdapter(step.runner.adapter as HerdrExternalAdapterId).normalize(evidenceInput, "Placed external run timed out; truthful pane retained for inspection.");
|
|
917
|
+
else return { agent: step.agent, context: step.context, output: "Placed external run stopped by user.", outputState: "present", exitCode: 1, stopped: true, runner, execution: { status: "stopped", success: false, exitCode: 1, stopped: true } };
|
|
918
|
+
} catch (error) {
|
|
919
|
+
retain = true;
|
|
920
|
+
settled = await settleHerdrExternalRunnerError(error, step.runner.adapter as HerdrExternalAdapterId, evidenceInput, () => session.retain());
|
|
921
|
+
}
|
|
922
|
+
const output = settled.output;
|
|
923
|
+
try { fs.writeFileSync(ctx.outputFile, output, "utf-8"); } catch { /* Observability output is best-effort. */ }
|
|
924
|
+
const resolvedOutput = step.outputPath ? resolveSingleOutput(step.outputPath, output, outputSnapshot, step.outputClaimPath) : { fullOutput: output };
|
|
925
|
+
const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, resolvedOutput.fullOutput) : undefined;
|
|
926
|
+
const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({ fullOutput: resolvedOutput.fullOutput, outputPath: step.outputPath, outputMode: step.outputMode, exitCode: 1, preserveSavedOutput: true, savedPath: resolvedOutput.savedPath, outputReference, saveError: resolvedOutput.saveError }));
|
|
927
|
+
const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false ? persistStepArtifacts({ artifactPaths, artifactConfig: ctx.artifactConfig, output: formatOutputArtifactContent(omitUndefinedProperties({ output: resolvedOutput.fullOutput, metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath })), metadata: { runId: ctx.id, agent: step.agent, task: PROMPT_REDACTED, runner, placement: session.owner.identity, settlement: settled.settlement, outcome: settled.outcome, timestamp: Date.now() } }) : {};
|
|
928
|
+
return omitUndefinedProperties({ agent: step.agent, ...(childSessionName ? { sessionName: childSessionName } : {}), context: step.context, output: finalizedOutput.displayOutput, outputState: output.trim() ? "present" : "absent", exitCode: 1, error: resolvedOutput.fatalError ? resolvedOutput.saveError : undefined, timedOut, stopped, artifactPaths, outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined, metadataSaveError: artifactErrors.metadataSaveError, runner, execution: { status: "partial", success: false, exitCode: 1 } });
|
|
929
|
+
} catch (error) {
|
|
930
|
+
// Any post-allocation uncertainty retains the pane; only explicit stop
|
|
931
|
+
// and successful exact settlement are destructive.
|
|
932
|
+
retain = true;
|
|
933
|
+
throw error;
|
|
934
|
+
} finally {
|
|
935
|
+
ctx.registerTimeout?.(undefined); ctx.registerStop?.(undefined);
|
|
936
|
+
if (retain) await session.retain(); else await session.dispose();
|
|
937
|
+
}
|
|
938
|
+
}
|
|
878
939
|
const adapterLaunch = step.runner.adapter === "codex-exec" || step.runner.adapter === "codex-exec-writer"
|
|
879
940
|
? resolveCodexExecLaunch({ adapter: step.runner.adapter, command: step.runner.command, asyncDir: path.dirname(ctx.outputFile), stepIndex: ctx.flatIndex })
|
|
880
941
|
: step.runner.adapter === "claude-code" || step.runner.adapter === "claude-code-writer"
|
|
@@ -882,9 +943,13 @@ export async function runSingleStepInner(
|
|
|
882
943
|
: step.runner.adapter === "cursor-agent" || step.runner.adapter === "cursor-agent-writer"
|
|
883
944
|
? resolveCursorAgentLaunch({ adapter: step.runner.adapter, command: step.runner.command, cwd: externalCwd, asyncDir: path.dirname(ctx.outputFile), stepIndex: ctx.flatIndex })
|
|
884
945
|
: undefined;
|
|
885
|
-
const runner = resolveExternalCliRunnerStatus({ ...step.runner, ...(adapterLaunch ? { args: adapterLaunch.args } : {}) });
|
|
946
|
+
const runner = resolveExternalCliRunnerStatus({ ...step.runner, ...(adapterLaunch ? { args: adapterLaunch.args } : {}), ...(step.machine ? { machine: step.machine } : {}) });
|
|
886
947
|
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
887
|
-
const
|
|
948
|
+
const onExternalOutput = (chunk: Buffer): void => {
|
|
949
|
+
if (chunk.length > 0) ctx.onExternalStreamActivity?.();
|
|
950
|
+
ctx.orcaProgressTab?.append(chunk.toString("utf-8"));
|
|
951
|
+
};
|
|
952
|
+
const externalInput = omitUndefinedProperties({
|
|
888
953
|
command: adapterLaunch?.command ?? runner.command,
|
|
889
954
|
args: adapterLaunch?.args ?? runner.args,
|
|
890
955
|
cwd: externalCwd,
|
|
@@ -902,9 +967,13 @@ export async function runSingleStepInner(
|
|
|
902
967
|
timeoutMessage: ctx.timeoutMessage,
|
|
903
968
|
stopMessage: ctx.stopMessage,
|
|
904
969
|
onProcess: ctx.onExternalProcess,
|
|
905
|
-
onStdout:
|
|
906
|
-
onStderr:
|
|
907
|
-
})
|
|
970
|
+
onStdout: onExternalOutput,
|
|
971
|
+
onStderr: onExternalOutput,
|
|
972
|
+
});
|
|
973
|
+
const preparedExternal = prepareHerdrMachineExternalCliRun(externalInput, step.machine ? { machine: step.machine, ...(step.machineEnv ? { env: step.machineEnv } : {}) } : undefined, { localCwd: ctx.cwd });
|
|
974
|
+
const ran = await runExternalCli(preparedExternal.input);
|
|
975
|
+
const externalProcess = preparedExternal.decorateProcess(ran.externalProcess);
|
|
976
|
+
const external = step.machine ? decorateHerdrMachineResult(ran, step.machine, step.runner.adapter) : ran;
|
|
908
977
|
try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
|
|
909
978
|
const resolvedOutput = step.outputPath && external.exitCode === 0
|
|
910
979
|
? resolveSingleOutput(step.outputPath, external.output, outputSnapshot, step.outputClaimPath)
|
|
@@ -928,7 +997,7 @@ export async function runSingleStepInner(
|
|
|
928
997
|
artifactPaths,
|
|
929
998
|
artifactConfig: ctx.artifactConfig,
|
|
930
999
|
output: formatOutputArtifactContent(omitUndefinedProperties({ output: resolvedOutput.fullOutput, error: external.error, metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath })),
|
|
931
|
-
metadata: { runId: ctx.id, agent: step.agent, task: PROMPT_REDACTED, runner, externalProcess:
|
|
1000
|
+
metadata: { runId: ctx.id, agent: step.agent, task: PROMPT_REDACTED, runner, externalProcess: externalProcess, exitCode: external.exitCode, error: external.error, timestamp: Date.now() },
|
|
932
1001
|
})
|
|
933
1002
|
: {};
|
|
934
1003
|
return omitUndefinedProperties({
|
|
@@ -944,9 +1013,10 @@ export async function runSingleStepInner(
|
|
|
944
1013
|
processSignal: external.processSignal,
|
|
945
1014
|
artifactPaths,
|
|
946
1015
|
outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
|
|
1016
|
+
artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
|
|
947
1017
|
metadataSaveError: artifactErrors.metadataSaveError,
|
|
948
1018
|
runner,
|
|
949
|
-
externalProcess:
|
|
1019
|
+
externalProcess: externalProcess,
|
|
950
1020
|
});
|
|
951
1021
|
}
|
|
952
1022
|
|
|
@@ -1013,6 +1083,7 @@ export async function runSingleStepInner(
|
|
|
1013
1083
|
stopped: external.stopped,
|
|
1014
1084
|
artifactPaths,
|
|
1015
1085
|
outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
|
|
1086
|
+
artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
|
|
1016
1087
|
metadataSaveError: artifactErrors.metadataSaveError,
|
|
1017
1088
|
runner,
|
|
1018
1089
|
externalJob: external.externalJob,
|
|
@@ -1026,16 +1097,9 @@ export async function runSingleStepInner(
|
|
|
1026
1097
|
alignForkedSessionCwd(step.sessionFile, effectiveCwd);
|
|
1027
1098
|
}
|
|
1028
1099
|
|
|
1029
|
-
const
|
|
1030
|
-
? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
|
|
1031
|
-
: step.model
|
|
1032
|
-
? [step.model]
|
|
1033
|
-
: [undefined];
|
|
1034
|
-
const attemptedModels: string[] = [];
|
|
1100
|
+
const candidate = step.model;
|
|
1035
1101
|
let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
|
|
1036
1102
|
let launchResolvedExtensions = step.launchResolvedExtensions;
|
|
1037
|
-
const modelAttempts: ModelAttempt[] = [];
|
|
1038
|
-
const attemptNotes: string[] = [];
|
|
1039
1103
|
let finalRequiredOutputMissing: boolean | undefined;
|
|
1040
1104
|
const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
|
|
1041
1105
|
let finalResult: RunChildSessionResult | undefined;
|
|
@@ -1046,48 +1110,21 @@ export async function runSingleStepInner(
|
|
|
1046
1110
|
let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
|
|
1047
1111
|
let toolBudgetBlocked = false;
|
|
1048
1112
|
let actualLaunchContractDigest = step.launchContractDigest;
|
|
1049
|
-
const mutationSnapshot = snapshotTrackedMutations(step.cwd ?? ctx.cwd);
|
|
1113
|
+
const mutationSnapshot = step.machine ? { source: "tracked-files" as const, trackedOnly: true as const, cwd: step.cwd ?? ctx.cwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(step.cwd ?? ctx.cwd);
|
|
1050
1114
|
let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
|
|
1051
1115
|
|
|
1052
|
-
let modelIndex = 0;
|
|
1053
1116
|
let contextOverflow = false;
|
|
1054
1117
|
let launchWarningsEmitted = false;
|
|
1055
|
-
|
|
1056
|
-
let
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
const
|
|
1061
|
-
if (exhausted === true) return "exhausted" as const;
|
|
1062
|
-
if (!ctx.usageBudget) return "unconfigured" as const;
|
|
1063
|
-
if (ctx.usageBudget.costUsd || !ctx.onChildEvent || exhausted !== false) return "unknown" as const;
|
|
1064
|
-
return "available" as const;
|
|
1065
|
-
};
|
|
1066
|
-
const lifecycleAllowsContinuation = () => !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted
|
|
1067
|
-
&& !ctx.skipAcceptance?.() && (ctx.deadlineAt === undefined || Date.now() < ctx.deadlineAt);
|
|
1068
|
-
const canContinue = () => lifecycleAllowsContinuation() && ["available", "unconfigured"].includes(continuationBudget());
|
|
1069
|
-
const failContinuationLaunch = (candidate: string | undefined, error: unknown) => {
|
|
1070
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1071
|
-
modelAttempts.push({ model: candidate ?? "default", success: false, exitCode: 1, error: message });
|
|
1072
|
-
if (candidate) attemptedModels.push(candidate);
|
|
1073
|
-
if (finalResult) finalResult = { ...finalResult, exitCode: 1, error: message };
|
|
1074
|
-
};
|
|
1075
|
-
let nextAttemptTask = task;
|
|
1076
|
-
modelAttemptsLoop: while (modelIndex < candidates.length) {
|
|
1077
|
-
if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
1078
|
-
if (readonlyContinuation && !canContinue()) break;
|
|
1079
|
-
const recoveringAbort = recoveryState === "abort-recovery";
|
|
1080
|
-
const attemptTask = nextAttemptTask;
|
|
1081
|
-
const candidate = candidates[modelIndex];
|
|
1082
|
-
const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
|
|
1118
|
+
const aggregateUsage = emptyUsage();
|
|
1119
|
+
let launched = false;
|
|
1120
|
+
let recoveryTask = task;
|
|
1121
|
+
singleLaunch: for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
|
|
1122
|
+
if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break singleLaunch;
|
|
1123
|
+
const expectedModelForVerification = candidate && !step.skipPrimaryModelVerification ? candidate : undefined;
|
|
1083
1124
|
try {
|
|
1084
1125
|
assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
|
|
1085
1126
|
} catch (error) {
|
|
1086
1127
|
const message = error instanceof Error ? error.message : String(error);
|
|
1087
|
-
if (readonlyContinuation) {
|
|
1088
|
-
failContinuationLaunch(candidate, error);
|
|
1089
|
-
break modelAttemptsLoop;
|
|
1090
|
-
}
|
|
1091
1128
|
return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
|
|
1092
1129
|
}
|
|
1093
1130
|
ctx.onAttemptStart?.(omitUndefinedProperties({
|
|
@@ -1124,11 +1161,7 @@ export async function runSingleStepInner(
|
|
|
1124
1161
|
childWatchdog,
|
|
1125
1162
|
watchdogStatus: (event) => watchdogSink?.(event),
|
|
1126
1163
|
});
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
if (!readonlyContinuation) throw error;
|
|
1129
|
-
failContinuationLaunch(candidate, error);
|
|
1130
|
-
break modelAttemptsLoop;
|
|
1131
|
-
}
|
|
1164
|
+
} catch (error) { throw error; }
|
|
1132
1165
|
if (effectiveStructuredOutput && launch.config.structuredOutput) {
|
|
1133
1166
|
// The runner reads the value back from the runtime's files after the run.
|
|
1134
1167
|
launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
|
|
@@ -1147,13 +1180,13 @@ export async function runSingleStepInner(
|
|
|
1147
1180
|
subagentOnlyExtensions: step.subagentOnlyExtensions,
|
|
1148
1181
|
fast: step.fast,
|
|
1149
1182
|
model: step.model,
|
|
1150
|
-
modelCandidates: step.modelCandidates,
|
|
1151
1183
|
mcpDirectTools: step.mcpDirectTools,
|
|
1152
1184
|
cwd: step.cwd ?? ctx.cwd,
|
|
1153
1185
|
requireReadTool: Boolean(step.skills?.length),
|
|
1154
1186
|
structuredOutput: Boolean(effectiveStructuredOutput),
|
|
1155
1187
|
capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
|
|
1156
1188
|
inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
|
|
1189
|
+
requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
|
|
1157
1190
|
permissionRules: step.permissionRules,
|
|
1158
1191
|
hostAvailableBuiltins: ctx.hostAvailableBuiltins,
|
|
1159
1192
|
}));
|
|
@@ -1165,7 +1198,7 @@ export async function runSingleStepInner(
|
|
|
1165
1198
|
inheritGlobalContext: step.inheritGlobalContext,
|
|
1166
1199
|
inheritSkills: step.inheritSkills,
|
|
1167
1200
|
task: step.launchBindingTask ?? task,
|
|
1168
|
-
|
|
1201
|
+
model: candidate,
|
|
1169
1202
|
fast: step.fast,
|
|
1170
1203
|
thinking: resolveEffectiveThinking(candidate, step.thinking),
|
|
1171
1204
|
systemPrompt: step.systemPrompt ?? "",
|
|
@@ -1184,10 +1217,7 @@ export async function runSingleStepInner(
|
|
|
1184
1217
|
const run = await runChildSession(omitUndefinedProperties({
|
|
1185
1218
|
factory: ctx.childSessions,
|
|
1186
1219
|
launch,
|
|
1187
|
-
|
|
1188
|
-
readonlyContinuation,
|
|
1189
|
-
canContinue,
|
|
1190
|
-
prompt: `Task: ${attemptTask}`,
|
|
1220
|
+
prompt: `Task: ${recoveryTask}`,
|
|
1191
1221
|
childWatchdog,
|
|
1192
1222
|
childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
1193
1223
|
appendChildEvent: (event) => appendDiagnosticJsonl(eventsPath, JSON.stringify(event), typeof event.type === "string" ? event.type : undefined),
|
|
@@ -1216,6 +1246,13 @@ export async function runSingleStepInner(
|
|
|
1216
1246
|
modelResponseAliases: step.modelResponseAliases,
|
|
1217
1247
|
mutationTools: step.mutationTools,
|
|
1218
1248
|
}));
|
|
1249
|
+
launched = true;
|
|
1250
|
+
aggregateUsage.input += run.usage.input;
|
|
1251
|
+
aggregateUsage.output += run.usage.output;
|
|
1252
|
+
aggregateUsage.cacheRead += run.usage.cacheRead;
|
|
1253
|
+
aggregateUsage.cacheWrite += run.usage.cacheWrite;
|
|
1254
|
+
aggregateUsage.cost += run.usage.cost;
|
|
1255
|
+
aggregateUsage.turns += run.usage.turns;
|
|
1219
1256
|
// A parked run still owes completion evidence when it actually finishes.
|
|
1220
1257
|
// Stopped/timedOut runs already have terminal failures; checking completion evidence there is meaningless.
|
|
1221
1258
|
const completionDiagnosticsEligible = !run.interrupted && !run.stopped && !run.timedOut;
|
|
@@ -1274,7 +1311,8 @@ export async function runSingleStepInner(
|
|
|
1274
1311
|
const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
|
|
1275
1312
|
const completionToolPlan = resolvedTaskToolPlan;
|
|
1276
1313
|
const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
|
|
1277
|
-
const
|
|
1314
|
+
const remoteGitChanged = run.nativeMachine?.initialGit && run.nativeMachine.finalGit ? run.nativeMachine.initialGit.head !== run.nativeMachine.finalGit.head || run.nativeMachine.initialGit.dirty !== run.nativeMachine.finalGit.dirty : undefined;
|
|
1315
|
+
const mutationEvidence = run.nativeMachine ? { source: "tracked-files" as const, trackedOnly: true as const, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
|
|
1278
1316
|
finalMutationEvidence = mutationEvidence;
|
|
1279
1317
|
const completionMutationEvidence = ctx.trackedMutationEvidenceForCompletionGuard === false ? undefined : mutationEvidence;
|
|
1280
1318
|
const completionGuard = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
|
|
@@ -1340,15 +1378,6 @@ export async function runSingleStepInner(
|
|
|
1340
1378
|
: `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
|
|
1341
1379
|
: undefined);
|
|
1342
1380
|
const error = underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError;
|
|
1343
|
-
const attempt: ModelAttempt = omitUndefinedProperties({
|
|
1344
|
-
model: candidate ?? run.model ?? step.model ?? "default",
|
|
1345
|
-
success: effectiveExitCode === 0 && !error,
|
|
1346
|
-
exitCode: effectiveExitCode,
|
|
1347
|
-
error,
|
|
1348
|
-
usage: run.usage,
|
|
1349
|
-
});
|
|
1350
|
-
modelAttempts.push(attempt);
|
|
1351
|
-
if (!recoveringAbort && candidate) attemptedModels.push(candidate);
|
|
1352
1381
|
completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
|
|
1353
1382
|
finalOutputSnapshot = outputSnapshot;
|
|
1354
1383
|
if (step.toolBudget) {
|
|
@@ -1366,11 +1395,14 @@ export async function runSingleStepInner(
|
|
|
1366
1395
|
});
|
|
1367
1396
|
const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
|
|
1368
1397
|
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunChildSessionResult;
|
|
1369
|
-
|
|
1398
|
+
if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break singleLaunch;
|
|
1399
|
+
if (effectiveExitCode === 0 && !error) break singleLaunch;
|
|
1400
|
+
if (completionEvidence.guardTriggered) break singleLaunch;
|
|
1401
|
+
const recovery = planAbortRecovery({
|
|
1370
1402
|
messages: run.messages,
|
|
1371
1403
|
error,
|
|
1372
1404
|
sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
|
|
1373
|
-
alreadyResumed:
|
|
1405
|
+
alreadyResumed: attemptIndex > 0,
|
|
1374
1406
|
stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
|
|
1375
1407
|
interrupted: run.interrupted,
|
|
1376
1408
|
timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
|
|
@@ -1380,83 +1412,26 @@ export async function runSingleStepInner(
|
|
|
1380
1412
|
acceptanceFailed: false,
|
|
1381
1413
|
currentTool: run.currentTool,
|
|
1382
1414
|
afterCompactionSettlement: run.afterCompactionSettlement,
|
|
1383
|
-
})
|
|
1384
|
-
if (
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
: abortRecovery.diagnostic;
|
|
1388
|
-
if (finalResult) finalResult.abortRecoveryDiagnostic = abortRecovery.diagnostic;
|
|
1389
|
-
}
|
|
1390
|
-
if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
|
|
1391
|
-
if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
|
|
1392
|
-
if (attempt.success) break modelAttemptsLoop;
|
|
1393
|
-
// The shared token is consumed before sibling creation; no third dispatch of any kind.
|
|
1394
|
-
if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
|
|
1395
|
-
if (recoveringAbort) break modelAttemptsLoop;
|
|
1396
|
-
if (abortRecovery?.action === "resume") {
|
|
1397
|
-
recoveryState = "abort-recovery";
|
|
1398
|
-
nextAttemptTask = abortRecovery.prompt;
|
|
1399
|
-
attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
|
|
1400
|
-
continue;
|
|
1415
|
+
});
|
|
1416
|
+
if (recovery.action === "resume") {
|
|
1417
|
+
recoveryTask = recovery.prompt;
|
|
1418
|
+
continue singleLaunch;
|
|
1401
1419
|
}
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1404
|
-
const source = getSettledReadonlyChild(run);
|
|
1405
|
-
if (source) {
|
|
1406
|
-
const captured = launch.capture.completionIntentContext?.();
|
|
1407
|
-
const sourceModel = captured?.model;
|
|
1408
|
-
const retained = getReadonlySessionEvidence(source)!;
|
|
1409
|
-
// Modality/capacity assessment, not another history/provenance validator.
|
|
1410
|
-
// Images and unknown content have no certified token bound in this host slice.
|
|
1411
|
-
const textOnly = (JSON.parse(retained.contextJson) as Message[]).every((message) => typeof message.content === "string"
|
|
1412
|
-
|| (Array.isArray(message.content) && message.content.every((block) => ["text", "thinking", "toolCall"].includes(block.type))));
|
|
1413
|
-
const retainedBytes = Buffer.byteLength(retained.contextJson, "utf8");
|
|
1414
|
-
const resolvedCandidates = candidates.map((reference, index) => {
|
|
1415
|
-
// Only exact registry identities qualify; aliases/default guesses remain ineligible.
|
|
1416
|
-
const base = reference ? splitKnownThinkingSuffix(reference).baseModel : "";
|
|
1417
|
-
const slash = base.indexOf("/");
|
|
1418
|
-
const model = slash > 0 ? captured?.modelRegistry.find(base.slice(0, slash), base.slice(slash + 1)) : undefined;
|
|
1419
|
-
// Reserve the source window for runtime/system/tool overhead, plus a conservative
|
|
1420
|
-
// byte bound for the complete retained text and new prompt; never assume a fit
|
|
1421
|
-
// merely because a 429 request was sent. Equal/smaller windows remain denied.
|
|
1422
|
-
const compatible = textOnly && model && sourceModel && model.input?.includes("text")
|
|
1423
|
-
&& Number.isFinite(sourceModel.contextWindow) && sourceModel.contextWindow > 0
|
|
1424
|
-
&& model.contextWindow >= sourceModel.contextWindow + retainedBytes + Buffer.byteLength(READONLY_CONTINUATION_PROMPT, "utf8")
|
|
1425
|
-
&& model.maxTokens > 0 && model.maxTokens <= sourceModel.maxTokens;
|
|
1426
|
-
return { resolved: model ? { provider: model.provider, model: model.id, api: model.api } : undefined,
|
|
1427
|
-
tried: index <= modelIndex, compatibility: compatible ? "compatible" as const : "unknown" as const };
|
|
1428
|
-
});
|
|
1429
|
-
const plan = planReadonlyModelContinuation({ source, recoveryState, candidates: resolvedCandidates, currentIndex: modelIndex,
|
|
1430
|
-
lifecycleAllowsContinuation: lifecycleAllowsContinuation() && !run.interrupted && !run.stopped && !run.timedOut,
|
|
1431
|
-
effectsAllowContinuation: !run.currentTool && !run.observedMutationAttempt && !mutationEvidence.attemptedMutation
|
|
1432
|
-
&& !structuredError && !effectiveStructuredOutput && !missingRequiredOutputError && !toolAvailabilityError
|
|
1433
|
-
&& !completionEvidence.guardTriggered && !midToolExitError && !hiddenError?.hasError,
|
|
1434
|
-
budget: continuationBudget(), knownContextOverflow: isContextOverflow(error) });
|
|
1435
|
-
if (plan.kind === "continue" && canContinue()) {
|
|
1436
|
-
recoveryState = plan.recoveryState;
|
|
1437
|
-
const selected = resolvedCandidates[plan.candidateIndex]!.resolved!;
|
|
1438
|
-
readonlyContinuation = { source, expected: plan.expected, modelId: `${selected.provider}/${selected.model}` };
|
|
1439
|
-
nextAttemptTask = plan.prompt;
|
|
1440
|
-
modelIndex = plan.candidateIndex;
|
|
1441
|
-
attemptNotes.push(`[readonly-continuation] ${attempt.model} returned HTTP 429 after read-only progress; continuing the retained session once with ${candidates[modelIndex]}.`);
|
|
1442
|
-
continue;
|
|
1443
|
-
}
|
|
1420
|
+
if (recovery.diagnostic) {
|
|
1421
|
+
finalResult.abortRecoveryDiagnostic = recovery.diagnostic;
|
|
1444
1422
|
}
|
|
1445
1423
|
|
|
1446
|
-
const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
|
|
1447
|
-
if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
|
|
1448
1424
|
if (isContextOverflow(error)) {
|
|
1449
1425
|
contextOverflow = true;
|
|
1450
|
-
|
|
1451
|
-
break modelAttemptsLoop;
|
|
1426
|
+
break singleLaunch;
|
|
1452
1427
|
}
|
|
1453
|
-
|
|
1454
|
-
attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
|
|
1455
|
-
modelIndex += 1;
|
|
1428
|
+
break singleLaunch;
|
|
1456
1429
|
}
|
|
1457
1430
|
|
|
1458
1431
|
const rawOutput = finalResult?.finalOutput ?? "";
|
|
1459
|
-
|
|
1432
|
+
let outputForPersistence = stripAcceptanceReport(rawOutput);
|
|
1433
|
+
if (!outputForPersistence.trim() && finalResult?.structuredOutput !== undefined)
|
|
1434
|
+
outputForPersistence = JSON.stringify(finalResult.structuredOutput, null, 2);
|
|
1460
1435
|
const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
|
|
1461
1436
|
? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot, step.outputClaimPath)
|
|
1462
1437
|
: { fullOutput: outputForPersistence };
|
|
@@ -1469,9 +1444,6 @@ export async function runSingleStepInner(
|
|
|
1469
1444
|
const output = stripAcceptanceReport(resolvedOutput.fullOutput);
|
|
1470
1445
|
const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
|
|
1471
1446
|
let outputForSummary = output;
|
|
1472
|
-
if (attemptNotes.length > 0) {
|
|
1473
|
-
outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
|
|
1474
|
-
}
|
|
1475
1447
|
if (finalResult?.stopped && !outputForSummary.trim()) {
|
|
1476
1448
|
outputForSummary = ctx.stopMessage ?? "Subagent stopped by user.";
|
|
1477
1449
|
}
|
|
@@ -1556,7 +1528,7 @@ export async function runSingleStepInner(
|
|
|
1556
1528
|
abortRecoveryDiagnostic: effectiveFinalExitCode !== 0 ? finalResult?.abortRecoveryDiagnostic : undefined,
|
|
1557
1529
|
requiredOutput: effectiveFinalExitCode !== 0 ? finalResult?.effects?.settlementDiagnostic?.requiredOutput : undefined,
|
|
1558
1530
|
});
|
|
1559
|
-
const usage =
|
|
1531
|
+
const usage = launched ? aggregateUsage : undefined;
|
|
1560
1532
|
|
|
1561
1533
|
const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
|
|
1562
1534
|
? persistStepArtifacts({
|
|
@@ -1574,8 +1546,8 @@ export async function runSingleStepInner(
|
|
|
1574
1546
|
task: PROMPT_REDACTED,
|
|
1575
1547
|
exitCode: effectiveFinalExitCode,
|
|
1576
1548
|
model: finalResult?.model,
|
|
1577
|
-
|
|
1578
|
-
|
|
1549
|
+
nativeMachine: finalResult?.nativeMachine,
|
|
1550
|
+
requestedModel: step.requestedModel,
|
|
1579
1551
|
usage,
|
|
1580
1552
|
error: effectiveFinalError,
|
|
1581
1553
|
acceptance: effectiveAcceptance,
|
|
@@ -1604,15 +1576,16 @@ export async function runSingleStepInner(
|
|
|
1604
1576
|
sessionFile: step.sessionFile,
|
|
1605
1577
|
intercomTarget: ctx.childIntercomTarget,
|
|
1606
1578
|
model: finalResult?.model,
|
|
1579
|
+
nativeMachine: finalResult?.nativeMachine,
|
|
1607
1580
|
thinking: resolveEffectiveThinking(finalResult?.model, step.thinking),
|
|
1608
|
-
|
|
1609
|
-
modelAttempts,
|
|
1581
|
+
requestedModel: step.requestedModel,
|
|
1610
1582
|
contextOverflow: contextOverflow || undefined,
|
|
1611
|
-
totalCost:
|
|
1583
|
+
totalCost: costSummaryFromUsage(usage),
|
|
1612
1584
|
usage,
|
|
1613
1585
|
artifactPaths,
|
|
1614
1586
|
savedOutputPath: finalizedOutput.savedPath,
|
|
1615
1587
|
outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
|
|
1588
|
+
artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
|
|
1616
1589
|
metadataSaveError: artifactErrors.metadataSaveError,
|
|
1617
1590
|
transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
|
|
1618
1591
|
transcriptError: transcriptWriter?.getError(),
|
|
@@ -1856,8 +1829,12 @@ function missingRequiredOutputAfterUsefulMutation(result: SingleStepResult): boo
|
|
|
1856
1829
|
}
|
|
1857
1830
|
|
|
1858
1831
|
function partialExecutionWithUsefulMutation(result: SingleStepResult): boolean {
|
|
1832
|
+
if (result.execution?.status !== "partial") return false;
|
|
1833
|
+
// Pane-native external results are deliberately partial from bounded terminal
|
|
1834
|
+
// evidence alone; Pi mutation-based partials retain their existing behavior.
|
|
1835
|
+
if (result.runner?.type === "external-cli" && result.runner.machine) return true;
|
|
1859
1836
|
const fileMutation = result.effects?.fileMutation;
|
|
1860
|
-
return
|
|
1837
|
+
return fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length);
|
|
1861
1838
|
}
|
|
1862
1839
|
|
|
1863
1840
|
function partialEvidenceResult(result: SingleStepResult): boolean {
|
|
@@ -1960,6 +1937,7 @@ export async function runSubagent(
|
|
|
1960
1937
|
let currentActivityState: ActivityState | undefined;
|
|
1961
1938
|
let activityTimer: NodeJS.Timeout | undefined;
|
|
1962
1939
|
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
1940
|
+
let checkpointTimer: NodeJS.Timeout | undefined;
|
|
1963
1941
|
let timedOut = false;
|
|
1964
1942
|
let stopped = false;
|
|
1965
1943
|
let usageBudgetExceeded = false;
|
|
@@ -1968,6 +1946,7 @@ export async function runSubagent(
|
|
|
1968
1946
|
const timeoutAbortController = new AbortController();
|
|
1969
1947
|
const stopAbortController = new AbortController();
|
|
1970
1948
|
const setupInterruptController = new AbortController();
|
|
1949
|
+
const runStopSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal]);
|
|
1971
1950
|
const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
|
|
1972
1951
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
1973
1952
|
let latestSessionFile: string | undefined;
|
|
@@ -2009,7 +1988,7 @@ export async function runSubagent(
|
|
|
2009
1988
|
model: task.model,
|
|
2010
1989
|
...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
|
|
2011
1990
|
thinking: task.thinking,
|
|
2012
|
-
|
|
1991
|
+
requestedModel: task.requestedModel,
|
|
2013
1992
|
recentTools: [],
|
|
2014
1993
|
recentOutput: [],
|
|
2015
1994
|
}));
|
|
@@ -2063,7 +2042,7 @@ export async function runSubagent(
|
|
|
2063
2042
|
model: step.model,
|
|
2064
2043
|
...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
|
|
2065
2044
|
thinking: step.thinking,
|
|
2066
|
-
|
|
2045
|
+
requestedModel: step.requestedModel,
|
|
2067
2046
|
recentTools: [],
|
|
2068
2047
|
recentOutput: [],
|
|
2069
2048
|
}));
|
|
@@ -2290,9 +2269,7 @@ export async function runSubagent(
|
|
|
2290
2269
|
sessionFile: step.sessionFile,
|
|
2291
2270
|
model: step.model,
|
|
2292
2271
|
thinking: step.thinking,
|
|
2293
|
-
|
|
2294
|
-
modelAttempts: step.modelAttempts,
|
|
2295
|
-
usage: usageFromAttempts(step.modelAttempts),
|
|
2272
|
+
requestedModel: step.requestedModel,
|
|
2296
2273
|
contextOverflow: step.contextOverflow,
|
|
2297
2274
|
})),
|
|
2298
2275
|
exitCode: state === "complete" || state === "paused" ? 0 : 1,
|
|
@@ -2457,6 +2434,8 @@ export async function runSubagent(
|
|
|
2457
2434
|
yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
|
|
2458
2435
|
}
|
|
2459
2436
|
};
|
|
2437
|
+
const isNestedControlDescendant = (run: NestedRunSummary): boolean =>
|
|
2438
|
+
!config.nestedSelf || run.path.some((entry) => entry.runId === id);
|
|
2460
2439
|
const interruptNestedAsyncDescendants = (): void => {
|
|
2461
2440
|
if (!config.nestedRoute) return;
|
|
2462
2441
|
let registry: ReturnType<typeof projectNestedEvents>;
|
|
@@ -2472,7 +2451,7 @@ export async function runSubagent(
|
|
|
2472
2451
|
return;
|
|
2473
2452
|
}
|
|
2474
2453
|
for (const run of nestedRuns(registry.children)) {
|
|
2475
|
-
if (run.state !== "running" && run.state !== "queued") continue;
|
|
2454
|
+
if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
|
|
2476
2455
|
const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
|
|
2477
2456
|
if (!nestedAsyncDir) continue;
|
|
2478
2457
|
try {
|
|
@@ -2503,7 +2482,7 @@ export async function runSubagent(
|
|
|
2503
2482
|
return;
|
|
2504
2483
|
}
|
|
2505
2484
|
for (const run of nestedRuns(registry.children)) {
|
|
2506
|
-
if (run.state !== "running" && run.state !== "queued") continue;
|
|
2485
|
+
if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
|
|
2507
2486
|
const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
|
|
2508
2487
|
if (!nestedAsyncDir) continue;
|
|
2509
2488
|
try {
|
|
@@ -2534,7 +2513,7 @@ export async function runSubagent(
|
|
|
2534
2513
|
return;
|
|
2535
2514
|
}
|
|
2536
2515
|
for (const run of nestedRuns(registry.children)) {
|
|
2537
|
-
if (run.state !== "running" && run.state !== "queued") continue;
|
|
2516
|
+
if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
|
|
2538
2517
|
const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
|
|
2539
2518
|
if (!nestedAsyncDir) continue;
|
|
2540
2519
|
try {
|
|
@@ -2590,6 +2569,7 @@ export async function runSubagent(
|
|
|
2590
2569
|
}
|
|
2591
2570
|
const appendedSteps = requests.flatMap((request) => request.steps);
|
|
2592
2571
|
steps.push(...appendedSteps);
|
|
2572
|
+
flatSteps.push(...flattenSteps(appendedSteps));
|
|
2593
2573
|
const now = Date.now();
|
|
2594
2574
|
const pendingAppends = countPendingChainAppendRequests(asyncDir);
|
|
2595
2575
|
const added = appendRunnerStepsToStatus({
|
|
@@ -2623,9 +2603,66 @@ export async function runSubagent(
|
|
|
2623
2603
|
setOptionalProperty(groupNode, "acceptanceStatus", acceptance?.status ?? groupNode.acceptanceStatus);
|
|
2624
2604
|
};
|
|
2625
2605
|
|
|
2606
|
+
const EXTERNAL_GIT_PROBE_MIN_INTERVAL_MS = 2_000;
|
|
2607
|
+
type ExternalActivityEvidence = {
|
|
2608
|
+
cwd?: string;
|
|
2609
|
+
lastStreamActivityAt?: number;
|
|
2610
|
+
fingerprint?: string;
|
|
2611
|
+
probeInFlight?: Promise<void>;
|
|
2612
|
+
lastProbeStartedAt?: number;
|
|
2613
|
+
};
|
|
2614
|
+
const externalActivityEvidence = new Map<number, ExternalActivityEvidence>();
|
|
2615
|
+
const gitProbesByCwd = new Map<string, Promise<string | undefined>>();
|
|
2616
|
+
let periodicGitProbeFailure: { index: number; error: Error; message: string } | undefined;
|
|
2617
|
+
const reportedGitProbeErrors = new Set<string>();
|
|
2618
|
+
const reportGitProbeError = (externalCwd: string, error: unknown): void => {
|
|
2619
|
+
if (reportedGitProbeErrors.has(externalCwd)) return;
|
|
2620
|
+
reportedGitProbeErrors.add(externalCwd);
|
|
2621
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2622
|
+
console.error(`[pi-subagents] Git activity evidence unavailable for '${externalCwd.slice(-300)}'; check the cwd and Git installation: ${detail.slice(0, 500)}`);
|
|
2623
|
+
};
|
|
2624
|
+
const readSharedGitFingerprint = (externalCwd: string): Promise<string | undefined> => {
|
|
2625
|
+
const inFlight = gitProbesByCwd.get(externalCwd);
|
|
2626
|
+
if (inFlight) return inFlight;
|
|
2627
|
+
const probe = readGitFingerprint(externalCwd, runStopSignal, (error) => reportGitProbeError(externalCwd, error)).finally(() => gitProbesByCwd.delete(externalCwd));
|
|
2628
|
+
gitProbesByCwd.set(externalCwd, probe);
|
|
2629
|
+
return probe;
|
|
2630
|
+
};
|
|
2631
|
+
const recordPeriodicGitProbeFailure = (index: number, error: unknown): void => {
|
|
2632
|
+
if (periodicGitProbeFailure) return;
|
|
2633
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
2634
|
+
const message = `PROCESS_TREE_UNVERIFIED: ${cause.message}`;
|
|
2635
|
+
periodicGitProbeFailure = { index, error: cause, message };
|
|
2636
|
+
statusPayload.error = message;
|
|
2637
|
+
const step = statusPayload.steps[index];
|
|
2638
|
+
if (step) step.error = message;
|
|
2639
|
+
statusPayload.lastUpdate = Date.now();
|
|
2640
|
+
writeStatusPayload();
|
|
2641
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
2642
|
+
type: "subagent.run.process_tree_unverified",
|
|
2643
|
+
ts: statusPayload.lastUpdate,
|
|
2644
|
+
runId: id,
|
|
2645
|
+
index,
|
|
2646
|
+
code: "PROCESS_TREE_UNVERIFIED",
|
|
2647
|
+
message,
|
|
2648
|
+
}));
|
|
2649
|
+
};
|
|
2650
|
+
const prepareExternalActivity = async (index: number, externalCwd: string, signal: AbortSignal): Promise<void> => {
|
|
2651
|
+
if (!controlConfig.enabled) return;
|
|
2652
|
+
const resolvedCwd = path.resolve(externalCwd);
|
|
2653
|
+
const evidence: ExternalActivityEvidence = { cwd: resolvedCwd, lastProbeStartedAt: performance.now() };
|
|
2654
|
+
externalActivityEvidence.set(index, evidence);
|
|
2655
|
+
evidence.fingerprint = await readGitFingerprint(resolvedCwd, signal, (error) => reportGitProbeError(resolvedCwd, error));
|
|
2656
|
+
};
|
|
2657
|
+
const recordExternalStreamActivity = (index: number): void => {
|
|
2658
|
+
const evidence = externalActivityEvidence.get(index) ?? {};
|
|
2659
|
+
externalActivityEvidence.set(index, evidence);
|
|
2660
|
+
evidence.lastStreamActivityAt = Date.now();
|
|
2661
|
+
};
|
|
2626
2662
|
const stepOutputActivityAt = (index: number): number => {
|
|
2627
2663
|
const step = statusPayload.steps[index];
|
|
2628
2664
|
let lastActivityAt = step?.lastActivityAt ?? step?.startedAt ?? overallStartTime;
|
|
2665
|
+
lastActivityAt = Math.max(lastActivityAt, externalActivityEvidence.get(index)?.lastStreamActivityAt ?? 0);
|
|
2629
2666
|
const outputPath = path.join(asyncDir, `output-${index}.log`);
|
|
2630
2667
|
try {
|
|
2631
2668
|
lastActivityAt = Math.max(lastActivityAt, fs.statSync(outputPath).mtimeMs);
|
|
@@ -2993,7 +3030,7 @@ export async function runSubagent(
|
|
|
2993
3030
|
return;
|
|
2994
3031
|
}
|
|
2995
3032
|
if (event.type === "message_end") {
|
|
2996
|
-
const next = applyChildWatchdogMessage(step.watchdog, event.message
|
|
3033
|
+
const next = applyChildWatchdogMessage(step.watchdog, event.message);
|
|
2997
3034
|
if (next) step.watchdog = next;
|
|
2998
3035
|
if (next && (event.message as { role?: unknown } | undefined)?.role === "custom") {
|
|
2999
3036
|
statusPayload.lastUpdate = now;
|
|
@@ -3134,7 +3171,7 @@ export async function runSubagent(
|
|
|
3134
3171
|
// A sibling may keep aggregate attention unchanged; publish this step's transition.
|
|
3135
3172
|
writeStatusPayload(step.activityState !== previousActivityState);
|
|
3136
3173
|
};
|
|
3137
|
-
const updateRunnerActivityState = (now: number): boolean => {
|
|
3174
|
+
const updateRunnerActivityState = (now: number, skipExternalProbeIndex?: number): boolean => {
|
|
3138
3175
|
if (!controlConfig.enabled) return false;
|
|
3139
3176
|
let changed = false;
|
|
3140
3177
|
let runLastActivityAt = statusPayload.lastActivityAt ?? overallStartTime;
|
|
@@ -3151,12 +3188,35 @@ export async function runSubagent(
|
|
|
3151
3188
|
config: controlConfig,
|
|
3152
3189
|
startedAt: step.startedAt ?? overallStartTime,
|
|
3153
3190
|
lastActivityAt,
|
|
3154
|
-
turnCount: step.turnCount,
|
|
3191
|
+
turnCount: flatSteps[index]?.runner?.type === "external-cli" ? 1 : step.turnCount,
|
|
3155
3192
|
currentTool: step.currentTool,
|
|
3156
3193
|
thinking: step.thinking,
|
|
3157
3194
|
now,
|
|
3158
3195
|
}));
|
|
3159
3196
|
if (idleState === "needs_attention") {
|
|
3197
|
+
const evidence = externalActivityEvidence.get(index);
|
|
3198
|
+
if (index !== skipExternalProbeIndex && evidence?.cwd && evidence.fingerprint) {
|
|
3199
|
+
if (evidence.probeInFlight) continue;
|
|
3200
|
+
if (performance.now() - (evidence.lastProbeStartedAt ?? 0) >= EXTERNAL_GIT_PROBE_MIN_INTERVAL_MS) {
|
|
3201
|
+
evidence.lastProbeStartedAt = performance.now();
|
|
3202
|
+
const baseline = evidence.fingerprint;
|
|
3203
|
+
evidence.probeInFlight = readSharedGitFingerprint(evidence.cwd).then((fingerprint) => {
|
|
3204
|
+
if (requiredStatusStep(statusPayload, index).status !== "running") return;
|
|
3205
|
+
if (fingerprint && fingerprint !== baseline) {
|
|
3206
|
+
evidence.fingerprint = fingerprint;
|
|
3207
|
+
evidence.lastStreamActivityAt = Date.now();
|
|
3208
|
+
updateRunnerActivityState(Date.now());
|
|
3209
|
+
} else {
|
|
3210
|
+
updateRunnerActivityState(Date.now(), index);
|
|
3211
|
+
}
|
|
3212
|
+
}).catch((error: unknown) => {
|
|
3213
|
+
recordPeriodicGitProbeFailure(index, error);
|
|
3214
|
+
}).finally(() => {
|
|
3215
|
+
delete evidence.probeInFlight;
|
|
3216
|
+
});
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3160
3220
|
const previous = step.activityState;
|
|
3161
3221
|
step.activityState = "needs_attention";
|
|
3162
3222
|
if (previous !== "needs_attention") {
|
|
@@ -3218,11 +3278,15 @@ export async function runSubagent(
|
|
|
3218
3278
|
};
|
|
3219
3279
|
const finalizeWorktree = async (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number, action: () => void, deadlineAt?: number): Promise<void> => {
|
|
3220
3280
|
let admitted = false;
|
|
3221
|
-
try {
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3281
|
+
try {
|
|
3282
|
+
await Promise.all([...externalActivityEvidence.values()].map((evidence) => evidence.probeInFlight).filter((probe): probe is Promise<void> => probe !== undefined));
|
|
3283
|
+
if (periodicGitProbeFailure) throw periodicGitProbeFailure.error;
|
|
3284
|
+
await withWorktreeTransaction(() => {
|
|
3285
|
+
if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
|
|
3286
|
+
admitted = true;
|
|
3287
|
+
action();
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3226
3290
|
catch (error) {
|
|
3227
3291
|
if (admitted) throw error;
|
|
3228
3292
|
const reason = `Worktree finalization retained; manual reconciliation required: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -3277,16 +3341,17 @@ export async function runSubagent(
|
|
|
3277
3341
|
interruptActiveChildren();
|
|
3278
3342
|
};
|
|
3279
3343
|
const stopRunner = () => {
|
|
3280
|
-
if (stopped || timedOut ||
|
|
3344
|
+
if (stopped || timedOut || (statusPayload.state !== "running" && statusPayload.state !== "paused")) return;
|
|
3281
3345
|
stopped = true;
|
|
3346
|
+
interrupted = false;
|
|
3282
3347
|
const now = Date.now();
|
|
3283
3348
|
statusPayload.stopped = true;
|
|
3284
3349
|
statusPayload.error = stopMessage;
|
|
3285
3350
|
currentActivityState = undefined;
|
|
3286
3351
|
delete statusPayload.activityState;
|
|
3287
3352
|
statusPayload.lastUpdate = now;
|
|
3288
|
-
for (const step of statusPayload.steps) {
|
|
3289
|
-
if (step.status !== "running" && step.status !== "pending") continue;
|
|
3353
|
+
for (const [index, step] of statusPayload.steps.entries()) {
|
|
3354
|
+
if (step.status !== "running" && step.status !== "pending" && step.status !== "paused") continue;
|
|
3290
3355
|
step.status = "stopped";
|
|
3291
3356
|
step.error = stopMessage;
|
|
3292
3357
|
step.exitCode = 1;
|
|
@@ -3295,6 +3360,8 @@ export async function runSubagent(
|
|
|
3295
3360
|
step.endedAt = now;
|
|
3296
3361
|
step.durationMs = step.startedAt ? now - step.startedAt : 0;
|
|
3297
3362
|
step.lastActivityAt = now;
|
|
3363
|
+
const result = results[index];
|
|
3364
|
+
if (result?.interrupted) results[index] = { ...result, output: stopMessage, error: stopMessage, exitCode: 1, success: false, interrupted: false, stopped: true };
|
|
3298
3365
|
}
|
|
3299
3366
|
writeStatusPayload();
|
|
3300
3367
|
appendJsonl(eventsPath, JSON.stringify({
|
|
@@ -3366,6 +3433,30 @@ export async function runSubagent(
|
|
|
3366
3433
|
const remainingMs = Math.max(0, config.deadlineAt - Date.now());
|
|
3367
3434
|
timeoutTimer = setTimeout(timeoutRunner, remainingMs);
|
|
3368
3435
|
timeoutTimer.unref?.();
|
|
3436
|
+
// Route the pre-deadline checkpoint like any external steer so its lifecycle records the receipt.
|
|
3437
|
+
const checkpointBeforeDeadlineMs = config.checkpointBeforeDeadlineMs;
|
|
3438
|
+
const checkpointDelayMs = checkpointBeforeDeadlineMs === undefined || !Number.isInteger(checkpointBeforeDeadlineMs) || checkpointBeforeDeadlineMs <= 0
|
|
3439
|
+
? undefined
|
|
3440
|
+
: remainingMs - checkpointBeforeDeadlineMs;
|
|
3441
|
+
if (checkpointDelayMs !== undefined && checkpointDelayMs >= 1_000) {
|
|
3442
|
+
const deadlineAt = config.deadlineAt;
|
|
3443
|
+
checkpointTimer = setTimeout(() => {
|
|
3444
|
+
checkpointTimer = undefined;
|
|
3445
|
+
if (timedOut || stopped || interrupted) return;
|
|
3446
|
+
if (!statusPayload.steps.some((step) => step.status === "running")) return;
|
|
3447
|
+
const now = Date.now();
|
|
3448
|
+
const seconds = Math.round(Math.max(0, deadlineAt - now) / 1000);
|
|
3449
|
+
deliverSteerRequest({
|
|
3450
|
+
type: "steer",
|
|
3451
|
+
id: `deadline-checkpoint-${now}`,
|
|
3452
|
+
ts: now,
|
|
3453
|
+
mode: "steer",
|
|
3454
|
+
source: "deadline-checkpoint",
|
|
3455
|
+
message: `Deadline checkpoint from the runner: this run is killed in about ${seconds} seconds. Finish the current tool call only, then stop and reply with a handoff: changed files, build/test state, remaining work, and commit/PR state. Do not start new work.`,
|
|
3456
|
+
});
|
|
3457
|
+
}, checkpointDelayMs);
|
|
3458
|
+
checkpointTimer.unref?.();
|
|
3459
|
+
}
|
|
3369
3460
|
}
|
|
3370
3461
|
appendJsonl(
|
|
3371
3462
|
eventsPath,
|
|
@@ -3411,14 +3502,7 @@ export async function runSubagent(
|
|
|
3411
3502
|
const thinkingOverride = step.thinkingOverrides?.[itemIndex];
|
|
3412
3503
|
const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
|
|
3413
3504
|
const configThinking = thinkingOverride ? thinkingOverride : step.parallel.thinking;
|
|
3414
|
-
|
|
3415
|
-
? step.parallel.modelCandidates.length > 0
|
|
3416
|
-
? step.parallel.modelCandidates.map((candidate) => thinkingOverride ? applyThinkingSuffix(candidate, thinkingOverride, true) ?? candidate : candidate)
|
|
3417
|
-
: [undefined]
|
|
3418
|
-
: model ? [model] : [undefined];
|
|
3419
|
-
for (const candidate of candidates) {
|
|
3420
|
-
assertThinkingWithinCeiling({ model: candidate, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
|
|
3421
|
-
}
|
|
3505
|
+
assertThinkingWithinCeiling({ model, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
|
|
3422
3506
|
}
|
|
3423
3507
|
if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
|
|
3424
3508
|
if (!config.runFanoutBudget) throw new Error("Async runner is missing its run fan-out budget identity.");
|
|
@@ -3551,10 +3635,6 @@ export async function runSubagent(
|
|
|
3551
3635
|
...(thinkingOverride ? {
|
|
3552
3636
|
...(model ? { model } : {}),
|
|
3553
3637
|
...(thinking ? { thinking } : {}),
|
|
3554
|
-
...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.flatMap((candidate) => {
|
|
3555
|
-
const resolved = applyThinkingSuffix(candidate, thinkingOverride, true);
|
|
3556
|
-
return resolved ? [resolved] : [];
|
|
3557
|
-
}) } : {}),
|
|
3558
3638
|
} : {}),
|
|
3559
3639
|
structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
|
|
3560
3640
|
});
|
|
@@ -3581,7 +3661,7 @@ export async function runSubagent(
|
|
|
3581
3661
|
...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
|
|
3582
3662
|
...(task.thinking ? { thinking: task.thinking } : {}),
|
|
3583
3663
|
...(task.thinkingCeiling ? { thinkingCeiling: task.thinkingCeiling } : {}),
|
|
3584
|
-
...(task.
|
|
3664
|
+
...(task.requestedModel ? { requestedModel: task.requestedModel } : {}),
|
|
3585
3665
|
recentTools: [],
|
|
3586
3666
|
recentOutput: [],
|
|
3587
3667
|
});
|
|
@@ -3710,6 +3790,8 @@ export async function runSubagent(
|
|
|
3710
3790
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
|
|
3711
3791
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
3712
3792
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
3793
|
+
prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(fi, externalCwd, signal),
|
|
3794
|
+
onExternalStreamActivity: () => recordExternalStreamActivity(fi),
|
|
3713
3795
|
onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
|
|
3714
3796
|
skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
|
|
3715
3797
|
usageBudgetExhausted: continuationUsageBudgetExhausted,
|
|
@@ -3719,7 +3801,7 @@ export async function runSubagent(
|
|
|
3719
3801
|
const taskEndTime = Date.now();
|
|
3720
3802
|
const childInterrupted = singleResult.interrupted === true;
|
|
3721
3803
|
const childStopped = singleResult.stopped === true;
|
|
3722
|
-
requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
3804
|
+
requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
3723
3805
|
requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
|
|
3724
3806
|
requiredStatusStep(statusPayload, fi).durationMs = taskEndTime - taskStartTime;
|
|
3725
3807
|
requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
@@ -3732,8 +3814,7 @@ export async function runSubagent(
|
|
|
3732
3814
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
|
|
3733
3815
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
|
|
3734
3816
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
|
|
3735
|
-
setOptionalProperty(requiredStatusStep(statusPayload, fi), "
|
|
3736
|
-
setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
|
|
3817
|
+
setOptionalProperty(requiredStatusStep(statusPayload, fi), "requestedModel", singleResult.requestedModel);
|
|
3737
3818
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
|
|
3738
3819
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
|
|
3739
3820
|
if (singleResult.totalCost) {
|
|
@@ -3790,7 +3871,7 @@ export async function runSubagent(
|
|
|
3790
3871
|
output: pr.output,
|
|
3791
3872
|
outputState: pr.outputState,
|
|
3792
3873
|
error: pr.error,
|
|
3793
|
-
success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
|
|
3874
|
+
success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0 && pr.execution?.status !== "partial",
|
|
3794
3875
|
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
3795
3876
|
skipped: pr.skipped,
|
|
3796
3877
|
interrupted: pr.interrupted,
|
|
@@ -3802,12 +3883,13 @@ export async function runSubagent(
|
|
|
3802
3883
|
intercomTarget: pr.intercomTarget,
|
|
3803
3884
|
model: pr.model,
|
|
3804
3885
|
thinking: pr.thinking,
|
|
3805
|
-
|
|
3806
|
-
modelAttempts: pr.modelAttempts,
|
|
3886
|
+
requestedModel: pr.requestedModel,
|
|
3807
3887
|
contextOverflow: pr.contextOverflow,
|
|
3808
3888
|
totalCost: pr.totalCost,
|
|
3809
3889
|
usage: pr.usage,
|
|
3810
3890
|
artifactPaths: pr.artifactPaths,
|
|
3891
|
+
outputSaveError: pr.outputSaveError,
|
|
3892
|
+
artifactOutputSaveFailed: pr.artifactOutputSaveFailed,
|
|
3811
3893
|
transcriptPath: pr.transcriptPath,
|
|
3812
3894
|
transcriptError: pr.transcriptError,
|
|
3813
3895
|
effects: pr.effects,
|
|
@@ -4122,6 +4204,8 @@ export async function runSubagent(
|
|
|
4122
4204
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
|
|
4123
4205
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
4124
4206
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
4207
|
+
prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(fi, externalCwd, signal),
|
|
4208
|
+
onExternalStreamActivity: () => recordExternalStreamActivity(fi),
|
|
4125
4209
|
onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
|
|
4126
4210
|
skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
|
|
4127
4211
|
usageBudgetExhausted: continuationUsageBudgetExhausted,
|
|
@@ -4137,7 +4221,7 @@ export async function runSubagent(
|
|
|
4137
4221
|
const childInterrupted = singleResult.interrupted === true;
|
|
4138
4222
|
const childStopped = singleResult.stopped === true;
|
|
4139
4223
|
|
|
4140
|
-
requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
4224
|
+
requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
4141
4225
|
requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
|
|
4142
4226
|
requiredStatusStep(statusPayload, fi).durationMs = taskDuration;
|
|
4143
4227
|
requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
@@ -4150,8 +4234,7 @@ export async function runSubagent(
|
|
|
4150
4234
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
|
|
4151
4235
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
|
|
4152
4236
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
|
|
4153
|
-
setOptionalProperty(requiredStatusStep(statusPayload, fi), "
|
|
4154
|
-
setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
|
|
4237
|
+
setOptionalProperty(requiredStatusStep(statusPayload, fi), "requestedModel", singleResult.requestedModel);
|
|
4155
4238
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
|
|
4156
4239
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
|
|
4157
4240
|
if (singleResult.totalCost) {
|
|
@@ -4218,7 +4301,7 @@ export async function runSubagent(
|
|
|
4218
4301
|
const sessionTokens = config.sessionDir
|
|
4219
4302
|
? parseSessionTokens(path.join(config.sessionDir, `parallel-${t}`))
|
|
4220
4303
|
: null;
|
|
4221
|
-
const fallbackTokens =
|
|
4304
|
+
const fallbackTokens = tokenUsageFromUsage(parallelResults[t]?.usage);
|
|
4222
4305
|
const observedTokens = requiredStatusStep(statusPayload, fi).tokens;
|
|
4223
4306
|
const taskTokens = sessionTokens ?? (fallbackTokens
|
|
4224
4307
|
? { ...fallbackTokens, ...(observedTokens?.window !== undefined ? { window: observedTokens.window } : {}), ...(observedTokens?.windowPeak !== undefined ? { windowPeak: observedTokens.windowPeak } : {}) }
|
|
@@ -4249,7 +4332,7 @@ export async function runSubagent(
|
|
|
4249
4332
|
output: pr.output,
|
|
4250
4333
|
outputState: pr.outputState,
|
|
4251
4334
|
error: pr.error,
|
|
4252
|
-
success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
|
|
4335
|
+
success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0 && pr.execution?.status !== "partial",
|
|
4253
4336
|
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
4254
4337
|
skipped: pr.skipped,
|
|
4255
4338
|
interrupted: pr.interrupted,
|
|
@@ -4261,12 +4344,13 @@ export async function runSubagent(
|
|
|
4261
4344
|
intercomTarget: pr.intercomTarget,
|
|
4262
4345
|
model: pr.model,
|
|
4263
4346
|
thinking: pr.thinking,
|
|
4264
|
-
|
|
4265
|
-
modelAttempts: pr.modelAttempts,
|
|
4347
|
+
requestedModel: pr.requestedModel,
|
|
4266
4348
|
contextOverflow: pr.contextOverflow,
|
|
4267
4349
|
totalCost: pr.totalCost,
|
|
4268
4350
|
usage: pr.usage,
|
|
4269
4351
|
artifactPaths: pr.artifactPaths,
|
|
4352
|
+
outputSaveError: pr.outputSaveError,
|
|
4353
|
+
artifactOutputSaveFailed: pr.artifactOutputSaveFailed,
|
|
4270
4354
|
transcriptPath: pr.transcriptPath,
|
|
4271
4355
|
transcriptError: pr.transcriptError,
|
|
4272
4356
|
effects: pr.effects,
|
|
@@ -4299,7 +4383,7 @@ export async function runSubagent(
|
|
|
4299
4383
|
exitCode: r.exitCode,
|
|
4300
4384
|
error: r.error,
|
|
4301
4385
|
model: r.model,
|
|
4302
|
-
|
|
4386
|
+
requestedModel: r.requestedModel,
|
|
4303
4387
|
})),
|
|
4304
4388
|
);
|
|
4305
4389
|
if (worktreeSetup) {
|
|
@@ -4523,6 +4607,8 @@ export async function runSubagent(
|
|
|
4523
4607
|
onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
|
|
4524
4608
|
onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
|
|
4525
4609
|
onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
|
|
4610
|
+
prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(flatIndex, externalCwd, signal),
|
|
4611
|
+
onExternalStreamActivity: () => recordExternalStreamActivity(flatIndex),
|
|
4526
4612
|
onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
|
|
4527
4613
|
skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
|
|
4528
4614
|
usageBudgetExhausted: continuationUsageBudgetExhausted,
|
|
@@ -4550,19 +4636,20 @@ export async function runSubagent(
|
|
|
4550
4636
|
output: stopped || childStopped ? stopMessage : timedOut ? singleResult.output || (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
|
|
4551
4637
|
outputState: singleResult.outputState,
|
|
4552
4638
|
error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
|
|
4553
|
-
success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
|
|
4639
|
+
success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0 && singleResult.execution?.status !== "partial",
|
|
4554
4640
|
exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
|
|
4555
4641
|
sessionFile: singleResult.sessionFile,
|
|
4556
4642
|
intercomTarget: singleResult.intercomTarget,
|
|
4557
4643
|
model: singleResult.model,
|
|
4558
4644
|
thinking: singleResult.thinking,
|
|
4559
|
-
|
|
4560
|
-
modelAttempts: singleResult.modelAttempts,
|
|
4645
|
+
requestedModel: singleResult.requestedModel,
|
|
4561
4646
|
contextOverflow: singleResult.contextOverflow,
|
|
4562
4647
|
totalCost: singleResult.totalCost,
|
|
4563
4648
|
usage: singleResult.usage,
|
|
4564
4649
|
artifactPaths: singleResult.artifactPaths,
|
|
4565
4650
|
savedOutputPath: singleResult.savedOutputPath,
|
|
4651
|
+
outputSaveError: singleResult.outputSaveError,
|
|
4652
|
+
artifactOutputSaveFailed: singleResult.artifactOutputSaveFailed,
|
|
4566
4653
|
transcriptPath: singleResult.transcriptPath,
|
|
4567
4654
|
transcriptError: singleResult.transcriptError,
|
|
4568
4655
|
effects: singleResult.effects,
|
|
@@ -4607,7 +4694,7 @@ export async function runSubagent(
|
|
|
4607
4694
|
if (cumulativeTokens) {
|
|
4608
4695
|
previousCumulativeTokens = cumulativeTokens;
|
|
4609
4696
|
} else {
|
|
4610
|
-
const fallbackTokens =
|
|
4697
|
+
const fallbackTokens = tokenUsageFromUsage(singleResult.usage);
|
|
4611
4698
|
const observedTokens = requiredStatusStep(statusPayload, flatIndex).tokens;
|
|
4612
4699
|
stepTokens = fallbackTokens
|
|
4613
4700
|
? { ...fallbackTokens, ...(observedTokens?.window !== undefined ? { window: observedTokens.window } : {}), ...(observedTokens?.windowPeak !== undefined ? { windowPeak: observedTokens.windowPeak } : {}) }
|
|
@@ -4627,7 +4714,7 @@ export async function runSubagent(
|
|
|
4627
4714
|
|
|
4628
4715
|
const stepEndTime = Date.now();
|
|
4629
4716
|
const childInterrupted = singleResult.interrupted === true;
|
|
4630
|
-
requiredStatusStep(statusPayload, flatIndex).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
4717
|
+
requiredStatusStep(statusPayload, flatIndex).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
4631
4718
|
requiredStatusStep(statusPayload, flatIndex).endedAt = stepEndTime;
|
|
4632
4719
|
requiredStatusStep(statusPayload, flatIndex).durationMs = stepEndTime - stepStartTime;
|
|
4633
4720
|
requiredStatusStep(statusPayload, flatIndex).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
@@ -4640,8 +4727,7 @@ export async function runSubagent(
|
|
|
4640
4727
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "sessionName", singleResult.sessionName);
|
|
4641
4728
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "model", singleResult.model);
|
|
4642
4729
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
|
|
4643
|
-
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "
|
|
4644
|
-
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "modelAttempts", singleResult.modelAttempts);
|
|
4730
|
+
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "requestedModel", singleResult.requestedModel);
|
|
4645
4731
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "contextOverflow", singleResult.contextOverflow);
|
|
4646
4732
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "totalCost", singleResult.totalCost);
|
|
4647
4733
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
|
|
@@ -4756,6 +4842,34 @@ export async function runSubagent(
|
|
|
4756
4842
|
}
|
|
4757
4843
|
}
|
|
4758
4844
|
|
|
4845
|
+
await Promise.all([...externalActivityEvidence.values()].map((evidence) => evidence.probeInFlight).filter((probe): probe is Promise<void> => probe !== undefined));
|
|
4846
|
+
if (periodicGitProbeFailure) {
|
|
4847
|
+
stopped = false;
|
|
4848
|
+
timedOut = false;
|
|
4849
|
+
interrupted = false;
|
|
4850
|
+
delete statusPayload.stopped;
|
|
4851
|
+
delete statusPayload.timedOut;
|
|
4852
|
+
statusPayload.error = periodicGitProbeFailure.message;
|
|
4853
|
+
const step = statusPayload.steps[periodicGitProbeFailure.index];
|
|
4854
|
+
if (step) {
|
|
4855
|
+
step.status = "failed";
|
|
4856
|
+
step.error = periodicGitProbeFailure.message;
|
|
4857
|
+
step.exitCode = 1;
|
|
4858
|
+
delete step.stopped;
|
|
4859
|
+
delete step.timedOut;
|
|
4860
|
+
}
|
|
4861
|
+
const result = results[periodicGitProbeFailure.index];
|
|
4862
|
+
if (result) {
|
|
4863
|
+
result.success = false;
|
|
4864
|
+
result.exitCode = 1;
|
|
4865
|
+
result.error = periodicGitProbeFailure.message;
|
|
4866
|
+
result.output = periodicGitProbeFailure.message;
|
|
4867
|
+
result.stopped = false;
|
|
4868
|
+
result.timedOut = false;
|
|
4869
|
+
result.interrupted = false;
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4759
4873
|
let summary = results.map((r) => `${r.agent}:\n${r.output || (r.exitCode !== 0 ? r.error : undefined) || "(no output)"}`).join("\n\n");
|
|
4760
4874
|
let truncated = false;
|
|
4761
4875
|
|
|
@@ -4821,9 +4935,15 @@ export async function runSubagent(
|
|
|
4821
4935
|
clearTimeout(timeoutTimer);
|
|
4822
4936
|
timeoutTimer = undefined;
|
|
4823
4937
|
}
|
|
4938
|
+
if (checkpointTimer) {
|
|
4939
|
+
clearTimeout(checkpointTimer);
|
|
4940
|
+
checkpointTimer = undefined;
|
|
4941
|
+
}
|
|
4824
4942
|
if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && timeoutMessage !== undefined && results.some((result) => result.timedOut === true && result.error?.startsWith(timeoutMessage))) {
|
|
4825
4943
|
timedOut = true;
|
|
4826
4944
|
}
|
|
4945
|
+
disposeControlInbox();
|
|
4946
|
+
for (const request of consumeStopRequestPayloads(asyncDir)) stopChildStep(request);
|
|
4827
4947
|
const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
|
|
4828
4948
|
processSignal: result.processSignal,
|
|
4829
4949
|
interrupted: result.interrupted,
|
|
@@ -4838,7 +4958,6 @@ export async function runSubagent(
|
|
|
4838
4958
|
});
|
|
4839
4959
|
statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
|
|
4840
4960
|
closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
|
|
4841
|
-
disposeControlInbox();
|
|
4842
4961
|
for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
|
|
4843
4962
|
const effectiveSessionFile = sessionFile ?? latestSessionFile;
|
|
4844
4963
|
const steeringLifecycle = steeringStatus(statusPayload);
|
|
@@ -4874,7 +4993,7 @@ export async function runSubagent(
|
|
|
4874
4993
|
statusPayload.activityState = "needs_attention";
|
|
4875
4994
|
statusPayload.error = partialResult?.error ?? statusPayload.error;
|
|
4876
4995
|
for (const step of statusPayload.steps) {
|
|
4877
|
-
if (step.status === "failed") step.activityState = "needs_attention";
|
|
4996
|
+
if (step.status === "failed" || step.status === "partial") step.activityState = "needs_attention";
|
|
4878
4997
|
}
|
|
4879
4998
|
}
|
|
4880
4999
|
statusPayload.endedAt = runEndedAt;
|
|
@@ -4931,14 +5050,14 @@ export async function runSubagent(
|
|
|
4931
5050
|
intercomTarget: r.intercomTarget,
|
|
4932
5051
|
model: r.model,
|
|
4933
5052
|
thinking: r.thinking,
|
|
4934
|
-
|
|
4935
|
-
modelAttempts: r.modelAttempts,
|
|
5053
|
+
requestedModel: r.requestedModel,
|
|
4936
5054
|
contextOverflow: r.contextOverflow,
|
|
4937
5055
|
totalCost: r.totalCost,
|
|
4938
5056
|
usage: r.usage,
|
|
4939
5057
|
artifactPaths: r.artifactPaths,
|
|
4940
5058
|
savedOutputPath: r.savedOutputPath,
|
|
4941
5059
|
outputSaveError: r.outputSaveError,
|
|
5060
|
+
artifactOutputSaveFailed: r.artifactOutputSaveFailed,
|
|
4942
5061
|
metadataSaveError: r.metadataSaveError,
|
|
4943
5062
|
truncated: r.truncated,
|
|
4944
5063
|
transcriptPath: r.transcriptPath,
|
|
@@ -5074,7 +5193,7 @@ export async function runSubagent(
|
|
|
5074
5193
|
async function waitForStartupControl(
|
|
5075
5194
|
controlPath: string,
|
|
5076
5195
|
token: string,
|
|
5077
|
-
action: "ack" | "proceed",
|
|
5196
|
+
action: "ack" | "confirm" | "proceed",
|
|
5078
5197
|
timeoutMs = 30_000,
|
|
5079
5198
|
): Promise<void> {
|
|
5080
5199
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -5088,7 +5207,7 @@ async function waitForStartupControl(
|
|
|
5088
5207
|
}
|
|
5089
5208
|
if (payload.token !== token) throw new Error("Runner startup control token does not match.");
|
|
5090
5209
|
if (payload.action === action) return;
|
|
5091
|
-
if (payload.action !== "ack" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
|
|
5210
|
+
if (payload.action !== "ack" && payload.action !== "confirm" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
|
|
5092
5211
|
}
|
|
5093
5212
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
5094
5213
|
}
|
|
@@ -5125,6 +5244,8 @@ export async function runConfiguredSubagent(config: SubagentRunConfig, options?:
|
|
|
5125
5244
|
writeAtomicJson(startupPath, { state: "ready", token: lease.owner.token, pid: process.pid, owner: lease.owner });
|
|
5126
5245
|
await waitForStartupControl(startupAckPath, lease.owner.token, "ack");
|
|
5127
5246
|
writeAtomicJson(startupPath, { state: "acknowledged", token: lease.owner.token, pid: process.pid });
|
|
5247
|
+
await waitForStartupControl(startupAckPath, lease.owner.token, "confirm");
|
|
5248
|
+
writeAtomicJson(startupPath, { state: "confirmed", token: lease.owner.token, pid: process.pid });
|
|
5128
5249
|
await waitForStartupControl(startupProceedPath, lease.owner.token, "proceed");
|
|
5129
5250
|
startupCommitted = true;
|
|
5130
5251
|
for (const controlPath of [startupAckPath, startupProceedPath]) {
|