@claudexor/orchestrator 3.17.1 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/account-pool.d.ts +11 -6
  2. package/dist/account-pool.d.ts.map +1 -1
  3. package/dist/account-pool.js +5 -8
  4. package/dist/account-pool.js.map +1 -1
  5. package/dist/account-resolution.d.ts +5 -1
  6. package/dist/account-resolution.d.ts.map +1 -1
  7. package/dist/account-resolution.js +5 -2
  8. package/dist/account-resolution.js.map +1 -1
  9. package/dist/attemptOutputMarkers.d.ts +5 -0
  10. package/dist/attemptOutputMarkers.d.ts.map +1 -1
  11. package/dist/attemptOutputMarkers.js +3 -1
  12. package/dist/attemptOutputMarkers.js.map +1 -1
  13. package/dist/attemptTelemetry.d.ts +3 -4
  14. package/dist/attemptTelemetry.d.ts.map +1 -1
  15. package/dist/attemptTelemetry.js +6 -5
  16. package/dist/attemptTelemetry.js.map +1 -1
  17. package/dist/auth-route-classification.d.ts +22 -1
  18. package/dist/auth-route-classification.d.ts.map +1 -1
  19. package/dist/auth-route-classification.js +38 -0
  20. package/dist/auth-route-classification.js.map +1 -1
  21. package/dist/candidateEvidence.d.ts +1 -1
  22. package/dist/candidateEvidence.d.ts.map +1 -1
  23. package/dist/candidateEvidence.js +1 -1
  24. package/dist/candidateEvidence.js.map +1 -1
  25. package/dist/candidateTerminals.d.ts.map +1 -1
  26. package/dist/candidateTerminals.js +1 -0
  27. package/dist/candidateTerminals.js.map +1 -1
  28. package/dist/continuity-facts.d.ts +2 -0
  29. package/dist/continuity-facts.d.ts.map +1 -1
  30. package/dist/continuity-facts.js +2 -0
  31. package/dist/continuity-facts.js.map +1 -1
  32. package/dist/continuity-summary.d.ts +3 -0
  33. package/dist/continuity-summary.d.ts.map +1 -1
  34. package/dist/continuity-summary.js +4 -3
  35. package/dist/continuity-summary.js.map +1 -1
  36. package/dist/credential-differential.d.ts +1 -1
  37. package/dist/credential-differential.d.ts.map +1 -1
  38. package/dist/credential-differential.js +5 -2
  39. package/dist/credential-differential.js.map +1 -1
  40. package/dist/credential-profile-rotation.d.ts +4 -0
  41. package/dist/credential-profile-rotation.d.ts.map +1 -1
  42. package/dist/credential-profile-rotation.js +12 -13
  43. package/dist/credential-profile-rotation.js.map +1 -1
  44. package/dist/credential-profiles.d.ts +15 -2
  45. package/dist/credential-profiles.d.ts.map +1 -1
  46. package/dist/credential-profiles.js +19 -5
  47. package/dist/credential-profiles.js.map +1 -1
  48. package/dist/deepScanReducer.js +1 -1
  49. package/dist/deepScanReducer.js.map +1 -1
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/modelGovernance.d.ts +5 -31
  55. package/dist/modelGovernance.d.ts.map +1 -1
  56. package/dist/modelGovernance.js +49 -4
  57. package/dist/modelGovernance.js.map +1 -1
  58. package/dist/orchestrator-credentials.d.ts +60 -4
  59. package/dist/orchestrator-credentials.d.ts.map +1 -1
  60. package/dist/orchestrator-credentials.js +128 -7
  61. package/dist/orchestrator-credentials.js.map +1 -1
  62. package/dist/orchestrator.d.ts +10 -19
  63. package/dist/orchestrator.d.ts.map +1 -1
  64. package/dist/orchestrator.js +153 -189
  65. package/dist/orchestrator.js.map +1 -1
  66. package/dist/planRun.d.ts +9 -21
  67. package/dist/planRun.d.ts.map +1 -1
  68. package/dist/planRun.js +30 -34
  69. package/dist/planRun.js.map +1 -1
  70. package/dist/plannerAttempt.d.ts +15 -4
  71. package/dist/plannerAttempt.d.ts.map +1 -1
  72. package/dist/plannerAttempt.js +52 -26
  73. package/dist/plannerAttempt.js.map +1 -1
  74. package/dist/pre-progress-refusal.d.ts +61 -0
  75. package/dist/pre-progress-refusal.d.ts.map +1 -0
  76. package/dist/pre-progress-refusal.js +67 -0
  77. package/dist/pre-progress-refusal.js.map +1 -0
  78. package/dist/processing-dispatch.d.ts +7 -5
  79. package/dist/processing-dispatch.d.ts.map +1 -1
  80. package/dist/processing-dispatch.js +65 -31
  81. package/dist/processing-dispatch.js.map +1 -1
  82. package/dist/prompt-framing.d.ts +8 -0
  83. package/dist/prompt-framing.d.ts.map +1 -0
  84. package/dist/prompt-framing.js +33 -0
  85. package/dist/prompt-framing.js.map +1 -0
  86. package/dist/reviewerPanel.d.ts.map +1 -1
  87. package/dist/reviewerPanel.js +3 -2
  88. package/dist/reviewerPanel.js.map +1 -1
  89. package/dist/rotation-predicate.d.ts +3 -0
  90. package/dist/rotation-predicate.d.ts.map +1 -1
  91. package/dist/rotation-predicate.js +3 -0
  92. package/dist/rotation-predicate.js.map +1 -1
  93. package/dist/runSupport.d.ts.map +1 -1
  94. package/dist/runSupport.js +2 -4
  95. package/dist/runSupport.js.map +1 -1
  96. package/dist/runTerminalResults.d.ts +4 -1
  97. package/dist/runTerminalResults.d.ts.map +1 -1
  98. package/dist/runTerminalResults.js +14 -2
  99. package/dist/runTerminalResults.js.map +1 -1
  100. package/package.json +17 -17
@@ -1,9 +1,11 @@
1
+ import { applyWorkEnvelope, promptWithPointers as renderPromptWithPointers, threadContextPointer, } from "./prompt-framing.js";
2
+ import { requestRefusalFailure } from "./runTerminalResults.js";
1
3
  import { delegationBeltFor } from "./delegationBelt.js";
2
- import { bindProcessingAdmission, processingAdmissionForLease, updateProcessingStreamHold, ProcessingBudgetAdmissionError, } from "./processing-dispatch.js";
4
+ import { bindProcessingAdmission, processingAttemptCostEvidence, processingAdmissionForLease, updateProcessingStreamHold, ProcessingBudgetAdmissionError, } from "./processing-dispatch.js";
3
5
  import { cancelledCandidatesResult, emptyCandidateResult, failedCandidatesResult, } from "./candidateTerminals.js";
4
6
  import { existsSync } from "node:fs";
5
- import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, } from "./credential-profiles.js";
6
- import { OrchestratorCredentials, reviewerProfileResolver, rotatedSpecInLaneHome, } from "./orchestrator-credentials.js";
7
+ import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, rotateSpecOnTypedLimit, } from "./credential-profiles.js";
8
+ import { AdmissionProfileProbes, OrchestratorCredentials, reviewerProfileResolver, rotatedSpecInLaneHome, } from "./orchestrator-credentials.js";
7
9
  import { accountPoolRows } from "./account-pool.js";
