@claudexor/orchestrator 3.1.1 → 3.2.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/dist/attemptFinalize.d.ts +14 -11
- package/dist/attemptFinalize.d.ts.map +1 -1
- package/dist/attemptFinalize.js +41 -14
- package/dist/attemptFinalize.js.map +1 -1
- package/dist/attemptTelemetry.d.ts +16 -20
- package/dist/attemptTelemetry.d.ts.map +1 -1
- package/dist/attemptTelemetry.js +38 -70
- package/dist/attemptTelemetry.js.map +1 -1
- package/dist/attemptUsageCost.d.ts +47 -0
- package/dist/attemptUsageCost.d.ts.map +1 -0
- package/dist/attemptUsageCost.js +191 -0
- package/dist/attemptUsageCost.js.map +1 -0
- package/dist/budgetFailure.d.ts +3 -1
- package/dist/budgetFailure.d.ts.map +1 -1
- package/dist/budgetFailure.js.map +1 -1
- package/dist/candidateEvidence.d.ts +5 -0
- package/dist/candidateEvidence.d.ts.map +1 -1
- package/dist/candidateEvidence.js +3 -1
- package/dist/candidateEvidence.js.map +1 -1
- package/dist/candidateOutputs.d.ts +19 -0
- package/dist/candidateOutputs.d.ts.map +1 -1
- package/dist/candidateOutputs.js +201 -67
- package/dist/candidateOutputs.js.map +1 -1
- package/dist/credential-profile-rotation.d.ts +145 -0
- package/dist/credential-profile-rotation.d.ts.map +1 -0
- package/dist/credential-profile-rotation.js +315 -0
- package/dist/credential-profile-rotation.js.map +1 -0
- package/dist/credential-profiles.d.ts +35 -134
- package/dist/credential-profiles.d.ts.map +1 -1
- package/dist/credential-profiles.js +73 -295
- package/dist/credential-profiles.js.map +1 -1
- package/dist/decisionBudget.d.ts +43 -0
- package/dist/decisionBudget.d.ts.map +1 -0
- package/dist/decisionBudget.js +84 -0
- package/dist/decisionBudget.js.map +1 -0
- package/dist/decisionTerminalReconciliation.d.ts +18 -0
- package/dist/decisionTerminalReconciliation.d.ts.map +1 -0
- package/dist/decisionTerminalReconciliation.js +26 -0
- package/dist/decisionTerminalReconciliation.js.map +1 -0
- package/dist/deepScanReducer.d.ts +14 -44
- package/dist/deepScanReducer.d.ts.map +1 -1
- package/dist/deepScanReducer.js +240 -128
- package/dist/deepScanReducer.js.map +1 -1
- package/dist/delegation-parent-activation.d.ts +6 -0
- package/dist/delegation-parent-activation.d.ts.map +1 -0
- package/dist/delegation-parent-activation.js +13 -0
- package/dist/delegation-parent-activation.js.map +1 -0
- package/dist/delegationBudgetAuthority.d.ts +30 -0
- package/dist/delegationBudgetAuthority.d.ts.map +1 -0
- package/dist/delegationBudgetAuthority.js +157 -0
- package/dist/delegationBudgetAuthority.js.map +1 -0
- package/dist/delegationFailure.d.ts +58 -0
- package/dist/delegationFailure.d.ts.map +1 -0
- package/dist/delegationFailure.js +184 -0
- package/dist/delegationFailure.js.map +1 -0
- package/dist/delegationToolEvidence.d.ts +20 -0
- package/dist/delegationToolEvidence.d.ts.map +1 -0
- package/dist/delegationToolEvidence.js +46 -0
- package/dist/delegationToolEvidence.js.map +1 -0
- package/dist/git-precondition.d.ts +11 -0
- package/dist/git-precondition.d.ts.map +1 -0
- package/dist/git-precondition.js +69 -0
- package/dist/git-precondition.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +14 -6
- package/dist/interaction.d.ts.map +1 -1
- package/dist/interaction.js +106 -19
- package/dist/interaction.js.map +1 -1
- package/dist/orchestrator.d.ts +62 -115
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +671 -965
- package/dist/orchestrator.js.map +1 -1
- package/dist/plan-prompt.d.ts +3 -0
- package/dist/plan-prompt.d.ts.map +1 -0
- package/dist/plan-prompt.js +28 -0
- package/dist/plan-prompt.js.map +1 -0
- package/dist/planBrief.d.ts +27 -0
- package/dist/planBrief.d.ts.map +1 -0
- package/dist/planBrief.js +42 -0
- package/dist/planBrief.js.map +1 -0
- package/dist/planRun.d.ts +4 -3
- package/dist/planRun.d.ts.map +1 -1
- package/dist/planRun.js +33 -33
- package/dist/planRun.js.map +1 -1
- package/dist/plannerAttempt.d.ts +64 -0
- package/dist/plannerAttempt.d.ts.map +1 -0
- package/dist/plannerAttempt.js +235 -0
- package/dist/plannerAttempt.js.map +1 -0
- package/dist/policyFindings.js.map +1 -1
- package/dist/requestRequirements.d.ts +45 -1
- package/dist/requestRequirements.d.ts.map +1 -1
- package/dist/requestRequirements.js +158 -11
- package/dist/requestRequirements.js.map +1 -1
- package/dist/reviewerPanel.js.map +1 -1
- package/dist/root-ledger.d.ts +13 -0
- package/dist/root-ledger.d.ts.map +1 -0
- package/dist/root-ledger.js +21 -0
- package/dist/root-ledger.js.map +1 -0
- package/dist/routing-failure.d.ts +6 -0
- package/dist/routing-failure.d.ts.map +1 -0
- package/dist/routing-failure.js +12 -0
- package/dist/routing-failure.js.map +1 -0
- package/dist/run-bounded.d.ts +3 -0
- package/dist/run-bounded.d.ts.map +1 -0
- package/dist/run-bounded.js +23 -0
- package/dist/run-bounded.js.map +1 -0
- package/dist/run-input-resolution.d.ts +10 -0
- package/dist/run-input-resolution.d.ts.map +1 -0
- package/dist/run-input-resolution.js +88 -0
- package/dist/run-input-resolution.js.map +1 -0
- package/dist/runEventLog.d.ts +48 -0
- package/dist/runEventLog.d.ts.map +1 -0
- package/dist/runEventLog.js +63 -0
- package/dist/runEventLog.js.map +1 -0
- package/dist/runFacts.d.ts +23 -0
- package/dist/runFacts.d.ts.map +1 -0
- package/dist/runFacts.js +179 -0
- package/dist/runFacts.js.map +1 -0
- package/dist/runFactsBuilder.d.ts +5 -0
- package/dist/runFactsBuilder.d.ts.map +1 -0
- package/dist/runFactsBuilder.js +412 -0
- package/dist/runFactsBuilder.js.map +1 -0
- package/dist/runFactsDeliverable.d.ts +13 -0
- package/dist/runFactsDeliverable.d.ts.map +1 -0
- package/dist/runFactsDeliverable.js +94 -0
- package/dist/runFactsDeliverable.js.map +1 -0
- package/dist/runFactsPresentation.d.ts +7 -0
- package/dist/runFactsPresentation.d.ts.map +1 -0
- package/dist/runFactsPresentation.js +113 -0
- package/dist/runFactsPresentation.js.map +1 -0
- package/dist/runFactsReview.d.ts +7 -0
- package/dist/runFactsReview.d.ts.map +1 -0
- package/dist/runFactsReview.js +63 -0
- package/dist/runFactsReview.js.map +1 -0
- package/dist/runSupport.d.ts +17 -15
- package/dist/runSupport.d.ts.map +1 -1
- package/dist/runSupport.js +16 -0
- package/dist/runSupport.js.map +1 -1
- package/dist/runTelemetryWriter.d.ts +5 -2
- package/dist/runTelemetryWriter.d.ts.map +1 -1
- package/dist/runTelemetryWriter.js +72 -1
- package/dist/runTelemetryWriter.js.map +1 -1
- package/dist/runTerminalContext.d.ts +31 -0
- package/dist/runTerminalContext.d.ts.map +1 -0
- package/dist/runTerminalContext.js +12 -0
- package/dist/runTerminalContext.js.map +1 -0
- package/dist/runTerminalResults.d.ts +55 -0
- package/dist/runTerminalResults.d.ts.map +1 -0
- package/dist/runTerminalResults.js +134 -0
- package/dist/runTerminalResults.js.map +1 -0
- package/dist/runTerminals.d.ts +8 -57
- package/dist/runTerminals.d.ts.map +1 -1
- package/dist/runTerminals.js +423 -135
- package/dist/runTerminals.js.map +1 -1
- package/dist/secretDiff.d.ts +32 -0
- package/dist/secretDiff.d.ts.map +1 -0
- package/dist/secretDiff.js +140 -0
- package/dist/secretDiff.js.map +1 -0
- package/dist/structuredOutput.d.ts.map +1 -1
- package/dist/structuredOutput.js.map +1 -1
- package/dist/task-contract-builder.d.ts +35 -0
- package/dist/task-contract-builder.d.ts.map +1 -0
- package/dist/task-contract-builder.js +114 -0
- package/dist/task-contract-builder.js.map +1 -0
- package/dist/terminalOutcome.d.ts +22 -0
- package/dist/terminalOutcome.d.ts.map +1 -0
- package/dist/terminalOutcome.js +39 -0
- package/dist/terminalOutcome.js.map +1 -0
- package/dist/transientClassify.js.map +1 -1
- package/package.json +17 -17
package/dist/orchestrator.js
CHANGED
|
@@ -1,96 +1,68 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, } from "./credential-profiles.js";
|
|
2
|
+
import { effectiveAuthPreference, observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, probeCredentialProfileStatus, profileHeadroomBreach, profileStatusAdmits, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, staticRotationCandidates, } from "./credential-profiles.js";
|
|
3
3
|
import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
|
|
4
4
|
import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
|
|
5
5
|
import { processAttemptUsage } from "./attemptUsage.js";
|
|
6
|
+
import * as AC from "./attemptUsageCost.js";
|
|
6
7
|
import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
|
|
7
8
|
import { capabilityIntents } from "@claudexor/gateway";
|
|
8
9
|
import { policyFindings } from "./policyFindings.js";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./planRun.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
|
|
13
|
+
import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
|
|
14
|
+
import { globalConfigDir, loadConfig } from "@claudexor/config";
|
|
15
|
+
import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
|
|
14
16
|
import { assertRouteModelsAllowed } from "./modelGovernance.js";
|
|
15
17
|
import { governRouteEffort } from "./effortGovernance.js";
|
|
16
|
-
import { RequestRequirementsResolver } from "./requestRequirements.js";
|
|
18
|
+
import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
|
|
19
|
+
import { activateDelegationParent } from "./delegation-parent-activation.js";
|
|
20
|
+
import { routingFailureClassification } from "./routing-failure.js";
|
|
21
|
+
export { routingFailureClassification } from "./routing-failure.js";
|
|
22
|
+
import { runBounded } from "./run-bounded.js";
|
|
23
|
+
import { planPrompt } from "./plan-prompt.js";
|
|
24
|
+
import { verifiedPlanBrief, withPlanBrief } from "./planBrief.js";
|
|
25
|
+
import { resolveRunInputDefaults } from "./run-input-resolution.js";
|
|
26
|
+
import { beginAnnouncedRun } from "./runEventLog.js";
|
|
27
|
+
import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudget.js";
|
|
17
28
|
import { buildRevisePrompt } from "./revisePrompt.js";
|
|
18
29
|
import { cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
|
|
19
30
|
import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
|
|
20
31
|
import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
|
|
21
|
-
import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
|
|
32
|
+
import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, winnerNeedsHuman, writeRaceDeliveryDecision, } from "./runSupport.js";
|
|
22
33
|
import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
|
|
23
34
|
import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
|
|
35
|
+
import { ensureWriteModeGitBoundary } from "./git-precondition.js";
|
|
24
36
|
import { buildContinuation, } from "./continuity.js";
|
|
25
37
|
import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from "./continuity-facts.js";
|
|
26
38
|
import { runDiffReview } from "./diffReview.js";
|
|
27
39
|
import { rawScoutBundle, resolveDeepScanSynthesis, } from "./deepScanReducer.js";
|
|
28
40
|
import { classifyAdapterThrow, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
|
|
41
|
+
import * as delegateFailure from "./delegationFailure.js";
|
|
42
|
+
import * as secretDiff from "./secretDiff.js";
|
|
29
43
|
import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
|
|
30
44
|
import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
|
|
31
45
|
import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
|
|
32
46
|
import { interactionChannelFor } from "./interaction.js";
|
|
33
|
-
import { gateSpecsFromContract, renderTestsEvidence
|
|
47
|
+
import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
|
|
48
|
+
import { buildTaskContract } from "./task-contract-builder.js";
|
|
34
49
|
import { ArtifactStore } from "@claudexor/artifact-store";
|
|
35
|
-
import { EventLog } from "@claudexor/event-log";
|
|
36
50
|
import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
|
|
37
|
-
import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge,
|
|
51
|
+
import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
|
|
38
52
|
import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, verifyAndDeliver, } from "@claudexor/delivery";
|
|
39
53
|
import { HarnessGateway } from "@claudexor/gateway";
|
|
40
54
|
import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
|
|
41
55
|
import { arbitrate } from "@claudexor/arbitration";
|
|
42
56
|
import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
|
|
43
|
-
import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint,
|
|
44
|
-
import { readTextSafe, appendLine, assertNoInlineSecretValues,
|
|
57
|
+
import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
|
|
58
|
+
import { readTextSafe, appendLine, assertNoInlineSecretValues, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
|
|
45
59
|
import { assertWriteIsolation } from "./write-isolation.js";
|
|
46
|
-
/** A routed candidate adapter plus its manifest capabilities and user settings. */
|
|
47
|
-
/** The two access profiles that map to codex `danger-full-access` / an
|
|
48
|
-
* unsandboxed lane — the only ones under which a full-access-requiring MCP
|
|
49
|
-
* injection (the belt on codex) can reach the daemon. */
|
|
50
|
-
export function isFullAccess(access) {
|
|
51
|
-
return access === "full" || access === "external_sandbox_full";
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* A routing preflight refusal (`RoutingPreflightError`: quality routing with no
|
|
55
|
-
* comparable user-declared tier for the intent) is a CONFIGURATION error, not a
|
|
56
|
-
* harness-availability problem (A-1/D-9/#22). Classifying it as
|
|
57
|
-
* `harness_unavailable` sent the operator to re-auth or wait for a harness; the
|
|
58
|
-
* real fix is to configure a tier or change the routing goal. Detected by the
|
|
59
|
-
* typed `code` (robust across duplicate `@claudexor/budget` package copies) so
|
|
60
|
-
* EVERY strategy's routing catch (ask/agent/plan/deep-scan/council) classifies
|
|
61
|
-
* it identically. Returns the failure category + matching remediation.
|
|
62
|
-
*/
|
|
63
|
-
export function routingFailureClassification(err) {
|
|
64
|
-
const isPreflightRefusal = !!err &&
|
|
65
|
-
typeof err === "object" &&
|
|
66
|
-
err.code === "routing_preflight_refused";
|
|
67
|
-
if (isPreflightRefusal) {
|
|
68
|
-
return { category: "config_error", nextActions: harnessFailureNextActions("config_error") };
|
|
69
|
-
}
|
|
70
|
-
return { category: "harness_unavailable" };
|
|
71
|
-
}
|
|
72
60
|
const LABELS = "ABCDEFGHIJ".split("");
|
|
73
61
|
const NO_PROJECT_ROOT = noProjectRepoRoot();
|
|
74
62
|
/** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
|
|
75
63
|
const MAX_PARALLEL_CANDIDATES = 4;
|
|
76
64
|
/** Default wait for one interactive answer before a benign decline. */
|
|
77
65
|
const DEFAULT_INTERACTION_TIMEOUT_MS = 900_000;
|
|
78
|
-
/** Run `work` over `items` with bounded concurrency, preserving item order via index. */
|
|
79
|
-
async function runBounded(items, limit, work) {
|
|
80
|
-
if (items.length === 0)
|
|
81
|
-
return;
|
|
82
|
-
const concurrency = Math.max(1, Math.min(limit, items.length));
|
|
83
|
-
let next = 0;
|
|
84
|
-
const workers = Array.from({ length: concurrency }, async () => {
|
|
85
|
-
for (;;) {
|
|
86
|
-
const idx = next++;
|
|
87
|
-
if (idx >= items.length)
|
|
88
|
-
return;
|
|
89
|
-
await work(items[idx], idx);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
await Promise.all(workers);
|
|
93
|
-
}
|
|
94
66
|
export class Orchestrator {
|
|
95
67
|
deps;
|
|
96
68
|
gateway;
|
|
@@ -118,7 +90,7 @@ export class Orchestrator {
|
|
|
118
90
|
});
|
|
119
91
|
}
|
|
120
92
|
async run(input) {
|
|
121
|
-
|
|
93
|
+
let resolved = this.resolveRunInput(input);
|
|
122
94
|
// INV-062 at the ENGINE boundary: every surface fences prompts already,
|
|
123
95
|
// but a direct embedder (or the daemon-less local REPL fallback) reaches
|
|
124
96
|
// this entry without one. Prompts, per-run instructions, AND outputSchema
|
|
@@ -137,6 +109,25 @@ export class Orchestrator {
|
|
|
137
109
|
throw new Error(`unknown mode: ${String(resolved.mode)}`);
|
|
138
110
|
}
|
|
139
111
|
const mode = parsedMode.data;
|
|
112
|
+
if (resolved.delegate === true && mode !== "agent") {
|
|
113
|
+
throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
|
|
114
|
+
}
|
|
115
|
+
const runId = resolved.runId ?? newId("run");
|
|
116
|
+
let delegatedLedgerAttached = false;
|
|
117
|
+
resolved = {
|
|
118
|
+
...resolved,
|
|
119
|
+
runId,
|
|
120
|
+
taskId: resolved.taskId ?? newId("task"),
|
|
121
|
+
onDelegatedLedgerAttached: () => {
|
|
122
|
+
delegatedLedgerAttached = true;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
if (resolved.delegate === true) {
|
|
126
|
+
resolved = {
|
|
127
|
+
...resolved,
|
|
128
|
+
delegationParentRunId: runId,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
140
131
|
const projectProtectedPaths = mode === "agent" ? this.projectConfig(resolved.repoRoot).constraints.protected_paths : [];
|
|
141
132
|
assertWriteIsolation({
|
|
142
133
|
mode,
|
|
@@ -173,41 +164,63 @@ export class Orchestrator {
|
|
|
173
164
|
if (resolved.repoRoot !== NO_PROJECT_ROOT) {
|
|
174
165
|
assertMandatoryContext(resolved.repoRoot, this.projectConfig(resolved.repoRoot).context.mandatory_files);
|
|
175
166
|
}
|
|
176
|
-
// Reviewer panels are validated only inside
|
|
177
|
-
// review (race/convergence
|
|
167
|
+
// Reviewer panels are validated only inside Agent strategies that actually
|
|
168
|
+
// review (race/convergence; Plan Council is the plan critique path) — AFTER run-dir
|
|
178
169
|
// creation, so a doomed explicit panel yields typed failure ARTIFACTS
|
|
179
170
|
// (failure.yaml naming the refusal) instead of a bare pre-run throw.
|
|
180
|
-
//
|
|
171
|
+
// Ask and Plan never spawn code reviewers, so a panel there never spends doctor/
|
|
181
172
|
// model probes and never fails a run that would not use it.
|
|
182
173
|
// Whole-strategy terminal net: once a strategy ANNOUNCES its
|
|
183
174
|
// run, any escaped throw still stamps failure.yaml + summary + run.failed
|
|
184
175
|
// instead of orphaning events.jsonl.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
176
|
+
const releaseRunState = (settledRunId) => {
|
|
177
|
+
this.routingRationaleByRun.delete(settledRunId);
|
|
178
|
+
this.deps.delegationBudgetAuthority?.releaseRun(settledRunId);
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
return await guardAnnouncedRun(resolved.signal, (announce) => {
|
|
182
|
+
switch (mode) {
|
|
183
|
+
case "ask":
|
|
184
|
+
// `--deep-scan` widens the answer into the bounded multi-scout
|
|
185
|
+
// research sweep with synthesis (the old `audit --swarm`/`explore`).
|
|
186
|
+
return resolved.deepScan
|
|
187
|
+
? this.runDeepScan(resolved, announce)
|
|
188
|
+
: this.runAsk(resolved, announce);
|
|
189
|
+
case "agent":
|
|
190
|
+
// Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
|
|
191
|
+
// and `--attempts` select the convergence loop; `--n` selects the race
|
|
192
|
+
// width; `--create` switches the candidate intent to create_from_scratch.
|
|
193
|
+
if (resolved.untilClean)
|
|
194
|
+
return this.runConvergence(resolved, mode, null, announce);
|
|
195
|
+
if (resolved.attempts !== undefined && resolved.attempts !== null) {
|
|
196
|
+
return this.runConvergence(resolved, mode, resolved.attempts, announce);
|
|
197
|
+
}
|
|
198
|
+
return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announce);
|
|
199
|
+
case "plan":
|
|
200
|
+
return this.runPlan(resolved, announce);
|
|
201
|
+
}
|
|
202
|
+
}, async ({ runId }) => {
|
|
203
|
+
const authority = this.deps.delegationBudgetAuthority;
|
|
204
|
+
if (!authority?.hasParent(runId))
|
|
205
|
+
return;
|
|
206
|
+
authority.beginParentClose(runId);
|
|
207
|
+
await authority.waitForChildren(runId);
|
|
208
|
+
},
|
|
209
|
+
// Single per-run terminalization hook: release the routing-rationale map
|
|
210
|
+
// entry on EVERY terminal (incl. a run that died before its telemetry
|
|
211
|
+
// writer ran, which is the leak this closes).
|
|
212
|
+
releaseRunState);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
// A durable startup sink may refuse `run.created` before the strategy can
|
|
216
|
+
// announce its context. Release only a delegated child ledger acquired by
|
|
217
|
+
// THIS invocation; a caller-supplied run-id collision must never release
|
|
218
|
+
// another live parent/child authority or its routing state.
|
|
219
|
+
if (delegatedLedgerAttached) {
|
|
220
|
+
this.deps.delegationBudgetAuthority?.releaseRun(runId);
|
|
205
221
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// entry on EVERY terminal (incl. a run that died before its telemetry
|
|
209
|
-
// writer ran, which is the leak this closes).
|
|
210
|
-
(runId) => this.routingRationaleByRun.delete(runId));
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
211
224
|
}
|
|
212
225
|
async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
|
|
213
226
|
if (this.deps.reviewers)
|
|
@@ -280,11 +293,7 @@ export class Orchestrator {
|
|
|
280
293
|
}
|
|
281
294
|
authPreferenceForHarness(repoRoot, harnessId, runAuthPreference) {
|
|
282
295
|
const cfg = this.config(repoRoot)?.global;
|
|
283
|
-
|
|
284
|
-
return (explicit(runAuthPreference) ??
|
|
285
|
-
explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
|
|
286
|
-
explicit(cfg?.routing?.auth_preference) ??
|
|
287
|
-
"auto");
|
|
296
|
+
return effectiveAuthPreference(runAuthPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference);
|
|
288
297
|
}
|
|
289
298
|
artifactStore(input) {
|
|
290
299
|
if (input.mode === "ask" && input.contextMode === "off" && input.repoRoot === NO_PROJECT_ROOT) {
|
|
@@ -307,20 +316,23 @@ export class Orchestrator {
|
|
|
307
316
|
estimateUsdFloor(repoRoot) {
|
|
308
317
|
return this.config(repoRoot)?.global.budget.estimate_usd_floor ?? 0.05;
|
|
309
318
|
}
|
|
319
|
+
/** Delegate children overlap their still-running parent, so child-side paid
|
|
320
|
+
* units use the same bounded floor as later slots in a parallel wave. */
|
|
321
|
+
reservationEstimateUsd(input, parallel = false) {
|
|
322
|
+
return parallel || Boolean(input.delegatedFromRunId)
|
|
323
|
+
? this.estimateUsdFloor(input.repoRoot)
|
|
324
|
+
: undefined;
|
|
325
|
+
}
|
|
310
326
|
execRootOf(input) {
|
|
311
327
|
return input.executionRoot ?? input.repoRoot;
|
|
312
328
|
}
|
|
313
|
-
sessionSpecFields(input, harnessId, log) {
|
|
329
|
+
async sessionSpecFields(input, harnessId, log, defaultRoute = null) {
|
|
314
330
|
const cfg = this.config(input.repoRoot)?.global;
|
|
315
|
-
const profile = this.preflightProfile(input, harnessId, log);
|
|
316
|
-
const explicit = (v) => (v && v !== "auto" ? v : undefined);
|
|
331
|
+
const profile = await this.preflightProfile(input, harnessId, log, defaultRoute);
|
|
317
332
|
return {
|
|
318
333
|
// "auto" at ANY level falls through (thread turns send the thread default
|
|
319
334
|
// "auto" as a per-run value; it must not shadow a configured preference).
|
|
320
|
-
auth_preference:
|
|
321
|
-
explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
|
|
322
|
-
explicit(cfg?.routing?.auth_preference) ??
|
|
323
|
-
"auto",
|
|
335
|
+
auth_preference: effectiveAuthPreference(input.authPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference),
|
|
324
336
|
resume_session_id: resumeSessionForProfile(input.resumeSessions?.[harnessId], profile),
|
|
325
337
|
credential_profile: profile,
|
|
326
338
|
};
|
|
@@ -395,19 +407,60 @@ export class Orchestrator {
|
|
|
395
407
|
const policy = this.config(repoRoot)?.global.harnesses?.[harnessId]?.profile_policy;
|
|
396
408
|
return policy ?? { limit_action: "fail", rotation_eligible: [], headroom_threshold: 0.9 };
|
|
397
409
|
}
|
|
398
|
-
|
|
410
|
+
/** Fresh profile readiness for one rotation decision epoch. Accounts uses
|
|
411
|
+
* the same probe wrapper + admission predicate when projecting next_up. */
|
|
412
|
+
async readyProfileIdsForRotation(input, harnessId, current, excluded = new Set()) {
|
|
413
|
+
const profiles = staticRotationCandidates({
|
|
414
|
+
registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
|
|
415
|
+
harnessId,
|
|
416
|
+
policy: this.profilePolicy(input.repoRoot, harnessId),
|
|
417
|
+
current,
|
|
418
|
+
excluded,
|
|
419
|
+
});
|
|
420
|
+
const adapter = this.deps.registry.get(harnessId);
|
|
421
|
+
const entries = await Promise.all(profiles.map(async (profile) => ({
|
|
422
|
+
profile,
|
|
423
|
+
status: await probeCredentialProfileStatus(profile, adapter?.probeCredentialProfile?.bind(adapter)),
|
|
424
|
+
})));
|
|
425
|
+
return new Set(entries
|
|
426
|
+
.filter(({ profile, status }) => profileStatusAdmits(profile, status))
|
|
427
|
+
.map(({ profile }) => profile.profile_id));
|
|
428
|
+
}
|
|
429
|
+
async preflightProfile(input, harnessId, log, defaultRoute) {
|
|
399
430
|
const profile = this.resolveCredentialProfile(input, harnessId);
|
|
400
431
|
const policy = this.profilePolicy(input.repoRoot, harnessId);
|
|
401
432
|
const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
|
|
402
433
|
const snapshots = this.deps.quotaSnapshots?.() ?? [];
|
|
403
434
|
const emit = (type, payload) => log?.emit(type, payload);
|
|
435
|
+
const breach = profileHeadroomBreach(snapshots, harnessId, profile?.profile_id ?? null, policy.headroom_threshold);
|
|
436
|
+
const readyProfileIds = policy.limit_action === "rotate" &&
|
|
437
|
+
breach !== null &&
|
|
438
|
+
(profile !== null || defaultRoute === "local_session")
|
|
439
|
+
? await this.readyProfileIdsForRotation(input, harnessId, profile)
|
|
440
|
+
: new Set();
|
|
404
441
|
if (!profile) {
|
|
405
442
|
// Unpinned runs (INV-135 auto-balance): under `rotate`, a fresh
|
|
406
443
|
// default-subject headroom breach starts on the next eligible
|
|
407
444
|
// subscription profile instead; `fail`/`ask` change nothing.
|
|
408
|
-
return preflightDefaultSubject({
|
|
445
|
+
return preflightDefaultSubject({
|
|
446
|
+
harnessId,
|
|
447
|
+
policy,
|
|
448
|
+
registry,
|
|
449
|
+
snapshots,
|
|
450
|
+
readyProfileIds,
|
|
451
|
+
defaultRoute,
|
|
452
|
+
emit,
|
|
453
|
+
});
|
|
409
454
|
}
|
|
410
|
-
return preflightCredentialProfile({
|
|
455
|
+
return preflightCredentialProfile({
|
|
456
|
+
profile,
|
|
457
|
+
harnessId,
|
|
458
|
+
policy,
|
|
459
|
+
registry,
|
|
460
|
+
snapshots,
|
|
461
|
+
readyProfileIds,
|
|
462
|
+
emit,
|
|
463
|
+
});
|
|
411
464
|
}
|
|
412
465
|
/**
|
|
413
466
|
* Resolve candidate adapters: explicit `--harness`, else available real harnesses, then
|
|
@@ -416,89 +469,11 @@ export class Orchestrator {
|
|
|
416
469
|
* expand to n. Fails loudly if nothing can perform the intent.
|
|
417
470
|
*/
|
|
418
471
|
resolveRunInput(input) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const configuredPool = cfg?.global.routing.eligible_harnesses;
|
|
425
|
-
const harnesses = input.harnesses ?? (configuredPool && configuredPool.length > 0 ? configuredPool : undefined);
|
|
426
|
-
// GH #25 precedence: an explicit --primary-harness wins and is validated
|
|
427
|
-
// against the pool; else a single-item explicit pool infers itself as
|
|
428
|
-
// primary (shipped in #34); else the configured default primary applies.
|
|
429
|
-
const explicitPrimary = input.primaryHarness;
|
|
430
|
-
const configPrimary = cfg?.global.routing.primary_harness;
|
|
431
|
-
const primaryHarness = explicitPrimary ??
|
|
432
|
-
(input.harnesses?.length === 1 ? input.harnesses[0] : undefined) ??
|
|
433
|
-
configPrimary ??
|
|
434
|
-
undefined;
|
|
435
|
-
if (primaryHarness &&
|
|
436
|
-
harnesses &&
|
|
437
|
-
harnesses.length > 0 &&
|
|
438
|
-
!harnesses.includes(primaryHarness)) {
|
|
439
|
-
if (explicitPrimary) {
|
|
440
|
-
// An explicit primary must be a member of the eligible pool (authoritative).
|
|
441
|
-
throw new Error(`primary harness '${explicitPrimary}' is not in the eligible harness pool (${harnesses.join(", ")}); ` +
|
|
442
|
-
`pass --primary-harness as one of [${harnesses.join(", ")}], or add '${explicitPrimary}' to --harness`);
|
|
443
|
-
}
|
|
444
|
-
// GH #25 remainder: a MULTI-harness pool whose CONFIGURED default primary
|
|
445
|
-
// is absent, with no --primary-harness pinned, is ambiguous — the engine
|
|
446
|
-
// must not silently reroute. Refuse with a structured, copy-pasteable fix
|
|
447
|
-
// naming the pool, the missing primary, and the exact flag to add.
|
|
448
|
-
throw new HarnessUnavailableError(`ambiguous primary harness: the configured default primary '${primaryHarness}' is not in the selected pool [${harnesses.join(", ")}], ` +
|
|
449
|
-
`and no --primary-harness was given. Pin one explicitly, e.g. \`--primary-harness ${harnesses[0]}\` ` +
|
|
450
|
-
`(or another of [${harnesses.join(", ")}]).`);
|
|
451
|
-
}
|
|
452
|
-
if (input.web && input.externalContextPolicy && input.web !== input.externalContextPolicy) {
|
|
453
|
-
throw new Error(`contradictory web policy: web='${input.web}' vs externalContextPolicy='${input.externalContextPolicy}' (pass one, or equal values)`);
|
|
454
|
-
}
|
|
455
|
-
const web = input.web ?? input.externalContextPolicy ?? "auto";
|
|
456
|
-
// INV-103: scalar `model` expands only to the resolved primary, never the pool;
|
|
457
|
-
// an explicit per-harness map wins. Unknown map keys fail loudly (INV-021).
|
|
458
|
-
const knownHarnessIds = new Set(this.deps.registry.keys());
|
|
459
|
-
for (const key of Object.keys(input.models ?? {})) {
|
|
460
|
-
if (!knownHarnessIds.has(key)) {
|
|
461
|
-
throw new Error(`models map names unknown harness '${key}' (registered: ${[...knownHarnessIds].sort().join(", ")}); ` +
|
|
462
|
-
`run \`claudexor harness list --all\``);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
const models = { ...input.models };
|
|
466
|
-
if (input.model) {
|
|
467
|
-
const scalarTarget = primaryHarness ?? (harnesses && harnesses.length === 1 ? harnesses[0] : undefined);
|
|
468
|
-
if (!scalarTarget) {
|
|
469
|
-
throw new Error(`a scalar model ('${input.model}') is ambiguous without a primary harness: ` +
|
|
470
|
-
`the pool is ${harnesses && harnesses.length > 0 ? `[${harnesses.join(", ")}]` : "auto-resolved"} — ` +
|
|
471
|
-
`set a primary harness, pass exactly one --harness, or use a harness-scoped model map`);
|
|
472
|
-
}
|
|
473
|
-
models[scalarTarget] ??= input.model;
|
|
474
|
-
}
|
|
475
|
-
// QA-035: FREEZE the config-derived per-harness default_model into the
|
|
476
|
-
// resolved model map at initial normalization, exactly like an explicit
|
|
477
|
-
// input. Without this the TaskContract records `routing_models: {}` and an
|
|
478
|
-
// Exact Retry re-resolves the model against CURRENT settings — silently
|
|
479
|
-
// changing the route after a settings edit. A per-turn/scalar value already
|
|
480
|
-
// set wins (??=). Only a known resolved pool can be frozen here; a pure
|
|
481
|
-
// auto pool's lanes are not yet known (documented seam).
|
|
482
|
-
const harnessCfg = cfg?.global.harnesses ?? {};
|
|
483
|
-
for (const hid of harnesses ?? []) {
|
|
484
|
-
const def = harnessCfg[hid]?.default_model;
|
|
485
|
-
if (def)
|
|
486
|
-
models[hid] ??= def;
|
|
487
|
-
}
|
|
488
|
-
return {
|
|
489
|
-
...input,
|
|
490
|
-
harnesses,
|
|
491
|
-
primaryHarness,
|
|
492
|
-
model: undefined,
|
|
493
|
-
models,
|
|
494
|
-
routingGoal: input.routingGoal ??
|
|
495
|
-
this.deps.routingGoal ??
|
|
496
|
-
cfg?.project.budget?.routing_goal ??
|
|
497
|
-
cfg?.global.routing.goal ??
|
|
498
|
-
"auto",
|
|
499
|
-
web,
|
|
500
|
-
externalContextPolicy: web,
|
|
501
|
-
};
|
|
472
|
+
return resolveRunInputDefaults(input, {
|
|
473
|
+
config: this.config(input.repoRoot),
|
|
474
|
+
registryIds: this.deps.registry.keys(),
|
|
475
|
+
routingGoal: this.deps.routingGoal,
|
|
476
|
+
});
|
|
502
477
|
}
|
|
503
478
|
async resolveCandidateAdapters(input, intent, ledger, log, routeContext,
|
|
504
479
|
/** QA-034: when provided, the pool-ordering rationale is recorded under this
|
|
@@ -562,6 +537,27 @@ export class Orchestrator {
|
|
|
562
537
|
}
|
|
563
538
|
}
|
|
564
539
|
}
|
|
540
|
+
const attachments = input.attachments ?? [];
|
|
541
|
+
// Resolve the complete attachment pool once from manifest truth. If any
|
|
542
|
+
// lane still lacks usable discovery truth, preserve the existing per-lane
|
|
543
|
+
// discovery/doctor precedence and resolve that lane inside the route loop.
|
|
544
|
+
const canResolveAttachmentPool = attachments.length > 0 &&
|
|
545
|
+
ids.every((id) => {
|
|
546
|
+
const status = statusById.get(id);
|
|
547
|
+
return status?.manifest != null && status.status !== "unavailable";
|
|
548
|
+
});
|
|
549
|
+
const attachmentPoolAdmission = canResolveAttachmentPool
|
|
550
|
+
? this.requestRequirements.resolveAttachmentPool(explicitPool ? "explicit" : "auto", attachments, ids.map((id) => ({
|
|
551
|
+
harnessId: id,
|
|
552
|
+
declarations: statusById.get(id)?.manifest?.capability_profile.attachment_inputs ?? null,
|
|
553
|
+
available: true,
|
|
554
|
+
})))
|
|
555
|
+
: null;
|
|
556
|
+
if (attachmentPoolAdmission?.outcome === "refused") {
|
|
557
|
+
throw new HarnessUnavailableError(attachmentPoolAdmission.message ??
|
|
558
|
+
"no available harness lane can receive the selected attachments");
|
|
559
|
+
}
|
|
560
|
+
const attachmentRejectionById = new Map(attachmentPoolAdmission?.rejected.map((admission) => [admission.harnessId, admission]) ?? []);
|
|
565
561
|
const policy = input.web ?? input.externalContextPolicy ?? "auto";
|
|
566
562
|
const pool = [];
|
|
567
563
|
const dropped = [];
|
|
@@ -695,9 +691,10 @@ export class Orchestrator {
|
|
|
695
691
|
dropLane(id, "web", why);
|
|
696
692
|
continue;
|
|
697
693
|
}
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
694
|
+
const attachmentAdmission = attachmentRejectionById.get(id) ??
|
|
695
|
+
this.requestRequirements.resolveAttachmentLane(id, attachments, manifest.capability_profile.attachment_inputs);
|
|
696
|
+
if (!attachmentAdmission.admitted) {
|
|
697
|
+
dropLane(id, "attachment", attachmentAdmission.message ?? `${id} rejects attachments`);
|
|
701
698
|
continue;
|
|
702
699
|
}
|
|
703
700
|
const reason = status.reasons.length > 0 ? `: ${status.reasons.join("; ")}` : "";
|
|
@@ -717,6 +714,14 @@ export class Orchestrator {
|
|
|
717
714
|
access: requiredAccess,
|
|
718
715
|
}),
|
|
719
716
|
denyRequirement: this.requestRequirements.resolveDenyPaths(id, (input.denyPaths?.length ?? 0) > 0),
|
|
717
|
+
delegationRequirement: this.requestRequirements.resolveDelegation({
|
|
718
|
+
harnessId: id,
|
|
719
|
+
requested: input.delegate === true,
|
|
720
|
+
runtimeAvailable: input.delegationBelt != null,
|
|
721
|
+
manifestCapable: manifest.capability_profile.mcp_injection,
|
|
722
|
+
requiresFullAccess: manifest.capability_profile.mcp_injection_requires_full_access,
|
|
723
|
+
fullAccess: isFullAccess(requiredAccess),
|
|
724
|
+
}),
|
|
720
725
|
effortLevels: manifest.capabilities.effort_levels,
|
|
721
726
|
knownModels: manifest.capabilities.known_models,
|
|
722
727
|
// A selected profile's credential_kind IS the route (round-18 #2);
|
|
@@ -763,6 +768,7 @@ export class Orchestrator {
|
|
|
763
768
|
}
|
|
764
769
|
emitPrimaryDivergence(log, input.primaryHarness, ordered, pool, dropped);
|
|
765
770
|
const n = input.n ?? ordered.length;
|
|
771
|
+
const selectionOrder = ordered;
|
|
766
772
|
const out = [];
|
|
767
773
|
if (droppedLanes.length > 0 && !allowDuplicateFill) {
|
|
768
774
|
// QA-043: lanes were dropped from an AUTO best-of pool (an explicit pool
|
|
@@ -771,15 +777,15 @@ export class Orchestrator {
|
|
|
771
777
|
// masks the omission. Clamp to distinct survivors and disclose below.
|
|
772
778
|
// (Deep-scan sets allowDuplicateFill: its width is scout coverage, not
|
|
773
779
|
// harness diversity, so a dropped lane must not cut the scout count.)
|
|
774
|
-
for (let i = 0; i < Math.min(n,
|
|
775
|
-
out.push(
|
|
780
|
+
for (let i = 0; i < Math.min(n, selectionOrder.length); i++)
|
|
781
|
+
out.push(selectionOrder[i]);
|
|
776
782
|
}
|
|
777
783
|
else {
|
|
778
784
|
// No lane was dropped: a pool smaller than `n` is an intentional
|
|
779
785
|
// best-of-N on the available harness(es) (e.g. explicit `--harness codex
|
|
780
786
|
// -n 3`), so the historical width fill is preserved.
|
|
781
787
|
for (let i = 0; i < n; i++)
|
|
782
|
-
out.push(
|
|
788
|
+
out.push(selectionOrder[i % selectionOrder.length]);
|
|
783
789
|
}
|
|
784
790
|
// Disclose an auto-pool omission / width clamp once, with the
|
|
785
791
|
// requested-vs-effective route receipt (never silent — QA-043).
|
|
@@ -791,25 +797,24 @@ export class Orchestrator {
|
|
|
791
797
|
droppedLanes,
|
|
792
798
|
});
|
|
793
799
|
this.requestRequirements.requireEffectiveBrowser(input.browser === true, out.map((lane) => lane.browserRequirement));
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
const names = [...new Set(out.map((lane) => lane.adapter.id))].join(", ");
|
|
799
|
-
throw new HarnessUnavailableError(`--delegate requires a harness that can host the Claudexor delegation belt (capability_profile.mcp_injection); the routed harness(es) [${names}] cannot inject MCP servers — choose claude or codex, or drop --delegate`);
|
|
800
|
-
}
|
|
801
|
-
// A belt-injecting lane may still be UNABLE to reach the daemon at its
|
|
802
|
-
// access: codex's workspace-write seatbelt cancels the belt's daemon-crossing
|
|
803
|
-
// MCP call, so codex only hosts the belt at FULL access (same as its browser
|
|
804
|
-
// MCP). If EVERY injecting lane requires full access but runs below it, the
|
|
805
|
-
// belt would be injected only to be silently cancelled by the sandbox — the
|
|
806
|
-
// exact non-delegation this guard prevents. Refuse with the real remedy.
|
|
800
|
+
// Owner decision (2026-07-26): known PRE-START belt unavailability does
|
|
801
|
+
// not discard the requested Agent work. Continue without Delegate and emit
|
|
802
|
+
// a durable typed warning. Once a descriptor is injected, typed startup
|
|
803
|
+
// failure stays terminal in attemptTelemetry (no mid-attempt downgrade).
|
|
807
804
|
if (input.delegate === true) {
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
805
|
+
const unavailable = out
|
|
806
|
+
.map((lane) => lane.delegationRequirement)
|
|
807
|
+
.filter((resolution) => !resolution.effective);
|
|
808
|
+
if (unavailable.length > 0) {
|
|
809
|
+
log?.emit("delegation.belt.degraded", {
|
|
810
|
+
requested: true,
|
|
811
|
+
effective: out.some((lane) => lane.delegationRequirement.effective),
|
|
812
|
+
reason: unavailable[0]?.reason ?? "runtime_unavailable",
|
|
813
|
+
lanes: unavailable.map((resolution) => ({
|
|
814
|
+
harness_id: resolution.harness_id,
|
|
815
|
+
reason: resolution.reason,
|
|
816
|
+
})),
|
|
817
|
+
});
|
|
813
818
|
}
|
|
814
819
|
}
|
|
815
820
|
// outputSchema is MANDATORY (Quiz-6a): a selected lane that cannot
|
|
@@ -846,6 +851,8 @@ export class Orchestrator {
|
|
|
846
851
|
*/
|
|
847
852
|
orderPool(pool, input, intent, statusById, ledger, runId) {
|
|
848
853
|
let ordered = pool;
|
|
854
|
+
let rationale = null;
|
|
855
|
+
let selectionReason = null;
|
|
849
856
|
if (pool.length > 0) {
|
|
850
857
|
const routeLedger = ledger ?? new BudgetLedger();
|
|
851
858
|
const config = this.config(input.repoRoot).global;
|
|
@@ -927,16 +934,35 @@ export class Orchestrator {
|
|
|
927
934
|
const ranked = rankHarnesses(remaining, routeCtx)
|
|
928
935
|
.map((candidate) => byId.get(candidate.harnessId))
|
|
929
936
|
.filter((candidate) => Boolean(candidate));
|
|
930
|
-
|
|
931
|
-
// pinned to routeCtx.now so it cannot disagree with the order just taken.
|
|
932
|
-
if (runId)
|
|
933
|
-
this.routingRationaleByRun.set(runId, explainRanking(remaining, routeCtx));
|
|
937
|
+
rationale = explainRanking(remaining, routeCtx);
|
|
934
938
|
ordered = ranked;
|
|
935
939
|
}
|
|
940
|
+
if (input.delegate === true && input.primaryHarnessExplicit !== true) {
|
|
941
|
+
const delegateFirst = [
|
|
942
|
+
...ordered.filter((lane) => lane.delegationRequirement.effective),
|
|
943
|
+
...ordered.filter((lane) => !lane.delegationRequirement.effective),
|
|
944
|
+
];
|
|
945
|
+
if (delegateFirst.some((lane, index) => lane !== ordered[index])) {
|
|
946
|
+
ordered = delegateFirst;
|
|
947
|
+
selectionReason = "delegate_effective_first";
|
|
948
|
+
}
|
|
949
|
+
}
|
|
936
950
|
if (input.primaryHarness) {
|
|
937
951
|
const primary = ordered.find((r) => r.adapter.id === input.primaryHarness);
|
|
938
|
-
if (primary)
|
|
952
|
+
if (primary && primary !== ordered[0]) {
|
|
939
953
|
ordered = [primary, ...ordered.filter((r) => r !== primary)];
|
|
954
|
+
selectionReason = "explicit_primary";
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
// QA-034: persist the FINAL selected order, including request constraints
|
|
958
|
+
// that intentionally override the underlying cost/quota ranking. This is
|
|
959
|
+
// what keeps route evidence aligned with the lane actually executed.
|
|
960
|
+
if (runId && rationale) {
|
|
961
|
+
this.routingRationaleByRun.set(runId, {
|
|
962
|
+
...rationale,
|
|
963
|
+
order: ordered.map((lane) => lane.adapter.id),
|
|
964
|
+
reason: selectionReason ?? rationale.reason,
|
|
965
|
+
});
|
|
940
966
|
}
|
|
941
967
|
return ordered;
|
|
942
968
|
}
|
|
@@ -1037,114 +1063,9 @@ export class Orchestrator {
|
|
|
1037
1063
|
return this.config(repoRoot).project;
|
|
1038
1064
|
}
|
|
1039
1065
|
buildContract(input, taskId, mode) {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const requestedAccess = input.access ?? (readOnlyMode ? "readonly" : resolvedCfg.trust.access_default);
|
|
1044
|
-
// Effective access is COMPUTED by the engine, never echoed from a client:
|
|
1045
|
-
// read-only modes clamp to readonly regardless of the request.
|
|
1046
|
-
const effectiveAccess = readOnlyMode ? "readonly" : requestedAccess;
|
|
1047
|
-
// TrustConfig is USER-LEVEL only (versioned repo config must never
|
|
1048
|
-
// self-grant sensitive powers): unsandboxed full access requires an
|
|
1049
|
-
// explicit allow in ~/.claudexor trust settings — loud error, no downgrade.
|
|
1050
|
-
// The gate applies to the EFFECTIVE profile: a read-only run clamped to
|
|
1051
|
-
// readonly never runs unsandboxed and needs no trust allow.
|
|
1052
|
-
if (effectiveAccess === "full" && !resolvedCfg.trust.allow_full_access) {
|
|
1053
|
-
// Typed refusal: the `code` rides the daemon job record onto the thread
|
|
1054
|
-
// turn (TurnEnqueueError.code), so surfaces key remedies on the CODE —
|
|
1055
|
-
// never on substring-matching this human message.
|
|
1056
|
-
throw Object.assign(new Error(`access profile 'full' requires allow_full_access: true in the user-level trust file for this repo ` +
|
|
1057
|
-
`(${trustConfigPath(input.repoRoot)}); enable it with \`claudexor trust --allow-full-access\` — refusing to run unsandboxed`),
|
|
1058
|
-
// Refusal semantics are born at the throw (W24): the one-time grant is
|
|
1059
|
-
// a 403, and the daemon persists this status onto the job record.
|
|
1060
|
-
{ code: TRUST_FULL_ACCESS_CODE, status: 403 });
|
|
1061
|
-
}
|
|
1062
|
-
const externalContextPolicy = input.web ?? input.externalContextPolicy ?? "auto";
|
|
1063
|
-
// Deterministic gate commands come from explicit run input, then versioned
|
|
1064
|
-
// project config. Without these, gateSpecs is empty and convergence is
|
|
1065
|
-
// review-only; with them, convergence is test-driven.
|
|
1066
|
-
const resolvedGates = resolveContractGates({
|
|
1067
|
-
repoRoot: input.repoRoot,
|
|
1068
|
-
effectiveAccess,
|
|
1069
|
-
config: cfg,
|
|
1070
|
-
trustGrants: resolvedCfg.trust.test_command_grants,
|
|
1071
|
-
operatorCommands: input.tests ?? [],
|
|
1072
|
-
projectCommands: cfg.tests?.commands ?? [],
|
|
1073
|
-
});
|
|
1074
|
-
const commands = resolvedGates.commands;
|
|
1075
|
-
const protectedPaths = [...new Set(cfg.constraints.protected_paths)];
|
|
1076
|
-
const autoProtectedPaths = resolvedGates.autoProtectedPaths;
|
|
1077
|
-
const protectedPathApprovals = [
|
|
1078
|
-
...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
|
|
1079
|
-
];
|
|
1080
|
-
return TaskContractSchema.parse({
|
|
1081
|
-
schema_version: SCHEMA_VERSION,
|
|
1082
|
-
task_id: taskId,
|
|
1083
|
-
created_at: nowIso(),
|
|
1084
|
-
repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
|
|
1085
|
-
mode: { kind: mode },
|
|
1086
|
-
user_intent: { raw: redactSecrets(input.prompt) },
|
|
1087
|
-
// Redacted for symmetry with user_intent.raw — a no-op on fenced input
|
|
1088
|
-
// (the inline-secret fence already blocked any secret-like value at every
|
|
1089
|
-
// ingress incl. this engine boundary), so task-producing lanes read back
|
|
1090
|
-
// the real instructions via harnessSpecKnobs().
|
|
1091
|
-
instructions: input.instructions === undefined ? undefined : redactSecrets(input.instructions),
|
|
1092
|
-
// Already normalized/strictified at the engine boundary (run() refuses
|
|
1093
|
-
// unsupported shapes before any run dir exists).
|
|
1094
|
-
output_schema: input.outputSchema ?? null,
|
|
1095
|
-
auth_preference: input.authPreference ?? "auto",
|
|
1096
|
-
credential_profile_id: input.credentialProfileId ?? null,
|
|
1097
|
-
max_turns: input.maxTurns ?? null,
|
|
1098
|
-
constraints: {
|
|
1099
|
-
protected_paths: protectedPaths,
|
|
1100
|
-
deny_paths: [...new Set(input.denyPaths ?? [])],
|
|
1101
|
-
auto_protected_paths: autoProtectedPaths,
|
|
1102
|
-
protected_path_approvals: protectedPathApprovals,
|
|
1103
|
-
},
|
|
1104
|
-
tests: { commands },
|
|
1105
|
-
access: {
|
|
1106
|
-
requested_profile: requestedAccess,
|
|
1107
|
-
effective_profile: effectiveAccess,
|
|
1108
|
-
},
|
|
1109
|
-
external_context: {
|
|
1110
|
-
policy: externalContextPolicy,
|
|
1111
|
-
web_required: externalContextPolicy === "cached" || externalContextPolicy === "live",
|
|
1112
|
-
// Per-route upgrades (e.g. claude cached->live) are disclosed in events
|
|
1113
|
-
// and telemetry.yaml; the immutable contract records the requested policy.
|
|
1114
|
-
effective_mode: externalContextPolicy,
|
|
1115
|
-
},
|
|
1116
|
-
// Harness-native tool names are adapter knowledge; the neutral contract
|
|
1117
|
-
// carries only the policy plus user-configured allow/deny lists (wired
|
|
1118
|
-
// from per-harness settings).
|
|
1119
|
-
tool_permission_policy: {
|
|
1120
|
-
web: externalContextPolicy,
|
|
1121
|
-
allow: [],
|
|
1122
|
-
deny: [],
|
|
1123
|
-
},
|
|
1124
|
-
budget: {
|
|
1125
|
-
routing_goal: input.routingGoal ?? this.deps.routingGoal ?? cfg?.budget?.routing_goal ?? "auto",
|
|
1126
|
-
paid_budget: this.resolvePaidBudget(input.paidBudget, resolvedCfg),
|
|
1127
|
-
},
|
|
1128
|
-
// The resolved harness-scoped model map (scalar already expanded to the
|
|
1129
|
-
// primary by resolveRunInput). The contract is what route spec building
|
|
1130
|
-
// reads — there is no run-global model (INV-103).
|
|
1131
|
-
routing_models: input.models ?? {},
|
|
1132
|
-
// QA-035: freeze the RESOLVED reasoning-effort per known lane so Exact
|
|
1133
|
-
// Retry replays it instead of re-resolving current settings. Precedence
|
|
1134
|
-
// (specific beats general): the harness-scoped `efforts` map entry, then a
|
|
1135
|
-
// per-turn scalar `input.effort`, then the harness settings default — the
|
|
1136
|
-
// same map that Exact Retry replays so a NON-PRIMARY lane keeps its own
|
|
1137
|
-
// frozen effort (QA-035 completeness). Only known-pool lanes are frozen
|
|
1138
|
-
// here (a pure auto pool's lanes resolve later — documented seam).
|
|
1139
|
-
routing_efforts: Object.fromEntries([...new Set([...(input.harnesses ?? []), ...Object.keys(input.efforts ?? {})])]
|
|
1140
|
-
.map((hid) => [
|
|
1141
|
-
hid,
|
|
1142
|
-
input.efforts?.[hid] ??
|
|
1143
|
-
input.effort ??
|
|
1144
|
-
resolvedCfg.global.harnesses?.[hid]?.effort ??
|
|
1145
|
-
null,
|
|
1146
|
-
])
|
|
1147
|
-
.filter((entry) => entry[1] !== null)),
|
|
1066
|
+
return buildTaskContract(input, taskId, mode, {
|
|
1067
|
+
paidBudget: this.deps.paidBudget,
|
|
1068
|
+
routingGoal: this.deps.routingGoal,
|
|
1148
1069
|
});
|
|
1149
1070
|
}
|
|
1150
1071
|
/**
|
|
@@ -1168,13 +1089,15 @@ export class Orchestrator {
|
|
|
1168
1089
|
* a WRITING agent intent (the delegator integrates results in its workspace;
|
|
1169
1090
|
* read lanes and reviewers have nothing to delegate). */
|
|
1170
1091
|
delegationBeltFor(input, intent, routed, resolvedBudget) {
|
|
1171
|
-
if (!input?.delegate ||
|
|
1092
|
+
if (!input?.delegate ||
|
|
1093
|
+
!input.delegationBelt ||
|
|
1094
|
+
!input.delegationParentRunId ||
|
|
1095
|
+
!routed.delegationRequirement.effective)
|
|
1172
1096
|
return [];
|
|
1173
1097
|
// A lane that sandbox-cancels the belt below full access (codex) must NOT
|
|
1174
|
-
// receive a belt it cannot use
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
// that cannot host it, so a mixed pool keeps the belt on the lanes that can.
|
|
1098
|
+
// receive a belt it cannot use. Per-lane requirement resolution records the
|
|
1099
|
+
// typed degradation, while a mixed pool keeps the belt on lanes that can
|
|
1100
|
+
// host it.
|
|
1178
1101
|
if (routed.mcpInjectionRequiresFullAccess && !isFullAccess(routed.adapterAccess))
|
|
1179
1102
|
return [];
|
|
1180
1103
|
const writingIntents = ["implement", "create_from_scratch", "repair"];
|
|
@@ -1190,6 +1113,8 @@ export class Orchestrator {
|
|
|
1190
1113
|
...input.delegationBelt,
|
|
1191
1114
|
env: {
|
|
1192
1115
|
...input.delegationBelt.env,
|
|
1116
|
+
[DELEGATION_ENV.parentRunId]: input.delegationParentRunId,
|
|
1117
|
+
[DELEGATION_ENV.repoRoot]: input.repoRoot,
|
|
1193
1118
|
[DELEGATION_ENV.budget]: JSON.stringify(resolvedBudget),
|
|
1194
1119
|
},
|
|
1195
1120
|
},
|
|
@@ -1219,9 +1144,9 @@ export class Orchestrator {
|
|
|
1219
1144
|
/**
|
|
1220
1145
|
* D-16: the WorkReport transport envelope for one route. Called at every
|
|
1221
1146
|
* task-producing spec-build site AFTER harnessSpecKnobs so it OVERRIDES the
|
|
1222
|
-
* plain caller-schema transport with the
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1147
|
+
* plain caller-schema transport with the resolved WorkReport channel on
|
|
1148
|
+
* capable routes. The returned `mode` is retained by the caller and handed
|
|
1149
|
+
* to `unwrapWorkReportEnvelope` when the answer is finalized.
|
|
1225
1150
|
*/
|
|
1226
1151
|
workReportEnvelopeFor(routed, contract, interactive) {
|
|
1227
1152
|
return resolveWorkReportEnvelope({
|
|
@@ -1235,7 +1160,7 @@ export class Orchestrator {
|
|
|
1235
1160
|
/**
|
|
1236
1161
|
* D-16: apply the resolved WorkReport transport to a built spec — set the
|
|
1237
1162
|
* envelope output_schema (constrained/side_tool routes) and APPEND the fenced
|
|
1238
|
-
*
|
|
1163
|
+
* metadata instruction (validated routes, e.g. cursor). Mutates the spec in
|
|
1239
1164
|
* place and returns the mode the answer unwrap consumes. Called at every
|
|
1240
1165
|
* task-producing spec-build site so the transport is never wired one-off.
|
|
1241
1166
|
*/
|
|
@@ -1312,7 +1237,7 @@ export class Orchestrator {
|
|
|
1312
1237
|
* Returns null (no packet, no pointer) for native resume, a fresh thread, or
|
|
1313
1238
|
* a non-thread run. Never throws: continuity failure degrades to no packet.
|
|
1314
1239
|
*/
|
|
1315
|
-
async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, store, paths, repoRoot, log) {
|
|
1240
|
+
async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, sessionFields, store, paths, repoRoot, log) {
|
|
1316
1241
|
const ctx = runInput.threadContinuity;
|
|
1317
1242
|
if (!runInput.threadId || !ctx)
|
|
1318
1243
|
return null;
|
|
@@ -1350,7 +1275,6 @@ export class Orchestrator {
|
|
|
1350
1275
|
// summary. Same credential route + scoped lane home a real read-only
|
|
1351
1276
|
// thread turn uses (INV-034/135). Best-effort in its OWN guard — a summary
|
|
1352
1277
|
// failure keeps the full mechanical packet, never drops it.
|
|
1353
|
-
const sessionFields = this.sessionSpecFields(runInput, harnessId);
|
|
1354
1278
|
req.cachedSummary = await resolveContinuitySummary({
|
|
1355
1279
|
req,
|
|
1356
1280
|
threadId: runInput.threadId,
|
|
@@ -1418,13 +1342,15 @@ export class Orchestrator {
|
|
|
1418
1342
|
// Isolated scoped-home sessions are never retained after disposal.
|
|
1419
1343
|
const inPlaceEnvelope = envelope.worktree_path === envelope.repo_root;
|
|
1420
1344
|
const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
|
|
1421
|
-
const sessionFields = runInput
|
|
1345
|
+
const sessionFields = runInput
|
|
1346
|
+
? await this.sessionSpecFields(runInput, adapter.id, log, routed.authRouteEstimate)
|
|
1347
|
+
: undefined;
|
|
1422
1348
|
// Continuity (INV-137): once the lane (harness + resolved profile) is known,
|
|
1423
1349
|
// build the continuation packet, materialize context/THREAD.md, and point
|
|
1424
1350
|
// the prompt at it — never embed the packet body in the prompt. Replaces the
|
|
1425
1351
|
// old static session.rebound "not_portable" phrase with a real disclosure.
|
|
1426
1352
|
const laneContinuity = runInput
|
|
1427
|
-
? await this.resolveContinuity(runInput, adapter.id, sessionFields?.credential_profile?.profile_id ?? runInput.credentialProfileId ?? null, inPlaceEnvelope && !!sessionFields?.resume_session_id, store, paths, envelope.repo_root, log)
|
|
1353
|
+
? await this.resolveContinuity(runInput, adapter.id, sessionFields?.credential_profile?.profile_id ?? runInput.credentialProfileId ?? null, inPlaceEnvelope && !!sessionFields?.resume_session_id, sessionFields, store, paths, envelope.repo_root, log)
|
|
1428
1354
|
: null;
|
|
1429
1355
|
let spec = HarnessRunSpec.parse({
|
|
1430
1356
|
session_id: newId("ses"),
|
|
@@ -1493,7 +1419,7 @@ export class Orchestrator {
|
|
|
1493
1419
|
const browserServerName = spec.browser ? "browser" : null;
|
|
1494
1420
|
const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
|
|
1495
1421
|
knobs.webPolicy === "cached" ||
|
|
1496
|
-
knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement], knobs.model, beltServerName, browserServerName);
|
|
1422
|
+
knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model, beltServerName, browserServerName);
|
|
1497
1423
|
let activeSessionId = spec.session_id;
|
|
1498
1424
|
const onAbort = () => {
|
|
1499
1425
|
void adapter.cancel?.(activeSessionId)?.catch(() => { });
|
|
@@ -1527,13 +1453,16 @@ export class Orchestrator {
|
|
|
1527
1453
|
try {
|
|
1528
1454
|
const watched = withInactivityWatchdog(adapter.run(runSpec), {
|
|
1529
1455
|
timeoutMs: inactivityMs,
|
|
1456
|
+
countsAsProgress: countsAsAgentProgress,
|
|
1530
1457
|
onTimeout: () => {
|
|
1531
1458
|
attemptAbort.abort();
|
|
1532
1459
|
void adapter.cancel?.(activeSessionId)?.catch(() => { });
|
|
1533
1460
|
},
|
|
1534
|
-
// Waiting on the USER
|
|
1535
|
-
//
|
|
1461
|
+
// Waiting on the USER is legitimate silence. The interaction policy
|
|
1462
|
+
// either enforces a finite deadline or waits until answer/cancel/
|
|
1463
|
+
// terminal/restart when the timeout is disabled.
|
|
1536
1464
|
isSuspended: () => (interaction?.pendingCount?.() ?? 0) > 0,
|
|
1465
|
+
suspensionVersion: () => interaction?.suspensionVersion?.() ?? 0,
|
|
1537
1466
|
});
|
|
1538
1467
|
for await (const ev of watched) {
|
|
1539
1468
|
if (signal?.aborted)
|
|
@@ -1573,10 +1502,9 @@ export class Orchestrator {
|
|
|
1573
1502
|
observeNativeSessionEvent(runInput, adapter.id, safeEv);
|
|
1574
1503
|
observeAuthSwitch(log, adapter.id, attemptId, safeEv);
|
|
1575
1504
|
observeAttemptTelemetry(telemetry, safeEv);
|
|
1576
|
-
// QA-024: the injected delegation belt's MCP server reported
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
// `mcp__<belt>__*` tools and may degrade to its own native subagent.
|
|
1505
|
+
// QA-024: the injected delegation belt's MCP server reported a
|
|
1506
|
+
// terminal startup failure. Disclose it ONCE while live; recoverable
|
|
1507
|
+
// exact tool-result failures are evaluated at attempt finalization.
|
|
1580
1508
|
// The terminal outcome axis (delegationBeltUnavailable) reflects it
|
|
1581
1509
|
// too; this event makes the failure visible while the run is live.
|
|
1582
1510
|
if (telemetry.delegationBelt.requested &&
|
|
@@ -1664,13 +1592,18 @@ export class Orchestrator {
|
|
|
1664
1592
|
// W5.4 failover: a typed-limit hit rebuilds the spec on a NEW vendor
|
|
1665
1593
|
// session under the next profile with provenance (vendor_limit_rejected).
|
|
1666
1594
|
if (harnessErrored && runInput && !signal?.aborted) {
|
|
1595
|
+
const rotationPolicy = this.profilePolicy(contract.repo.root, adapter.id);
|
|
1596
|
+
const readyProfileIds = sawTypedLimit && deliverableEmpty && rotationPolicy.limit_action === "rotate"
|
|
1597
|
+
? await this.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles)
|
|
1598
|
+
: new Set();
|
|
1667
1599
|
const rotated = rotateSpecOnTypedLimit({
|
|
1668
1600
|
spec,
|
|
1669
1601
|
harnessId: adapter.id,
|
|
1670
1602
|
attemptId,
|
|
1671
|
-
policy:
|
|
1603
|
+
policy: rotationPolicy,
|
|
1672
1604
|
registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
|
|
1673
1605
|
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
1606
|
+
readyProfileIds,
|
|
1674
1607
|
triedProfiles,
|
|
1675
1608
|
sawTypedLimit,
|
|
1676
1609
|
deliverableEmpty,
|
|
@@ -1727,20 +1660,20 @@ export class Orchestrator {
|
|
|
1727
1660
|
if (webUnsatisfied(telemetry)) {
|
|
1728
1661
|
errors.push(webEvidenceFailure(telemetry.web));
|
|
1729
1662
|
}
|
|
1730
|
-
|
|
1731
|
-
// D-16: un-nest {work_report, output} so answer.md persists the OUTPUT, not the envelope.
|
|
1663
|
+
// D-16: remove the WorkReport transport so answer.md persists only the deliverable.
|
|
1732
1664
|
const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
|
|
1733
1665
|
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
1734
1666
|
});
|
|
1735
|
-
// X119: persist the VERBATIM redacted bytes; trim ONLY for the emptiness check.
|
|
1736
1667
|
const redacted = redactSecrets(unwrapped.deliverable);
|
|
1737
|
-
const
|
|
1668
|
+
const candidateAnswer = redacted.trim().length > 0 ? redacted : undefined;
|
|
1669
|
+
const { diff, refusal: secretDiffRefusal } = await secretDiff.quarantineCandidateWorkspace(wsm, envelope, inPlaceEnvelope, candidateAnswer);
|
|
1670
|
+
harnessErrored = secretDiff.recordSecretDiffRefusal(secretDiffRefusal, errors, harnessErrored);
|
|
1671
|
+
const answerText = secretDiffRefusal ? undefined : candidateAnswer;
|
|
1738
1672
|
const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText);
|
|
1739
|
-
// Cancelled attempts skip gates
|
|
1740
|
-
// stop NOW; running a 600s-per-gate suite after the abort delays the ack
|
|
1673
|
+
// Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
|
|
1741
1674
|
// and burns compute on a result nobody will adopt. Diff/attempt.yaml
|
|
1742
1675
|
// still land, so partial work stays inspectable.
|
|
1743
|
-
const gateSignalAborted = signal?.aborted === true;
|
|
1676
|
+
const gateSignalAborted = signal?.aborted === true || secretDiffRefusal !== undefined;
|
|
1744
1677
|
if (!gateSignalAborted) {
|
|
1745
1678
|
log?.emit("gate.started", {
|
|
1746
1679
|
attempt_id: attemptId,
|
|
@@ -1770,6 +1703,14 @@ export class Orchestrator {
|
|
|
1770
1703
|
});
|
|
1771
1704
|
}
|
|
1772
1705
|
const webBlocked = webUnsatisfied(telemetry);
|
|
1706
|
+
// A descriptor that was injected and then reported failed is past the
|
|
1707
|
+
// pre-start degradation boundary. Hard-fail this attempt; never continue as
|
|
1708
|
+
// ordinary Agent or let a native vendor subagent masquerade as belt work.
|
|
1709
|
+
const delegationError = delegateFailure.delegationFailureError(telemetry);
|
|
1710
|
+
if (delegationError) {
|
|
1711
|
+
harnessErrored = true;
|
|
1712
|
+
errors.push(delegationError);
|
|
1713
|
+
}
|
|
1773
1714
|
// D-16 unified finalizer: fold the WorkReport / context signals into the
|
|
1774
1715
|
// deliverable + work_state. A broken contract on a constrained route
|
|
1775
1716
|
// elevates harnessErrored (never a prose success).
|
|
@@ -1794,14 +1735,6 @@ export class Orchestrator {
|
|
|
1794
1735
|
workState: finalized.workState,
|
|
1795
1736
|
});
|
|
1796
1737
|
const attemptDir = join(paths.attemptsDir, attemptId);
|
|
1797
|
-
try {
|
|
1798
|
-
assertNoSecretLikeTokens("candidate patch diff", diff);
|
|
1799
|
-
}
|
|
1800
|
-
catch (err) {
|
|
1801
|
-
// The stream already settled real spend; a post-stream assertion throw
|
|
1802
|
-
// must carry it so the slot catch settles the TRUE cost, not 0.
|
|
1803
|
-
throw Object.assign(err instanceof Error ? err : new Error(String(err)), { costUsd: cost });
|
|
1804
|
-
}
|
|
1805
1738
|
recordCleanAttemptMetrics(globalConfigDir(), adapter.id, {
|
|
1806
1739
|
costUsd: cost,
|
|
1807
1740
|
streamMs: attemptStreamEndedMs - attemptStartedMs,
|
|
@@ -1809,11 +1742,13 @@ export class Orchestrator {
|
|
|
1809
1742
|
aborted: signal?.aborted === true,
|
|
1810
1743
|
authMode: telemetry.authMode,
|
|
1811
1744
|
});
|
|
1812
|
-
const producedFiles = writeCandidateAttemptArtifacts({
|
|
1745
|
+
const producedFiles = AC.withAttemptFailureCost(() => writeCandidateAttemptArtifacts({
|
|
1813
1746
|
store,
|
|
1814
1747
|
attemptDir,
|
|
1815
1748
|
worktreePath: envelope.worktree_path,
|
|
1816
1749
|
diff,
|
|
1750
|
+
persistPatch: secretDiffRefusal === undefined,
|
|
1751
|
+
persistProducedMedia: secretDiffRefusal === undefined,
|
|
1817
1752
|
answerText,
|
|
1818
1753
|
record: {
|
|
1819
1754
|
attempt_id: attemptId,
|
|
@@ -1825,9 +1760,14 @@ export class Orchestrator {
|
|
|
1825
1760
|
errors: errors.slice(0, 5),
|
|
1826
1761
|
...telemetrySummary(telemetry),
|
|
1827
1762
|
outcome: telemetry.outcome,
|
|
1763
|
+
...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
|
|
1828
1764
|
gates: gates.map((g) => ({ id: g.id, status: g.status })),
|
|
1829
1765
|
branch: envelope.branch_name,
|
|
1830
1766
|
},
|
|
1767
|
+
}), {
|
|
1768
|
+
totalUsd: cost,
|
|
1769
|
+
estimated: costEstimated,
|
|
1770
|
+
settlement: attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost),
|
|
1831
1771
|
});
|
|
1832
1772
|
return {
|
|
1833
1773
|
attemptId,
|
|
@@ -1844,6 +1784,7 @@ export class Orchestrator {
|
|
|
1844
1784
|
costEstimated,
|
|
1845
1785
|
errors: errors.slice(0, 8),
|
|
1846
1786
|
telemetry,
|
|
1787
|
+
...(secretDiffRefusal ? { secretDiffRefusal } : {}),
|
|
1847
1788
|
outcomeClass: finalized.outcomeClass,
|
|
1848
1789
|
};
|
|
1849
1790
|
}
|
|
@@ -1854,66 +1795,17 @@ export class Orchestrator {
|
|
|
1854
1795
|
// Thin delegate — the channel mechanics live in interaction.ts.
|
|
1855
1796
|
return interactionChannelFor(input, log, runId, taskId, attemptId, harnessId, supportsInteractive, DEFAULT_INTERACTION_TIMEOUT_MS);
|
|
1856
1797
|
}
|
|
1857
|
-
/**
|
|
1858
|
-
* Guarantee a git boundary for write-mode runs. Non-git project folders are
|
|
1859
|
-
* initialized in place (`git init`, deterministic baseline commit) without
|
|
1860
|
-
* creating or editing `.gitignore`, and the action is announced via a
|
|
1861
|
-
* `project.git.initialized` event. Returns the failure message when the
|
|
1862
|
-
* boundary cannot be established (the terminal failure events are already
|
|
1863
|
-
* emitted); null on success.
|
|
1864
|
-
*/
|
|
1865
|
-
async ensureWriteModeGitBoundary(repoRoot, log, store, paths, runId, mode) {
|
|
1866
|
-
if (repoRoot === NO_PROJECT_ROOT)
|
|
1867
|
-
return null;
|
|
1868
|
-
try {
|
|
1869
|
-
const result = await ensureGitRepository(repoRoot);
|
|
1870
|
-
if (result.initialized || result.baselineCommitted) {
|
|
1871
|
-
log.emit("project.git.initialized", {
|
|
1872
|
-
repo_root: repoRoot,
|
|
1873
|
-
initialized: result.initialized,
|
|
1874
|
-
baseline_committed: result.baselineCommitted,
|
|
1875
|
-
gitignore_seeded: result.gitignoreSeeded,
|
|
1876
|
-
head_sha: result.headSha,
|
|
1877
|
-
});
|
|
1878
|
-
}
|
|
1879
|
-
return null;
|
|
1880
|
-
}
|
|
1881
|
-
catch (err) {
|
|
1882
|
-
const message = safeErrorMessage(err);
|
|
1883
|
-
writeFailure(store, paths, {
|
|
1884
|
-
phase: "workspace",
|
|
1885
|
-
category: "project",
|
|
1886
|
-
safeMessage: message,
|
|
1887
|
-
runDir: paths.root,
|
|
1888
|
-
nextActions: [
|
|
1889
|
-
"Check the project folder permissions",
|
|
1890
|
-
"Initialize git manually (git init)",
|
|
1891
|
-
"Retry the run",
|
|
1892
|
-
],
|
|
1893
|
-
});
|
|
1894
|
-
store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: failed\n- Phase: workspace\n\n${message}\n`);
|
|
1895
|
-
log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
|
|
1896
|
-
log.emit("run.failed", {
|
|
1897
|
-
lifecycle: "failed",
|
|
1898
|
-
facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
|
|
1899
|
-
reason: "harness_failed",
|
|
1900
|
-
phase: "workspace",
|
|
1901
|
-
error: message,
|
|
1902
|
-
failure_ref: "final/failure.yaml",
|
|
1903
|
-
});
|
|
1904
|
-
return message;
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
1798
|
/**
|
|
1908
1799
|
* D-14 layer 3 (AGENTS.md unification, INV-113): the ONE new live-tree write.
|
|
1909
1800
|
* When the PROJECT root has `AGENTS.md` and no `CLAUDE.md`, drop a thin
|
|
1910
1801
|
* `CLAUDE.md` (`@AGENTS.md` import + Claudexor ownership marker) so a Claude
|
|
1911
1802
|
* Code route reads the same instruction file codex/cursor read natively.
|
|
1912
1803
|
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1915
|
-
* untouched
|
|
1916
|
-
*
|
|
1804
|
+
* The project-root bridge has its own narrower fence: read-only modes never
|
|
1805
|
+
* reach this run-prep stage and `--in-place` stateful targets are left
|
|
1806
|
+
* untouched. Git admission is independently owned by `runStartRequiresGit`.
|
|
1807
|
+
* The write targets the PROJECT root (`repoRoot`), never a worktree envelope.
|
|
1808
|
+
* The workspace helper adds exclusive-create + no-follow +
|
|
1917
1809
|
* idempotency, so a hand-written or symlinked `CLAUDE.md` is never overwritten
|
|
1918
1810
|
* and a concurrent/second prep is a no-op. Announced via a typed
|
|
1919
1811
|
* `project.claude_bridge.created` event on an actual create only — the git-init
|
|
@@ -1938,37 +1830,6 @@ export class Orchestrator {
|
|
|
1938
1830
|
});
|
|
1939
1831
|
}
|
|
1940
1832
|
}
|
|
1941
|
-
/**
|
|
1942
|
-
* Freeze-on-implement delivery (D17/D27): verify the frozen plan's hash and
|
|
1943
|
-
* materialize it as context/PLAN.md in the run artifact tree — OUTSIDE every
|
|
1944
|
-
* worktree, so it can never dirty a diff — then point the prompt at the
|
|
1945
|
-
* absolute path. A mismatched or unreadable plan fails LOUDLY before any
|
|
1946
|
-
* harness spawns (the tamper fence; retry replays planRef verbatim, so a
|
|
1947
|
-
* retried implement can never silently run without its plan).
|
|
1948
|
-
*/
|
|
1949
|
-
withPlanBrief(input, store, paths, log) {
|
|
1950
|
-
if (!input.planRef)
|
|
1951
|
-
return input;
|
|
1952
|
-
const text = readTextSafe(input.planRef.path);
|
|
1953
|
-
if (!text || !text.trim()) {
|
|
1954
|
-
throw new Error(`implement plan: the frozen plan at ${input.planRef.path} is missing or unreadable`);
|
|
1955
|
-
}
|
|
1956
|
-
const digest = sha256(text).replace(/^sha256:/, "");
|
|
1957
|
-
if (digest !== input.planRef.sha256) {
|
|
1958
|
-
throw new Error(`implement plan: plan hash mismatch (expected ${input.planRef.sha256}, got ${digest}) — the plan was modified after freeze; re-run Implement from the plan turn`);
|
|
1959
|
-
}
|
|
1960
|
-
const briefPath = join(paths.contextDir, "PLAN.md");
|
|
1961
|
-
store.writeText(briefPath, text);
|
|
1962
|
-
log.emit("plan.brief.materialized", {
|
|
1963
|
-
plan_run_id: input.planRef.runId,
|
|
1964
|
-
sha256: input.planRef.sha256,
|
|
1965
|
-
path: "context/PLAN.md",
|
|
1966
|
-
});
|
|
1967
|
-
return {
|
|
1968
|
-
...input,
|
|
1969
|
-
prompt: `${input.prompt}\n\nThe approved plan is at: ${briefPath} — read it before starting and re-read it as needed.`,
|
|
1970
|
-
};
|
|
1971
|
-
}
|
|
1972
1833
|
async runRace(input, mode, announce) {
|
|
1973
1834
|
const taskId = input.taskId ?? newId("task");
|
|
1974
1835
|
const runId = input.runId ?? newId("run");
|
|
@@ -1976,29 +1837,28 @@ export class Orchestrator {
|
|
|
1976
1837
|
// announced: a refused run must fail the request loudly, not 200 a runId
|
|
1977
1838
|
// and leave an orphaned run dir without a terminal event.
|
|
1978
1839
|
const contract = this.buildContract(input, taskId, mode);
|
|
1979
|
-
const
|
|
1980
|
-
const
|
|
1981
|
-
const
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
const execRoot = this.execRootOf(input);
|
|
1988
|
-
const wsm = new WorkspaceManager(execRoot);
|
|
1989
|
-
safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
|
|
1990
|
-
log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
|
|
1991
|
-
const ledger = this.rootLedger(input, contract, log);
|
|
1992
|
-
announce?.({
|
|
1993
|
-
log,
|
|
1994
|
-
store,
|
|
1995
|
-
paths,
|
|
1840
|
+
const planBrief = verifiedPlanBrief(input);
|
|
1841
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
1842
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
1843
|
+
input,
|
|
1844
|
+
contract,
|
|
1845
|
+
quotaSnapshots,
|
|
1846
|
+
store: this.artifactStore(input),
|
|
1847
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
1996
1848
|
runId,
|
|
1997
1849
|
taskId,
|
|
1998
1850
|
mode,
|
|
1999
1851
|
phase: "race",
|
|
2000
|
-
|
|
2001
|
-
});
|
|
1852
|
+
prompt: input.prompt,
|
|
1853
|
+
}, announce);
|
|
1854
|
+
input = withPlanBrief(input, store, paths, log, planBrief);
|
|
1855
|
+
// The execution root is the tree the harness mutates: the project itself
|
|
1856
|
+
// for in-place threads/ordinary runs, or the thread's persistent worktree
|
|
1857
|
+
// for an isolated thread. Config/artifacts/contract stay anchored to
|
|
1858
|
+
// repoRoot. Both the WorkspaceManager and the git boundary resolve against
|
|
1859
|
+
// this SINGLE root.
|
|
1860
|
+
const execRoot = this.execRootOf(input);
|
|
1861
|
+
const wsm = new WorkspaceManager(execRoot);
|
|
2002
1862
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
2003
1863
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
2004
1864
|
// Write modes need a git boundary for worktree isolation and honest diffs.
|
|
@@ -2007,17 +1867,17 @@ export class Orchestrator {
|
|
|
2007
1867
|
// silent mutation (user-locked decision, comparator: Codex requires git).
|
|
2008
1868
|
// For an isolated thread the execution root is already a git worktree, so
|
|
2009
1869
|
// this is a no-op there; for in-place it ensures the live project is git.
|
|
2010
|
-
const gitPreconditionError = await
|
|
1870
|
+
const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
|
|
2011
1871
|
if (gitPreconditionError) {
|
|
2012
1872
|
return {
|
|
2013
1873
|
runId,
|
|
2014
1874
|
taskId,
|
|
2015
1875
|
mode,
|
|
2016
1876
|
lifecycle: "failed",
|
|
2017
|
-
facts: makeOutcomeFacts("failed", { reason:
|
|
1877
|
+
facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
|
|
2018
1878
|
winner: null,
|
|
2019
1879
|
runDir: paths.root,
|
|
2020
|
-
summary: gitPreconditionError,
|
|
1880
|
+
summary: gitPreconditionError.message,
|
|
2021
1881
|
candidates: [],
|
|
2022
1882
|
};
|
|
2023
1883
|
}
|
|
@@ -2087,6 +1947,7 @@ export class Orchestrator {
|
|
|
2087
1947
|
candidates: [],
|
|
2088
1948
|
};
|
|
2089
1949
|
}
|
|
1950
|
+
activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapters, log);
|
|
2090
1951
|
const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode);
|
|
2091
1952
|
if ("failed" in reviewersOutcome)
|
|
2092
1953
|
return reviewersOutcome.failed;
|
|
@@ -2114,7 +1975,7 @@ export class Orchestrator {
|
|
|
2114
1975
|
attemptId,
|
|
2115
1976
|
intent: this.candidateIntent(input),
|
|
2116
1977
|
harnessId: routed.adapter.id,
|
|
2117
|
-
cost: attemptCostEvidence(routed.adapter.id, attemptId, i > 0
|
|
1978
|
+
cost: attemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id)),
|
|
2118
1979
|
});
|
|
2119
1980
|
log.emit("budget.lease.created", {
|
|
2120
1981
|
granted: lease.granted,
|
|
@@ -2280,6 +2141,13 @@ export class Orchestrator {
|
|
|
2280
2141
|
continuation_count: candidateContinuationCount,
|
|
2281
2142
|
packet_turns: packet.continuity.disclosure.packetTurns,
|
|
2282
2143
|
});
|
|
2144
|
+
log.emit("harness.started", {
|
|
2145
|
+
harness_id: adapter.id,
|
|
2146
|
+
attempt_id: contAttemptId,
|
|
2147
|
+
external_context_policy: knobs.webPolicy,
|
|
2148
|
+
...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
|
|
2149
|
+
continuation_of: run.attemptId,
|
|
2150
|
+
});
|
|
2283
2151
|
const contLeaseId = contLease.lease?.lease_id ?? "";
|
|
2284
2152
|
try {
|
|
2285
2153
|
const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
|
|
@@ -2311,7 +2179,7 @@ export class Orchestrator {
|
|
|
2311
2179
|
effectiveRun = contRun;
|
|
2312
2180
|
}
|
|
2313
2181
|
catch (err) {
|
|
2314
|
-
ledger.settle(contLeaseId,
|
|
2182
|
+
ledger.settle(contLeaseId, AC.attemptFailureCost(err, "continuation-error", 0).settlement);
|
|
2315
2183
|
log.emit("harness.completed", {
|
|
2316
2184
|
harness_id: adapter.id,
|
|
2317
2185
|
attempt_id: contAttemptId,
|
|
@@ -2335,18 +2203,9 @@ export class Orchestrator {
|
|
|
2335
2203
|
envelope = undefined;
|
|
2336
2204
|
}
|
|
2337
2205
|
catch (err) {
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
// post-stream throw (e.g. the secret-token assertion) carries its
|
|
2341
|
-
// streamed spend on the error — settle the TRUE cost, never launder
|
|
2342
|
-
// real spend down to 0.
|
|
2343
|
-
const carriedCost = typeof err?.costUsd === "number"
|
|
2344
|
-
? err.costUsd
|
|
2345
|
-
: 0;
|
|
2346
|
-
ledger.settle(slot.leaseId, unknownCostSettlement("post-stream-error", carriedCost));
|
|
2206
|
+
const failureCost = AC.attemptFailureCost(err, "post-stream-error", 0);
|
|
2207
|
+
ledger.settle(slot.leaseId, failureCost.settlement);
|
|
2347
2208
|
const message = safeErrorMessage(err);
|
|
2348
|
-
// envelope is still undefined when wsm.create() itself threw — that is
|
|
2349
|
-
// a workspace-phase infrastructure failure, not a harness error.
|
|
2350
2209
|
const infraPhase = envelope === undefined ? "workspace" : "harness";
|
|
2351
2210
|
log.emit("harness.completed", {
|
|
2352
2211
|
harness_id: adapter.id,
|
|
@@ -2355,26 +2214,22 @@ export class Orchestrator {
|
|
|
2355
2214
|
error: message,
|
|
2356
2215
|
phase: infraPhase,
|
|
2357
2216
|
});
|
|
2358
|
-
|
|
2359
|
-
store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), {
|
|
2360
|
-
attempt_id: slot.attemptId,
|
|
2361
|
-
harness_id: adapter.id,
|
|
2362
|
-
cost_usd: carriedCost,
|
|
2363
|
-
errored: true,
|
|
2364
|
-
phase: infraPhase,
|
|
2365
|
-
errors: [message],
|
|
2366
|
-
});
|
|
2217
|
+
store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message));
|
|
2367
2218
|
runsBySlot[slotIdx] = {
|
|
2368
2219
|
attemptId: slot.attemptId,
|
|
2369
2220
|
harnessId: adapter.id,
|
|
2370
2221
|
label: slot.label,
|
|
2371
2222
|
diff: "",
|
|
2372
2223
|
gates: [],
|
|
2373
|
-
cost:
|
|
2224
|
+
cost: failureCost.totalUsd,
|
|
2374
2225
|
errored: true,
|
|
2375
|
-
costEstimated:
|
|
2226
|
+
costEstimated: failureCost.estimated,
|
|
2376
2227
|
errors: [message],
|
|
2377
|
-
telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [
|
|
2228
|
+
telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [
|
|
2229
|
+
slot.routed.browserRequirement,
|
|
2230
|
+
slot.routed.denyRequirement,
|
|
2231
|
+
slot.routed.delegationRequirement,
|
|
2232
|
+
], knobs.model),
|
|
2378
2233
|
infraPhase,
|
|
2379
2234
|
};
|
|
2380
2235
|
}
|
|
@@ -2397,7 +2252,7 @@ export class Orchestrator {
|
|
|
2397
2252
|
// arbitration (as the race-adoption path does) would fold those user edits
|
|
2398
2253
|
// into the revert target and let a later revert clobber them.
|
|
2399
2254
|
let earlyPostTurnSha = null;
|
|
2400
|
-
if (input.inPlace
|
|
2255
|
+
if (input.inPlace && requestedSingleCandidate && runs.every((run) => !run.secretDiffRefusal)) {
|
|
2401
2256
|
try {
|
|
2402
2257
|
earlyPostTurnSha = await snapshotTree(execRoot);
|
|
2403
2258
|
}
|
|
@@ -2409,6 +2264,20 @@ export class Orchestrator {
|
|
|
2409
2264
|
await disposeReviewEnvelopes();
|
|
2410
2265
|
return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
|
|
2411
2266
|
}
|
|
2267
|
+
const failedDelegation = delegateFailure.dominantRaceCandidateFailure(runs);
|
|
2268
|
+
if (failedDelegation) {
|
|
2269
|
+
const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
|
|
2270
|
+
await disposeReviewEnvelopes();
|
|
2271
|
+
await delegateFailure.persistFailedInPlaceWorkProduct({
|
|
2272
|
+
...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
|
|
2273
|
+
live: input.inPlace === true && failedDelegation.reviewCwd === execRoot,
|
|
2274
|
+
run: failedDelegation,
|
|
2275
|
+
postTurnSha: earlyPostTurnSha,
|
|
2276
|
+
kind: input.create === true ? "new_repo" : "patch",
|
|
2277
|
+
});
|
|
2278
|
+
this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
|
|
2279
|
+
return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
|
|
2280
|
+
}
|
|
2412
2281
|
if (runs.length === 0) {
|
|
2413
2282
|
const budgetReason = ledger.terminal();
|
|
2414
2283
|
// QA-050: when the zero-candidate cause is a budget refusal, the shared
|
|
@@ -2428,12 +2297,7 @@ export class Orchestrator {
|
|
|
2428
2297
|
why_winner: why,
|
|
2429
2298
|
evidence_facts: ["no candidates were produced"],
|
|
2430
2299
|
apply_recommendation: "continue",
|
|
2431
|
-
budget_summary:
|
|
2432
|
-
spend_usd: ledger.spend(),
|
|
2433
|
-
estimated: false,
|
|
2434
|
-
cash_usd: ledger.spend(),
|
|
2435
|
-
valuation_usd: ledger.valuation(),
|
|
2436
|
-
},
|
|
2300
|
+
budget_summary: decisionBudgetSummary(ledger),
|
|
2437
2301
|
});
|
|
2438
2302
|
store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: ${agentBudgetMapping ? "budget" : "executor"}\n\n${why}\n`);
|
|
2439
2303
|
if (agentBudgetMapping) {
|
|
@@ -2480,7 +2344,7 @@ export class Orchestrator {
|
|
|
2480
2344
|
if (workingRuns.length === 0) {
|
|
2481
2345
|
await disposeReviewEnvelopes();
|
|
2482
2346
|
const first = runs[0];
|
|
2483
|
-
const phase = first.infraPhase ?? "harness";
|
|
2347
|
+
const phase = first.secretDiffRefusal ? "artifact_security" : (first.infraPhase ?? "harness");
|
|
2484
2348
|
const { facts, why: rootCause } = partitionCandidates(runs);
|
|
2485
2349
|
store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
|
|
2486
2350
|
winner: null,
|
|
@@ -2488,12 +2352,7 @@ export class Orchestrator {
|
|
|
2488
2352
|
why_winner: rootCause,
|
|
2489
2353
|
evidence_facts: runs.map((r) => `${r.attemptId} produced no work: ${r.errors[0] ?? "unknown"}`),
|
|
2490
2354
|
apply_recommendation: "continue",
|
|
2491
|
-
budget_summary:
|
|
2492
|
-
spend_usd: ledger.spend(),
|
|
2493
|
-
estimated: false,
|
|
2494
|
-
cash_usd: ledger.spend(),
|
|
2495
|
-
valuation_usd: ledger.valuation(),
|
|
2496
|
-
},
|
|
2355
|
+
budget_summary: decisionBudgetSummary(ledger),
|
|
2497
2356
|
});
|
|
2498
2357
|
this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
|
|
2499
2358
|
store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}\n- Phase: ${phase}\n\n${rootCause}\n`);
|
|
@@ -2513,9 +2372,11 @@ export class Orchestrator {
|
|
|
2513
2372
|
rawDetailRef: `attempts/${first.attemptId}/attempt.yaml`,
|
|
2514
2373
|
eventRefs: existingEventRefs,
|
|
2515
2374
|
runDir: paths.root,
|
|
2516
|
-
nextActions:
|
|
2517
|
-
?
|
|
2518
|
-
:
|
|
2375
|
+
nextActions: first.secretDiffRefusal
|
|
2376
|
+
? secretDiff.secretDiffNextActions(first.secretDiffRefusal)
|
|
2377
|
+
: phase === "workspace"
|
|
2378
|
+
? ["Check the project folder", "Open diagnostics", "Retry the run"]
|
|
2379
|
+
: harnessFailureNextActions(harnessCategory),
|
|
2519
2380
|
});
|
|
2520
2381
|
log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
|
|
2521
2382
|
log.emit("run.failed", {
|
|
@@ -2574,7 +2435,7 @@ export class Orchestrator {
|
|
|
2574
2435
|
// candidates (a trivial greeting in agent mode no longer burns two reviewers on
|
|
2575
2436
|
// "(empty diff)"). Candidates still flow through arbitration/gates so the
|
|
2576
2437
|
// no_op/answer outcome and gate failures are unchanged.
|
|
2577
|
-
evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
|
|
2438
|
+
evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
|
|
2578
2439
|
}
|
|
2579
2440
|
catch (err) {
|
|
2580
2441
|
// Review preflight/evidence failures end TERMINALLY with artifacts —
|
|
@@ -2599,7 +2460,7 @@ export class Orchestrator {
|
|
|
2599
2460
|
attemptId: "synth",
|
|
2600
2461
|
intent: "synthesize",
|
|
2601
2462
|
harnessId: synthRouted.adapter.id,
|
|
2602
|
-
cost: attemptCostEvidence(synthRouted.adapter.id, "synth",
|
|
2463
|
+
cost: attemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id)),
|
|
2603
2464
|
});
|
|
2604
2465
|
if (lease.granted) {
|
|
2605
2466
|
let envelope;
|
|
@@ -2635,7 +2496,7 @@ export class Orchestrator {
|
|
|
2635
2496
|
runs.push(run);
|
|
2636
2497
|
try {
|
|
2637
2498
|
if (isWorkingCandidate(run)) {
|
|
2638
|
-
const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
|
|
2499
|
+
const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
|
|
2639
2500
|
evidences.push(...synthEvidence);
|
|
2640
2501
|
workingRuns.push(run);
|
|
2641
2502
|
}
|
|
@@ -2648,7 +2509,7 @@ export class Orchestrator {
|
|
|
2648
2509
|
}
|
|
2649
2510
|
}
|
|
2650
2511
|
catch (err) {
|
|
2651
|
-
ledger.settle(lease.lease?.lease_id ?? "",
|
|
2512
|
+
ledger.settle(lease.lease?.lease_id ?? "", AC.attemptFailureCost(err, "synthesis-error").settlement);
|
|
2652
2513
|
log.emit("harness.completed", {
|
|
2653
2514
|
attempt_id: "synth",
|
|
2654
2515
|
status: "failed",
|
|
@@ -2666,14 +2527,7 @@ export class Orchestrator {
|
|
|
2666
2527
|
}
|
|
2667
2528
|
let result;
|
|
2668
2529
|
try {
|
|
2669
|
-
result = arbitrate(evidences,
|
|
2670
|
-
spendUsd: ledger.spend(),
|
|
2671
|
-
estimatedSpend: runs.some((r) => r.costEstimated),
|
|
2672
|
-
// QA-010b: carry the settled cash + subscription-valuation totals
|
|
2673
|
-
// (reviewer panel included) onto the decision record.
|
|
2674
|
-
cashUsd: ledger.spend(),
|
|
2675
|
-
valuationUsd: ledger.valuation(),
|
|
2676
|
-
});
|
|
2530
|
+
result = arbitrate(evidences, arbitrationBudgetOptions(ledger));
|
|
2677
2531
|
}
|
|
2678
2532
|
catch (err) {
|
|
2679
2533
|
// Arbitration throws end terminally with artifacts, never as an orphan.
|
|
@@ -2691,8 +2545,12 @@ export class Orchestrator {
|
|
|
2691
2545
|
// Winner can only be a candidate that actually produced work; corpses are
|
|
2692
2546
|
// excluded from arbitration upstream and from the fallback here.
|
|
2693
2547
|
const winnerRun = workingRuns.find((r) => r.attemptId === result.decision.winner) ?? workingRuns[0];
|
|
2694
|
-
|
|
2695
|
-
|
|
2548
|
+
const winnerEvidence = winnerRun
|
|
2549
|
+
? evidences.find((e) => e.attemptId === winnerRun.attemptId)
|
|
2550
|
+
: undefined;
|
|
2551
|
+
// D9 winner-only NEEDS_HUMAN gate, fail-closed on a winner with no review
|
|
2552
|
+
// evidence record (see winnerNeedsHuman).
|
|
2553
|
+
const needsHuman = winnerNeedsHuman(winnerRun?.attemptId ?? null, evidences);
|
|
2696
2554
|
// Run-level review_verified is the WINNER's verification: an
|
|
2697
2555
|
// empty-diff loser's unverified route must not drag the shipped result's
|
|
2698
2556
|
// flag false. No winner -> fall back to the all-candidates view.
|
|
@@ -2755,12 +2613,11 @@ export class Orchestrator {
|
|
|
2755
2613
|
})) {
|
|
2756
2614
|
log.emit("output.ready", { kind: "artifact", path });
|
|
2757
2615
|
}
|
|
2758
|
-
assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
|
|
2616
|
+
secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
|
|
2759
2617
|
const patchSha256 = sha256(winnerRun.diff);
|
|
2760
2618
|
store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
|
|
2761
2619
|
const wstats = diffStats(winnerRun.diff);
|
|
2762
2620
|
const hasDiff = winnerRun.diff.trim().length > 0;
|
|
2763
|
-
const winnerEvidence = evidences.find((e) => e.attemptId === winnerRun.attemptId);
|
|
2764
2621
|
const blockers = winnerEvidence
|
|
2765
2622
|
? winnerEvidence.findings.filter((f) => isBlocking(f)).length
|
|
2766
2623
|
: 0;
|
|
@@ -3073,7 +2930,7 @@ export class Orchestrator {
|
|
|
3073
2930
|
env: reviewHome.env,
|
|
3074
2931
|
}).finally(() => reviewHome.dispose());
|
|
3075
2932
|
}
|
|
3076
|
-
async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal) {
|
|
2933
|
+
async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd) {
|
|
3077
2934
|
const evidences = [];
|
|
3078
2935
|
for (const run of runs) {
|
|
3079
2936
|
const candidateCwd = run.reviewCwd ?? cwd;
|
|
@@ -3086,13 +2943,13 @@ export class Orchestrator {
|
|
|
3086
2943
|
// (so a failing test gate or no_op outcome is unchanged), just unreviewed.
|
|
3087
2944
|
const hasDiff = run.diff.trim().length > 0;
|
|
3088
2945
|
// Reviewer panels spend real money: reserve before, settle the observed cost.
|
|
3089
|
-
const reviewLease = hasDiff
|
|
2946
|
+
const reviewLease = hasDiff && reviewers.length > 0
|
|
3090
2947
|
? ledger?.reserve({
|
|
3091
2948
|
taskId: taskId ?? "task",
|
|
3092
2949
|
attemptId: run.attemptId,
|
|
3093
2950
|
intent: "review",
|
|
3094
2951
|
harnessId: "review-panel",
|
|
3095
|
-
cost: attemptCostEvidence("review-panel", run.attemptId),
|
|
2952
|
+
cost: attemptCostEvidence("review-panel", run.attemptId, reservationEstimateUsd),
|
|
3096
2953
|
})
|
|
3097
2954
|
: undefined;
|
|
3098
2955
|
const result = hasDiff && reviewers.length > 0 && (reviewLease?.granted ?? true)
|
|
@@ -3119,11 +2976,16 @@ export class Orchestrator {
|
|
|
3119
2976
|
reviewSpendUsd: 0,
|
|
3120
2977
|
reviewSpendEstimated: false,
|
|
3121
2978
|
reviewCashUsd: 0,
|
|
2979
|
+
reviewCashKnowledge: "unknown",
|
|
3122
2980
|
reviewValuationUsd: 0,
|
|
2981
|
+
reviewValuationKnowledge: "unknown",
|
|
3123
2982
|
reviewUnknownUsd: 0,
|
|
3124
2983
|
};
|
|
3125
2984
|
if (reviewLease?.granted) {
|
|
3126
|
-
ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd,
|
|
2985
|
+
ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd, {
|
|
2986
|
+
cash: result.reviewCashKnowledge,
|
|
2987
|
+
valuation: result.reviewValuationKnowledge,
|
|
2988
|
+
}, [`attempt:${run.attemptId}`, "review:panel"], result.reviewUnknownUsd));
|
|
3127
2989
|
if ((result.reviewSpendUsd ?? 0) > 0) {
|
|
3128
2990
|
log.emit("budget.observation", {
|
|
3129
2991
|
harness_id: "review-panel",
|
|
@@ -3231,10 +3093,21 @@ export class Orchestrator {
|
|
|
3231
3093
|
const runId = input.runId ?? newId("run");
|
|
3232
3094
|
// Contract validation BEFORE the run is announced (see runRace).
|
|
3233
3095
|
const contract = this.buildContract(input, taskId, mode);
|
|
3234
|
-
const
|
|
3235
|
-
const
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3096
|
+
const planBrief = verifiedPlanBrief(input);
|
|
3097
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
3098
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
3099
|
+
input,
|
|
3100
|
+
contract,
|
|
3101
|
+
quotaSnapshots,
|
|
3102
|
+
store: this.artifactStore(input),
|
|
3103
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
3104
|
+
runId,
|
|
3105
|
+
taskId,
|
|
3106
|
+
mode,
|
|
3107
|
+
phase: "convergence",
|
|
3108
|
+
prompt: input.prompt,
|
|
3109
|
+
}, announce);
|
|
3110
|
+
input = withPlanBrief(input, store, paths, log, planBrief);
|
|
3238
3111
|
// The execution root is the tree the harness mutates (thread worktree for an
|
|
3239
3112
|
// isolated thread, else the project). The WorkspaceManager AND the git
|
|
3240
3113
|
// boundary must resolve against the SAME root — the race path does so via the
|
|
@@ -3243,24 +3116,11 @@ export class Orchestrator {
|
|
|
3243
3116
|
const execRoot = this.execRootOf(input);
|
|
3244
3117
|
const wsm = new WorkspaceManager(execRoot);
|
|
3245
3118
|
const readiness = new ReadinessLedger();
|
|
3246
|
-
const ledger = this.rootLedger(input, contract, log);
|
|
3247
3119
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
3248
|
-
safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
|
|
3249
|
-
log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
|
|
3250
|
-
announce?.({
|
|
3251
|
-
log,
|
|
3252
|
-
store,
|
|
3253
|
-
paths,
|
|
3254
|
-
runId,
|
|
3255
|
-
taskId,
|
|
3256
|
-
mode,
|
|
3257
|
-
phase: "convergence",
|
|
3258
|
-
spend: () => ledger.spend(),
|
|
3259
|
-
});
|
|
3260
3120
|
// Live (in-place) isolation deliberately tolerates non-git stateful
|
|
3261
3121
|
// environments; only envelope isolation needs the git boundary.
|
|
3262
3122
|
if (!input.inPlace) {
|
|
3263
|
-
const gitPreconditionError = await
|
|
3123
|
+
const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
|
|
3264
3124
|
if (gitPreconditionError) {
|
|
3265
3125
|
return {
|
|
3266
3126
|
spendUsd: ledger.spend(),
|
|
@@ -3268,10 +3128,10 @@ export class Orchestrator {
|
|
|
3268
3128
|
taskId,
|
|
3269
3129
|
mode,
|
|
3270
3130
|
lifecycle: "failed",
|
|
3271
|
-
facts: makeOutcomeFacts("failed", { reason:
|
|
3131
|
+
facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
|
|
3272
3132
|
winner: null,
|
|
3273
3133
|
runDir: paths.root,
|
|
3274
|
-
summary: gitPreconditionError,
|
|
3134
|
+
summary: gitPreconditionError.message,
|
|
3275
3135
|
candidates: [],
|
|
3276
3136
|
};
|
|
3277
3137
|
}
|
|
@@ -3334,6 +3194,7 @@ export class Orchestrator {
|
|
|
3334
3194
|
candidates: [],
|
|
3335
3195
|
};
|
|
3336
3196
|
}
|
|
3197
|
+
activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapterPool, log);
|
|
3337
3198
|
// Fail fast on a provably unwinnable predicate instead of burning paid
|
|
3338
3199
|
// rounds: the default convergence predicate requires a clean cross-family
|
|
3339
3200
|
// review, which needs >=2 healthy reviewer provider families.
|
|
@@ -3405,7 +3266,6 @@ export class Orchestrator {
|
|
|
3405
3266
|
const allCooledDown = () => adapterPool.every((a) => ledger.cooldownActive(a.adapter.id));
|
|
3406
3267
|
const attemptTelemetries = [];
|
|
3407
3268
|
let lastDiffStable = true;
|
|
3408
|
-
let reviewSpendEstimated = false;
|
|
3409
3269
|
try {
|
|
3410
3270
|
// The contract's ENGINE-COMPUTED effective profile drives the envelope and
|
|
3411
3271
|
// every attempt spec (parity with runRace); telemetry must never claim an
|
|
@@ -3460,7 +3320,7 @@ export class Orchestrator {
|
|
|
3460
3320
|
attemptId,
|
|
3461
3321
|
intent: "repair",
|
|
3462
3322
|
harnessId: adapter.id,
|
|
3463
|
-
cost: attemptCostEvidence(adapter.id, attemptId,
|
|
3323
|
+
cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id)),
|
|
3464
3324
|
});
|
|
3465
3325
|
if (!lease.granted) {
|
|
3466
3326
|
exhausted = true;
|
|
@@ -3495,30 +3355,48 @@ export class Orchestrator {
|
|
|
3495
3355
|
});
|
|
3496
3356
|
}
|
|
3497
3357
|
catch (err) {
|
|
3498
|
-
//
|
|
3499
|
-
//
|
|
3500
|
-
|
|
3358
|
+
// Setup failures remain unknown; post-stream persistence failures
|
|
3359
|
+
// carry their route-specific settlement from runCandidateInEnvelope.
|
|
3360
|
+
const failureCost = AC.attemptFailureCost(err, "attempt-error");
|
|
3361
|
+
const message = safeErrorMessage(err);
|
|
3362
|
+
ledger.settle(lease.lease?.lease_id ?? "", failureCost.settlement);
|
|
3501
3363
|
log.emit("harness.completed", {
|
|
3502
3364
|
harness_id: adapter.id,
|
|
3503
3365
|
attempt_id: attemptId,
|
|
3504
3366
|
status: "failed",
|
|
3505
|
-
error:
|
|
3367
|
+
error: message,
|
|
3506
3368
|
});
|
|
3369
|
+
store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message));
|
|
3507
3370
|
run = {
|
|
3508
3371
|
attemptId,
|
|
3509
3372
|
harnessId: adapter.id,
|
|
3510
3373
|
label: `Attempt ${attempt}`,
|
|
3511
3374
|
diff: "",
|
|
3512
3375
|
gates: [],
|
|
3513
|
-
cost:
|
|
3376
|
+
cost: failureCost.totalUsd,
|
|
3514
3377
|
errored: true,
|
|
3515
|
-
costEstimated:
|
|
3516
|
-
errors: [
|
|
3517
|
-
telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement], knobs.model),
|
|
3378
|
+
costEstimated: failureCost.estimated,
|
|
3379
|
+
errors: [message],
|
|
3380
|
+
telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model),
|
|
3518
3381
|
};
|
|
3519
3382
|
}
|
|
3520
3383
|
lastRun = run;
|
|
3521
3384
|
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry: run.telemetry });
|
|
3385
|
+
// Cancellation/deadline keeps priority over a belt failure finalized concurrently.
|
|
3386
|
+
if (input.signal?.aborted)
|
|
3387
|
+
break;
|
|
3388
|
+
if (delegateFailure.candidateFailureKind(run)) {
|
|
3389
|
+
const failure = delegateFailure.candidateFailureTerminal(run, "convergence");
|
|
3390
|
+
await delegateFailure.persistFailedInPlaceWorkProduct({
|
|
3391
|
+
...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
|
|
3392
|
+
live: input.inPlace === true,
|
|
3393
|
+
run,
|
|
3394
|
+
kind: input.create === true ? "new_repo" : "patch",
|
|
3395
|
+
attempts: attempt,
|
|
3396
|
+
});
|
|
3397
|
+
this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null);
|
|
3398
|
+
return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
|
|
3399
|
+
}
|
|
3522
3400
|
// D-16 r8: interrupted (errored===false) would CONVERGE a partial diff
|
|
3523
3401
|
// as clean — break BEFORE review; a harness error still gate-retries.
|
|
3524
3402
|
if (run.outcomeClass === "interrupted") {
|
|
@@ -3554,7 +3432,7 @@ export class Orchestrator {
|
|
|
3554
3432
|
attemptId,
|
|
3555
3433
|
intent: "review",
|
|
3556
3434
|
harnessId: "review-panel",
|
|
3557
|
-
cost: attemptCostEvidence("review-panel", attemptId),
|
|
3435
|
+
cost: attemptCostEvidence("review-panel", attemptId, this.reservationEstimateUsd(input)),
|
|
3558
3436
|
})
|
|
3559
3437
|
: null;
|
|
3560
3438
|
const reviewResult = reviewers.length > 0 && (reviewLease?.granted ?? false)
|
|
@@ -3580,11 +3458,16 @@ export class Orchestrator {
|
|
|
3580
3458
|
reviewSpendUsd: 0,
|
|
3581
3459
|
reviewSpendEstimated: false,
|
|
3582
3460
|
reviewCashUsd: 0,
|
|
3461
|
+
reviewCashKnowledge: "unknown",
|
|
3583
3462
|
reviewValuationUsd: 0,
|
|
3463
|
+
reviewValuationKnowledge: "unknown",
|
|
3584
3464
|
reviewUnknownUsd: 0,
|
|
3585
3465
|
};
|
|
3586
3466
|
if (reviewLease?.granted) {
|
|
3587
|
-
ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd,
|
|
3467
|
+
ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd, {
|
|
3468
|
+
cash: reviewResult.reviewCashKnowledge,
|
|
3469
|
+
valuation: reviewResult.reviewValuationKnowledge,
|
|
3470
|
+
}, [`attempt:${attemptId}`, "review:panel"], reviewResult.reviewUnknownUsd));
|
|
3588
3471
|
if ((reviewResult.reviewSpendUsd ?? 0) > 0) {
|
|
3589
3472
|
log.emit("budget.observation", {
|
|
3590
3473
|
harness_id: "review-panel",
|
|
@@ -3596,8 +3479,6 @@ export class Orchestrator {
|
|
|
3596
3479
|
unknown_usd: reviewResult.reviewUnknownUsd,
|
|
3597
3480
|
estimated: reviewResult.reviewSpendEstimated === true,
|
|
3598
3481
|
});
|
|
3599
|
-
if (reviewResult.reviewSpendEstimated === true)
|
|
3600
|
-
reviewSpendEstimated = true;
|
|
3601
3482
|
}
|
|
3602
3483
|
}
|
|
3603
3484
|
else if (reviewLease && !reviewLease.granted) {
|
|
@@ -3779,13 +3660,7 @@ export class Orchestrator {
|
|
|
3779
3660
|
if (lastRun && !interrupted) {
|
|
3780
3661
|
const arb = arbitrate([
|
|
3781
3662
|
toCandidateEvidence(lastRun, contract, lastFindings, lastFinalReviewClean, actualReviewVerified),
|
|
3782
|
-
],
|
|
3783
|
-
spendUsd: ledger.spend(),
|
|
3784
|
-
estimatedSpend: lastRun.costEstimated || reviewSpendEstimated,
|
|
3785
|
-
// QA-010b: settled cash + valuation (reviewer panel included).
|
|
3786
|
-
cashUsd: ledger.spend(),
|
|
3787
|
-
valuationUsd: ledger.valuation(),
|
|
3788
|
-
});
|
|
3663
|
+
], arbitrationBudgetOptions(ledger));
|
|
3789
3664
|
decision = arb.decision;
|
|
3790
3665
|
store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
|
|
3791
3666
|
// A converged run adopts the arbitration axes (checks/review); an
|
|
@@ -3847,7 +3722,7 @@ export class Orchestrator {
|
|
|
3847
3722
|
// work_product (its partial patch.diff stays diagnostic via attempts/);
|
|
3848
3723
|
// in-place keeps the product so the honest Revert offer survives.
|
|
3849
3724
|
if (lastRun && (!interrupted || input.inPlace === true)) {
|
|
3850
|
-
assertNoSecretLikeTokens("final patch diff", lastRun.diff);
|
|
3725
|
+
secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
|
|
3851
3726
|
const patchSha256 = sha256(lastRun.diff);
|
|
3852
3727
|
store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
|
|
3853
3728
|
// Honest apply-state (parity with runRace single-candidate in-place): a
|
|
@@ -4017,285 +3892,57 @@ export class Orchestrator {
|
|
|
4017
3892
|
reviewVerified: actualReviewVerified,
|
|
4018
3893
|
};
|
|
4019
3894
|
}
|
|
4020
|
-
/**
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
3895
|
+
/** Bind private route/session preparation to the planner-attempt owner. */
|
|
3896
|
+
plannerAttemptDeps() {
|
|
3897
|
+
return {
|
|
3898
|
+
billingKnowledge: (input, harnessId) => this.routeBillingKnowledge(input, harnessId),
|
|
3899
|
+
inactivityTimeoutMs: (repoRoot) => harnessInactivityTimeoutMs(this.config(repoRoot)),
|
|
3900
|
+
quotaEventSink: this.deps.quotaEventSink,
|
|
3901
|
+
prepare: async (args) => {
|
|
3902
|
+
const { input, contract, taskId, runId, log, store, paths, routed, attemptId } = args;
|
|
3903
|
+
const adapter = routed.adapter;
|
|
3904
|
+
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
3905
|
+
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
3906
|
+
const sessionFields = await this.sessionSpecFields(input, adapter.id, log, routed.authRouteEstimate);
|
|
3907
|
+
const laneContinuity = args.laneRun
|
|
3908
|
+
? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, sessionFields.resume_session_id !== null, sessionFields, store, paths, this.execRootOf(input), log)
|
|
3909
|
+
: null;
|
|
3910
|
+
const spec = HarnessRunSpec.parse({
|
|
3911
|
+
session_id: newId("ses"),
|
|
3912
|
+
intent: args.intent,
|
|
3913
|
+
prompt: laneContinuity?.pointerLine
|
|
3914
|
+
? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
|
|
3915
|
+
: args.promptBody,
|
|
3916
|
+
cwd: this.execRootOf(input),
|
|
3917
|
+
access: "readonly",
|
|
3918
|
+
attachments: input.attachments ?? [],
|
|
3919
|
+
...sessionFields,
|
|
3920
|
+
...this.harnessSpecKnobs(contract, knobs, args.intent),
|
|
3921
|
+
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
3922
|
+
env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
|
|
3923
|
+
});
|
|
3924
|
+
const plannerAbort = new AbortController();
|
|
3925
|
+
spec.extra["abortSignal"] = input.signal
|
|
3926
|
+
? AbortSignal.any([input.signal, plannerAbort.signal])
|
|
3927
|
+
: plannerAbort.signal;
|
|
3928
|
+
const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
|
|
3929
|
+
if (planInteraction)
|
|
3930
|
+
spec.extra["interactionChannel"] = planInteraction;
|
|
3931
|
+
const planWorkMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
|
|
3932
|
+
return {
|
|
3933
|
+
knobs,
|
|
3934
|
+
effectiveWeb,
|
|
3935
|
+
spec,
|
|
3936
|
+
plannerAbort,
|
|
3937
|
+
planInteraction,
|
|
3938
|
+
planWorkMode,
|
|
3939
|
+
};
|
|
3940
|
+
},
|
|
3941
|
+
};
|
|
4053
3942
|
}
|
|
4054
3943
|
/** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
|
|
4055
3944
|
async runPlannerAttempt(args) {
|
|
4056
|
-
|
|
4057
|
-
const adapter = routed.adapter;
|
|
4058
|
-
const lease = ledger.reserve({
|
|
4059
|
-
taskId,
|
|
4060
|
-
attemptId,
|
|
4061
|
-
intent: args.intent,
|
|
4062
|
-
harnessId: adapter.id,
|
|
4063
|
-
cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
|
|
4064
|
-
});
|
|
4065
|
-
if (!lease.granted) {
|
|
4066
|
-
log.emit("budget.lease.created", {
|
|
4067
|
-
granted: false,
|
|
4068
|
-
reason: lease.reason,
|
|
4069
|
-
denied: lease.denied,
|
|
4070
|
-
attempt_id: attemptId,
|
|
4071
|
-
harness_id: adapter.id,
|
|
4072
|
-
});
|
|
4073
|
-
return {
|
|
4074
|
-
attemptId,
|
|
4075
|
-
harnessId: adapter.id,
|
|
4076
|
-
status: "failed",
|
|
4077
|
-
outcomeClass: "clean", // never spawned: refused pre-flight by the budget gate
|
|
4078
|
-
error: lease.reason ?? "budget lease denied",
|
|
4079
|
-
text: null,
|
|
4080
|
-
telemetry: null,
|
|
4081
|
-
budgetDenied: true,
|
|
4082
|
-
budgetDenial: {
|
|
4083
|
-
code: lease.denied ?? "hard_cap",
|
|
4084
|
-
reason: lease.reason ?? "budget lease denied",
|
|
4085
|
-
harnessId: adapter.id,
|
|
4086
|
-
attemptId,
|
|
4087
|
-
},
|
|
4088
|
-
};
|
|
4089
|
-
}
|
|
4090
|
-
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
4091
|
-
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
4092
|
-
const planSessionFields = this.sessionSpecFields(input, adapter.id, log);
|
|
4093
|
-
// Continuity (INV-137): a thread PLAN turn is a chat turn — hydrate a
|
|
4094
|
-
// lane switch/gap with a packet and disclose it.
|
|
4095
|
-
const laneContinuity = args.laneRun
|
|
4096
|
-
? await this.resolveContinuity(input, adapter.id, planSessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, planSessionFields.resume_session_id !== null, store, paths, this.execRootOf(input), log)
|
|
4097
|
-
: null;
|
|
4098
|
-
const spec = HarnessRunSpec.parse({
|
|
4099
|
-
session_id: newId("ses"),
|
|
4100
|
-
intent: args.intent,
|
|
4101
|
-
prompt: laneContinuity?.pointerLine
|
|
4102
|
-
? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
|
|
4103
|
-
: args.promptBody,
|
|
4104
|
-
cwd: this.execRootOf(input),
|
|
4105
|
-
access: "readonly",
|
|
4106
|
-
// Planners must SEE any image/file the user attached (e.g. "plan a fix for
|
|
4107
|
-
// what's in this screenshot"), not just agent/race runs.
|
|
4108
|
-
attachments: input.attachments ?? [],
|
|
4109
|
-
...planSessionFields,
|
|
4110
|
-
...this.harnessSpecKnobs(contract, knobs, args.intent),
|
|
4111
|
-
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
4112
|
-
// A thread plan turn spawns in its DURABLE per-lane home so its native
|
|
4113
|
-
// session is reachable for resume next turn (INV-034); a non-thread
|
|
4114
|
-
// plan keeps the disposable route-context home.
|
|
4115
|
-
env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
|
|
4116
|
-
});
|
|
4117
|
-
const plannerAbort = new AbortController();
|
|
4118
|
-
spec.extra["abortSignal"] = input.signal
|
|
4119
|
-
? AbortSignal.any([input.signal, plannerAbort.signal])
|
|
4120
|
-
: plannerAbort.signal;
|
|
4121
|
-
const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
|
|
4122
|
-
if (planInteraction)
|
|
4123
|
-
spec.extra["interactionChannel"] = planInteraction;
|
|
4124
|
-
// D-16: compile the WorkReport envelope for the plan lane (require plan text
|
|
4125
|
-
// below folds the deliverable; the veto rides work_state).
|
|
4126
|
-
const planWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction));
|
|
4127
|
-
const planWorkMode = this.applyWorkEnvelope(spec, planWorkEnvelope);
|
|
4128
|
-
const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
|
|
4129
|
-
const answer = new AnswerAssembly();
|
|
4130
|
-
const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
|
|
4131
|
-
knobs.webPolicy === "cached" ||
|
|
4132
|
-
knobs.webPolicy === "live", effectiveWeb, [],
|
|
4133
|
-
// Requested-model capture: a plan lane silently downgraded to another
|
|
4134
|
-
// model surfaces the mismatch in its route receipt, just like agent.
|
|
4135
|
-
knobs.model);
|
|
4136
|
-
const onAbort = () => {
|
|
4137
|
-
void adapter.cancel?.(spec.session_id)?.catch(() => { });
|
|
4138
|
-
};
|
|
4139
|
-
if (input.signal) {
|
|
4140
|
-
if (input.signal.aborted)
|
|
4141
|
-
onAbort();
|
|
4142
|
-
else
|
|
4143
|
-
input.signal.addEventListener("abort", onAbort, { once: true });
|
|
4144
|
-
}
|
|
4145
|
-
let cost = 0;
|
|
4146
|
-
let costEstimated = false;
|
|
4147
|
-
let harnessError = null;
|
|
4148
|
-
const budgetSignalState = { quotaPressureDisclosed: false };
|
|
4149
|
-
try {
|
|
4150
|
-
log.emit("harness.started", {
|
|
4151
|
-
harness_id: adapter.id,
|
|
4152
|
-
attempt_id: attemptId,
|
|
4153
|
-
external_context_policy: knobs.webPolicy,
|
|
4154
|
-
...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
|
|
4155
|
-
});
|
|
4156
|
-
if (!input.signal?.aborted) {
|
|
4157
|
-
const watchedPlan = withInactivityWatchdog(adapter.run(spec), {
|
|
4158
|
-
timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
|
|
4159
|
-
onTimeout: () => {
|
|
4160
|
-
plannerAbort.abort();
|
|
4161
|
-
void adapter.cancel?.(spec.session_id)?.catch(() => { });
|
|
4162
|
-
},
|
|
4163
|
-
isSuspended: () => (planInteraction?.pendingCount?.() ?? 0) > 0,
|
|
4164
|
-
});
|
|
4165
|
-
for await (const ev of watchedPlan) {
|
|
4166
|
-
if (input.signal?.aborted)
|
|
4167
|
-
break;
|
|
4168
|
-
const safeEv = redactHarnessEvent(ev);
|
|
4169
|
-
safeInvoke(input.onHarnessEvent, safeEv);
|
|
4170
|
-
// A thread PLAN turn IS a chat turn now (INV-034): its native
|
|
4171
|
-
// session lives in the DURABLE per-lane home, so record it for the
|
|
4172
|
-
// next lane turn's resume. Council members are distinct lanes.
|
|
4173
|
-
if (args.laneRun)
|
|
4174
|
-
observeNativeSessionEvent(input, adapter.id, safeEv);
|
|
4175
|
-
observeAuthSwitch(log, adapter.id, attemptId, safeEv);
|
|
4176
|
-
log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
|
|
4177
|
-
appendLine(attemptEventsPath, JSON.stringify(safeEv));
|
|
4178
|
-
observeAttemptTelemetry(telemetry, safeEv);
|
|
4179
|
-
if (safeEv.plan_progress) {
|
|
4180
|
-
log.emit("plan.progress", {
|
|
4181
|
-
attempt_id: attemptId,
|
|
4182
|
-
harness_id: adapter.id,
|
|
4183
|
-
items: safeEv.plan_progress.items,
|
|
4184
|
-
});
|
|
4185
|
-
}
|
|
4186
|
-
// read-only routes burn quota too — same single owner as the agent loop.
|
|
4187
|
-
observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
|
|
4188
|
-
this.deps.quotaEventSink?.(adapter.id, safeEv);
|
|
4189
|
-
if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
|
|
4190
|
-
cost += safeEv.usage.cost_usd;
|
|
4191
|
-
if (safeEv.usage.estimated)
|
|
4192
|
-
costEstimated = true;
|
|
4193
|
-
log.emit("budget.observation", {
|
|
4194
|
-
harness_id: adapter.id,
|
|
4195
|
-
attempt_id: attemptId,
|
|
4196
|
-
kind: "spend",
|
|
4197
|
-
usd: safeEv.usage.cost_usd,
|
|
4198
|
-
estimated: safeEv.usage.estimated === true,
|
|
4199
|
-
});
|
|
4200
|
-
}
|
|
4201
|
-
// A TYPED final message wins verbatim over joined narration.
|
|
4202
|
-
answer.observe(safeEv);
|
|
4203
|
-
if (safeEv.type === "error")
|
|
4204
|
-
harnessError = safeEv.error ? redactSecrets(safeEv.error) : "harness emitted an error";
|
|
4205
|
-
}
|
|
4206
|
-
}
|
|
4207
|
-
}
|
|
4208
|
-
catch (err) {
|
|
4209
|
-
harnessError = safeErrorMessage(err);
|
|
4210
|
-
}
|
|
4211
|
-
finally {
|
|
4212
|
-
input.signal?.removeEventListener("abort", onAbort);
|
|
4213
|
-
ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
|
|
4214
|
-
}
|
|
4215
|
-
// D-16: unwrap and require PLAN TEXT — a plan with no text is not delivered.
|
|
4216
|
-
// The unwrap runs BEFORE the error axes: the deliverable it yields is what
|
|
4217
|
-
// decides whether an unrecovered tool error is fatal (explorer parity).
|
|
4218
|
-
const planUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", planWorkMode, {
|
|
4219
|
-
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
4220
|
-
});
|
|
4221
|
-
const planText = redactSecrets(planUnwrapped.deliverable).trim();
|
|
4222
|
-
const unrecovered = unrecoveredToolErrors(telemetry);
|
|
4223
|
-
const webBlocked = webUnsatisfied(telemetry);
|
|
4224
|
-
if (!harnessError && webBlocked) {
|
|
4225
|
-
harnessError = webEvidenceFailure(telemetry.web);
|
|
4226
|
-
}
|
|
4227
|
-
// INV-043/INV-044, explorer parity: a DELIVERED plan keeps an unrecovered
|
|
4228
|
-
// non-web tool error as warning evidence instead of discarding the plan (see
|
|
4229
|
-
// the helper). Web keeps its hard gate above; the finalizer outranks both.
|
|
4230
|
-
harnessError ??= unrecoveredToolErrorFailure(unrecovered, planText.length > 0);
|
|
4231
|
-
const planFinalized = finalizeAttempt({
|
|
4232
|
-
deliverableEvidence: planText.length > 0,
|
|
4233
|
-
harnessErrored: harnessError !== null && !webBlocked,
|
|
4234
|
-
workReport: planUnwrapped.workReport,
|
|
4235
|
-
workReportSource: planUnwrapped.source,
|
|
4236
|
-
workReportViolation: planUnwrapped.contractViolation,
|
|
4237
|
-
contextTerminalExhausted: telemetry.contextExhausted,
|
|
4238
|
-
});
|
|
4239
|
-
// A broken WorkReport contract is a hard failure only when the finalizer
|
|
4240
|
-
// ranked it so (a terminal context exhaustion outranks it).
|
|
4241
|
-
if (!harnessError && planFinalized.outcomeClass === "contract_failure") {
|
|
4242
|
-
harnessError = `work_report contract: ${planUnwrapped.contractViolation}`;
|
|
4243
|
-
}
|
|
4244
|
-
// D-16 r9: an interrupted (context-exhausted) planner is NEVER a clean
|
|
4245
|
-
// plan — partial text must not become final/plan.md as success. A VETO
|
|
4246
|
-
// (needs_input/incomplete work_state) is DIFFERENT by the sealed D-16
|
|
4247
|
-
// contract (X35, INV-116 canaries): the plan still delivers, lifecycle
|
|
4248
|
-
// succeeded, and the work_state veto rides the OUTCOME (non-zero exit) —
|
|
4249
|
-
// it must not be laundered into a harness failure either direction.
|
|
4250
|
-
if (!harnessError && planFinalized.outcomeClass === "interrupted") {
|
|
4251
|
-
harnessError = "context capacity exhausted before the plan completed";
|
|
4252
|
-
}
|
|
4253
|
-
const attemptError = harnessError ??
|
|
4254
|
-
(planFinalized.deliverablePresent ? null : "planner produced no plan text") ??
|
|
4255
|
-
(input.signal?.aborted ? "planner cancelled" : null);
|
|
4256
|
-
setAttemptOutcome(telemetry, {
|
|
4257
|
-
deliverablePresent: planFinalized.deliverablePresent,
|
|
4258
|
-
gatesPassed: null,
|
|
4259
|
-
harnessErrored: (harnessError !== null && !webBlocked) || planFinalized.harnessErrored,
|
|
4260
|
-
webRequiredUnsatisfied: webBlocked,
|
|
4261
|
-
workState: planFinalized.workState,
|
|
4262
|
-
});
|
|
4263
|
-
if (attemptError) {
|
|
4264
|
-
log.emit("harness.completed", {
|
|
4265
|
-
harness_id: adapter.id,
|
|
4266
|
-
attempt_id: attemptId,
|
|
4267
|
-
status: webBlocked ? "blocked" : "failed",
|
|
4268
|
-
error: attemptError,
|
|
4269
|
-
...telemetrySummary(telemetry),
|
|
4270
|
-
});
|
|
4271
|
-
return {
|
|
4272
|
-
attemptId,
|
|
4273
|
-
harnessId: adapter.id,
|
|
4274
|
-
status: webBlocked ? "blocked" : "failed",
|
|
4275
|
-
outcomeClass: planFinalized.outcomeClass,
|
|
4276
|
-
error: attemptError,
|
|
4277
|
-
text: null,
|
|
4278
|
-
telemetry,
|
|
4279
|
-
budgetDenied: false,
|
|
4280
|
-
};
|
|
4281
|
-
}
|
|
4282
|
-
const text = planText || "(no output)";
|
|
4283
|
-
log.emit("harness.completed", {
|
|
4284
|
-
harness_id: adapter.id,
|
|
4285
|
-
attempt_id: attemptId,
|
|
4286
|
-
status: "success",
|
|
4287
|
-
...telemetrySummary(telemetry),
|
|
4288
|
-
});
|
|
4289
|
-
return {
|
|
4290
|
-
attemptId,
|
|
4291
|
-
harnessId: adapter.id,
|
|
4292
|
-
status: "success",
|
|
4293
|
-
outcomeClass: planFinalized.outcomeClass,
|
|
4294
|
-
error: null,
|
|
4295
|
-
text,
|
|
4296
|
-
telemetry,
|
|
4297
|
-
budgetDenied: false,
|
|
4298
|
-
};
|
|
3945
|
+
return executePlannerAttempt(this.plannerAttemptDeps(), args);
|
|
4299
3946
|
}
|
|
4300
3947
|
async runPlan(input, announce) {
|
|
4301
3948
|
const taskId = input.taskId ?? newId("task");
|
|
@@ -4303,22 +3950,19 @@ export class Orchestrator {
|
|
|
4303
3950
|
// Plan runs get the same immutable contract truth as every other mode;
|
|
4304
3951
|
// contract validation runs BEFORE the run is announced (see runRace).
|
|
4305
3952
|
const contract = this.buildContract(input, taskId, "plan");
|
|
4306
|
-
const
|
|
4307
|
-
const paths =
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
log,
|
|
4314
|
-
store,
|
|
4315
|
-
paths,
|
|
3953
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
3954
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
3955
|
+
input,
|
|
3956
|
+
contract,
|
|
3957
|
+
quotaSnapshots,
|
|
3958
|
+
store: this.artifactStore(input),
|
|
3959
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
4316
3960
|
runId,
|
|
4317
3961
|
taskId,
|
|
4318
3962
|
mode: "plan",
|
|
4319
3963
|
phase: "plan",
|
|
4320
|
-
|
|
4321
|
-
});
|
|
3964
|
+
prompt: input.prompt,
|
|
3965
|
+
}, announce);
|
|
4322
3966
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
4323
3967
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
4324
3968
|
// W3.3: ONE resolved read-only context — the routing point-probe and every
|
|
@@ -4426,6 +4070,7 @@ export class Orchestrator {
|
|
|
4426
4070
|
roHome,
|
|
4427
4071
|
contextSection,
|
|
4428
4072
|
laneRun,
|
|
4073
|
+
estimateUsdFloor: this.estimateUsdFloor(input.repoRoot),
|
|
4429
4074
|
});
|
|
4430
4075
|
}
|
|
4431
4076
|
const plans = [];
|
|
@@ -4453,8 +4098,9 @@ export class Orchestrator {
|
|
|
4453
4098
|
attemptId,
|
|
4454
4099
|
laneRun,
|
|
4455
4100
|
fallbackHome: roHome.env,
|
|
4456
|
-
promptBody:
|
|
4101
|
+
promptBody: planPrompt(input.prompt) + contextSection,
|
|
4457
4102
|
intent: "plan",
|
|
4103
|
+
reservationEstimateUsd: this.reservationEstimateUsd(input),
|
|
4458
4104
|
});
|
|
4459
4105
|
if (outcome.budgetDenied) {
|
|
4460
4106
|
// QA-050: retain the denied planner slot before breaking so the
|
|
@@ -4533,7 +4179,6 @@ export class Orchestrator {
|
|
|
4533
4179
|
}
|
|
4534
4180
|
if (plans.length === 0) {
|
|
4535
4181
|
return writePlanHarnessFailure(this.planRunDeps(), {
|
|
4536
|
-
input,
|
|
4537
4182
|
contract,
|
|
4538
4183
|
taskId,
|
|
4539
4184
|
runId,
|
|
@@ -4576,7 +4221,7 @@ export class Orchestrator {
|
|
|
4576
4221
|
runPlannerAttempt: (a) => this.runPlannerAttempt(a),
|
|
4577
4222
|
writeRunTelemetry: (store, paths, contract, runId, taskId, mode, attempts, finalAttemptId) => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attempts, finalAttemptId),
|
|
4578
4223
|
execRootOf: (input) => this.execRootOf(input),
|
|
4579
|
-
planPrompt
|
|
4224
|
+
planPrompt,
|
|
4580
4225
|
};
|
|
4581
4226
|
}
|
|
4582
4227
|
/** ask: one selected harness answers read-only questions; no patch/apply controls. */
|
|
@@ -4602,24 +4247,8 @@ export class Orchestrator {
|
|
|
4602
4247
|
defaultPrompt: "Explore this repository and synthesize evidence-cited findings, omissions, and follow-up questions.",
|
|
4603
4248
|
}, announce);
|
|
4604
4249
|
}
|
|
4605
|
-
|
|
4606
|
-
return
|
|
4607
|
-
}
|
|
4608
|
-
rootLedger(_input, contract, log) {
|
|
4609
|
-
// The root ledger discloses into THIS run's log: the ledger is the one
|
|
4610
|
-
// owner of the cash fact (subscription-entitled work settles to 0 there),
|
|
4611
|
-
// and the UI renders `budget.cash` verbatim — never inferring money from
|
|
4612
|
-
// route labels (W4.3 sol #15).
|
|
4613
|
-
const ledger = new BudgetLedger(contract.budget.paid_budget, undefined, {
|
|
4614
|
-
onCashSettled: (cashSpendUsd, valuationUsd) => log.emit("budget.cash", {
|
|
4615
|
-
cash_spend_usd: cashSpendUsd,
|
|
4616
|
-
valuation_usd: valuationUsd,
|
|
4617
|
-
}),
|
|
4618
|
-
});
|
|
4619
|
-
for (const snapshot of this.deps.quotaSnapshots?.() ?? []) {
|
|
4620
|
-
ledger.observeQuotaSnapshot(snapshot);
|
|
4621
|
-
}
|
|
4622
|
-
return ledger;
|
|
4250
|
+
quotaSnapshotPreflight() {
|
|
4251
|
+
return [...(this.deps.quotaSnapshots?.() ?? [])].map((snapshot) => QuotaSnapshotSchema.parse(snapshot));
|
|
4623
4252
|
}
|
|
4624
4253
|
routeBillingKnowledge(input, harnessId) {
|
|
4625
4254
|
// A selected profile's credential_kind decides billing (round-18 #2).
|
|
@@ -4649,10 +4278,10 @@ export class Orchestrator {
|
|
|
4649
4278
|
// scout reserve) so a subscription route is not refused for lacking a
|
|
4650
4279
|
// cash quote.
|
|
4651
4280
|
attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId)),
|
|
4652
|
-
buildSpec: (routed, homeEnv, prompt, attemptId) => {
|
|
4281
|
+
buildSpec: async (routed, homeEnv, prompt, attemptId) => {
|
|
4653
4282
|
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
4654
4283
|
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
4655
|
-
const sessionFields = this.sessionSpecFields(input, routed.adapter.id, log);
|
|
4284
|
+
const sessionFields = await this.sessionSpecFields(input, routed.adapter.id, log, routed.authRouteEstimate);
|
|
4656
4285
|
const spec = HarnessRunSpec.parse({
|
|
4657
4286
|
session_id: newId("ses"),
|
|
4658
4287
|
intent: "synthesize",
|
|
@@ -4693,22 +4322,19 @@ export class Orchestrator {
|
|
|
4693
4322
|
// Contract validation BEFORE the run is announced (see runRace). The
|
|
4694
4323
|
// recorded user intent is the CALLER's goal.
|
|
4695
4324
|
const contract = this.buildContract({ ...input, prompt: opts.contractIntent ?? prompt }, taskId, opts.mode);
|
|
4696
|
-
const
|
|
4697
|
-
const paths =
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
log,
|
|
4704
|
-
store,
|
|
4705
|
-
paths,
|
|
4325
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
4326
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
4327
|
+
input,
|
|
4328
|
+
contract,
|
|
4329
|
+
quotaSnapshots,
|
|
4330
|
+
store: this.artifactStore(input),
|
|
4331
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
4706
4332
|
runId,
|
|
4707
4333
|
taskId,
|
|
4708
4334
|
mode: opts.mode,
|
|
4709
4335
|
phase: "report",
|
|
4710
|
-
|
|
4711
|
-
});
|
|
4336
|
+
prompt,
|
|
4337
|
+
}, announce);
|
|
4712
4338
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
4713
4339
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
4714
4340
|
// The ask/deep-scan report stays bare (its scouts read the tree themselves);
|
|
@@ -4850,10 +4476,10 @@ export class Orchestrator {
|
|
|
4850
4476
|
intent: opts.intent,
|
|
4851
4477
|
harnessId: adapter.id,
|
|
4852
4478
|
// QA-019: an n>1 deep-scan scout admits under a FINITE estimate floor
|
|
4853
|
-
// (mirror of the candidate loop): the first scout reserves
|
|
4854
|
-
// floor
|
|
4855
|
-
//
|
|
4856
|
-
cost: attemptCostEvidence(adapter.id, attemptId, opts.deepScan && idx > 0
|
|
4479
|
+
// (mirror of the candidate loop): the first top-level scout reserves
|
|
4480
|
+
// without a floor; later scouts and every real Delegate child pass the
|
|
4481
|
+
// repo floor because they overlap an existing family unit.
|
|
4482
|
+
cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id)),
|
|
4857
4483
|
});
|
|
4858
4484
|
if (!lease.granted) {
|
|
4859
4485
|
log.emit("budget.lease.created", {
|
|
@@ -4882,67 +4508,120 @@ export class Orchestrator {
|
|
|
4882
4508
|
}
|
|
4883
4509
|
return { status: "budget_denied", reason: lease.reason ?? "budget lease denied" };
|
|
4884
4510
|
}
|
|
4885
|
-
//
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
const
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4511
|
+
// As with planners, the granted lease owns profile/continuity/spec
|
|
4512
|
+
// preparation. Contain a pre-stream rejection as this attempt's failure;
|
|
4513
|
+
// parallel siblings can then finish before the shared HOME is disposed.
|
|
4514
|
+
const preparation = await (async () => {
|
|
4515
|
+
// Lease granted: the attempt is now committed to run — disclose the launch.
|
|
4516
|
+
onLaunch?.();
|
|
4517
|
+
const knobs = this.routeSpecKnobs(routed, contract, modelOverride, input.effort);
|
|
4518
|
+
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
4519
|
+
const explorerPrompt = (opts.deepScan
|
|
4520
|
+
? `${prompt}\n\nExplorer ${idx + 1}/${adapters.length}: focus on a distinct slice. Emit evidence-cited findings, explicit unknowns/omissions, and follow-up questions. Do not edit files.`
|
|
4521
|
+
: prompt) + contextSection;
|
|
4522
|
+
const sessionFields = await this.sessionSpecFields(input, adapter.id, log, routed.authRouteEstimate);
|
|
4523
|
+
const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
|
|
4524
|
+
if (grantResume)
|
|
4525
|
+
resumeGranted.add(adapter.id);
|
|
4526
|
+
// Continuity (INV-137): a thread ASK turn is a chat turn — hydrate a lane
|
|
4527
|
+
// switch/gap with a packet and disclose it. Gated on laneRun (deep-scan
|
|
4528
|
+
// scouts are excluded from laneRun); native resume is available only when
|
|
4529
|
+
// this slot was granted the lane's recorded session.
|
|
4530
|
+
const laneContinuity = laneRun
|
|
4531
|
+
? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, sessionFields, store, paths, this.execRootOf(input), log)
|
|
4532
|
+
: null;
|
|
4533
|
+
// D-16d: the continuation packet pointer rides after the lane pointer so
|
|
4534
|
+
// the fresh session is re-grounded in the exhausted attempt's work.
|
|
4535
|
+
const promptWithPointers = [
|
|
4536
|
+
explorerPrompt,
|
|
4537
|
+
laneContinuity?.pointerLine,
|
|
4538
|
+
continuationPointer,
|
|
4539
|
+
]
|
|
4540
|
+
.filter((p) => Boolean(p))
|
|
4541
|
+
.join("\n\n");
|
|
4542
|
+
const spec = HarnessRunSpec.parse({
|
|
4543
|
+
session_id: newId("ses"),
|
|
4544
|
+
intent: opts.intent,
|
|
4545
|
+
prompt: promptWithPointers,
|
|
4546
|
+
cwd: this.execRootOf(input),
|
|
4547
|
+
access: "readonly",
|
|
4548
|
+
// ASK/EXPLORE/AUDIT read-only runs must forward the user's attachments —
|
|
4549
|
+
// a live "describe this image" turn sent an image that was being dropped here, so
|
|
4550
|
+
// the model honestly reported it saw nothing (the v0.13 attachment bug).
|
|
4551
|
+
attachments: input.attachments ?? [],
|
|
4552
|
+
auth_preference: sessionFields.auth_preference,
|
|
4553
|
+
credential_profile: sessionFields.credential_profile,
|
|
4554
|
+
resume_session_id: grantResume ? sessionFields.resume_session_id : null,
|
|
4555
|
+
...this.harnessSpecKnobs(contract, knobs, opts.intent),
|
|
4556
|
+
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
4557
|
+
// A thread lane turn spawns in its DURABLE per-lane home so the native
|
|
4558
|
+
// session it records is reachable for resume next turn; everything else
|
|
4559
|
+
// uses the disposable route-context home.
|
|
4560
|
+
env: (laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? roHome.env,
|
|
4561
|
+
});
|
|
4562
|
+
const reportAbort = new AbortController();
|
|
4563
|
+
spec.extra["abortSignal"] = input.signal
|
|
4564
|
+
? AbortSignal.any([input.signal, reportAbort.signal])
|
|
4565
|
+
: reportAbort.signal;
|
|
4566
|
+
const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
|
|
4567
|
+
if (reportInteraction)
|
|
4568
|
+
spec.extra["interactionChannel"] = reportInteraction;
|
|
4569
|
+
// D-16: compile the WorkReport envelope for the read-only lane.
|
|
4570
|
+
const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
|
|
4571
|
+
const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
|
|
4572
|
+
const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
|
|
4573
|
+
const answer = new AnswerAssembly();
|
|
4574
|
+
const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
|
|
4575
|
+
knobs.webPolicy === "cached" ||
|
|
4576
|
+
knobs.webPolicy === "live", effectiveWeb, [],
|
|
4577
|
+
// Requested-model capture so ask/audit route receipts detect a silent
|
|
4578
|
+
// model downgrade (typed model_mismatch), not just agent runs.
|
|
4579
|
+
knobs.model);
|
|
4580
|
+
return {
|
|
4581
|
+
knobs,
|
|
4582
|
+
spec,
|
|
4583
|
+
reportAbort,
|
|
4584
|
+
reportInteraction,
|
|
4585
|
+
readonlyWorkMode,
|
|
4586
|
+
attemptEventsPath,
|
|
4587
|
+
answer,
|
|
4588
|
+
telemetry,
|
|
4589
|
+
};
|
|
4590
|
+
})().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
|
|
4591
|
+
if (!preparation.ok) {
|
|
4592
|
+
const message = `read-only attempt setup failed: ${safeErrorMessage(preparation.error)}`;
|
|
4593
|
+
AC.settleGrantedAttemptLease({
|
|
4594
|
+
ledger,
|
|
4595
|
+
leaseId: lease.lease?.lease_id ?? "",
|
|
4596
|
+
attemptId,
|
|
4597
|
+
harnessId: adapter.id,
|
|
4598
|
+
costUsd: 0,
|
|
4599
|
+
costEstimated: false,
|
|
4600
|
+
preStreamFailureSource: "readonly-pre-stream",
|
|
4601
|
+
});
|
|
4602
|
+
const telemetry = createAttemptTelemetry(contract.external_context.policy, contract.external_context.web_required, contract.external_context.effective_mode);
|
|
4603
|
+
setAttemptOutcome(telemetry, {
|
|
4604
|
+
deliverablePresent: false,
|
|
4605
|
+
gatesPassed: null,
|
|
4606
|
+
harnessErrored: true,
|
|
4607
|
+
webRequiredUnsatisfied: false,
|
|
4608
|
+
});
|
|
4609
|
+
attempts.push({
|
|
4610
|
+
attemptId,
|
|
4611
|
+
harnessId: adapter.id,
|
|
4612
|
+
status: "failed",
|
|
4613
|
+
report: "",
|
|
4614
|
+
error: message,
|
|
4615
|
+
telemetry,
|
|
4616
|
+
});
|
|
4617
|
+
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
|
|
4618
|
+
if (opts.deepScan) {
|
|
4619
|
+
store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${message}\n`);
|
|
4620
|
+
}
|
|
4621
|
+
return { status: "launched" };
|
|
4622
|
+
}
|
|
4623
|
+
const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, answer, telemetry, } = preparation.value;
|
|
4624
|
+
let spec = preparedSpec;
|
|
4946
4625
|
const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
|
|
4947
4626
|
let activeSessionId = spec.session_id;
|
|
4948
4627
|
const onAbort = () => {
|
|
@@ -4982,11 +4661,13 @@ export class Orchestrator {
|
|
|
4982
4661
|
try {
|
|
4983
4662
|
const watchedReport = withInactivityWatchdog(adapter.run(runSpec), {
|
|
4984
4663
|
timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
|
|
4664
|
+
countsAsProgress: countsAsAgentProgress,
|
|
4985
4665
|
onTimeout: () => {
|
|
4986
4666
|
reportAbort.abort();
|
|
4987
4667
|
void adapter.cancel?.(activeSessionId)?.catch(() => { });
|
|
4988
4668
|
},
|
|
4989
4669
|
isSuspended: () => (reportInteraction?.pendingCount?.() ?? 0) > 0,
|
|
4670
|
+
suspensionVersion: () => reportInteraction?.suspensionVersion?.() ?? 0,
|
|
4990
4671
|
});
|
|
4991
4672
|
for await (const ev of watchedReport) {
|
|
4992
4673
|
if (input.signal?.aborted)
|
|
@@ -5050,13 +4731,18 @@ export class Orchestrator {
|
|
|
5050
4731
|
// W5.4 reactive failover, READ-ONLY lane (same contract as the
|
|
5051
4732
|
// candidate lane; typed limits only, never plain transients).
|
|
5052
4733
|
if (harnessError && !input.signal?.aborted) {
|
|
4734
|
+
const rotationPolicy = this.profilePolicy(input.repoRoot, adapter.id);
|
|
4735
|
+
const readyProfileIds = sawTypedLimit && reportSoFar.length === 0 && rotationPolicy.limit_action === "rotate"
|
|
4736
|
+
? await this.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles)
|
|
4737
|
+
: new Set();
|
|
5053
4738
|
const rotated = rotateSpecOnTypedLimit({
|
|
5054
4739
|
spec,
|
|
5055
4740
|
harnessId: adapter.id,
|
|
5056
4741
|
attemptId,
|
|
5057
|
-
policy:
|
|
4742
|
+
policy: rotationPolicy,
|
|
5058
4743
|
registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
|
|
5059
4744
|
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
4745
|
+
readyProfileIds,
|
|
5060
4746
|
triedProfiles,
|
|
5061
4747
|
sawTypedLimit,
|
|
5062
4748
|
deliverableEmpty: reportSoFar.length === 0,
|
|
@@ -5098,7 +4784,17 @@ export class Orchestrator {
|
|
|
5098
4784
|
}
|
|
5099
4785
|
finally {
|
|
5100
4786
|
input.signal?.removeEventListener("abort", onAbort);
|
|
5101
|
-
|
|
4787
|
+
AC.settleGrantedAttemptLease({
|
|
4788
|
+
ledger,
|
|
4789
|
+
leaseId: lease.lease?.lease_id ?? "",
|
|
4790
|
+
attemptId,
|
|
4791
|
+
harnessId: adapter.id,
|
|
4792
|
+
costUsd: cost,
|
|
4793
|
+
costEstimated,
|
|
4794
|
+
authMode: telemetry.authMode,
|
|
4795
|
+
usageCost: telemetry.usageCost,
|
|
4796
|
+
preStreamFailureSource: "readonly-pre-stream",
|
|
4797
|
+
});
|
|
5102
4798
|
}
|
|
5103
4799
|
if (harnessError && telemetry.transientFailures.length > 0) {
|
|
5104
4800
|
log.emit("route.transient.exhausted", {
|
|
@@ -5109,11 +4805,14 @@ export class Orchestrator {
|
|
|
5109
4805
|
});
|
|
5110
4806
|
}
|
|
5111
4807
|
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
|
|
5112
|
-
// D-16:
|
|
4808
|
+
// D-16: remove the WorkReport transport; the deliverable is the report.
|
|
5113
4809
|
const roUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", readonlyWorkMode, {
|
|
5114
4810
|
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
5115
4811
|
});
|
|
5116
|
-
|
|
4812
|
+
// Trim symmetrically with the plan path: a whitespace-only answer is not
|
|
4813
|
+
// a delivered report (the final-artifact wrapper heading would otherwise
|
|
4814
|
+
// make it read as present content by construction).
|
|
4815
|
+
const report = redactSecrets(roUnwrapped.deliverable).trim();
|
|
5117
4816
|
const unrecovered = unrecoveredToolErrors(telemetry);
|
|
5118
4817
|
const webBlocked = webUnsatisfied(telemetry);
|
|
5119
4818
|
const reportPresent = report.length > 0;
|
|
@@ -5141,6 +4840,13 @@ export class Orchestrator {
|
|
|
5141
4840
|
harnessErrored: harnessError !== null && !webBlocked,
|
|
5142
4841
|
webRequiredUnsatisfied: webBlocked,
|
|
5143
4842
|
workState: roFinalized.workState,
|
|
4843
|
+
// A read-only attempt that completed CLEANLY with an honestly empty
|
|
4844
|
+
// answer is a success with deliverable_present=false ("(no output)"),
|
|
4845
|
+
// never a fake contract failure — the trim above must not convert a
|
|
4846
|
+
// phantom deliverable into a phantom harness failure. Only the clean
|
|
4847
|
+
// finalizer class qualifies: contract failures, vetoes, and context
|
|
4848
|
+
// interruptions keep the strict deliverable requirement.
|
|
4849
|
+
emptyDeliverableAllowed: roFinalized.outcomeClass === "clean",
|
|
5144
4850
|
});
|
|
5145
4851
|
if (harnessError) {
|
|
5146
4852
|
log.emit("harness.completed", {
|
|
@@ -5571,7 +5277,7 @@ export class Orchestrator {
|
|
|
5571
5277
|
}
|
|
5572
5278
|
this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, opts.deepScan ? null : (succeeded[0]?.attemptId ?? null), deepScanSynthesis);
|
|
5573
5279
|
log.emit("output.ready", {
|
|
5574
|
-
kind: opts.
|
|
5280
|
+
kind: opts.deepScan ? "report" : "answer",
|
|
5575
5281
|
path: `final/${opts.artifactName}`,
|
|
5576
5282
|
});
|
|
5577
5283
|
if (opts.deepScan) {
|
|
@@ -5629,11 +5335,16 @@ export class Orchestrator {
|
|
|
5629
5335
|
.map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
|
|
5630
5336
|
.join(", ");
|
|
5631
5337
|
store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harnesses: ${harnessLabel}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}\n\n${report}\n`);
|
|
5338
|
+
const reportProducerAttemptId = opts.deepScan &&
|
|
5339
|
+
deepScanSynthesis?.status === "succeeded" &&
|
|
5340
|
+
deepScanSynthesis.reducer_attempt_id
|
|
5341
|
+
? deepScanSynthesis.reducer_attempt_id
|
|
5342
|
+
: (succeeded[0]?.attemptId ?? "a01");
|
|
5632
5343
|
store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
|
|
5633
5344
|
id: newId("wp"),
|
|
5634
5345
|
kind: "report",
|
|
5635
5346
|
source_task_id: taskId,
|
|
5636
|
-
producer_attempt_id:
|
|
5347
|
+
producer_attempt_id: reportProducerAttemptId,
|
|
5637
5348
|
files: Object.fromEntries([[opts.artifactName, join(paths.finalDir, opts.artifactName)]]),
|
|
5638
5349
|
meta: {
|
|
5639
5350
|
harnesses: attempts.map((a) => a.harnessId),
|
|
@@ -5642,7 +5353,7 @@ export class Orchestrator {
|
|
|
5642
5353
|
read_only: true,
|
|
5643
5354
|
},
|
|
5644
5355
|
});
|
|
5645
|
-
log.emit("work_product.emitted", { kind: "report", winner:
|
|
5356
|
+
log.emit("work_product.emitted", { kind: "report", winner: reportProducerAttemptId });
|
|
5646
5357
|
const workVetoed = terminalFacts.work_state?.state === "needs_input" ||
|
|
5647
5358
|
terminalFacts.work_state?.state === "incomplete";
|
|
5648
5359
|
if (terminalFacts.lifecycle !== "succeeded") {
|
|
@@ -5694,9 +5405,4 @@ export class Orchestrator {
|
|
|
5694
5405
|
};
|
|
5695
5406
|
}
|
|
5696
5407
|
}
|
|
5697
|
-
function assertNoSecretLikeTokens(label, text) {
|
|
5698
|
-
if (containsSecretLikeToken(text)) {
|
|
5699
|
-
throw new Error(`${label} contains secret-like token; refusing to persist artifact`);
|
|
5700
|
-
}
|
|
5701
|
-
}
|
|
5702
5408
|
//# sourceMappingURL=orchestrator.js.map
|