@claudexor/orchestrator 3.1.2 → 3.2.1
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/README.md +3 -1
- 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 +7 -0
- package/dist/attemptTelemetry.d.ts.map +1 -1
- package/dist/attemptTelemetry.js +2 -1
- package/dist/attemptTelemetry.js.map +1 -1
- package/dist/attemptUsageCost.d.ts +17 -1
- package/dist/attemptUsageCost.d.ts.map +1 -1
- package/dist/attemptUsageCost.js +26 -2
- package/dist/attemptUsageCost.js.map +1 -1
- package/dist/budgetFailure.js.map +1 -1
- package/dist/credential-profile-rotation.d.ts +148 -0
- package/dist/credential-profile-rotation.d.ts.map +1 -0
- package/dist/credential-profile-rotation.js +322 -0
- package/dist/credential-profile-rotation.js.map +1 -0
- package/dist/credential-profiles.d.ts +38 -134
- package/dist/credential-profiles.d.ts.map +1 -1
- package/dist/credential-profiles.js +74 -296
- package/dist/credential-profiles.js.map +1 -1
- package/dist/decisionBudget.d.ts +2 -2
- package/dist/decisionBudget.d.ts.map +1 -1
- 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/delegationBudgetAuthority.d.ts.map +1 -1
- package/dist/delegationBudgetAuthority.js.map +1 -1
- package/dist/delegationFailure.d.ts.map +1 -1
- 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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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 +51 -83
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +558 -704
- package/dist/orchestrator.js.map +1 -1
- 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/planQuestions.js +4 -1
- package/dist/planQuestions.js.map +1 -1
- 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 +28 -1
- package/dist/requestRequirements.d.ts.map +1 -1
- package/dist/requestRequirements.js +89 -10
- package/dist/requestRequirements.js.map +1 -1
- package/dist/reviewerPanel.js.map +1 -1
- package/dist/root-ledger.d.ts.map +1 -1
- package/dist/root-ledger.js +2 -0
- package/dist/root-ledger.js.map +1 -1
- package/dist/run-bounded.d.ts.map +1 -1
- package/dist/run-bounded.js +7 -1
- package/dist/run-bounded.js.map +1 -1
- package/dist/run-input-resolution.d.ts.map +1 -1
- package/dist/run-input-resolution.js +26 -8
- package/dist/run-input-resolution.js.map +1 -1
- 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/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 +4 -80
- package/dist/runTerminals.d.ts.map +1 -1
- package/dist/runTerminals.js +357 -260
- package/dist/runTerminals.js.map +1 -1
- package/dist/secretDiff.d.ts +3 -0
- package/dist/secretDiff.d.ts.map +1 -1
- package/dist/secretDiff.js +7 -0
- package/dist/secretDiff.js.map +1 -1
- 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/transientClassify.js.map +1 -1
- package/package.json +17 -17
package/dist/orchestrator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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";
|
|
@@ -9,9 +9,10 @@ import { capabilityIntents } from "@claudexor/gateway";
|
|
|
9
9
|
import { policyFindings } from "./policyFindings.js";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./planRun.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
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";
|
|
15
16
|
import { assertRouteModelsAllowed } from "./modelGovernance.js";
|
|
16
17
|
import { governRouteEffort } from "./effortGovernance.js";
|
|
17
18
|
import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
|
|
@@ -20,16 +21,18 @@ import { routingFailureClassification } from "./routing-failure.js";
|
|
|
20
21
|
export { routingFailureClassification } from "./routing-failure.js";
|
|
21
22
|
import { runBounded } from "./run-bounded.js";
|
|
22
23
|
import { planPrompt } from "./plan-prompt.js";
|
|
24
|
+
import { verifiedPlanBrief, withPlanBrief } from "./planBrief.js";
|
|
23
25
|
import { resolveRunInputDefaults } from "./run-input-resolution.js";
|
|
24
|
-
import {
|
|
26
|
+
import { beginAnnouncedRun } from "./runEventLog.js";
|
|
25
27
|
import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudget.js";
|
|
26
28
|
import { buildRevisePrompt } from "./revisePrompt.js";
|
|
27
|
-
import {
|
|
29
|
+
import { cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
|
|
28
30
|
import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
|
|
29
31
|
import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
|
|
30
|
-
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";
|
|
31
33
|
import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
|
|
32
34
|
import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
|
|
35
|
+
import { ensureWriteModeGitBoundary } from "./git-precondition.js";
|
|
33
36
|
import { buildContinuation, } from "./continuity.js";
|
|
34
37
|
import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from "./continuity-facts.js";
|
|
35
38
|
import { runDiffReview } from "./diffReview.js";
|
|
@@ -41,18 +44,18 @@ import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./har
|
|
|
41
44
|
import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
|
|
42
45
|
import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
|
|
43
46
|
import { interactionChannelFor } from "./interaction.js";
|
|
44
|
-
import { gateSpecsFromContract, renderTestsEvidence
|
|
47
|
+
import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
|
|
48
|
+
import { buildTaskContract } from "./task-contract-builder.js";
|
|
45
49
|
import { ArtifactStore } from "@claudexor/artifact-store";
|
|
46
|
-
import { EventLog } from "@claudexor/event-log";
|
|
47
50
|
import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
|
|
48
|
-
import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge,
|
|
51
|
+
import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
|
|
49
52
|
import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, verifyAndDeliver, } from "@claudexor/delivery";
|
|
50
53
|
import { HarnessGateway } from "@claudexor/gateway";
|
|
51
54
|
import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
|
|
52
55
|
import { arbitrate } from "@claudexor/arbitration";
|
|
53
56
|
import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
|
|
54
57
|
import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
|
|
55
|
-
import { readTextSafe, appendLine, assertNoInlineSecretValues,
|
|
58
|
+
import { readTextSafe, appendLine, assertNoInlineSecretValues, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
|
|
56
59
|
import { assertWriteIsolation } from "./write-isolation.js";
|
|
57
60
|
const LABELS = "ABCDEFGHIJ".split("");
|
|
58
61
|
const NO_PROJECT_ROOT = noProjectRepoRoot();
|
|
@@ -110,10 +113,14 @@ export class Orchestrator {
|
|
|
110
113
|
throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
|
|
111
114
|
}
|
|
112
115
|
const runId = resolved.runId ?? newId("run");
|
|
116
|
+
let delegatedLedgerAttached = false;
|
|
113
117
|
resolved = {
|
|
114
118
|
...resolved,
|
|
115
119
|
runId,
|
|
116
120
|
taskId: resolved.taskId ?? newId("task"),
|
|
121
|
+
onDelegatedLedgerAttached: () => {
|
|
122
|
+
delegatedLedgerAttached = true;
|
|
123
|
+
},
|
|
117
124
|
};
|
|
118
125
|
if (resolved.delegate === true) {
|
|
119
126
|
resolved = {
|
|
@@ -157,50 +164,63 @@ export class Orchestrator {
|
|
|
157
164
|
if (resolved.repoRoot !== NO_PROJECT_ROOT) {
|
|
158
165
|
assertMandatoryContext(resolved.repoRoot, this.projectConfig(resolved.repoRoot).context.mandatory_files);
|
|
159
166
|
}
|
|
160
|
-
// Reviewer panels are validated only inside
|
|
161
|
-
// 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
|
|
162
169
|
// creation, so a doomed explicit panel yields typed failure ARTIFACTS
|
|
163
170
|
// (failure.yaml naming the refusal) instead of a bare pre-run throw.
|
|
164
|
-
//
|
|
171
|
+
// Ask and Plan never spawn code reviewers, so a panel there never spends doctor/
|
|
165
172
|
// model probes and never fails a run that would not use it.
|
|
166
173
|
// Whole-strategy terminal net: once a strategy ANNOUNCES its
|
|
167
174
|
// run, any escaped throw still stamps failure.yaml + summary + run.failed
|
|
168
175
|
// instead of orphaning events.jsonl.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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);
|
|
189
221
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!authority?.hasParent(runId))
|
|
193
|
-
return;
|
|
194
|
-
authority.beginParentClose(runId);
|
|
195
|
-
await authority.waitForChildren(runId);
|
|
196
|
-
},
|
|
197
|
-
// Single per-run terminalization hook: release the routing-rationale map
|
|
198
|
-
// entry on EVERY terminal (incl. a run that died before its telemetry
|
|
199
|
-
// writer ran, which is the leak this closes).
|
|
200
|
-
(runId) => {
|
|
201
|
-
this.routingRationaleByRun.delete(runId);
|
|
202
|
-
this.deps.delegationBudgetAuthority?.releaseRun(runId);
|
|
203
|
-
});
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
204
224
|
}
|
|
205
225
|
async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
|
|
206
226
|
if (this.deps.reviewers)
|
|
@@ -273,11 +293,7 @@ export class Orchestrator {
|
|
|
273
293
|
}
|
|
274
294
|
authPreferenceForHarness(repoRoot, harnessId, runAuthPreference) {
|
|
275
295
|
const cfg = this.config(repoRoot)?.global;
|
|
276
|
-
|
|
277
|
-
return (explicit(runAuthPreference) ??
|
|
278
|
-
explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
|
|
279
|
-
explicit(cfg?.routing?.auth_preference) ??
|
|
280
|
-
"auto");
|
|
296
|
+
return effectiveAuthPreference(runAuthPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference);
|
|
281
297
|
}
|
|
282
298
|
artifactStore(input) {
|
|
283
299
|
if (input.mode === "ask" && input.contextMode === "off" && input.repoRoot === NO_PROJECT_ROOT) {
|
|
@@ -300,20 +316,25 @@ export class Orchestrator {
|
|
|
300
316
|
estimateUsdFloor(repoRoot) {
|
|
301
317
|
return this.config(repoRoot)?.global.budget.estimate_usd_floor ?? 0.05;
|
|
302
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
|
+
}
|
|
303
326
|
execRootOf(input) {
|
|
304
327
|
return input.executionRoot ?? input.repoRoot;
|
|
305
328
|
}
|
|
306
|
-
sessionSpecFields(input, harnessId, log) {
|
|
329
|
+
async sessionSpecFields(input, harnessId, model, log, defaultRoute = null, quotaAdmission) {
|
|
307
330
|
const cfg = this.config(input.repoRoot)?.global;
|
|
308
|
-
const profile =
|
|
309
|
-
|
|
331
|
+
const profile = quotaAdmission?.model === model
|
|
332
|
+
? quotaAdmission.profile
|
|
333
|
+
: await this.preflightProfile(input, harnessId, model, log, defaultRoute);
|
|
310
334
|
return {
|
|
311
335
|
// "auto" at ANY level falls through (thread turns send the thread default
|
|
312
336
|
// "auto" as a per-run value; it must not shadow a configured preference).
|
|
313
|
-
auth_preference:
|
|
314
|
-
explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
|
|
315
|
-
explicit(cfg?.routing?.auth_preference) ??
|
|
316
|
-
"auto",
|
|
337
|
+
auth_preference: effectiveAuthPreference(input.authPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference),
|
|
317
338
|
resume_session_id: resumeSessionForProfile(input.resumeSessions?.[harnessId], profile),
|
|
318
339
|
credential_profile: profile,
|
|
319
340
|
};
|
|
@@ -388,19 +409,62 @@ export class Orchestrator {
|
|
|
388
409
|
const policy = this.config(repoRoot)?.global.harnesses?.[harnessId]?.profile_policy;
|
|
389
410
|
return policy ?? { limit_action: "fail", rotation_eligible: [], headroom_threshold: 0.9 };
|
|
390
411
|
}
|
|
391
|
-
|
|
412
|
+
/** Fresh profile readiness for one rotation decision epoch. Accounts uses
|
|
413
|
+
* the same probe wrapper + admission predicate when projecting next_up. */
|
|
414
|
+
async readyProfileIdsForRotation(input, harnessId, current, excluded = new Set()) {
|
|
415
|
+
const profiles = staticRotationCandidates({
|
|
416
|
+
registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
|
|
417
|
+
harnessId,
|
|
418
|
+
policy: this.profilePolicy(input.repoRoot, harnessId),
|
|
419
|
+
current,
|
|
420
|
+
excluded,
|
|
421
|
+
});
|
|
422
|
+
const adapter = this.deps.registry.get(harnessId);
|
|
423
|
+
const entries = await Promise.all(profiles.map(async (profile) => ({
|
|
424
|
+
profile,
|
|
425
|
+
status: await probeCredentialProfileStatus(profile, adapter?.probeCredentialProfile?.bind(adapter)),
|
|
426
|
+
})));
|
|
427
|
+
return new Set(entries
|
|
428
|
+
.filter(({ profile, status }) => profileStatusAdmits(profile, status))
|
|
429
|
+
.map(({ profile }) => profile.profile_id));
|
|
430
|
+
}
|
|
431
|
+
async preflightProfile(input, harnessId, model, log, defaultRoute) {
|
|
392
432
|
const profile = this.resolveCredentialProfile(input, harnessId);
|
|
393
433
|
const policy = this.profilePolicy(input.repoRoot, harnessId);
|
|
394
434
|
const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
|
|
395
435
|
const snapshots = this.deps.quotaSnapshots?.() ?? [];
|
|
396
436
|
const emit = (type, payload) => log?.emit(type, payload);
|
|
437
|
+
const breach = profileHeadroomBreach(snapshots, harnessId, profile?.profile_id ?? null, policy.headroom_threshold, model);
|
|
438
|
+
const readyProfileIds = policy.limit_action === "rotate" &&
|
|
439
|
+
breach !== null &&
|
|
440
|
+
(profile !== null || defaultRoute === "local_session")
|
|
441
|
+
? await this.readyProfileIdsForRotation(input, harnessId, profile)
|
|
442
|
+
: new Set();
|
|
397
443
|
if (!profile) {
|
|
398
444
|
// Unpinned runs (INV-135 auto-balance): under `rotate`, a fresh
|
|
399
445
|
// default-subject headroom breach starts on the next eligible
|
|
400
446
|
// subscription profile instead; `fail`/`ask` change nothing.
|
|
401
|
-
return preflightDefaultSubject({
|
|
447
|
+
return preflightDefaultSubject({
|
|
448
|
+
harnessId,
|
|
449
|
+
policy,
|
|
450
|
+
registry,
|
|
451
|
+
snapshots,
|
|
452
|
+
readyProfileIds,
|
|
453
|
+
defaultRoute,
|
|
454
|
+
model,
|
|
455
|
+
emit,
|
|
456
|
+
});
|
|
402
457
|
}
|
|
403
|
-
return preflightCredentialProfile({
|
|
458
|
+
return preflightCredentialProfile({
|
|
459
|
+
profile,
|
|
460
|
+
harnessId,
|
|
461
|
+
policy,
|
|
462
|
+
registry,
|
|
463
|
+
snapshots,
|
|
464
|
+
readyProfileIds,
|
|
465
|
+
model,
|
|
466
|
+
emit,
|
|
467
|
+
});
|
|
404
468
|
}
|
|
405
469
|
/**
|
|
406
470
|
* Resolve candidate adapters: explicit `--harness`, else available real harnesses, then
|
|
@@ -477,6 +541,27 @@ export class Orchestrator {
|
|
|
477
541
|
}
|
|
478
542
|
}
|
|
479
543
|
}
|
|
544
|
+
const attachments = input.attachments ?? [];
|
|
545
|
+
// Resolve the complete attachment pool once from manifest truth. If any
|
|
546
|
+
// lane still lacks usable discovery truth, preserve the existing per-lane
|
|
547
|
+
// discovery/doctor precedence and resolve that lane inside the route loop.
|
|
548
|
+
const canResolveAttachmentPool = attachments.length > 0 &&
|
|
549
|
+
ids.every((id) => {
|
|
550
|
+
const status = statusById.get(id);
|
|
551
|
+
return status?.manifest != null && status.status !== "unavailable";
|
|
552
|
+
});
|
|
553
|
+
const attachmentPoolAdmission = canResolveAttachmentPool
|
|
554
|
+
? this.requestRequirements.resolveAttachmentPool(explicitPool ? "explicit" : "auto", attachments, ids.map((id) => ({
|
|
555
|
+
harnessId: id,
|
|
556
|
+
declarations: statusById.get(id)?.manifest?.capability_profile.attachment_inputs ?? null,
|
|
557
|
+
available: true,
|
|
558
|
+
})))
|
|
559
|
+
: null;
|
|
560
|
+
if (attachmentPoolAdmission?.outcome === "refused") {
|
|
561
|
+
throw new HarnessUnavailableError(attachmentPoolAdmission.message ??
|
|
562
|
+
"no available harness lane can receive the selected attachments");
|
|
563
|
+
}
|
|
564
|
+
const attachmentRejectionById = new Map(attachmentPoolAdmission?.rejected.map((admission) => [admission.harnessId, admission]) ?? []);
|
|
480
565
|
const policy = input.web ?? input.externalContextPolicy ?? "auto";
|
|
481
566
|
const pool = [];
|
|
482
567
|
const dropped = [];
|
|
@@ -610,9 +695,10 @@ export class Orchestrator {
|
|
|
610
695
|
dropLane(id, "web", why);
|
|
611
696
|
continue;
|
|
612
697
|
}
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
698
|
+
const attachmentAdmission = attachmentRejectionById.get(id) ??
|
|
699
|
+
this.requestRequirements.resolveAttachmentLane(id, attachments, manifest.capability_profile.attachment_inputs);
|
|
700
|
+
if (!attachmentAdmission.admitted) {
|
|
701
|
+
dropLane(id, "attachment", attachmentAdmission.message ?? `${id} rejects attachments`);
|
|
616
702
|
continue;
|
|
617
703
|
}
|
|
618
704
|
const reason = status.reasons.length > 0 ? `: ${status.reasons.join("; ")}` : "";
|
|
@@ -646,6 +732,7 @@ export class Orchestrator {
|
|
|
646
732
|
// the default store's sources apply only to profile-less runs.
|
|
647
733
|
authRouteEstimate: this.profileAuthRoute(input, id) ??
|
|
648
734
|
estimateEffectiveAuthRoute(this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), status.authSources),
|
|
735
|
+
quotaAdmission: { model: null, profile: null, route: null },
|
|
649
736
|
supportsSynthesize: manifest.capabilities.synthesize,
|
|
650
737
|
supportsInteractive: manifest.capabilities.interactive,
|
|
651
738
|
supportsJsonSchemaOutput: manifest.capabilities.json_schema_output,
|
|
@@ -680,11 +767,29 @@ export class Orchestrator {
|
|
|
680
767
|
if (pool.length === 0) {
|
|
681
768
|
throw new HarnessUnavailableError(`no harness can perform '${intent}' for this mode${dropped.length ? ` (skipped: ${dropped.join(", ")})` : ""}`);
|
|
682
769
|
}
|
|
683
|
-
|
|
770
|
+
// Quota admission must use the account that will actually spawn. In
|
|
771
|
+
// particular, an opt-in default-subject rotation has to select its ready
|
|
772
|
+
// profile before the budget router filters the exhausted default away.
|
|
773
|
+
// The same resolved profile is reused by the first spec build below.
|
|
774
|
+
const quotaPreparedPool = await Promise.all(pool.map(async (routed) => {
|
|
775
|
+
const model = input.models?.[routed.adapter.id] ?? routed.settings?.defaultModel ?? null;
|
|
776
|
+
const profile = await this.preflightProfile(input, routed.adapter.id, model, log, routed.authRouteEstimate);
|
|
777
|
+
const route = profile
|
|
778
|
+
? profile.credential_kind === "api_key"
|
|
779
|
+
? "managed_api_key"
|
|
780
|
+
: "vendor_native"
|
|
781
|
+
: routed.authRouteEstimate === "api_key"
|
|
782
|
+
? "managed_api_key"
|
|
783
|
+
: routed.authRouteEstimate === "local_session"
|
|
784
|
+
? "vendor_native"
|
|
785
|
+
: null;
|
|
786
|
+
return { ...routed, quotaAdmission: { model, profile, route } };
|
|
787
|
+
}));
|
|
788
|
+
const ordered = this.orderPool(quotaPreparedPool, input, intent, statusById, ledger, runId);
|
|
684
789
|
if (ordered.length === 0) {
|
|
685
790
|
throw new HarnessUnavailableError(`no harness remains eligible for '${intent}' after budget and quota routing`);
|
|
686
791
|
}
|
|
687
|
-
emitPrimaryDivergence(log, input.primaryHarness, ordered,
|
|
792
|
+
emitPrimaryDivergence(log, input.primaryHarness, ordered, quotaPreparedPool, dropped);
|
|
688
793
|
const n = input.n ?? ordered.length;
|
|
689
794
|
const selectionOrder = ordered;
|
|
690
795
|
const out = [];
|
|
@@ -791,28 +896,25 @@ export class Orchestrator {
|
|
|
791
896
|
: authModes.includes("api_key")
|
|
792
897
|
? "api_key"
|
|
793
898
|
: "unknown";
|
|
794
|
-
//
|
|
795
|
-
// (round-18 #2)
|
|
899
|
+
// The quota-preflight profile's credential_kind decides the route
|
|
900
|
+
// outright (round-18 #2). This includes an unpinned default subject
|
|
901
|
+
// that rotated before ranking; an api_key profile must never inherit a
|
|
796
902
|
// subscription classification from the default store's metric.
|
|
797
|
-
const
|
|
903
|
+
const admittedProfileRoute = r.quotaAdmission.profile
|
|
904
|
+
? r.quotaAdmission.profile.credential_kind === "api_key"
|
|
905
|
+
? "api_key"
|
|
906
|
+
: "local_session"
|
|
907
|
+
: null;
|
|
908
|
+
const authMode = admittedProfileRoute ??
|
|
798
909
|
(input.authPreference === "api_key"
|
|
799
910
|
? "api_key"
|
|
800
911
|
: input.authPreference === "subscription"
|
|
801
912
|
? "local_session"
|
|
802
913
|
: (metric?.last_auth_mode ?? guessedAuthMode));
|
|
803
|
-
// The quota subject
|
|
804
|
-
//
|
|
805
|
-
// default
|
|
806
|
-
|
|
807
|
-
// as unknown (undefined) and stays conservatively any-subject.
|
|
808
|
-
let credentialSubjectId;
|
|
809
|
-
try {
|
|
810
|
-
credentialSubjectId =
|
|
811
|
-
this.resolveCredentialProfile(input, r.adapter.id)?.profile_id ?? null;
|
|
812
|
-
}
|
|
813
|
-
catch {
|
|
814
|
-
credentialSubjectId = undefined;
|
|
815
|
-
}
|
|
914
|
+
// The exact quota subject selected before ranking: profile id or the
|
|
915
|
+
// engine default. Profile A's cooldown never excludes profile B or the
|
|
916
|
+
// default on the same harness and route.
|
|
917
|
+
const credentialSubjectId = r.quotaAdmission.profile?.profile_id ?? null;
|
|
816
918
|
// QA-034: the typed auth-route evidence (doctor source verification x the
|
|
817
919
|
// resolved route) is AUTHORITATIVE for billing knowledge in the router —
|
|
818
920
|
// a VERIFIED native route proves subscription_entitlement, so it survives
|
|
@@ -823,20 +925,19 @@ export class Orchestrator {
|
|
|
823
925
|
return {
|
|
824
926
|
harnessId: r.adapter.id,
|
|
825
927
|
available: true,
|
|
826
|
-
model:
|
|
827
|
-
config.harnesses[r.adapter.id]?.default_model ??
|
|
828
|
-
undefined,
|
|
928
|
+
model: r.quotaAdmission.model,
|
|
829
929
|
effort: input.efforts?.[r.adapter.id] ??
|
|
830
930
|
input.effort ??
|
|
831
931
|
config.harnesses[r.adapter.id]?.effort ??
|
|
832
932
|
undefined,
|
|
833
933
|
billingKnowledge: authMode === "api_key" ? "metered" : "unknown",
|
|
834
934
|
incrementalCostUsd: authMode === "api_key" ? (metric?.avg_cost_usd ?? null) : null,
|
|
835
|
-
credentialRoute:
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
935
|
+
credentialRoute: r.quotaAdmission.route ??
|
|
936
|
+
(authMode === "api_key"
|
|
937
|
+
? "managed_api_key"
|
|
938
|
+
: authMode === "local_session"
|
|
939
|
+
? "vendor_native"
|
|
940
|
+
: undefined),
|
|
840
941
|
...(authRoute ? { authRoute } : {}),
|
|
841
942
|
credentialSubjectId,
|
|
842
943
|
};
|
|
@@ -884,6 +985,36 @@ export class Orchestrator {
|
|
|
884
985
|
}
|
|
885
986
|
return ordered;
|
|
886
987
|
}
|
|
988
|
+
/** Harness-only convergence helpers still need the exact quota identity
|
|
989
|
+
* selected before ranking. This facade keeps their small interface while
|
|
990
|
+
* preventing a scoped limit on one model/account from cooling another. */
|
|
991
|
+
quotaLedgerView(ledger, routes) {
|
|
992
|
+
const byId = new Map(routes.map((route) => [route.adapter.id, route]));
|
|
993
|
+
const identity = (id) => {
|
|
994
|
+
const route = byId.get(id);
|
|
995
|
+
return route
|
|
996
|
+
? {
|
|
997
|
+
credentialRoute: route.quotaAdmission.route ?? undefined,
|
|
998
|
+
credentialSubjectId: route.quotaAdmission.profile?.profile_id ?? null,
|
|
999
|
+
model: route.quotaAdmission.model,
|
|
1000
|
+
}
|
|
1001
|
+
: null;
|
|
1002
|
+
};
|
|
1003
|
+
return {
|
|
1004
|
+
bindingPaceSlack: (id) => {
|
|
1005
|
+
const selected = identity(id);
|
|
1006
|
+
return selected
|
|
1007
|
+
? ledger.bindingPaceSlack(id, selected.credentialRoute, selected.credentialSubjectId, Date.now(), selected.model)
|
|
1008
|
+
: ledger.bindingPaceSlack(id);
|
|
1009
|
+
},
|
|
1010
|
+
cooldownActive: (id) => {
|
|
1011
|
+
const selected = identity(id);
|
|
1012
|
+
return selected
|
|
1013
|
+
? ledger.cooldownActive(id, selected.credentialRoute, selected.credentialSubjectId, Date.now(), selected.model)
|
|
1014
|
+
: ledger.cooldownActive(id);
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
887
1018
|
/**
|
|
888
1019
|
* Typed auth-route evidence for one candidate (QA-034): the concrete
|
|
889
1020
|
* credential route the resolved auth mode maps to, plus the doctor's
|
|
@@ -981,119 +1112,9 @@ export class Orchestrator {
|
|
|
981
1112
|
return this.config(repoRoot).project;
|
|
982
1113
|
}
|
|
983
1114
|
buildContract(input, taskId, mode) {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const requestedAccess = input.access ?? (readOnlyMode ? "readonly" : resolvedCfg.trust.access_default);
|
|
988
|
-
// Effective access is COMPUTED by the engine, never echoed from a client:
|
|
989
|
-
// read-only modes clamp to readonly regardless of the request.
|
|
990
|
-
const effectiveAccess = readOnlyMode ? "readonly" : requestedAccess;
|
|
991
|
-
// TrustConfig is USER-LEVEL only (versioned repo config must never
|
|
992
|
-
// self-grant sensitive powers): unsandboxed full access requires an
|
|
993
|
-
// explicit allow in ~/.claudexor trust settings — loud error, no downgrade.
|
|
994
|
-
// The gate applies to the EFFECTIVE profile: a read-only run clamped to
|
|
995
|
-
// readonly never runs unsandboxed and needs no trust allow.
|
|
996
|
-
if (effectiveAccess === "full" && !resolvedCfg.trust.allow_full_access) {
|
|
997
|
-
// Typed refusal: the `code` rides the daemon job record onto the thread
|
|
998
|
-
// turn (TurnEnqueueError.code), so surfaces key remedies on the CODE —
|
|
999
|
-
// never on substring-matching this human message.
|
|
1000
|
-
throw Object.assign(new Error(`access profile 'full' requires allow_full_access: true in the user-level trust file for this repo ` +
|
|
1001
|
-
`(${trustConfigPath(input.repoRoot)}); enable it with \`claudexor trust --allow-full-access\` — refusing to run unsandboxed`),
|
|
1002
|
-
// Refusal semantics are born at the throw (W24): the one-time grant is
|
|
1003
|
-
// a 403, and the daemon persists this status onto the job record.
|
|
1004
|
-
{ code: TRUST_FULL_ACCESS_CODE, status: 403 });
|
|
1005
|
-
}
|
|
1006
|
-
const externalContextPolicy = input.web ?? input.externalContextPolicy ?? "auto";
|
|
1007
|
-
// Deterministic gate commands come from explicit run input, then versioned
|
|
1008
|
-
// project config. Without these, gateSpecs is empty and convergence is
|
|
1009
|
-
// review-only; with them, convergence is test-driven.
|
|
1010
|
-
const resolvedGates = resolveContractGates({
|
|
1011
|
-
repoRoot: input.repoRoot,
|
|
1012
|
-
effectiveAccess,
|
|
1013
|
-
config: cfg,
|
|
1014
|
-
trustGrants: resolvedCfg.trust.test_command_grants,
|
|
1015
|
-
operatorCommands: input.tests ?? [],
|
|
1016
|
-
projectCommands: cfg.tests?.commands ?? [],
|
|
1017
|
-
});
|
|
1018
|
-
const commands = resolvedGates.commands;
|
|
1019
|
-
const protectedPaths = [...new Set(cfg.constraints.protected_paths)];
|
|
1020
|
-
const autoProtectedPaths = resolvedGates.autoProtectedPaths;
|
|
1021
|
-
const protectedPathApprovals = [
|
|
1022
|
-
...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
|
|
1023
|
-
];
|
|
1024
|
-
return TaskContractSchema.parse({
|
|
1025
|
-
schema_version: SCHEMA_VERSION,
|
|
1026
|
-
task_id: taskId,
|
|
1027
|
-
created_at: nowIso(),
|
|
1028
|
-
repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
|
|
1029
|
-
mode: { kind: mode },
|
|
1030
|
-
delegation_requested: input.delegate === true,
|
|
1031
|
-
run_lineage: {
|
|
1032
|
-
parent_run_id: input.parentRunId ?? null,
|
|
1033
|
-
delegated_from_run_id: input.delegatedFromRunId ?? null,
|
|
1034
|
-
},
|
|
1035
|
-
user_intent: { raw: redactSecrets(input.prompt) },
|
|
1036
|
-
// Redacted for symmetry with user_intent.raw — a no-op on fenced input
|
|
1037
|
-
// (the inline-secret fence already blocked any secret-like value at every
|
|
1038
|
-
// ingress incl. this engine boundary), so task-producing lanes read back
|
|
1039
|
-
// the real instructions via harnessSpecKnobs().
|
|
1040
|
-
instructions: input.instructions === undefined ? undefined : redactSecrets(input.instructions),
|
|
1041
|
-
// Already normalized/strictified at the engine boundary (run() refuses
|
|
1042
|
-
// unsupported shapes before any run dir exists).
|
|
1043
|
-
output_schema: input.outputSchema ?? null,
|
|
1044
|
-
auth_preference: input.authPreference ?? "auto",
|
|
1045
|
-
credential_profile_id: input.credentialProfileId ?? null,
|
|
1046
|
-
max_turns: input.maxTurns ?? null,
|
|
1047
|
-
constraints: {
|
|
1048
|
-
protected_paths: protectedPaths,
|
|
1049
|
-
deny_paths: [...new Set(input.denyPaths ?? [])],
|
|
1050
|
-
auto_protected_paths: autoProtectedPaths,
|
|
1051
|
-
protected_path_approvals: protectedPathApprovals,
|
|
1052
|
-
},
|
|
1053
|
-
tests: { commands },
|
|
1054
|
-
access: {
|
|
1055
|
-
requested_profile: requestedAccess,
|
|
1056
|
-
effective_profile: effectiveAccess,
|
|
1057
|
-
},
|
|
1058
|
-
external_context: {
|
|
1059
|
-
policy: externalContextPolicy,
|
|
1060
|
-
web_required: externalContextPolicy === "cached" || externalContextPolicy === "live",
|
|
1061
|
-
// Per-route upgrades (e.g. claude cached->live) are disclosed in events
|
|
1062
|
-
// and telemetry.yaml; the immutable contract records the requested policy.
|
|
1063
|
-
effective_mode: externalContextPolicy,
|
|
1064
|
-
},
|
|
1065
|
-
// Harness-native tool names are adapter knowledge; the neutral contract
|
|
1066
|
-
// carries only the policy plus user-configured allow/deny lists (wired
|
|
1067
|
-
// from per-harness settings).
|
|
1068
|
-
tool_permission_policy: {
|
|
1069
|
-
web: externalContextPolicy,
|
|
1070
|
-
allow: [],
|
|
1071
|
-
deny: [],
|
|
1072
|
-
},
|
|
1073
|
-
budget: {
|
|
1074
|
-
routing_goal: input.routingGoal ?? this.deps.routingGoal ?? cfg?.budget?.routing_goal ?? "auto",
|
|
1075
|
-
paid_budget: this.resolvePaidBudget(input.paidBudget, resolvedCfg),
|
|
1076
|
-
},
|
|
1077
|
-
// The resolved harness-scoped model map (scalar already expanded to the
|
|
1078
|
-
// primary by resolveRunInput). The contract is what route spec building
|
|
1079
|
-
// reads — there is no run-global model (INV-103).
|
|
1080
|
-
routing_models: input.models ?? {},
|
|
1081
|
-
// QA-035: freeze the RESOLVED reasoning-effort per known lane so Exact
|
|
1082
|
-
// Retry replays it instead of re-resolving current settings. Precedence
|
|
1083
|
-
// (specific beats general): the harness-scoped `efforts` map entry, then a
|
|
1084
|
-
// per-turn scalar `input.effort`, then the harness settings default — the
|
|
1085
|
-
// same map that Exact Retry replays so a NON-PRIMARY lane keeps its own
|
|
1086
|
-
// frozen effort (QA-035 completeness). Only known-pool lanes are frozen
|
|
1087
|
-
// here (a pure auto pool's lanes resolve later — documented seam).
|
|
1088
|
-
routing_efforts: Object.fromEntries([...new Set([...(input.harnesses ?? []), ...Object.keys(input.efforts ?? {})])]
|
|
1089
|
-
.map((hid) => [
|
|
1090
|
-
hid,
|
|
1091
|
-
input.efforts?.[hid] ??
|
|
1092
|
-
input.effort ??
|
|
1093
|
-
resolvedCfg.global.harnesses?.[hid]?.effort ??
|
|
1094
|
-
null,
|
|
1095
|
-
])
|
|
1096
|
-
.filter((entry) => entry[1] !== null)),
|
|
1115
|
+
return buildTaskContract(input, taskId, mode, {
|
|
1116
|
+
paidBudget: this.deps.paidBudget,
|
|
1117
|
+
routingGoal: this.deps.routingGoal,
|
|
1097
1118
|
});
|
|
1098
1119
|
}
|
|
1099
1120
|
/**
|
|
@@ -1172,9 +1193,9 @@ export class Orchestrator {
|
|
|
1172
1193
|
/**
|
|
1173
1194
|
* D-16: the WorkReport transport envelope for one route. Called at every
|
|
1174
1195
|
* task-producing spec-build site AFTER harnessSpecKnobs so it OVERRIDES the
|
|
1175
|
-
* plain caller-schema transport with the
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1196
|
+
* plain caller-schema transport with the resolved WorkReport channel on
|
|
1197
|
+
* capable routes. The returned `mode` is retained by the caller and handed
|
|
1198
|
+
* to `unwrapWorkReportEnvelope` when the answer is finalized.
|
|
1178
1199
|
*/
|
|
1179
1200
|
workReportEnvelopeFor(routed, contract, interactive) {
|
|
1180
1201
|
return resolveWorkReportEnvelope({
|
|
@@ -1188,7 +1209,7 @@ export class Orchestrator {
|
|
|
1188
1209
|
/**
|
|
1189
1210
|
* D-16: apply the resolved WorkReport transport to a built spec — set the
|
|
1190
1211
|
* envelope output_schema (constrained/side_tool routes) and APPEND the fenced
|
|
1191
|
-
*
|
|
1212
|
+
* metadata instruction (validated routes, e.g. cursor). Mutates the spec in
|
|
1192
1213
|
* place and returns the mode the answer unwrap consumes. Called at every
|
|
1193
1214
|
* task-producing spec-build site so the transport is never wired one-off.
|
|
1194
1215
|
*/
|
|
@@ -1265,7 +1286,7 @@ export class Orchestrator {
|
|
|
1265
1286
|
* Returns null (no packet, no pointer) for native resume, a fresh thread, or
|
|
1266
1287
|
* a non-thread run. Never throws: continuity failure degrades to no packet.
|
|
1267
1288
|
*/
|
|
1268
|
-
async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, store, paths, repoRoot, log) {
|
|
1289
|
+
async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, sessionFields, store, paths, repoRoot, log) {
|
|
1269
1290
|
const ctx = runInput.threadContinuity;
|
|
1270
1291
|
if (!runInput.threadId || !ctx)
|
|
1271
1292
|
return null;
|
|
@@ -1303,7 +1324,6 @@ export class Orchestrator {
|
|
|
1303
1324
|
// summary. Same credential route + scoped lane home a real read-only
|
|
1304
1325
|
// thread turn uses (INV-034/135). Best-effort in its OWN guard — a summary
|
|
1305
1326
|
// failure keeps the full mechanical packet, never drops it.
|
|
1306
|
-
const sessionFields = this.sessionSpecFields(runInput, harnessId);
|
|
1307
1327
|
req.cachedSummary = await resolveContinuitySummary({
|
|
1308
1328
|
req,
|
|
1309
1329
|
threadId: runInput.threadId,
|
|
@@ -1371,13 +1391,15 @@ export class Orchestrator {
|
|
|
1371
1391
|
// Isolated scoped-home sessions are never retained after disposal.
|
|
1372
1392
|
const inPlaceEnvelope = envelope.worktree_path === envelope.repo_root;
|
|
1373
1393
|
const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
|
|
1374
|
-
const sessionFields = runInput
|
|
1394
|
+
const sessionFields = runInput
|
|
1395
|
+
? await this.sessionSpecFields(runInput, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission)
|
|
1396
|
+
: undefined;
|
|
1375
1397
|
// Continuity (INV-137): once the lane (harness + resolved profile) is known,
|
|
1376
1398
|
// build the continuation packet, materialize context/THREAD.md, and point
|
|
1377
1399
|
// the prompt at it — never embed the packet body in the prompt. Replaces the
|
|
1378
1400
|
// old static session.rebound "not_portable" phrase with a real disclosure.
|
|
1379
1401
|
const laneContinuity = runInput
|
|
1380
|
-
? 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)
|
|
1402
|
+
? 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)
|
|
1381
1403
|
: null;
|
|
1382
1404
|
let spec = HarnessRunSpec.parse({
|
|
1383
1405
|
session_id: newId("ses"),
|
|
@@ -1480,13 +1502,16 @@ export class Orchestrator {
|
|
|
1480
1502
|
try {
|
|
1481
1503
|
const watched = withInactivityWatchdog(adapter.run(runSpec), {
|
|
1482
1504
|
timeoutMs: inactivityMs,
|
|
1505
|
+
countsAsProgress: countsAsAgentProgress,
|
|
1483
1506
|
onTimeout: () => {
|
|
1484
1507
|
attemptAbort.abort();
|
|
1485
1508
|
void adapter.cancel?.(activeSessionId)?.catch(() => { });
|
|
1486
1509
|
},
|
|
1487
|
-
// Waiting on the USER
|
|
1488
|
-
//
|
|
1510
|
+
// Waiting on the USER is legitimate silence. The interaction policy
|
|
1511
|
+
// either enforces a finite deadline or waits until answer/cancel/
|
|
1512
|
+
// terminal/restart when the timeout is disabled.
|
|
1489
1513
|
isSuspended: () => (interaction?.pendingCount?.() ?? 0) > 0,
|
|
1514
|
+
suspensionVersion: () => interaction?.suspensionVersion?.() ?? 0,
|
|
1490
1515
|
});
|
|
1491
1516
|
for await (const ev of watched) {
|
|
1492
1517
|
if (signal?.aborted)
|
|
@@ -1616,13 +1641,18 @@ export class Orchestrator {
|
|
|
1616
1641
|
// W5.4 failover: a typed-limit hit rebuilds the spec on a NEW vendor
|
|
1617
1642
|
// session under the next profile with provenance (vendor_limit_rejected).
|
|
1618
1643
|
if (harnessErrored && runInput && !signal?.aborted) {
|
|
1644
|
+
const rotationPolicy = this.profilePolicy(contract.repo.root, adapter.id);
|
|
1645
|
+
const readyProfileIds = sawTypedLimit && deliverableEmpty && rotationPolicy.limit_action === "rotate"
|
|
1646
|
+
? await this.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles)
|
|
1647
|
+
: new Set();
|
|
1619
1648
|
const rotated = rotateSpecOnTypedLimit({
|
|
1620
1649
|
spec,
|
|
1621
1650
|
harnessId: adapter.id,
|
|
1622
1651
|
attemptId,
|
|
1623
|
-
policy:
|
|
1652
|
+
policy: rotationPolicy,
|
|
1624
1653
|
registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
|
|
1625
1654
|
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
1655
|
+
readyProfileIds,
|
|
1626
1656
|
triedProfiles,
|
|
1627
1657
|
sawTypedLimit,
|
|
1628
1658
|
deliverableEmpty,
|
|
@@ -1679,7 +1709,7 @@ export class Orchestrator {
|
|
|
1679
1709
|
if (webUnsatisfied(telemetry)) {
|
|
1680
1710
|
errors.push(webEvidenceFailure(telemetry.web));
|
|
1681
1711
|
}
|
|
1682
|
-
// D-16:
|
|
1712
|
+
// D-16: remove the WorkReport transport so answer.md persists only the deliverable.
|
|
1683
1713
|
const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
|
|
1684
1714
|
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
1685
1715
|
});
|
|
@@ -1814,66 +1844,17 @@ export class Orchestrator {
|
|
|
1814
1844
|
// Thin delegate — the channel mechanics live in interaction.ts.
|
|
1815
1845
|
return interactionChannelFor(input, log, runId, taskId, attemptId, harnessId, supportsInteractive, DEFAULT_INTERACTION_TIMEOUT_MS);
|
|
1816
1846
|
}
|
|
1817
|
-
/**
|
|
1818
|
-
* Guarantee a git boundary for write-mode runs. Non-git project folders are
|
|
1819
|
-
* initialized in place (`git init`, deterministic baseline commit) without
|
|
1820
|
-
* creating or editing `.gitignore`, and the action is announced via a
|
|
1821
|
-
* `project.git.initialized` event. Returns the failure message when the
|
|
1822
|
-
* boundary cannot be established (the terminal failure events are already
|
|
1823
|
-
* emitted); null on success.
|
|
1824
|
-
*/
|
|
1825
|
-
async ensureWriteModeGitBoundary(repoRoot, log, store, paths, runId, mode) {
|
|
1826
|
-
if (repoRoot === NO_PROJECT_ROOT)
|
|
1827
|
-
return null;
|
|
1828
|
-
try {
|
|
1829
|
-
const result = await ensureGitRepository(repoRoot);
|
|
1830
|
-
if (result.initialized || result.baselineCommitted) {
|
|
1831
|
-
log.emit("project.git.initialized", {
|
|
1832
|
-
repo_root: repoRoot,
|
|
1833
|
-
initialized: result.initialized,
|
|
1834
|
-
baseline_committed: result.baselineCommitted,
|
|
1835
|
-
gitignore_seeded: result.gitignoreSeeded,
|
|
1836
|
-
head_sha: result.headSha,
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
return null;
|
|
1840
|
-
}
|
|
1841
|
-
catch (err) {
|
|
1842
|
-
const message = safeErrorMessage(err);
|
|
1843
|
-
writeFailure(store, paths, {
|
|
1844
|
-
phase: "workspace",
|
|
1845
|
-
category: "project",
|
|
1846
|
-
safeMessage: message,
|
|
1847
|
-
runDir: paths.root,
|
|
1848
|
-
nextActions: [
|
|
1849
|
-
"Check the project folder permissions",
|
|
1850
|
-
"Initialize git manually (git init)",
|
|
1851
|
-
"Retry the run",
|
|
1852
|
-
],
|
|
1853
|
-
});
|
|
1854
|
-
store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: failed\n- Phase: workspace\n\n${message}\n`);
|
|
1855
|
-
log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
|
|
1856
|
-
log.emit("run.failed", {
|
|
1857
|
-
lifecycle: "failed",
|
|
1858
|
-
facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
|
|
1859
|
-
reason: "harness_failed",
|
|
1860
|
-
phase: "workspace",
|
|
1861
|
-
error: message,
|
|
1862
|
-
failure_ref: "final/failure.yaml",
|
|
1863
|
-
});
|
|
1864
|
-
return message;
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
1847
|
/**
|
|
1868
1848
|
* D-14 layer 3 (AGENTS.md unification, INV-113): the ONE new live-tree write.
|
|
1869
1849
|
* When the PROJECT root has `AGENTS.md` and no `CLAUDE.md`, drop a thin
|
|
1870
1850
|
* `CLAUDE.md` (`@AGENTS.md` import + Claudexor ownership marker) so a Claude
|
|
1871
1851
|
* Code route reads the same instruction file codex/cursor read natively.
|
|
1872
1852
|
*
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1875
|
-
* untouched
|
|
1876
|
-
*
|
|
1853
|
+
* The project-root bridge has its own narrower fence: read-only modes never
|
|
1854
|
+
* reach this run-prep stage and `--in-place` stateful targets are left
|
|
1855
|
+
* untouched. Git admission is independently owned by `runStartRequiresGit`.
|
|
1856
|
+
* The write targets the PROJECT root (`repoRoot`), never a worktree envelope.
|
|
1857
|
+
* The workspace helper adds exclusive-create + no-follow +
|
|
1877
1858
|
* idempotency, so a hand-written or symlinked `CLAUDE.md` is never overwritten
|
|
1878
1859
|
* and a concurrent/second prep is a no-op. Announced via a typed
|
|
1879
1860
|
* `project.claude_bridge.created` event on an actual create only — the git-init
|
|
@@ -1898,37 +1879,6 @@ export class Orchestrator {
|
|
|
1898
1879
|
});
|
|
1899
1880
|
}
|
|
1900
1881
|
}
|
|
1901
|
-
/**
|
|
1902
|
-
* Freeze-on-implement delivery (D17/D27): verify the frozen plan's hash and
|
|
1903
|
-
* materialize it as context/PLAN.md in the run artifact tree — OUTSIDE every
|
|
1904
|
-
* worktree, so it can never dirty a diff — then point the prompt at the
|
|
1905
|
-
* absolute path. A mismatched or unreadable plan fails LOUDLY before any
|
|
1906
|
-
* harness spawns (the tamper fence; retry replays planRef verbatim, so a
|
|
1907
|
-
* retried implement can never silently run without its plan).
|
|
1908
|
-
*/
|
|
1909
|
-
withPlanBrief(input, store, paths, log) {
|
|
1910
|
-
if (!input.planRef)
|
|
1911
|
-
return input;
|
|
1912
|
-
const text = readTextSafe(input.planRef.path);
|
|
1913
|
-
if (!text || !text.trim()) {
|
|
1914
|
-
throw new Error(`implement plan: the frozen plan at ${input.planRef.path} is missing or unreadable`);
|
|
1915
|
-
}
|
|
1916
|
-
const digest = sha256(text).replace(/^sha256:/, "");
|
|
1917
|
-
if (digest !== input.planRef.sha256) {
|
|
1918
|
-
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`);
|
|
1919
|
-
}
|
|
1920
|
-
const briefPath = join(paths.contextDir, "PLAN.md");
|
|
1921
|
-
store.writeText(briefPath, text);
|
|
1922
|
-
log.emit("plan.brief.materialized", {
|
|
1923
|
-
plan_run_id: input.planRef.runId,
|
|
1924
|
-
sha256: input.planRef.sha256,
|
|
1925
|
-
path: "context/PLAN.md",
|
|
1926
|
-
});
|
|
1927
|
-
return {
|
|
1928
|
-
...input,
|
|
1929
|
-
prompt: `${input.prompt}\n\nThe approved plan is at: ${briefPath} — read it before starting and re-read it as needed.`,
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
1882
|
async runRace(input, mode, announce) {
|
|
1933
1883
|
const taskId = input.taskId ?? newId("task");
|
|
1934
1884
|
const runId = input.runId ?? newId("run");
|
|
@@ -1936,20 +1886,28 @@ export class Orchestrator {
|
|
|
1936
1886
|
// announced: a refused run must fail the request loudly, not 200 a runId
|
|
1937
1887
|
// and leave an orphaned run dir without a terminal event.
|
|
1938
1888
|
const contract = this.buildContract(input, taskId, mode);
|
|
1939
|
-
const
|
|
1940
|
-
const
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1889
|
+
const planBrief = verifiedPlanBrief(input);
|
|
1890
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
1891
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
1892
|
+
input,
|
|
1893
|
+
contract,
|
|
1894
|
+
quotaSnapshots,
|
|
1895
|
+
store: this.artifactStore(input),
|
|
1896
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
1897
|
+
runId,
|
|
1898
|
+
taskId,
|
|
1899
|
+
mode,
|
|
1900
|
+
phase: "race",
|
|
1901
|
+
prompt: input.prompt,
|
|
1902
|
+
}, announce);
|
|
1903
|
+
input = withPlanBrief(input, store, paths, log, planBrief);
|
|
1904
|
+
// The execution root is the tree the harness mutates: the project itself
|
|
1905
|
+
// for in-place threads/ordinary runs, or the thread's persistent worktree
|
|
1906
|
+
// for an isolated thread. Config/artifacts/contract stay anchored to
|
|
1907
|
+
// repoRoot. Both the WorkspaceManager and the git boundary resolve against
|
|
1908
|
+
// this SINGLE root.
|
|
1947
1909
|
const execRoot = this.execRootOf(input);
|
|
1948
1910
|
const wsm = new WorkspaceManager(execRoot);
|
|
1949
|
-
safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
|
|
1950
|
-
log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
|
|
1951
|
-
const ledger = this.rootLedger(input, contract, log);
|
|
1952
|
-
announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "race" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
|
|
1953
1911
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
1954
1912
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
1955
1913
|
// Write modes need a git boundary for worktree isolation and honest diffs.
|
|
@@ -1958,17 +1916,17 @@ export class Orchestrator {
|
|
|
1958
1916
|
// silent mutation (user-locked decision, comparator: Codex requires git).
|
|
1959
1917
|
// For an isolated thread the execution root is already a git worktree, so
|
|
1960
1918
|
// this is a no-op there; for in-place it ensures the live project is git.
|
|
1961
|
-
const gitPreconditionError = await
|
|
1919
|
+
const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
|
|
1962
1920
|
if (gitPreconditionError) {
|
|
1963
1921
|
return {
|
|
1964
1922
|
runId,
|
|
1965
1923
|
taskId,
|
|
1966
1924
|
mode,
|
|
1967
1925
|
lifecycle: "failed",
|
|
1968
|
-
facts: makeOutcomeFacts("failed", { reason:
|
|
1926
|
+
facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
|
|
1969
1927
|
winner: null,
|
|
1970
1928
|
runDir: paths.root,
|
|
1971
|
-
summary: gitPreconditionError,
|
|
1929
|
+
summary: gitPreconditionError.message,
|
|
1972
1930
|
candidates: [],
|
|
1973
1931
|
};
|
|
1974
1932
|
}
|
|
@@ -2066,7 +2024,7 @@ export class Orchestrator {
|
|
|
2066
2024
|
attemptId,
|
|
2067
2025
|
intent: this.candidateIntent(input),
|
|
2068
2026
|
harnessId: routed.adapter.id,
|
|
2069
|
-
cost: attemptCostEvidence(routed.adapter.id, attemptId, i > 0
|
|
2027
|
+
cost: attemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id)),
|
|
2070
2028
|
});
|
|
2071
2029
|
log.emit("budget.lease.created", {
|
|
2072
2030
|
granted: lease.granted,
|
|
@@ -2232,6 +2190,13 @@ export class Orchestrator {
|
|
|
2232
2190
|
continuation_count: candidateContinuationCount,
|
|
2233
2191
|
packet_turns: packet.continuity.disclosure.packetTurns,
|
|
2234
2192
|
});
|
|
2193
|
+
log.emit("harness.started", {
|
|
2194
|
+
harness_id: adapter.id,
|
|
2195
|
+
attempt_id: contAttemptId,
|
|
2196
|
+
external_context_policy: knobs.webPolicy,
|
|
2197
|
+
...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
|
|
2198
|
+
continuation_of: run.attemptId,
|
|
2199
|
+
});
|
|
2235
2200
|
const contLeaseId = contLease.lease?.lease_id ?? "";
|
|
2236
2201
|
try {
|
|
2237
2202
|
const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
|
|
@@ -2519,7 +2484,7 @@ export class Orchestrator {
|
|
|
2519
2484
|
// candidates (a trivial greeting in agent mode no longer burns two reviewers on
|
|
2520
2485
|
// "(empty diff)"). Candidates still flow through arbitration/gates so the
|
|
2521
2486
|
// no_op/answer outcome and gate failures are unchanged.
|
|
2522
|
-
evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
|
|
2487
|
+
evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
|
|
2523
2488
|
}
|
|
2524
2489
|
catch (err) {
|
|
2525
2490
|
// Review preflight/evidence failures end TERMINALLY with artifacts —
|
|
@@ -2544,7 +2509,7 @@ export class Orchestrator {
|
|
|
2544
2509
|
attemptId: "synth",
|
|
2545
2510
|
intent: "synthesize",
|
|
2546
2511
|
harnessId: synthRouted.adapter.id,
|
|
2547
|
-
cost: attemptCostEvidence(synthRouted.adapter.id, "synth",
|
|
2512
|
+
cost: attemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id)),
|
|
2548
2513
|
});
|
|
2549
2514
|
if (lease.granted) {
|
|
2550
2515
|
let envelope;
|
|
@@ -2580,7 +2545,7 @@ export class Orchestrator {
|
|
|
2580
2545
|
runs.push(run);
|
|
2581
2546
|
try {
|
|
2582
2547
|
if (isWorkingCandidate(run)) {
|
|
2583
|
-
const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
|
|
2548
|
+
const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
|
|
2584
2549
|
evidences.push(...synthEvidence);
|
|
2585
2550
|
workingRuns.push(run);
|
|
2586
2551
|
}
|
|
@@ -2629,8 +2594,12 @@ export class Orchestrator {
|
|
|
2629
2594
|
// Winner can only be a candidate that actually produced work; corpses are
|
|
2630
2595
|
// excluded from arbitration upstream and from the fallback here.
|
|
2631
2596
|
const winnerRun = workingRuns.find((r) => r.attemptId === result.decision.winner) ?? workingRuns[0];
|
|
2632
|
-
|
|
2633
|
-
|
|
2597
|
+
const winnerEvidence = winnerRun
|
|
2598
|
+
? evidences.find((e) => e.attemptId === winnerRun.attemptId)
|
|
2599
|
+
: undefined;
|
|
2600
|
+
// D9 winner-only NEEDS_HUMAN gate, fail-closed on a winner with no review
|
|
2601
|
+
// evidence record (see winnerNeedsHuman).
|
|
2602
|
+
const needsHuman = winnerNeedsHuman(winnerRun?.attemptId ?? null, evidences);
|
|
2634
2603
|
// Run-level review_verified is the WINNER's verification: an
|
|
2635
2604
|
// empty-diff loser's unverified route must not drag the shipped result's
|
|
2636
2605
|
// flag false. No winner -> fall back to the all-candidates view.
|
|
@@ -2693,12 +2662,11 @@ export class Orchestrator {
|
|
|
2693
2662
|
})) {
|
|
2694
2663
|
log.emit("output.ready", { kind: "artifact", path });
|
|
2695
2664
|
}
|
|
2696
|
-
assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
|
|
2665
|
+
secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
|
|
2697
2666
|
const patchSha256 = sha256(winnerRun.diff);
|
|
2698
2667
|
store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
|
|
2699
2668
|
const wstats = diffStats(winnerRun.diff);
|
|
2700
2669
|
const hasDiff = winnerRun.diff.trim().length > 0;
|
|
2701
|
-
const winnerEvidence = evidences.find((e) => e.attemptId === winnerRun.attemptId);
|
|
2702
2670
|
const blockers = winnerEvidence
|
|
2703
2671
|
? winnerEvidence.findings.filter((f) => isBlocking(f)).length
|
|
2704
2672
|
: 0;
|
|
@@ -3011,7 +2979,7 @@ export class Orchestrator {
|
|
|
3011
2979
|
env: reviewHome.env,
|
|
3012
2980
|
}).finally(() => reviewHome.dispose());
|
|
3013
2981
|
}
|
|
3014
|
-
async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal) {
|
|
2982
|
+
async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd) {
|
|
3015
2983
|
const evidences = [];
|
|
3016
2984
|
for (const run of runs) {
|
|
3017
2985
|
const candidateCwd = run.reviewCwd ?? cwd;
|
|
@@ -3030,7 +2998,7 @@ export class Orchestrator {
|
|
|
3030
2998
|
attemptId: run.attemptId,
|
|
3031
2999
|
intent: "review",
|
|
3032
3000
|
harnessId: "review-panel",
|
|
3033
|
-
cost: attemptCostEvidence("review-panel", run.attemptId),
|
|
3001
|
+
cost: attemptCostEvidence("review-panel", run.attemptId, reservationEstimateUsd),
|
|
3034
3002
|
})
|
|
3035
3003
|
: undefined;
|
|
3036
3004
|
const result = hasDiff && reviewers.length > 0 && (reviewLease?.granted ?? true)
|
|
@@ -3174,10 +3142,21 @@ export class Orchestrator {
|
|
|
3174
3142
|
const runId = input.runId ?? newId("run");
|
|
3175
3143
|
// Contract validation BEFORE the run is announced (see runRace).
|
|
3176
3144
|
const contract = this.buildContract(input, taskId, mode);
|
|
3177
|
-
const
|
|
3178
|
-
const
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3145
|
+
const planBrief = verifiedPlanBrief(input);
|
|
3146
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
3147
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
3148
|
+
input,
|
|
3149
|
+
contract,
|
|
3150
|
+
quotaSnapshots,
|
|
3151
|
+
store: this.artifactStore(input),
|
|
3152
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
3153
|
+
runId,
|
|
3154
|
+
taskId,
|
|
3155
|
+
mode,
|
|
3156
|
+
phase: "convergence",
|
|
3157
|
+
prompt: input.prompt,
|
|
3158
|
+
}, announce);
|
|
3159
|
+
input = withPlanBrief(input, store, paths, log, planBrief);
|
|
3181
3160
|
// The execution root is the tree the harness mutates (thread worktree for an
|
|
3182
3161
|
// isolated thread, else the project). The WorkspaceManager AND the git
|
|
3183
3162
|
// boundary must resolve against the SAME root — the race path does so via the
|
|
@@ -3186,27 +3165,11 @@ export class Orchestrator {
|
|
|
3186
3165
|
const execRoot = this.execRootOf(input);
|
|
3187
3166
|
const wsm = new WorkspaceManager(execRoot);
|
|
3188
3167
|
const readiness = new ReadinessLedger();
|
|
3189
|
-
|
|
3190
|
-
try {
|
|
3191
|
-
ledger = this.rootLedger(input, contract, log);
|
|
3192
|
-
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
3193
|
-
safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
|
|
3194
|
-
log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
|
|
3195
|
-
announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "convergence" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
|
|
3196
|
-
}
|
|
3197
|
-
catch (error) {
|
|
3198
|
-
// A delegated child attaches its scoped financial view before the run is
|
|
3199
|
-
// announced. If any fallible artifact/start callback in that narrow gap
|
|
3200
|
-
// throws, the terminal net has no run context, so detach here explicitly.
|
|
3201
|
-
if (input.delegatedFromRunId) {
|
|
3202
|
-
this.deps.delegationBudgetAuthority?.releaseRun(runId);
|
|
3203
|
-
}
|
|
3204
|
-
throw error;
|
|
3205
|
-
}
|
|
3168
|
+
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
3206
3169
|
// Live (in-place) isolation deliberately tolerates non-git stateful
|
|
3207
3170
|
// environments; only envelope isolation needs the git boundary.
|
|
3208
3171
|
if (!input.inPlace) {
|
|
3209
|
-
const gitPreconditionError = await
|
|
3172
|
+
const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
|
|
3210
3173
|
if (gitPreconditionError) {
|
|
3211
3174
|
return {
|
|
3212
3175
|
spendUsd: ledger.spend(),
|
|
@@ -3214,10 +3177,10 @@ export class Orchestrator {
|
|
|
3214
3177
|
taskId,
|
|
3215
3178
|
mode,
|
|
3216
3179
|
lifecycle: "failed",
|
|
3217
|
-
facts: makeOutcomeFacts("failed", { reason:
|
|
3180
|
+
facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
|
|
3218
3181
|
winner: null,
|
|
3219
3182
|
runDir: paths.root,
|
|
3220
|
-
summary: gitPreconditionError,
|
|
3183
|
+
summary: gitPreconditionError.message,
|
|
3221
3184
|
candidates: [],
|
|
3222
3185
|
};
|
|
3223
3186
|
}
|
|
@@ -3349,7 +3312,8 @@ export class Orchestrator {
|
|
|
3349
3312
|
// observed quota cooldown across all harnesses, or genuine no-progress (a stall on the same
|
|
3350
3313
|
// failure signature after every available harness has tried it).
|
|
3351
3314
|
const stallThreshold = input.untilClean === true ? 4 : 2;
|
|
3352
|
-
const
|
|
3315
|
+
const convergenceQuotaLedger = this.quotaLedgerView(ledger, adapterPool);
|
|
3316
|
+
const allCooledDown = () => adapterPool.every((a) => convergenceQuotaLedger.cooldownActive(a.adapter.id));
|
|
3353
3317
|
const attemptTelemetries = [];
|
|
3354
3318
|
let lastDiffStable = true;
|
|
3355
3319
|
try {
|
|
@@ -3406,7 +3370,7 @@ export class Orchestrator {
|
|
|
3406
3370
|
attemptId,
|
|
3407
3371
|
intent: "repair",
|
|
3408
3372
|
harnessId: adapter.id,
|
|
3409
|
-
cost: attemptCostEvidence(adapter.id, attemptId,
|
|
3373
|
+
cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id)),
|
|
3410
3374
|
});
|
|
3411
3375
|
if (!lease.granted) {
|
|
3412
3376
|
exhausted = true;
|
|
@@ -3518,7 +3482,7 @@ export class Orchestrator {
|
|
|
3518
3482
|
attemptId,
|
|
3519
3483
|
intent: "review",
|
|
3520
3484
|
harnessId: "review-panel",
|
|
3521
|
-
cost: attemptCostEvidence("review-panel", attemptId),
|
|
3485
|
+
cost: attemptCostEvidence("review-panel", attemptId, this.reservationEstimateUsd(input)),
|
|
3522
3486
|
})
|
|
3523
3487
|
: null;
|
|
3524
3488
|
const reviewResult = reviewers.length > 0 && (reviewLease?.granted ?? false)
|
|
@@ -3700,7 +3664,7 @@ export class Orchestrator {
|
|
|
3700
3664
|
if (adapterPool.length > 1 && triedSinceProgress.size < adapterPool.length) {
|
|
3701
3665
|
// Quota-headroom consumer (mid-run, where quota observations EXIST);
|
|
3702
3666
|
// pick + honest route event owned by runSupport.rotateOnStall.
|
|
3703
|
-
adapterIdx = rotateOnStall(adapterPool.map((a) => a.adapter.id), adapterIdx,
|
|
3667
|
+
adapterIdx = rotateOnStall(adapterPool.map((a) => a.adapter.id), adapterIdx, convergenceQuotaLedger, triedSinceProgress, log, lastRun?.harnessId ?? null);
|
|
3704
3668
|
routed = adapterPool[adapterIdx];
|
|
3705
3669
|
adapter = routed.adapter;
|
|
3706
3670
|
}
|
|
@@ -3808,7 +3772,7 @@ export class Orchestrator {
|
|
|
3808
3772
|
// work_product (its partial patch.diff stays diagnostic via attempts/);
|
|
3809
3773
|
// in-place keeps the product so the honest Revert offer survives.
|
|
3810
3774
|
if (lastRun && (!interrupted || input.inPlace === true)) {
|
|
3811
|
-
assertNoSecretLikeTokens("final patch diff", lastRun.diff);
|
|
3775
|
+
secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
|
|
3812
3776
|
const patchSha256 = sha256(lastRun.diff);
|
|
3813
3777
|
store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
|
|
3814
3778
|
// Honest apply-state (parity with runRace single-candidate in-place): a
|
|
@@ -3978,265 +3942,77 @@ export class Orchestrator {
|
|
|
3978
3942
|
reviewVerified: actualReviewVerified,
|
|
3979
3943
|
};
|
|
3980
3944
|
}
|
|
3981
|
-
/**
|
|
3982
|
-
|
|
3983
|
-
const { input, contract, taskId, runId, log, store, paths, ledger, routed, attemptId } = args;
|
|
3984
|
-
const adapter = routed.adapter;
|
|
3985
|
-
const lease = ledger.reserve({
|
|
3986
|
-
taskId,
|
|
3987
|
-
attemptId,
|
|
3988
|
-
intent: args.intent,
|
|
3989
|
-
harnessId: adapter.id,
|
|
3990
|
-
cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
|
|
3991
|
-
});
|
|
3992
|
-
if (!lease.granted) {
|
|
3993
|
-
log.emit("budget.lease.created", {
|
|
3994
|
-
granted: false,
|
|
3995
|
-
reason: lease.reason,
|
|
3996
|
-
denied: lease.denied,
|
|
3997
|
-
attempt_id: attemptId,
|
|
3998
|
-
harness_id: adapter.id,
|
|
3999
|
-
});
|
|
4000
|
-
return {
|
|
4001
|
-
attemptId,
|
|
4002
|
-
harnessId: adapter.id,
|
|
4003
|
-
status: "failed",
|
|
4004
|
-
outcomeClass: "clean", // never spawned: refused pre-flight by the budget gate
|
|
4005
|
-
error: lease.reason ?? "budget lease denied",
|
|
4006
|
-
text: null,
|
|
4007
|
-
telemetry: null,
|
|
4008
|
-
budgetDenied: true,
|
|
4009
|
-
budgetDenial: {
|
|
4010
|
-
code: lease.denied ?? "hard_cap",
|
|
4011
|
-
reason: lease.reason ?? "budget lease denied",
|
|
4012
|
-
harnessId: adapter.id,
|
|
4013
|
-
attemptId,
|
|
4014
|
-
},
|
|
4015
|
-
};
|
|
4016
|
-
}
|
|
4017
|
-
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
4018
|
-
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
4019
|
-
const planSessionFields = this.sessionSpecFields(input, adapter.id, log);
|
|
4020
|
-
// Continuity (INV-137): a thread PLAN turn is a chat turn — hydrate a
|
|
4021
|
-
// lane switch/gap with a packet and disclose it.
|
|
4022
|
-
const laneContinuity = args.laneRun
|
|
4023
|
-
? 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)
|
|
4024
|
-
: null;
|
|
4025
|
-
const spec = HarnessRunSpec.parse({
|
|
4026
|
-
session_id: newId("ses"),
|
|
4027
|
-
intent: args.intent,
|
|
4028
|
-
prompt: laneContinuity?.pointerLine
|
|
4029
|
-
? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
|
|
4030
|
-
: args.promptBody,
|
|
4031
|
-
cwd: this.execRootOf(input),
|
|
4032
|
-
access: "readonly",
|
|
4033
|
-
// Planners must SEE any image/file the user attached (e.g. "plan a fix for
|
|
4034
|
-
// what's in this screenshot"), not just agent/race runs.
|
|
4035
|
-
attachments: input.attachments ?? [],
|
|
4036
|
-
...planSessionFields,
|
|
4037
|
-
...this.harnessSpecKnobs(contract, knobs, args.intent),
|
|
4038
|
-
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
4039
|
-
// A thread plan turn spawns in its DURABLE per-lane home so its native
|
|
4040
|
-
// session is reachable for resume next turn (INV-034); a non-thread
|
|
4041
|
-
// plan keeps the disposable route-context home.
|
|
4042
|
-
env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
|
|
4043
|
-
});
|
|
4044
|
-
const plannerAbort = new AbortController();
|
|
4045
|
-
spec.extra["abortSignal"] = input.signal
|
|
4046
|
-
? AbortSignal.any([input.signal, plannerAbort.signal])
|
|
4047
|
-
: plannerAbort.signal;
|
|
4048
|
-
const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
|
|
4049
|
-
if (planInteraction)
|
|
4050
|
-
spec.extra["interactionChannel"] = planInteraction;
|
|
4051
|
-
// D-16: compile the WorkReport envelope for the plan lane (require plan text
|
|
4052
|
-
// below folds the deliverable; the veto rides work_state).
|
|
4053
|
-
const planWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction));
|
|
4054
|
-
const planWorkMode = this.applyWorkEnvelope(spec, planWorkEnvelope);
|
|
4055
|
-
const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
|
|
4056
|
-
const answer = new AnswerAssembly();
|
|
4057
|
-
const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
|
|
4058
|
-
knobs.webPolicy === "cached" ||
|
|
4059
|
-
knobs.webPolicy === "live", effectiveWeb, [],
|
|
4060
|
-
// Requested-model capture: a plan lane silently downgraded to another
|
|
4061
|
-
// model surfaces the mismatch in its route receipt, just like agent.
|
|
4062
|
-
knobs.model);
|
|
4063
|
-
const onAbort = () => {
|
|
4064
|
-
void adapter.cancel?.(spec.session_id)?.catch(() => { });
|
|
4065
|
-
};
|
|
4066
|
-
if (input.signal) {
|
|
4067
|
-
if (input.signal.aborted)
|
|
4068
|
-
onAbort();
|
|
4069
|
-
else
|
|
4070
|
-
input.signal.addEventListener("abort", onAbort, { once: true });
|
|
4071
|
-
}
|
|
4072
|
-
let cost = 0;
|
|
4073
|
-
let costEstimated = false;
|
|
4074
|
-
let harnessError = null;
|
|
4075
|
-
const budgetSignalState = { quotaPressureDisclosed: false };
|
|
4076
|
-
try {
|
|
4077
|
-
log.emit("harness.started", {
|
|
4078
|
-
harness_id: adapter.id,
|
|
4079
|
-
attempt_id: attemptId,
|
|
4080
|
-
external_context_policy: knobs.webPolicy,
|
|
4081
|
-
...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
|
|
4082
|
-
});
|
|
4083
|
-
if (!input.signal?.aborted) {
|
|
4084
|
-
const watchedPlan = withInactivityWatchdog(adapter.run(spec), {
|
|
4085
|
-
timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
|
|
4086
|
-
onTimeout: () => {
|
|
4087
|
-
plannerAbort.abort();
|
|
4088
|
-
void adapter.cancel?.(spec.session_id)?.catch(() => { });
|
|
4089
|
-
},
|
|
4090
|
-
isSuspended: () => (planInteraction?.pendingCount?.() ?? 0) > 0,
|
|
4091
|
-
});
|
|
4092
|
-
for await (const ev of watchedPlan) {
|
|
4093
|
-
if (input.signal?.aborted)
|
|
4094
|
-
break;
|
|
4095
|
-
const safeEv = redactHarnessEvent(ev);
|
|
4096
|
-
safeInvoke(input.onHarnessEvent, safeEv);
|
|
4097
|
-
// A thread PLAN turn IS a chat turn now (INV-034): its native
|
|
4098
|
-
// session lives in the DURABLE per-lane home, so record it for the
|
|
4099
|
-
// next lane turn's resume. Council members are distinct lanes.
|
|
4100
|
-
if (args.laneRun)
|
|
4101
|
-
observeNativeSessionEvent(input, adapter.id, safeEv);
|
|
4102
|
-
observeAuthSwitch(log, adapter.id, attemptId, safeEv);
|
|
4103
|
-
log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
|
|
4104
|
-
appendLine(attemptEventsPath, JSON.stringify(safeEv));
|
|
4105
|
-
observeAttemptTelemetry(telemetry, safeEv);
|
|
4106
|
-
if (safeEv.plan_progress) {
|
|
4107
|
-
log.emit("plan.progress", {
|
|
4108
|
-
attempt_id: attemptId,
|
|
4109
|
-
harness_id: adapter.id,
|
|
4110
|
-
items: safeEv.plan_progress.items,
|
|
4111
|
-
});
|
|
4112
|
-
}
|
|
4113
|
-
// read-only routes burn quota too — same single owner as the agent loop.
|
|
4114
|
-
observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
|
|
4115
|
-
this.deps.quotaEventSink?.(adapter.id, safeEv);
|
|
4116
|
-
if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
|
|
4117
|
-
cost += safeEv.usage.cost_usd;
|
|
4118
|
-
if (safeEv.usage.estimated)
|
|
4119
|
-
costEstimated = true;
|
|
4120
|
-
log.emit("budget.observation", {
|
|
4121
|
-
harness_id: adapter.id,
|
|
4122
|
-
attempt_id: attemptId,
|
|
4123
|
-
kind: "spend",
|
|
4124
|
-
usd: safeEv.usage.cost_usd,
|
|
4125
|
-
estimated: safeEv.usage.estimated === true,
|
|
4126
|
-
});
|
|
4127
|
-
}
|
|
4128
|
-
// A TYPED final message wins verbatim over joined narration.
|
|
4129
|
-
answer.observe(safeEv);
|
|
4130
|
-
if (safeEv.type === "error")
|
|
4131
|
-
harnessError = safeEv.error ? redactSecrets(safeEv.error) : "harness emitted an error";
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
}
|
|
4135
|
-
catch (err) {
|
|
4136
|
-
harnessError = safeErrorMessage(err);
|
|
4137
|
-
}
|
|
4138
|
-
finally {
|
|
4139
|
-
input.signal?.removeEventListener("abort", onAbort);
|
|
4140
|
-
ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
|
|
4141
|
-
}
|
|
4142
|
-
// D-16: unwrap and require PLAN TEXT — a plan with no text is not delivered.
|
|
4143
|
-
// The unwrap runs BEFORE the error axes: the deliverable it yields is what
|
|
4144
|
-
// decides whether an unrecovered tool error is fatal (explorer parity).
|
|
4145
|
-
const planUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", planWorkMode, {
|
|
4146
|
-
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
4147
|
-
});
|
|
4148
|
-
const planText = redactSecrets(planUnwrapped.deliverable).trim();
|
|
4149
|
-
const unrecovered = unrecoveredToolErrors(telemetry);
|
|
4150
|
-
const webBlocked = webUnsatisfied(telemetry);
|
|
4151
|
-
if (!harnessError && webBlocked) {
|
|
4152
|
-
harnessError = webEvidenceFailure(telemetry.web);
|
|
4153
|
-
}
|
|
4154
|
-
// INV-043/INV-044, explorer parity: a DELIVERED plan keeps an unrecovered
|
|
4155
|
-
// non-web tool error as warning evidence instead of discarding the plan (see
|
|
4156
|
-
// the helper). Web keeps its hard gate above; the finalizer outranks both.
|
|
4157
|
-
harnessError ??= unrecoveredToolErrorFailure(unrecovered, planText.length > 0);
|
|
4158
|
-
const planFinalized = finalizeAttempt({
|
|
4159
|
-
deliverableEvidence: planText.length > 0,
|
|
4160
|
-
harnessErrored: harnessError !== null && !webBlocked,
|
|
4161
|
-
workReport: planUnwrapped.workReport,
|
|
4162
|
-
workReportSource: planUnwrapped.source,
|
|
4163
|
-
workReportViolation: planUnwrapped.contractViolation,
|
|
4164
|
-
contextTerminalExhausted: telemetry.contextExhausted,
|
|
4165
|
-
});
|
|
4166
|
-
// A broken WorkReport contract is a hard failure only when the finalizer
|
|
4167
|
-
// ranked it so (a terminal context exhaustion outranks it).
|
|
4168
|
-
if (!harnessError && planFinalized.outcomeClass === "contract_failure") {
|
|
4169
|
-
harnessError = `work_report contract: ${planUnwrapped.contractViolation}`;
|
|
4170
|
-
}
|
|
4171
|
-
// D-16 r9: an interrupted (context-exhausted) planner is NEVER a clean
|
|
4172
|
-
// plan — partial text must not become final/plan.md as success. A VETO
|
|
4173
|
-
// (needs_input/incomplete work_state) is DIFFERENT by the sealed D-16
|
|
4174
|
-
// contract (X35, INV-116 canaries): the plan still delivers, lifecycle
|
|
4175
|
-
// succeeded, and the work_state veto rides the OUTCOME (non-zero exit) —
|
|
4176
|
-
// it must not be laundered into a harness failure either direction.
|
|
4177
|
-
if (!harnessError && planFinalized.outcomeClass === "interrupted") {
|
|
4178
|
-
harnessError = "context capacity exhausted before the plan completed";
|
|
4179
|
-
}
|
|
4180
|
-
const attemptError = harnessError ??
|
|
4181
|
-
(planFinalized.deliverablePresent ? null : "planner produced no plan text") ??
|
|
4182
|
-
(input.signal?.aborted ? "planner cancelled" : null);
|
|
4183
|
-
setAttemptOutcome(telemetry, {
|
|
4184
|
-
deliverablePresent: planFinalized.deliverablePresent,
|
|
4185
|
-
gatesPassed: null,
|
|
4186
|
-
harnessErrored: (harnessError !== null && !webBlocked) || planFinalized.harnessErrored,
|
|
4187
|
-
webRequiredUnsatisfied: webBlocked,
|
|
4188
|
-
workState: planFinalized.workState,
|
|
4189
|
-
});
|
|
4190
|
-
if (attemptError) {
|
|
4191
|
-
log.emit("harness.completed", {
|
|
4192
|
-
harness_id: adapter.id,
|
|
4193
|
-
attempt_id: attemptId,
|
|
4194
|
-
status: webBlocked ? "blocked" : "failed",
|
|
4195
|
-
error: attemptError,
|
|
4196
|
-
...telemetrySummary(telemetry),
|
|
4197
|
-
});
|
|
4198
|
-
return {
|
|
4199
|
-
attemptId,
|
|
4200
|
-
harnessId: adapter.id,
|
|
4201
|
-
status: webBlocked ? "blocked" : "failed",
|
|
4202
|
-
outcomeClass: planFinalized.outcomeClass,
|
|
4203
|
-
error: attemptError,
|
|
4204
|
-
text: null,
|
|
4205
|
-
telemetry,
|
|
4206
|
-
budgetDenied: false,
|
|
4207
|
-
};
|
|
4208
|
-
}
|
|
4209
|
-
const text = planText || "(no output)";
|
|
4210
|
-
log.emit("harness.completed", {
|
|
4211
|
-
harness_id: adapter.id,
|
|
4212
|
-
attempt_id: attemptId,
|
|
4213
|
-
status: "success",
|
|
4214
|
-
...telemetrySummary(telemetry),
|
|
4215
|
-
});
|
|
3945
|
+
/** Bind private route/session preparation to the planner-attempt owner. */
|
|
3946
|
+
plannerAttemptDeps() {
|
|
4216
3947
|
return {
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
3948
|
+
billingKnowledge: (input, harnessId) => this.routeBillingKnowledge(input, harnessId),
|
|
3949
|
+
inactivityTimeoutMs: (repoRoot) => harnessInactivityTimeoutMs(this.config(repoRoot)),
|
|
3950
|
+
quotaEventSink: this.deps.quotaEventSink,
|
|
3951
|
+
prepare: async (args) => {
|
|
3952
|
+
const { input, contract, taskId, runId, log, store, paths, routed, attemptId } = args;
|
|
3953
|
+
const adapter = routed.adapter;
|
|
3954
|
+
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
3955
|
+
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
3956
|
+
const sessionFields = await this.sessionSpecFields(input, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission);
|
|
3957
|
+
const laneContinuity = args.laneRun
|
|
3958
|
+
? 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)
|
|
3959
|
+
: null;
|
|
3960
|
+
const spec = HarnessRunSpec.parse({
|
|
3961
|
+
session_id: newId("ses"),
|
|
3962
|
+
intent: args.intent,
|
|
3963
|
+
prompt: laneContinuity?.pointerLine
|
|
3964
|
+
? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
|
|
3965
|
+
: args.promptBody,
|
|
3966
|
+
cwd: this.execRootOf(input),
|
|
3967
|
+
access: "readonly",
|
|
3968
|
+
attachments: input.attachments ?? [],
|
|
3969
|
+
...sessionFields,
|
|
3970
|
+
...this.harnessSpecKnobs(contract, knobs, args.intent),
|
|
3971
|
+
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
3972
|
+
env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
|
|
3973
|
+
});
|
|
3974
|
+
const plannerAbort = new AbortController();
|
|
3975
|
+
spec.extra["abortSignal"] = input.signal
|
|
3976
|
+
? AbortSignal.any([input.signal, plannerAbort.signal])
|
|
3977
|
+
: plannerAbort.signal;
|
|
3978
|
+
const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
|
|
3979
|
+
if (planInteraction)
|
|
3980
|
+
spec.extra["interactionChannel"] = planInteraction;
|
|
3981
|
+
const planWorkMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
|
|
3982
|
+
return {
|
|
3983
|
+
knobs,
|
|
3984
|
+
effectiveWeb,
|
|
3985
|
+
spec,
|
|
3986
|
+
plannerAbort,
|
|
3987
|
+
planInteraction,
|
|
3988
|
+
planWorkMode,
|
|
3989
|
+
};
|
|
3990
|
+
},
|
|
4225
3991
|
};
|
|
4226
3992
|
}
|
|
3993
|
+
/** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
|
|
3994
|
+
async runPlannerAttempt(args) {
|
|
3995
|
+
return executePlannerAttempt(this.plannerAttemptDeps(), args);
|
|
3996
|
+
}
|
|
4227
3997
|
async runPlan(input, announce) {
|
|
4228
3998
|
const taskId = input.taskId ?? newId("task");
|
|
4229
3999
|
const runId = input.runId ?? newId("run");
|
|
4230
4000
|
// Plan runs get the same immutable contract truth as every other mode;
|
|
4231
4001
|
// contract validation runs BEFORE the run is announced (see runRace).
|
|
4232
4002
|
const contract = this.buildContract(input, taskId, "plan");
|
|
4233
|
-
const
|
|
4234
|
-
const paths =
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4003
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
4004
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
4005
|
+
input,
|
|
4006
|
+
contract,
|
|
4007
|
+
quotaSnapshots,
|
|
4008
|
+
store: this.artifactStore(input),
|
|
4009
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
4010
|
+
runId,
|
|
4011
|
+
taskId,
|
|
4012
|
+
mode: "plan",
|
|
4013
|
+
phase: "plan",
|
|
4014
|
+
prompt: input.prompt,
|
|
4015
|
+
}, announce);
|
|
4240
4016
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
4241
4017
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
4242
4018
|
// W3.3: ONE resolved read-only context — the routing point-probe and every
|
|
@@ -4344,6 +4120,7 @@ export class Orchestrator {
|
|
|
4344
4120
|
roHome,
|
|
4345
4121
|
contextSection,
|
|
4346
4122
|
laneRun,
|
|
4123
|
+
estimateUsdFloor: this.estimateUsdFloor(input.repoRoot),
|
|
4347
4124
|
});
|
|
4348
4125
|
}
|
|
4349
4126
|
const plans = [];
|
|
@@ -4373,6 +4150,7 @@ export class Orchestrator {
|
|
|
4373
4150
|
fallbackHome: roHome.env,
|
|
4374
4151
|
promptBody: planPrompt(input.prompt) + contextSection,
|
|
4375
4152
|
intent: "plan",
|
|
4153
|
+
reservationEstimateUsd: this.reservationEstimateUsd(input),
|
|
4376
4154
|
});
|
|
4377
4155
|
if (outcome.budgetDenied) {
|
|
4378
4156
|
// QA-050: retain the denied planner slot before breaking so the
|
|
@@ -4451,7 +4229,6 @@ export class Orchestrator {
|
|
|
4451
4229
|
}
|
|
4452
4230
|
if (plans.length === 0) {
|
|
4453
4231
|
return writePlanHarnessFailure(this.planRunDeps(), {
|
|
4454
|
-
input,
|
|
4455
4232
|
contract,
|
|
4456
4233
|
taskId,
|
|
4457
4234
|
runId,
|
|
@@ -4520,17 +4297,8 @@ export class Orchestrator {
|
|
|
4520
4297
|
defaultPrompt: "Explore this repository and synthesize evidence-cited findings, omissions, and follow-up questions.",
|
|
4521
4298
|
}, announce);
|
|
4522
4299
|
}
|
|
4523
|
-
|
|
4524
|
-
return
|
|
4525
|
-
}
|
|
4526
|
-
rootLedger(input, contract, log) {
|
|
4527
|
-
return createRootLedger({
|
|
4528
|
-
input,
|
|
4529
|
-
contract,
|
|
4530
|
-
log,
|
|
4531
|
-
authority: this.deps.delegationBudgetAuthority,
|
|
4532
|
-
quotaSnapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
4533
|
-
});
|
|
4300
|
+
quotaSnapshotPreflight() {
|
|
4301
|
+
return [...(this.deps.quotaSnapshots?.() ?? [])].map((snapshot) => QuotaSnapshotSchema.parse(snapshot));
|
|
4534
4302
|
}
|
|
4535
4303
|
routeBillingKnowledge(input, harnessId) {
|
|
4536
4304
|
// A selected profile's credential_kind decides billing (round-18 #2).
|
|
@@ -4560,10 +4328,10 @@ export class Orchestrator {
|
|
|
4560
4328
|
// scout reserve) so a subscription route is not refused for lacking a
|
|
4561
4329
|
// cash quote.
|
|
4562
4330
|
attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId)),
|
|
4563
|
-
buildSpec: (routed, homeEnv, prompt, attemptId) => {
|
|
4331
|
+
buildSpec: async (routed, homeEnv, prompt, attemptId) => {
|
|
4564
4332
|
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
4565
4333
|
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
4566
|
-
const sessionFields = this.sessionSpecFields(input, routed.adapter.id, log);
|
|
4334
|
+
const sessionFields = await this.sessionSpecFields(input, routed.adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission);
|
|
4567
4335
|
const spec = HarnessRunSpec.parse({
|
|
4568
4336
|
session_id: newId("ses"),
|
|
4569
4337
|
intent: "synthesize",
|
|
@@ -4604,13 +4372,19 @@ export class Orchestrator {
|
|
|
4604
4372
|
// Contract validation BEFORE the run is announced (see runRace). The
|
|
4605
4373
|
// recorded user intent is the CALLER's goal.
|
|
4606
4374
|
const contract = this.buildContract({ ...input, prompt: opts.contractIntent ?? prompt }, taskId, opts.mode);
|
|
4607
|
-
const
|
|
4608
|
-
const paths =
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4375
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
4376
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
4377
|
+
input,
|
|
4378
|
+
contract,
|
|
4379
|
+
quotaSnapshots,
|
|
4380
|
+
store: this.artifactStore(input),
|
|
4381
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
4382
|
+
runId,
|
|
4383
|
+
taskId,
|
|
4384
|
+
mode: opts.mode,
|
|
4385
|
+
phase: "report",
|
|
4386
|
+
prompt,
|
|
4387
|
+
}, announce);
|
|
4614
4388
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
4615
4389
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
4616
4390
|
// The ask/deep-scan report stays bare (its scouts read the tree themselves);
|
|
@@ -4752,10 +4526,10 @@ export class Orchestrator {
|
|
|
4752
4526
|
intent: opts.intent,
|
|
4753
4527
|
harnessId: adapter.id,
|
|
4754
4528
|
// QA-019: an n>1 deep-scan scout admits under a FINITE estimate floor
|
|
4755
|
-
// (mirror of the candidate loop): the first scout reserves
|
|
4756
|
-
// floor
|
|
4757
|
-
//
|
|
4758
|
-
cost: attemptCostEvidence(adapter.id, attemptId, opts.deepScan && idx > 0
|
|
4529
|
+
// (mirror of the candidate loop): the first top-level scout reserves
|
|
4530
|
+
// without a floor; later scouts and every real Delegate child pass the
|
|
4531
|
+
// repo floor because they overlap an existing family unit.
|
|
4532
|
+
cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id)),
|
|
4759
4533
|
});
|
|
4760
4534
|
if (!lease.granted) {
|
|
4761
4535
|
log.emit("budget.lease.created", {
|
|
@@ -4784,67 +4558,120 @@ export class Orchestrator {
|
|
|
4784
4558
|
}
|
|
4785
4559
|
return { status: "budget_denied", reason: lease.reason ?? "budget lease denied" };
|
|
4786
4560
|
}
|
|
4787
|
-
//
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
const
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4561
|
+
// As with planners, the granted lease owns profile/continuity/spec
|
|
4562
|
+
// preparation. Contain a pre-stream rejection as this attempt's failure;
|
|
4563
|
+
// parallel siblings can then finish before the shared HOME is disposed.
|
|
4564
|
+
const preparation = await (async () => {
|
|
4565
|
+
// Lease granted: the attempt is now committed to run — disclose the launch.
|
|
4566
|
+
onLaunch?.();
|
|
4567
|
+
const knobs = this.routeSpecKnobs(routed, contract, modelOverride, input.effort);
|
|
4568
|
+
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
4569
|
+
const explorerPrompt = (opts.deepScan
|
|
4570
|
+
? `${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.`
|
|
4571
|
+
: prompt) + contextSection;
|
|
4572
|
+
const sessionFields = await this.sessionSpecFields(input, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission);
|
|
4573
|
+
const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
|
|
4574
|
+
if (grantResume)
|
|
4575
|
+
resumeGranted.add(adapter.id);
|
|
4576
|
+
// Continuity (INV-137): a thread ASK turn is a chat turn — hydrate a lane
|
|
4577
|
+
// switch/gap with a packet and disclose it. Gated on laneRun (deep-scan
|
|
4578
|
+
// scouts are excluded from laneRun); native resume is available only when
|
|
4579
|
+
// this slot was granted the lane's recorded session.
|
|
4580
|
+
const laneContinuity = laneRun
|
|
4581
|
+
? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, sessionFields, store, paths, this.execRootOf(input), log)
|
|
4582
|
+
: null;
|
|
4583
|
+
// D-16d: the continuation packet pointer rides after the lane pointer so
|
|
4584
|
+
// the fresh session is re-grounded in the exhausted attempt's work.
|
|
4585
|
+
const promptWithPointers = [
|
|
4586
|
+
explorerPrompt,
|
|
4587
|
+
laneContinuity?.pointerLine,
|
|
4588
|
+
continuationPointer,
|
|
4589
|
+
]
|
|
4590
|
+
.filter((p) => Boolean(p))
|
|
4591
|
+
.join("\n\n");
|
|
4592
|
+
const spec = HarnessRunSpec.parse({
|
|
4593
|
+
session_id: newId("ses"),
|
|
4594
|
+
intent: opts.intent,
|
|
4595
|
+
prompt: promptWithPointers,
|
|
4596
|
+
cwd: this.execRootOf(input),
|
|
4597
|
+
access: "readonly",
|
|
4598
|
+
// ASK/EXPLORE/AUDIT read-only runs must forward the user's attachments —
|
|
4599
|
+
// a live "describe this image" turn sent an image that was being dropped here, so
|
|
4600
|
+
// the model honestly reported it saw nothing (the v0.13 attachment bug).
|
|
4601
|
+
attachments: input.attachments ?? [],
|
|
4602
|
+
auth_preference: sessionFields.auth_preference,
|
|
4603
|
+
credential_profile: sessionFields.credential_profile,
|
|
4604
|
+
resume_session_id: grantResume ? sessionFields.resume_session_id : null,
|
|
4605
|
+
...this.harnessSpecKnobs(contract, knobs, opts.intent),
|
|
4606
|
+
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
4607
|
+
// A thread lane turn spawns in its DURABLE per-lane home so the native
|
|
4608
|
+
// session it records is reachable for resume next turn; everything else
|
|
4609
|
+
// uses the disposable route-context home.
|
|
4610
|
+
env: (laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? roHome.env,
|
|
4611
|
+
});
|
|
4612
|
+
const reportAbort = new AbortController();
|
|
4613
|
+
spec.extra["abortSignal"] = input.signal
|
|
4614
|
+
? AbortSignal.any([input.signal, reportAbort.signal])
|
|
4615
|
+
: reportAbort.signal;
|
|
4616
|
+
const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
|
|
4617
|
+
if (reportInteraction)
|
|
4618
|
+
spec.extra["interactionChannel"] = reportInteraction;
|
|
4619
|
+
// D-16: compile the WorkReport envelope for the read-only lane.
|
|
4620
|
+
const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
|
|
4621
|
+
const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
|
|
4622
|
+
const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
|
|
4623
|
+
const answer = new AnswerAssembly();
|
|
4624
|
+
const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
|
|
4625
|
+
knobs.webPolicy === "cached" ||
|
|
4626
|
+
knobs.webPolicy === "live", effectiveWeb, [],
|
|
4627
|
+
// Requested-model capture so ask/audit route receipts detect a silent
|
|
4628
|
+
// model downgrade (typed model_mismatch), not just agent runs.
|
|
4629
|
+
knobs.model);
|
|
4630
|
+
return {
|
|
4631
|
+
knobs,
|
|
4632
|
+
spec,
|
|
4633
|
+
reportAbort,
|
|
4634
|
+
reportInteraction,
|
|
4635
|
+
readonlyWorkMode,
|
|
4636
|
+
attemptEventsPath,
|
|
4637
|
+
answer,
|
|
4638
|
+
telemetry,
|
|
4639
|
+
};
|
|
4640
|
+
})().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
|
|
4641
|
+
if (!preparation.ok) {
|
|
4642
|
+
const message = `read-only attempt setup failed: ${safeErrorMessage(preparation.error)}`;
|
|
4643
|
+
AC.settleGrantedAttemptLease({
|
|
4644
|
+
ledger,
|
|
4645
|
+
leaseId: lease.lease?.lease_id ?? "",
|
|
4646
|
+
attemptId,
|
|
4647
|
+
harnessId: adapter.id,
|
|
4648
|
+
costUsd: 0,
|
|
4649
|
+
costEstimated: false,
|
|
4650
|
+
preStreamFailureSource: "readonly-pre-stream",
|
|
4651
|
+
});
|
|
4652
|
+
const telemetry = createAttemptTelemetry(contract.external_context.policy, contract.external_context.web_required, contract.external_context.effective_mode);
|
|
4653
|
+
setAttemptOutcome(telemetry, {
|
|
4654
|
+
deliverablePresent: false,
|
|
4655
|
+
gatesPassed: null,
|
|
4656
|
+
harnessErrored: true,
|
|
4657
|
+
webRequiredUnsatisfied: false,
|
|
4658
|
+
});
|
|
4659
|
+
attempts.push({
|
|
4660
|
+
attemptId,
|
|
4661
|
+
harnessId: adapter.id,
|
|
4662
|
+
status: "failed",
|
|
4663
|
+
report: "",
|
|
4664
|
+
error: message,
|
|
4665
|
+
telemetry,
|
|
4666
|
+
});
|
|
4667
|
+
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
|
|
4668
|
+
if (opts.deepScan) {
|
|
4669
|
+
store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${message}\n`);
|
|
4670
|
+
}
|
|
4671
|
+
return { status: "launched" };
|
|
4672
|
+
}
|
|
4673
|
+
const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, answer, telemetry, } = preparation.value;
|
|
4674
|
+
let spec = preparedSpec;
|
|
4848
4675
|
const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
|
|
4849
4676
|
let activeSessionId = spec.session_id;
|
|
4850
4677
|
const onAbort = () => {
|
|
@@ -4884,11 +4711,13 @@ export class Orchestrator {
|
|
|
4884
4711
|
try {
|
|
4885
4712
|
const watchedReport = withInactivityWatchdog(adapter.run(runSpec), {
|
|
4886
4713
|
timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
|
|
4714
|
+
countsAsProgress: countsAsAgentProgress,
|
|
4887
4715
|
onTimeout: () => {
|
|
4888
4716
|
reportAbort.abort();
|
|
4889
4717
|
void adapter.cancel?.(activeSessionId)?.catch(() => { });
|
|
4890
4718
|
},
|
|
4891
4719
|
isSuspended: () => (reportInteraction?.pendingCount?.() ?? 0) > 0,
|
|
4720
|
+
suspensionVersion: () => reportInteraction?.suspensionVersion?.() ?? 0,
|
|
4892
4721
|
});
|
|
4893
4722
|
for await (const ev of watchedReport) {
|
|
4894
4723
|
if (input.signal?.aborted)
|
|
@@ -4952,13 +4781,18 @@ export class Orchestrator {
|
|
|
4952
4781
|
// W5.4 reactive failover, READ-ONLY lane (same contract as the
|
|
4953
4782
|
// candidate lane; typed limits only, never plain transients).
|
|
4954
4783
|
if (harnessError && !input.signal?.aborted) {
|
|
4784
|
+
const rotationPolicy = this.profilePolicy(input.repoRoot, adapter.id);
|
|
4785
|
+
const readyProfileIds = sawTypedLimit && reportSoFar.length === 0 && rotationPolicy.limit_action === "rotate"
|
|
4786
|
+
? await this.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles)
|
|
4787
|
+
: new Set();
|
|
4955
4788
|
const rotated = rotateSpecOnTypedLimit({
|
|
4956
4789
|
spec,
|
|
4957
4790
|
harnessId: adapter.id,
|
|
4958
4791
|
attemptId,
|
|
4959
|
-
policy:
|
|
4792
|
+
policy: rotationPolicy,
|
|
4960
4793
|
registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
|
|
4961
4794
|
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
4795
|
+
readyProfileIds,
|
|
4962
4796
|
triedProfiles,
|
|
4963
4797
|
sawTypedLimit,
|
|
4964
4798
|
deliverableEmpty: reportSoFar.length === 0,
|
|
@@ -5000,7 +4834,17 @@ export class Orchestrator {
|
|
|
5000
4834
|
}
|
|
5001
4835
|
finally {
|
|
5002
4836
|
input.signal?.removeEventListener("abort", onAbort);
|
|
5003
|
-
|
|
4837
|
+
AC.settleGrantedAttemptLease({
|
|
4838
|
+
ledger,
|
|
4839
|
+
leaseId: lease.lease?.lease_id ?? "",
|
|
4840
|
+
attemptId,
|
|
4841
|
+
harnessId: adapter.id,
|
|
4842
|
+
costUsd: cost,
|
|
4843
|
+
costEstimated,
|
|
4844
|
+
authMode: telemetry.authMode,
|
|
4845
|
+
usageCost: telemetry.usageCost,
|
|
4846
|
+
preStreamFailureSource: "readonly-pre-stream",
|
|
4847
|
+
});
|
|
5004
4848
|
}
|
|
5005
4849
|
if (harnessError && telemetry.transientFailures.length > 0) {
|
|
5006
4850
|
log.emit("route.transient.exhausted", {
|
|
@@ -5011,11 +4855,14 @@ export class Orchestrator {
|
|
|
5011
4855
|
});
|
|
5012
4856
|
}
|
|
5013
4857
|
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
|
|
5014
|
-
// D-16:
|
|
4858
|
+
// D-16: remove the WorkReport transport; the deliverable is the report.
|
|
5015
4859
|
const roUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", readonlyWorkMode, {
|
|
5016
4860
|
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
5017
4861
|
});
|
|
5018
|
-
|
|
4862
|
+
// Trim symmetrically with the plan path: a whitespace-only answer is not
|
|
4863
|
+
// a delivered report (the final-artifact wrapper heading would otherwise
|
|
4864
|
+
// make it read as present content by construction).
|
|
4865
|
+
const report = redactSecrets(roUnwrapped.deliverable).trim();
|
|
5019
4866
|
const unrecovered = unrecoveredToolErrors(telemetry);
|
|
5020
4867
|
const webBlocked = webUnsatisfied(telemetry);
|
|
5021
4868
|
const reportPresent = report.length > 0;
|
|
@@ -5043,6 +4890,13 @@ export class Orchestrator {
|
|
|
5043
4890
|
harnessErrored: harnessError !== null && !webBlocked,
|
|
5044
4891
|
webRequiredUnsatisfied: webBlocked,
|
|
5045
4892
|
workState: roFinalized.workState,
|
|
4893
|
+
// A read-only attempt that completed CLEANLY with an honestly empty
|
|
4894
|
+
// answer is a success with deliverable_present=false ("(no output)"),
|
|
4895
|
+
// never a fake contract failure — the trim above must not convert a
|
|
4896
|
+
// phantom deliverable into a phantom harness failure. Only the clean
|
|
4897
|
+
// finalizer class qualifies: contract failures, vetoes, and context
|
|
4898
|
+
// interruptions keep the strict deliverable requirement.
|
|
4899
|
+
emptyDeliverableAllowed: roFinalized.outcomeClass === "clean",
|
|
5046
4900
|
});
|
|
5047
4901
|
if (harnessError) {
|
|
5048
4902
|
log.emit("harness.completed", {
|
|
@@ -5473,7 +5327,7 @@ export class Orchestrator {
|
|
|
5473
5327
|
}
|
|
5474
5328
|
this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, opts.deepScan ? null : (succeeded[0]?.attemptId ?? null), deepScanSynthesis);
|
|
5475
5329
|
log.emit("output.ready", {
|
|
5476
|
-
kind: opts.
|
|
5330
|
+
kind: opts.deepScan ? "report" : "answer",
|
|
5477
5331
|
path: `final/${opts.artifactName}`,
|
|
5478
5332
|
});
|
|
5479
5333
|
if (opts.deepScan) {
|
|
@@ -5531,11 +5385,16 @@ export class Orchestrator {
|
|
|
5531
5385
|
.map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
|
|
5532
5386
|
.join(", ");
|
|
5533
5387
|
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`);
|
|
5388
|
+
const reportProducerAttemptId = opts.deepScan &&
|
|
5389
|
+
deepScanSynthesis?.status === "succeeded" &&
|
|
5390
|
+
deepScanSynthesis.reducer_attempt_id
|
|
5391
|
+
? deepScanSynthesis.reducer_attempt_id
|
|
5392
|
+
: (succeeded[0]?.attemptId ?? "a01");
|
|
5534
5393
|
store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
|
|
5535
5394
|
id: newId("wp"),
|
|
5536
5395
|
kind: "report",
|
|
5537
5396
|
source_task_id: taskId,
|
|
5538
|
-
producer_attempt_id:
|
|
5397
|
+
producer_attempt_id: reportProducerAttemptId,
|
|
5539
5398
|
files: Object.fromEntries([[opts.artifactName, join(paths.finalDir, opts.artifactName)]]),
|
|
5540
5399
|
meta: {
|
|
5541
5400
|
harnesses: attempts.map((a) => a.harnessId),
|
|
@@ -5544,7 +5403,7 @@ export class Orchestrator {
|
|
|
5544
5403
|
read_only: true,
|
|
5545
5404
|
},
|
|
5546
5405
|
});
|
|
5547
|
-
log.emit("work_product.emitted", { kind: "report", winner:
|
|
5406
|
+
log.emit("work_product.emitted", { kind: "report", winner: reportProducerAttemptId });
|
|
5548
5407
|
const workVetoed = terminalFacts.work_state?.state === "needs_input" ||
|
|
5549
5408
|
terminalFacts.work_state?.state === "incomplete";
|
|
5550
5409
|
if (terminalFacts.lifecycle !== "succeeded") {
|
|
@@ -5596,9 +5455,4 @@ export class Orchestrator {
|
|
|
5596
5455
|
};
|
|
5597
5456
|
}
|
|
5598
5457
|
}
|
|
5599
|
-
function assertNoSecretLikeTokens(label, text) {
|
|
5600
|
-
if (containsSecretLikeToken(text)) {
|
|
5601
|
-
throw new Error(`${label} contains secret-like token; refusing to persist artifact`);
|
|
5602
|
-
}
|
|
5603
|
-
}
|
|
5604
5458
|
//# sourceMappingURL=orchestrator.js.map
|