8
10
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
9
11
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
@@ -11,18 +13,18 @@ import { processAttemptUsage } from "./attemptUsage.js";
11
13
  import { captureDirectoryCandidate, directoryHasOutput, directoryCandidateStable, observeDirectoryPaths, publishDirectoryCandidate, } from "./directoryCandidate.js";
12
14
  import { appliedAttemptFacts, assertDelegatedEvidence, isMutatingAccess, outerBoundaryNotice, scopedHarnessHome, } from "./delegatedHome.js";
13
15
  import * as AC from "./attemptUsageCost.js";
14
- import { candidateRoster, candidateStatuses, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
16
+ import { candidateRoster, candidateStatuses, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, unanimousDeclaredFailure, } from "./candidateEvidence.js";
15
17
  import { capabilityIntents } from "@claudexor/gateway";
16
18
  import { reviewCandidateRuns, resolveEngineReview, unreviewedCandidateEvidence, evaluateUnreviewedConvergence, } from "./candidateReview.js";
17
19
  import { join } from "node:path";
18
20
  import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./planRun.js";
19
- import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
21
+ import { runPlannerAttempt as executePlannerAttempt, plannerAttemptSummary, } from "./plannerAttempt.js";
20
22
  import { HarnessRunSpec, MAX_COUNCIL_MEMBERS_DEFAULT, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, reviewAllowsApply, workStateVetoes, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
21
23
  import { globalConfigDir, loadConfig } from "@claudexor/config";
22
24
  import { prepareRoutedProcessing, processingCostEvidence, prepareReviewerProcessing, } from "./processing-routing.js";
23
25
  import { acceptedTryOutput, AccessProfileIncompatibleError, AnswerAssembly, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
24
26
  import { assertRouteModelsAllowed, runModelGovernedRoute } from "./modelGovernance.js";
25
- import { authModeForCredentialRoute, authModeForPreference } from "./auth-route-classification.js";
27
+ import { authModeForCredentialRoute, authModeForPreference, authRouteEvidenceFor, } from "./auth-route-classification.js";
26
28
  import { governRouteEffort } from "./effortGovernance.js";
27
29
  import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
28
30
  import { activateDelegationParent } from "./delegation-parent-activation.js";
@@ -67,7 +69,7 @@ import { HarnessGateway } from "@claudexor/gateway";
67
69
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, runGates, } from "@claudexor/review";
68
70
  import { arbitrate } from "@claudexor/arbitration";
69
71
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
70
- import { attemptCostEvidence, billingKnowledgeForAuthRoute, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, } from "@claudexor/budget";
72
+ import { billingKnowledgeForAuthRoute, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, RoutingPreflightError, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, } from "@claudexor/budget";
71
73
  import { readTextSafe, appendLine, assertNoInlineSecretValues, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, } from "@claudexor/util";
72
74
  const LABELS = "ABCDEFGHIJ".split("");
73
75
  const NO_PROJECT_ROOT = noProjectRepoRoot();
@@ -366,6 +368,7 @@ export class Orchestrator {
366
368
  quotaAbsences: () => this.deps.quotaAbsences?.() ?? [],
367
369
  credentialUnusable: () => this.deps.credentialUnusable?.() ?? [],
368
370
  recordCredentialUnusable: (obs) => this.deps.recordCredentialUnusable?.(obs),
371
+ preProgressRefusals: () => this.deps.preProgressRefusals,
369
372
  authPreferenceForHarness: (repoRoot, harnessId, runPreference) => this.authPreferenceForHarness(repoRoot, harnessId, runPreference),
370
373
  });
371
374
  /**
@@ -486,9 +489,11 @@ export class Orchestrator {
486
489
  dropped.push(detail);
487
490
  droppedLanes.push({ harnessId, stage, detail });
488
491
  };
489
- // One observation set for the whole admission pass, so two lanes cannot be
490
- // judged against different vendor epochs.
492
+ // Initial lane readiness shares one quota observation. Selection and each
493
+ // physical dispatch still consume the latest exact-profile evidence.
491
494
  const vendorQuota = this.credentials.vendorQuotaObservations();
495
+ // Billing later reuses the probe that admitted each row (#260).
496
+ const admissionProbes = new AdmissionProfileProbes();
492
497
  const liveUnusable = this.deps.credentialUnusable?.() ?? [];
493
498
  for (const id of ids) {
494
499
  const adapter = this.deps.registry.get(id);
@@ -554,43 +559,20 @@ export class Orchestrator {
554
559
  // profile-admitted route joins even an AUTO pool (the run spawns with
555
560
  // the profile's transport, so the default store's state is not the
556
561
  // routing truth). Capability/manifest gating above still applies.
557
- let profileAdmitted = false;
558
562
  const profileAdapter = this.deps.registry.get(id);
559
- const profileProbe = profileAdapter?.probeCredentialProfile?.bind(profileAdapter);
563
+ const profileProbe = admissionProbes.record(profileAdapter?.probeCredentialProfile?.bind(profileAdapter));
560
564
  const explicitPin = this.credentials.effectiveProfileId(input, id);
561
- const model = input.models?.[id] ?? cfgEntry?.default_model ?? null;
562
565
  // Pins and bound/pool rows are checked against their own readiness.
563
- const rowCandidateIds = [];
564
- if (explicitPin) {
565
- rowCandidateIds.push(explicitPin);
566
- }
567
- else if (status.status !== "ok") {
568
- const pool = accountPoolRows(this.config(input.repoRoot)?.global.credential_profiles ?? [], id);
569
- const bound = input.threadAccountBindings?.[id] ?? null;
570
- rowCandidateIds.push(...(bound && pool.some((row) => row.profile_id === bound) ? [bound] : []), ...pool.map((row) => row.profile_id).filter((rowId) => rowId !== bound));
571
- }
572
- let lastRowVerdict = null;
573
- for (const candidateId of rowCandidateIds) {
574
- const verdict = await selectedProfileAvailability({
575
- registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
576
- profileId: candidateId,
577
- harnessId: id,
578
- probe: profileProbe,
579
- // `verification: passed` from the local store only means a login file
580
- // is present. The poller's authenticated vendor call is the only
581
- // liveness evidence; admission must act on it or dispatch may use a revoked token.
582
- quota: vendorQuota,
583
- unusable: liveUnusable,
584
- model,
585
- // Only an explicit/bound route may consume bounded stale LKG evidence.
586
- allowStale: explicitPin !== null || input.threadAccountBindings?.[id] === candidateId,
587
- });
588
- if (verdict === "available") {
589
- profileAdmitted = true;
590
- break;
591
- }
592
- lastRowVerdict = verdict;
593
- }
566
+ const rows = await this.credentials.admitRouteRows({
567
+ input,
568
+ harnessId: id,
569
+ defaultReady: status.status === "ok",
570
+ model: input.models?.[id] ?? cfgEntry?.default_model ?? null,
571
+ quota: vendorQuota,
572
+ unusable: liveUnusable,
573
+ probe: profileProbe,
574
+ });
575
+ const profileAdmitted = rows.admitted;
594
576
  if (profileAdmitted) {
595
577
  // A valid account row restores manifest intent truth when the default store failed.
596
578
  if (status.status !== "ok") {
@@ -603,14 +585,19 @@ export class Orchestrator {
603
585
  }
604
586
  }
605
587
  else if (explicitPin) {
606
- // An explicit pin that is not ready refuses/drops the lane; a
607
- // not-ready UNPINNED row merely leaves the default doctor verdict in
608
- // charge (the gates below keep the refusal honest when neither a row
609
- // nor a default login exists).
610
- const why = `${id} credential profile is not ready: ${lastRowVerdict}`;
588
+ // An explicit pin that is not ready refuses/drops the lane; not-ready
589
+ // UNPINNED rows are named below, and the default doctor verdict speaks
590
+ // only when neither a row nor a default login exists.
591
+ const why = `${id} credential profile is not ready: ${rows.pinVerdict}`;
611
592
  dropLane(id, "credential", why);
612
593
  continue;
613
594
  }
595
+ // #363: registered rows own this lane, so their observed states — not
596
+ // the default login's doctor advice — explain why none was admitted.
597
+ if (rows.unreadyRows && (status.status === "unavailable" || !explicitPool)) {
598
+ dropLane(id, "credential", rows.unreadyRows);
599
+ continue;
600
+ }
614
601
  if (status.status === "unavailable" && !profileAdmitted) {
615
602
  const why = `${id} is unavailable${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`;
616
603
  dropLane(id, "doctor", why);
@@ -677,7 +664,7 @@ export class Orchestrator {
677
664
  // the default store's sources apply only to profile-less runs.
678
665
  authRouteEstimate: this.credentials.profileAuthRoute(input, id) ??
679
666
  estimateEffectiveAuthRoute(this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), status.authSources),
680
- quotaAdmission: { model: null, profile: null, route: null },
667
+ quotaAdmission: { model: null, profile: null, route: null, profileVerification: null },
681
668
  supportsSynthesize: manifest.capabilities.synthesize,
682
669
  supportsInteractive: manifest.capabilities.interactive,
683
670
  liveInput: manifest.capability_profile.live_input,
@@ -722,7 +709,7 @@ export class Orchestrator {
722
709
  const model = input.models?.[routed.adapter.id] ?? routed.settings?.defaultModel ?? null;
723
710
  let profile;
724
711
  try {
725
- profile = await this.credentials.preflightProfile(input, routed.adapter.id, model, log, routed.authRouteEstimate);
712
+ profile = await this.credentials.preflightProfile(input, routed.adapter.id, model, log, routed.authRouteEstimate, admissionProbes);
726
713
  }
727
714
  catch (err) {
728
715
  // AUTO drops typed lane-local unavailability only when a sibling
@@ -746,8 +733,16 @@ export class Orchestrator {
746
733
  : routed.authRouteEstimate === "local_session"
747
734
  ? "vendor_native"
748
735
  : null;
736
+ const profileVerification = profile
737
+ ? await this.credentials.selectedProfileBillingVerification(profile, admissionProbes)
738
+ : null;
749
739
  return {
750
- prepared: { ...routed, quotaAdmission: { model, profile, route } },
740
+ prepared: {
741
+ ...routed,
742
+ quotaAdmission: { model, profile, route, profileVerification },
743
+ paidFallback: this.config(input.repoRoot).global.routing.paid_fallback,
744
+ billingVerificationForProfile: (actual) => this.credentials.selectedProfileBillingVerification(actual),
745
+ },
751
746
  refused: null,
752
747
  };
753
748
  }));
@@ -878,13 +873,14 @@ export class Orchestrator {
878
873
  // engine default. Profile A's cooldown never excludes profile B or the
879
874
  // default on the same harness and route.
880
875
  const credentialSubjectId = r.quotaAdmission.profile?.profile_id ?? null;
881
- // QA-034: the typed auth-route evidence (doctor source verification x the
882
- // resolved route) is AUTHORITATIVE for billing knowledge in the router —
883
- // a VERIFIED native route proves subscription_entitlement, so it survives
876
+ // QA-034: the typed auth-route evidence (the admitted profile's own
877
+ // verification, else doctor source verification, x the resolved route)
878
+ // is AUTHORITATIVE for billing knowledge in the router — a VERIFIED
879
+ // native route proves subscription_entitlement, so it survives
884
880
  // paid_fallback:never and ranks with a real economy tuple instead of
885
881
  // reading as unknown/paid. Absent (unknown route) falls back to the
886
882
  // metric-derived billingKnowledge below.
887
- const authRoute = this.authRouteEvidenceFor(authMode, status?.authSources ?? []);
883
+ const authRoute = authRouteEvidenceFor(authMode, status?.authSources ?? [], r.quotaAdmission.profileVerification);
888
884
  const processingCost = processingCostEvidence(r.processing, authRoute ? billingKnowledgeForAuthRoute(authRoute) : "unknown", [`harness:${r.adapter.id}`, `profile:${credentialSubjectId ?? "default"}`]);
889
885
  return {
890
886
  harnessId: r.adapter.id,
@@ -984,30 +980,6 @@ export class Orchestrator {
984
980
  },
985
981
  };
986
982
  }
987
- /**
988
- * Typed auth-route evidence for one candidate (QA-034): the concrete
989
- * credential route the resolved auth mode maps to, plus the doctor's
990
- * verification for the source that route runs under. `local_session` →
991
- * vendor_native + the native/OAuth source verification; `api_key` →
992
- * managed_api_key + the key source verification. Unknown route → no evidence
993
- * (the router keeps its conservative metric-derived billing). Verification is
994
- * the source's typed verdict — never inferred from mere availability.
995
- */
996
- authRouteEvidenceFor(authMode, sources) {
997
- const usable = (s) => s.availability === "available" && s.verification !== "failed";
998
- if (authMode === "local_session") {
999
- const native = sources.find((s) => usable(s) && (s.source === "native_session" || s.source === "oauth_token_env"));
1000
- return { route: "vendor_native", verification: native?.verification ?? "not_run" };
1001
- }
1002
- if (authMode === "api_key") {
1003
- const key = sources.find((s) => usable(s) &&
1004
- (s.source === "api_key_env" ||
1005
- s.source === "api_key_flag" ||
1006
- s.source === "provider_auth_file"));
1007
- return { route: "managed_api_key", verification: key?.verification ?? "not_run" };
1008
- }
1009
- return undefined;
1010
- }
1011
983
  /**
1012
984
  * Lazy ContextPack: built ONLY for the read-only report modes
1013
985
  * (explore/plan/readonly_audit) that consume it. Persisted to
@@ -1135,25 +1107,6 @@ export class Orchestrator {
1135
1107
  callerSchema: contract.output_schema ?? null,
1136
1108
  });
1137
1109
  }
1138
- /**
1139
- * D-16: apply the resolved WorkReport transport to a built spec — set the
1140
- * envelope output_schema (constrained/side_tool routes) and APPEND the fenced
1141
- * metadata instruction (validated routes, e.g. cursor). Mutates the spec in
1142
- * place and returns the mode the answer unwrap consumes. Called at every
1143
- * task-producing spec-build site so the transport is never wired one-off.
1144
- */
1145
- applyWorkEnvelope(spec, workEnvelope) {
1146
- if (workEnvelope.outputSchema !== undefined)
1147
- spec.output_schema = workEnvelope.outputSchema;
1148
- const instruction = workEnvelope.mode.instruction;
1149
- if (instruction) {
1150
- spec.instructions =
1151
- spec.instructions && spec.instructions.trim()
1152
- ? `${spec.instructions}\n\n${instruction}`
1153
- : instruction;
1154
- }
1155
- return workEnvelope.mode;
1156
- }
1157
1110
  routeSpecKnobs(routed, contract, overrideModel, effortHint) {
1158
1111
  const s = routed.settings;
1159
1112
  const contractPolicy = contract.external_context.policy;
@@ -1269,6 +1222,8 @@ export class Orchestrator {
1269
1222
  processing,
1270
1223
  processingAdmission,
1271
1224
  physicalDispatchStarted,
1225
+ billingVerificationForProfile: (profile) => this.credentials.selectedProfileBillingVerification(profile),
1226
+ paidFallback: this.config(runInput.repoRoot).global.routing.paid_fallback,
1272
1227
  });
1273
1228
  const result = buildContinuation(req);
1274
1229
  // Disclose on every lane and stamp the turn (INV-137: never silent).
@@ -1286,7 +1241,7 @@ export class Orchestrator {
1286
1241
  const briefPath = join(paths.contextDir, "THREAD.md");
1287
1242
  store.writeText(briefPath, result.packetMarkdown);
1288
1243
  return {
1289
- pointerLine: `Earlier conversation context for this thread is at: ${briefPath} — read it before answering.`,
1244
+ pointerLine: threadContextPointer(briefPath),
1290
1245
  };
1291
1246
  }
1292
1247
  catch (err) {
@@ -1354,7 +1309,7 @@ export class Orchestrator {
1354
1309
  const sessionFields = runInput
1355
1310
  ? await this.sessionSpecFields(runInput, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission)
1356
1311
  : undefined;
1357
- const processingAdmission = processingAdmissionForLease(ledger, processingLease.id, adapter.id, attemptId, processingLease.onDenied);
1312
+ const processingAdmission = processingAdmissionForLease(ledger, processingLease.lease, adapter.id, attemptId, processingLease.onDenied);
1358
1313
  const capturedProcessing = {
1359
1314
  processing_preference: contract.processing_preference,
1360
1315
  processing: knobs.processing?.receipt,
@@ -1362,7 +1317,7 @@ export class Orchestrator {
1362
1317
  processing_allow_paid: knobs.processingAllowPaid,
1363
1318
  };
1364
1319
  const laneContinuity = runInput
1365
- ? 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, capturedProcessing, processingAdmission, () => ledger.markPhysicalDispatchStarted(processingLease.id))
1320
+ ? 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, capturedProcessing, processingAdmission, () => ledger.markPhysicalDispatchStarted(processingLease.lease.lease_id))
1366
1321
  : null;
1367
1322
  const artifactRelativeDir = routed.browserRequirement.effective
1368
1323
  ? (wsm.ensureArtifactDirectory(envelope), wsm.ownedArtifactRelativeDirectory(envelope))
@@ -1405,20 +1360,18 @@ export class Orchestrator {
1405
1360
  raw_context_packet: rawContextPacket,
1406
1361
  stream_deltas: streamDeltas,
1407
1362
  });
1408
- bindProcessingAdmission(spec, ledger, processingLease.id, adapter.id, attemptId, processingLease.onDenied, processingAdmission);
1409
- const billingInput = runInput ?? { repoRoot: contract.repo.root };
1410
- // Keep billing route-bound until admission observes the actual prepared
1411
- // profile. The resolver is evaluated per physical dispatch; preparedCost
1412
- // gives an actual profile precedence over this pool fallback.
1413
- spec.extra["routeBillingKnowledge"] = (actual) => actual.credential_profile?.credential_kind === "api_key"
1414
- ? "metered"
1415
- : actual.credential_profile
1416
- ? "subscription_entitlement"
1417
- : this.routeBillingKnowledge(billingInput, adapter.id);
1363
+ if (runInput)
1364
+ this.credentials.stampProfileSelection(spec, runInput, adapter.id);
1365
+ bindProcessingAdmission(spec, ledger, processingLease.lease, adapter.id, attemptId, processingLease.onDenied, processingAdmission);
1366
+ // Profile-less processing keeps its existing route estimate. Named rows
1367
+ // receive their own verification at the shared physical-dispatch gate.
1368
+ spec.extra["routeBillingKnowledge"] = (actual) => actual.credential_profile
1369
+ ? "unknown"
1370
+ : this.routeBillingKnowledge(runInput ?? { repoRoot: contract.repo.root }, adapter.id);
1418
1371
  if (interaction)
1419
1372
  spec.extra["interactionChannel"] = interaction;
1420
1373
  const workEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(interaction));
1421
- const workReportMode = this.applyWorkEnvelope(spec, workEnvelope);
1374
+ const workReportMode = applyWorkEnvelope(spec, workEnvelope);
1422
1375
  const inactivityMs = harnessInactivityTimeoutMs(this.config(contract.repo.root));
1423
1376
  // Named once: the attempt record, the CandidateRun and the terminal gate all
1424
1377
  // read the SAME applied facts rather than three re-derivations. Derived
@@ -1482,6 +1435,8 @@ export class Orchestrator {
1482
1435
  ? AbortSignal.any([signal, attemptAbort.signal])
1483
1436
  : attemptAbort.signal;
1484
1437
  activeSessionId = runSpec.session_id;
1438
+ // #363: bound before the spawn — a credential change mid-try voids it.
1439
+ const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
1485
1440
  const transientStart = telemetry.transientFailures.length;
1486
1441
  const rateLimitStart = telemetry.rateLimits.length;
1487
1442
  let rawPatch = null;
@@ -1585,7 +1540,8 @@ export class Orchestrator {
1585
1540
  // gate and required-actions read a typed category, not a bare boolean.
1586
1541
  harnessErrored = true;
1587
1542
  errors.push(safeErrorMessage(err));
1588
- if (err instanceof ProcessingBudgetAdmissionError) {
1543
+ if (err instanceof ProcessingBudgetAdmissionError ||
1544
+ err instanceof RoutingPreflightError) {
1589
1545
  processingRefusal = err;
1590
1546
  break;
1591
1547
  }
@@ -1595,6 +1551,8 @@ export class Orchestrator {
1595
1551
  finally {
1596
1552
  clearFileBackedContext();
1597
1553
  }
1554
+ if (telemetry.requestRefusal)
1555
+ break;
1598
1556
  const newTransients = telemetry.transientFailures.slice(transientStart);
1599
1557
  // #31: the centralized retry gate reads the classified `retryable` verdict.
1600
1558
  const sawRetryable = newTransients.some((f) => f.retryable);
@@ -1606,6 +1564,8 @@ export class Orchestrator {
1606
1564
  ? directoryCapture.files?.noChanges === true
1607
1565
  : currentDiff.trim().length === 0;
1608
1566
  const deliverableEmpty = workspaceUnchanged && answer.text().length === 0;
1567
+ const delivered = !harnessErrored && !deliverableEmpty && !signal?.aborted;
1568
+ this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
1609
1569
  // W5.4 + A2 failover: a typed-limit hit OR a structural pre-progress
1610
1570
  // death rebuilds the spec on a NEW session under the next profile.
1611
1571
  if (harnessErrored && runInput && !signal?.aborted) {
@@ -1617,9 +1577,10 @@ export class Orchestrator {
1617
1577
  registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
1618
1578
  snapshots: this.deps.quotaSnapshots?.() ?? [],
1619
1579
  probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
1620
- ...this.credentials.rotationObservations(adapter, spec, newTransients),
1580
+ ...this.credentials.rotationObservations(adapter, spec, newTransients, refusal),
1621
1581
  triedProfiles,
1622
1582
  markers: telemetry.outputMarkers,
1583
+ requestRefused: Boolean(telemetry.requestRefusal),
1623
1584
  sawTypedLimit,
1624
1585
  sawRetryable,
1625
1586
  attemptErrored: harnessErrored,
@@ -1671,7 +1632,7 @@ export class Orchestrator {
1671
1632
  }
1672
1633
  // A pool-exhausted terminal is rotation's verdict, not the transient
1673
1634
  // machinery's — no `route.transient.exhausted` rides along with it.
1674
- if (harnessErrored && !poolExhausted && !processingRefusal) {
1635
+ if (harnessErrored && !poolExhausted && !processingRefusal && !telemetry.requestRefusal) {
1675
1636
  emitTransientExhausted((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
1676
1637
  }
1677
1638
  const attemptStreamEndedMs = Date.now();
@@ -1822,9 +1783,11 @@ export class Orchestrator {
1822
1783
  telemetry,
1823
1784
  ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1824
1785
  // A5: the typed refusal survives NORMAL attempt finalization (no throw).
1825
- ...(poolExhausted || processingRefusal
1826
- ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
1827
- : {}),
1786
+ ...(telemetry.requestRefusal
1787
+ ? { declaredFailure: requestRefusalFailure(telemetry.requestRefusal) }
1788
+ : poolExhausted || processingRefusal
1789
+ ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
1790
+ : {}),
1828
1791
  outcomeClass: finalized.outcomeClass,
1829
1792
  applied,
1830
1793
  };
@@ -1972,7 +1935,7 @@ export class Orchestrator {
1972
1935
  attemptId,
1973
1936
  intent: this.candidateIntent(input),
1974
1937
  harnessId: routed.adapter.id,
1975
- cost: attemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id), processingCostEvidence(routed.processing, this.routeBillingKnowledge(input, routed.adapter.id), [`harness:${routed.adapter.id}`])),
1938
+ cost: processingAttemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id, routed), routed.processing),
1976
1939
  });
1977
1940
  log.emit("budget.lease.created", {
1978
1941
  granted: lease.granted,
@@ -1997,7 +1960,7 @@ export class Orchestrator {
1997
1960
  routed,
1998
1961
  attemptId,
1999
1962
  label: `Candidate ${LABELS[i] ?? i + 1}`,
2000
- leaseId: lease.lease?.lease_id ?? "",
1963
+ lease: lease.lease,
2001
1964
  });
2002
1965
  }
2003
1966
  const runsBySlot = new Array(slots.length);
@@ -2008,14 +1971,14 @@ export class Orchestrator {
2008
1971
  let candidateContinuationCount = 0;
2009
1972
  const runSlot = async (slot, slotIdx) => {
2010
1973
  if (input.signal?.aborted) {
2011
- ledger.cancel(slot.leaseId);
1974
+ ledger.cancel(slot.lease.lease_id);
2012
1975
  return;
2013
1976
  }
2014
1977
  // Leases are granted upfront (before spend exists); a worker still
2015
1978
  // re-checks the circuit breaker so queued slots beyond the parallel wave
2016
1979
  // do not start after earlier candidates already blew the hard cap.
2017
1980
  if (budgetStopped || ledger.tier() === "hard") {
2018
- ledger.cancel(slot.leaseId);
1981
+ ledger.cancel(slot.lease.lease_id);
2019
1982
  log.emit("budget.lease.created", {
2020
1983
  granted: false,
2021
1984
  reason: "budget exhausted (hard cap reached)",
@@ -2079,7 +2042,7 @@ export class Orchestrator {
2079
2042
  });
2080
2043
  harnessHome = this.harnessHomeFor(wsm, envelope, slot.routed, input);
2081
2044
  const run = await this.runCandidateInEnvelope(slot.routed, envelope, slot.attemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, {
2082
- id: slot.leaseId,
2045
+ lease: slot.lease,
2083
2046
  onDenied: (denial) => {
2084
2047
  budgetStopped = true;
2085
2048
  budgetDenial ??= denial;
@@ -2089,14 +2052,14 @@ export class Orchestrator {
2089
2052
  safeInvoke(input.onHarnessEvent, safeEv);
2090
2053
  log.emit("harness.event", harnessEventPayload(adapter.id, slot.attemptId, safeEv));
2091
2054
  }, input.signal, downgradeModel ?? undefined, input.effort, this.candidateIntent(input), log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, slot.attemptId, adapter.id, slot.routed.supportsInteractive), (streamedUsd) => {
2092
- ledger.updateHold(slot.leaseId, streamedUsd);
2055
+ ledger.updateHold(slot.lease.lease_id, streamedUsd);
2093
2056
  if (ledger.tier() !== "hard")
2094
2057
  return false;
2095
2058
  budgetStopped = true;
2096
2059
  return true;
2097
2060
  }, input, requestedSingleCandidate, // W-C4 deltas: single-candidate chat lane only (racing = noise x N)
2098
2061
  undefined, undefined, harnessHome);
2099
- ledger.settle(slot.leaseId, attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2062
+ ledger.settle(slot.lease.lease_id, attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2100
2063
  log.emit("harness.completed", {
2101
2064
  harness_id: adapter.id,
2102
2065
  attempt_id: slot.attemptId,
@@ -2142,7 +2105,7 @@ export class Orchestrator {
2142
2105
  attemptId: contAttemptId,
2143
2106
  intent: this.candidateIntent(input),
2144
2107
  harnessId: adapter.id,
2145
- cost: attemptCostEvidence(adapter.id, contAttemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, adapter.id), processingCostEvidence(slot.routed.processing, this.routeBillingKnowledge(input, slot.routed.adapter.id), [`harness:${adapter.id}`])),
2108
+ cost: processingAttemptCostEvidence(adapter.id, contAttemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, adapter.id, slot.routed), slot.routed.processing),
2146
2109
  });
2147
2110
  if (contLease.granted) {
2148
2111
  candidateContinuationCount += 1; // claim the one-shot only once it launches
@@ -2162,7 +2125,7 @@ export class Orchestrator {
2162
2125
  const contLeaseId = contLease.lease?.lease_id ?? "";
2163
2126
  try {
2164
2127
  const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, {
2165
- id: contLeaseId,
2128
+ lease: contLease.lease,
2166
2129
  onDenied: (denial) => {
2167
2130
  budgetStopped = true;
2168
2131
  budgetDenial ??= denial;
@@ -2221,7 +2184,7 @@ export class Orchestrator {
2221
2184
  }
2222
2185
  catch (err) {
2223
2186
  const failureCost = AC.attemptFailureCost(err, "post-stream-error", 0);
2224
- ledger.settle(slot.leaseId, failureCost.settlement);
2187
+ ledger.settle(slot.lease.lease_id, failureCost.settlement);
2225
2188
  const message = safeErrorMessage(err);
2226
2189
  const declared = declaredFailure(err);
2227
2190
  const infraPhase = envelope === undefined ? "workspace" : "harness";
@@ -2428,7 +2391,7 @@ export class Orchestrator {
2428
2391
  attemptId: "synth",
2429
2392
  intent: "synthesize",
2430
2393
  harnessId: synthRouted.adapter.id,
2431
- cost: attemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id), processingCostEvidence(synthRouted.processing, this.routeBillingKnowledge(input, synthRouted.adapter.id), [`harness:${synthRouted.adapter.id}`])),
2394
+ cost: processingAttemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id, synthRouted), synthRouted.processing),
2432
2395
  });
2433
2396
  if (lease.granted) {
2434
2397
  let envelope;
@@ -2455,7 +2418,7 @@ export class Orchestrator {
2455
2418
  });
2456
2419
  const synthHome = this.harnessHomeFor(wsm, envelope, synthRouted, input);
2457
2420
  const run = await this.runCandidateInEnvelope(synthRouted, envelope, "synth", "Synthesis", contract, synthesisInput.prompt, store, paths, wsm, ledger, {
2458
- id: lease.lease.lease_id,
2421
+ lease: lease.lease,
2459
2422
  onDenied: (denial) => {
2460
2423
  budgetStopped = true;
2461
2424
  budgetDenial ??= denial;
@@ -3235,7 +3198,7 @@ export class Orchestrator {
3235
3198
  attemptId,
3236
3199
  intent: "repair",
3237
3200
  harnessId: adapter.id,
3238
- cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id), processingCostEvidence(routed.processing, this.routeBillingKnowledge(input, routed.adapter.id), [`harness:${adapter.id}`])),
3201
+ cost: processingAttemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id, routed), routed.processing),
3239
3202
  });
3240
3203
  if (!lease.granted) {
3241
3204
  exhausted = true;
@@ -3254,7 +3217,7 @@ export class Orchestrator {
3254
3217
  ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
3255
3218
  });
3256
3219
  run = await this.runCandidateInEnvelope(routed, envelope, attemptId, `Attempt ${attempt}`, contract, prompt, store, paths, wsm, ledger, {
3257
- id: lease.lease.lease_id,
3220
+ lease: lease.lease,
3258
3221
  onDenied: (denial) => {
3259
3222
  exhausted = true;
3260
3223
  processingBudgetDenial ??= denial;
@@ -3327,7 +3290,8 @@ export class Orchestrator {
3327
3290
  // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3328
3291
  if (input.signal?.aborted || processingBudgetDenial)
3329
3292
  break;
3330
- if (run.telemetry.effortResolution?.resolution === "rejected")
3293
+ if (run.telemetry.requestRefusal ||
3294
+ run.telemetry.effortResolution?.resolution === "rejected")
3331
3295
  break;
3332
3296
  if (delegateFailure.candidateFailureKind(run)) {
3333
3297
  const failure = delegateFailure.candidateFailureTerminal(run, "convergence");
@@ -3702,6 +3666,7 @@ export class Orchestrator {
3702
3666
  : (convDeclared?.category ?? "internal"),
3703
3667
  code: processingBudgetMapping?.code ?? convDeclared?.code ?? null,
3704
3668
  resetsAt: convDeclared?.resetsAt ?? null,
3669
+ requestRefusal: convDeclared?.requestRefusal,
3705
3670
  safeMessage: convNeedsDecision
3706
3671
  ? `review escalated to a human decision after ${attempt} attempt(s)`
3707
3672
  : facts.reason === "stuck_no_progress"
@@ -3818,17 +3783,17 @@ export class Orchestrator {
3818
3783
  /** Bind private route/session preparation to the planner-attempt owner. */
3819
3784
  plannerAttemptDeps() {
3820
3785
  return {
3821
- billingKnowledge: (input, harnessId) => this.routeBillingKnowledge(input, harnessId),
3786
+ billingKnowledge: (input, harnessId, routed) => this.routeBillingKnowledge(input, harnessId, routed),
3822
3787
  inactivityTimeoutMs: (repoRoot) => harnessInactivityTimeoutMs(this.config(repoRoot)),
3823
3788
  quotaEventSink: this.deps.quotaEventSink,
3824
- prepare: async (args) => {
3789
+ prepare: async (args, processingAdmission, physicalDispatchStarted) => {
3825
3790
  const { input, contract, taskId, runId, log, store, paths, routed, attemptId } = args;
3826
3791
  const adapter = routed.adapter;
3827
3792
  const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
3828
3793
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
3829
3794
  const sessionFields = await this.sessionSpecFields(input, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission);
3830
3795
  const laneContinuity = args.laneRun
3831
- ? 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)
3796
+ ? 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, this.harnessSpecKnobs(contract, knobs, args.intent), processingAdmission, physicalDispatchStarted)
3832
3797
  : null;
3833
3798
  const spec = HarnessRunSpec.parse({
3834
3799
  session_id: newId("ses"),
@@ -3846,6 +3811,7 @@ export class Orchestrator {
3846
3811
  ? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
3847
3812
  : null) ?? args.fallbackHome,
3848
3813
  });
3814
+ this.credentials.stampProfileSelection(spec, input, adapter.id);
3849
3815
  const plannerAbort = new AbortController();
3850
3816
  spec.extra["abortSignal"] = input.signal
3851
3817
  ? AbortSignal.any([input.signal, plannerAbort.signal])
@@ -3853,7 +3819,7 @@ export class Orchestrator {
3853
3819
  const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
3854
3820
  if (planInteraction)
3855
3821
  spec.extra["interactionChannel"] = planInteraction;
3856
- const planWorkMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
3822
+ const planWorkMode = applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
3857
3823
  return {
3858
3824
  knobs,
3859
3825
  effectiveWeb,
@@ -4016,13 +3982,7 @@ export class Orchestrator {
4016
3982
  // terminal names the refused route and does not read "all planners
4017
3983
  // failed"; capture the typed denial for the budget classifier.
4018
3984
  planBudgetDenial ??= outcome.budgetDenial ?? null;
4019
- planAttempts.push({
4020
- attemptId,
4021
- harnessId: outcome.harnessId,
4022
- status: outcome.status,
4023
- outcomeClass: outcome.outcomeClass,
4024
- error: outcome.error,
4025
- });
3985
+ planAttempts.push(plannerAttemptSummary(outcome));
4026
3986
  break;
4027
3987
  }
4028
3988
  if (outcome.telemetry)
@@ -4031,13 +3991,7 @@ export class Orchestrator {
4031
3991
  harnessId: outcome.harnessId,
4032
3992
  telemetry: outcome.telemetry,
4033
3993
  });
4034
- planAttempts.push({
4035
- attemptId,
4036
- harnessId: outcome.harnessId,
4037
- status: outcome.status,
4038
- outcomeClass: outcome.outcomeClass,
4039
- error: outcome.error,
4040
- });
3994
+ planAttempts.push(plannerAttemptSummary(outcome));
4041
3995
  if (outcome.status !== "success") {
4042
3996
  if (outcome.telemetry?.effortResolution?.resolution === "rejected")
4043
3997
  break;
@@ -4162,7 +4116,13 @@ export class Orchestrator {
4162
4116
  quotaSnapshotPreflight() {
4163
4117
  return [...(this.deps.quotaSnapshots?.() ?? [])].map((snapshot) => QuotaSnapshotSchema.parse(snapshot));
4164
4118
  }
4165
- routeBillingKnowledge(input, harnessId) {
4119
+ routeBillingKnowledge(input, harnessId, routed) {
4120
+ if (routed?.quotaAdmission.profile) {
4121
+ return billingKnowledgeForAuthRoute({
4122
+ route: routed.quotaAdmission.route,
4123
+ verification: routed.quotaAdmission.profileVerification ?? "not_run",
4124
+ });
4125
+ }
4166
4126
  // A selected profile's credential_kind decides billing (round-18 #2).
4167
4127
  const profileRoute = this.credentials.profileAuthRoute(input, harnessId);
4168
4128
  if (profileRoute)
@@ -4194,9 +4154,7 @@ export class Orchestrator {
4194
4154
  // The reducer admits under a finite estimate floor (mirror of the n>1
4195
4155
  // scout reserve) so a subscription route is not refused for lacking a
4196
4156
  // cash quote.
4197
- attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId), processingCostEvidence(routed?.processing, this.routeBillingKnowledge(input, harnessId), [
4198
- `harness:${harnessId}`,
4199
- ])),
4157
+ processingAttemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId, routed), routed?.processing),
4200
4158
  buildSpec: async (routed, homeEnv, prompt, attemptId) => {
4201
4159
  const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
4202
4160
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
@@ -4216,10 +4174,11 @@ export class Orchestrator {
4216
4174
  env_inheritance: envInheritance(this.config(input.repoRoot)),
4217
4175
  env: homeEnv,
4218
4176
  });
4177
+ this.credentials.stampProfileSelection(spec, input, routed.adapter.id);
4219
4178
  // D-16: compile the WorkReport transport onto the reducer spec (the
4220
4179
  // reducer is non-interactive) so its output is unwrapped + finalized
4221
4180
  // through the shared attempt contract, not a fourth deliverable predicate.
4222
- const workReportMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, false));
4181
+ const workReportMode = applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, false));
4223
4182
  return {
4224
4183
  spec,
4225
4184
  webPolicy: knobs.webPolicy,
@@ -4369,7 +4328,7 @@ export class Orchestrator {
4369
4328
  // (mirror of the candidate loop): the first top-level scout reserves
4370
4329
  // without a floor; later scouts and every real Delegate child pass the
4371
4330
  // repo floor because they overlap an existing family unit.
4372
- cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id), processingCostEvidence(routed.processing, this.routeBillingKnowledge(input, routed.adapter.id), [`harness:${adapter.id}`])),
4331
+ cost: processingAttemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id, routed), routed.processing),
4373
4332
  });
4374
4333
  if (!lease.granted) {
4375
4334
  log.emit("budget.lease.created", {
@@ -4398,6 +4357,10 @@ export class Orchestrator {
4398
4357
  }
4399
4358
  return { status: "budget_denied", reason: lease.reason ?? "budget lease denied" };
4400
4359
  }
4360
+ const processingAdmission = processingAdmissionForLease(ledger, lease.lease, adapter.id, attemptId, (denial) => {
4361
+ budgetStopped = true;
4362
+ budgetDenial ??= denial;
4363
+ });
4401
4364
  // As with planners, the granted lease owns profile/continuity/spec
4402
4365
  // preparation. Contain a pre-stream rejection as this attempt's failure;
4403
4366
  // parallel siblings can then finish before the shared HOME is disposed.
@@ -4418,17 +4381,11 @@ export class Orchestrator {
4418
4381
  // scouts are excluded from laneRun); native resume is available only when
4419
4382
  // this slot was granted the lane's recorded session.
4420
4383
  const laneContinuity = laneRun
4421
- ? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, sessionFields, store, paths, this.execRootOf(input), log)
4384
+ ? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, sessionFields, store, paths, this.execRootOf(input), log, this.harnessSpecKnobs(contract, knobs, opts.intent), processingAdmission, () => ledger.markPhysicalDispatchStarted(lease.lease.lease_id))
4422
4385
  : null;
4423
4386
  // D-16d: the continuation packet pointer rides after the lane pointer so
4424
4387
  // the fresh session is re-grounded in the exhausted attempt's work.
4425
- const promptWithPointers = [
4426
- explorerPrompt,
4427
- laneContinuity?.pointerLine,
4428
- continuationPointer,
4429
- ]
4430
- .filter((p) => Boolean(p))
4431
- .join("\n\n");
4388
+ const promptWithPointers = renderPromptWithPointers(explorerPrompt, laneContinuity?.pointerLine, continuationPointer);
4432
4389
  const spec = HarnessRunSpec.parse({
4433
4390
  session_id: newId("ses"),
4434
4391
  intent: opts.intent,
@@ -4451,6 +4408,7 @@ export class Orchestrator {
4451
4408
  ? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
4452
4409
  : null) ?? roHome.env,
4453
4410
  });
4411
+ this.credentials.stampProfileSelection(spec, input, adapter.id);
4454
4412
  const reportAbort = new AbortController();
4455
4413
  spec.extra["abortSignal"] = input.signal
4456
4414
  ? AbortSignal.any([input.signal, reportAbort.signal])
@@ -4460,7 +4418,7 @@ export class Orchestrator {
4460
4418
  spec.extra["interactionChannel"] = reportInteraction;
4461
4419
  // D-16: compile the WorkReport envelope for the read-only lane.
4462
4420
  const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4463
- const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
4421
+ const readonlyWorkMode = applyWorkEnvelope(spec, readonlyWorkEnvelope);
4464
4422
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4465
4423
  const answer = new AnswerAssembly();
4466
4424
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [],
@@ -4482,7 +4440,7 @@ export class Orchestrator {
4482
4440
  const message = `read-only attempt setup failed: ${safeErrorMessage(preparation.error)}`;
4483
4441
  AC.settleGrantedAttemptLease({
4484
4442
  ledger,
4485
- leaseId: lease.lease?.lease_id ?? "",
4443
+ leaseId: lease.lease.lease_id,
4486
4444
  attemptId,
4487
4445
  harnessId: adapter.id,
4488
4446
  costUsd: 0,
@@ -4515,10 +4473,7 @@ export class Orchestrator {
4515
4473
  const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, telemetry, } = preparation.value;
4516
4474
  let { answer } = preparation.value;
4517
4475
  let spec = preparedSpec;
4518
- bindProcessingAdmission(spec, ledger, lease.lease.lease_id, adapter.id, attemptId, (denial) => {
4519
- budgetStopped = true;
4520
- budgetDenial ??= denial;
4521
- });
4476
+ bindProcessingAdmission(spec, ledger, lease.lease, adapter.id, attemptId, undefined, processingAdmission);
4522
4477
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4523
4478
  let activeSessionId = spec.session_id;
4524
4479
  const live = liveAttempt(input, routed, paths, contract, attemptId, () => activeSessionId);
@@ -4553,6 +4508,8 @@ export class Orchestrator {
4553
4508
  extra: { ...spec.extra },
4554
4509
  });
4555
4510
  activeSessionId = runSpec.session_id;
4511
+ // #363: bound before the spawn — a credential change mid-try voids it.
4512
+ const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
4556
4513
  const transientStart = telemetry.transientFailures.length;
4557
4514
  const rateLimitStart = telemetry.rateLimits.length;
4558
4515
  log.emit("harness.started", {
@@ -4618,7 +4575,8 @@ export class Orchestrator {
4618
4575
  }
4619
4576
  catch (err) {
4620
4577
  harnessError = safeErrorMessage(err);
4621
- if (err instanceof ProcessingBudgetAdmissionError) {
4578
+ if (err instanceof ProcessingBudgetAdmissionError ||
4579
+ err instanceof RoutingPreflightError) {
4622
4580
  processingRefusal = err;
4623
4581
  break;
4624
4582
  }
@@ -4628,10 +4586,14 @@ export class Orchestrator {
4628
4586
  }
4629
4587
  if (streamBudgetDenied)
4630
4588
  break;
4589
+ if (telemetry.requestRefusal)
4590
+ break;
4631
4591
  const newTransients = telemetry.transientFailures.slice(transientStart);
4632
4592
  const sawRetryable = newTransients.some((f) => f.retryable);
4633
4593
  const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
4634
4594
  const reportSoFar = answer.text();
4595
+ const delivered = !harnessError && reportSoFar.length > 0 && !input.signal?.aborted;
4596
+ this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
4635
4597
  // W5.4 + A2 reactive failover, READ-ONLY lane (same contract as the
4636
4598
  // candidate lane: typed limit or structural pre-progress death).
4637
4599
  if (harnessError && !input.signal?.aborted) {
@@ -4643,9 +4605,10 @@ export class Orchestrator {
4643
4605
  registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
4644
4606
  snapshots: this.deps.quotaSnapshots?.() ?? [],
4645
4607
  probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
4646
- ...this.credentials.rotationObservations(adapter, spec, newTransients),
4608
+ ...this.credentials.rotationObservations(adapter, spec, newTransients, refusal),
4647
4609
  triedProfiles,
4648
4610
  markers: telemetry.outputMarkers,
4611
+ requestRefused: Boolean(telemetry.requestRefusal),
4649
4612
  sawTypedLimit,
4650
4613
  sawRetryable,
4651
4614
  attemptErrored: harnessError !== null,
@@ -4694,7 +4657,7 @@ export class Orchestrator {
4694
4657
  input.signal?.removeEventListener("abort", onAbort);
4695
4658
  AC.settleGrantedAttemptLease({
4696
4659
  ledger,
4697
- leaseId: lease.lease?.lease_id ?? "",
4660
+ leaseId: lease.lease.lease_id,
4698
4661
  attemptId,
4699
4662
  harnessId: adapter.id,
4700
4663
  costUsd: cost,
@@ -4704,7 +4667,7 @@ export class Orchestrator {
4704
4667
  preStreamFailureSource: "readonly-pre-stream",
4705
4668
  });
4706
4669
  }
4707
- if (harnessError && !poolExhausted && !processingRefusal) {
4670
+ if (harnessError && !poolExhausted && !processingRefusal && !telemetry.requestRefusal) {
4708
4671
  emitTransientExhausted((t, p) => log.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
4709
4672
  }
4710
4673
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
@@ -4765,9 +4728,11 @@ export class Orchestrator {
4765
4728
  report,
4766
4729
  error: harnessError,
4767
4730
  telemetry,
4768
- ...(poolExhausted || processingRefusal
4769
- ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
4770
- : {}),
4731
+ ...(telemetry.requestRefusal
4732
+ ? { declaredFailure: requestRefusalFailure(telemetry.requestRefusal) }
4733
+ : poolExhausted || processingRefusal
4734
+ ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
4735
+ : {}),
4771
4736
  });
4772
4737
  if (opts.deepScan) {
4773
4738
  store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
@@ -4882,6 +4847,7 @@ export class Orchestrator {
4882
4847
  const firstModel = contract.routing_models[routed.adapter.id] ?? routed.settings?.defaultModel ?? null;
4883
4848
  if (last &&
4884
4849
  last.status === "failed" &&
4850
+ !last.telemetry.requestRefusal &&
4885
4851
  fallbackModel &&
4886
4852
  fallbackModel !== firstModel &&
4887
4853
  !budgetStopped &&
@@ -5008,6 +4974,7 @@ export class Orchestrator {
5008
4974
  eventRefs: roEventRefs,
5009
4975
  runDir: paths.root,
5010
4976
  resetsAt: roDeclared?.resetsAt ?? null,
4977
+ requestRefusal: roDeclared?.requestRefusal,
5011
4978
  vendorFailure: attemptVendorFailure(attemptTelemetries, last?.attemptId),
5012
4979
  nextActions: harnessFailureNextActions(roCategory),
5013
4980
  });
@@ -5068,10 +5035,7 @@ export class Orchestrator {
5068
5035
  const succeeded = succeededReadonly;
5069
5036
  if (opts.deepScan && succeeded.length === 0) {
5070
5037
  const blocked = attempts.some((a) => a.status === "blocked");
5071
- // QA-050/QA-019: an all-denied scan (finite-zero, or every scout refused
5072
- // before spawn) is a BUDGET failure, not harness_error — route it through
5073
- // the shared classifier. Only a pure-denial scan (no scout actually errored
5074
- // in the harness) qualifies, so a real explorer failure is never masked.
5038
+ // QA-050/QA-019: only a pure-denial scan may claim a budget failure.
5075
5039
  const scanBudgetMapping = budgetStopped && !blocked && attempts.every((a) => a.budgetDenied === true)
5076
5040
  ? classifyBudgetFailure({ denial: budgetDenial, terminal: ledger.terminal() })
5077
5041
  : null;
@@ -5093,12 +5057,15 @@ export class Orchestrator {
5093
5057
  }));
5094
5058
  }
5095
5059
  else {
5096
- // #31: classify the scout failures; keep the scan-specific width hint but
5097
- // drop the unconditional auth line unless the cause was a real auth failure.
5060
+ // Mixed causes stay generic; only unanimous evidence may name the scan's cause.
5061
+ const declared = blocked ? null : unanimousDeclaredFailure(attempts);
5098
5062
  const scanCategory = dominantHarnessFailureCategory(attemptTelemetries.flatMap((a) => a.telemetry.transientFailures));
5099
5063
  writeFailure(store, paths, {
5100
5064
  phase: "harness",
5101
- category: blocked ? "policy" : "harness_error",
5065
+ category: blocked ? "policy" : (declared?.category ?? "harness_error"),
5066
+ code: declared?.code,
5067
+ resetsAt: declared?.resetsAt,
5068
+ requestRefusal: declared?.requestRefusal,
5102
5069
  safeMessage: message || "all explorers failed",
5103
5070
  eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
5104
5071
  runDir: paths.root,
@@ -5109,10 +5076,7 @@ export class Orchestrator {
5109
5076
  ],
5110
5077
  });
5111
5078
  }
5112
- // QA-036: with ZERO successful explorers there is no synthesizable
5113
- // deliverable, so a blocked scan can no longer read as a succeeded
5114
- // "needs review" run (exit 0). An empty scan is a failure whether the
5115
- // explorers were blocked or errored.
5079
+ // QA-036: no successful explorer means no synthesizable deliverable.
5116
5080
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Lifecycle: ${scanFailFacts.lifecycle}${scanFailFacts.reason ? ` (${scanFailFacts.reason})` : ""}\n\n${message}\n`);
5117
5081
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
5118
5082
  log.emit("run.failed", {