@claudexor/orchestrator 3.17.0 → 3.17.2

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 (84) 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 +6 -10
  14. package/dist/attemptTelemetry.d.ts.map +1 -1
  15. package/dist/attemptTelemetry.js +9 -5
  16. package/dist/attemptTelemetry.js.map +1 -1
  17. package/dist/candidateEvidence.d.ts +1 -1
  18. package/dist/candidateEvidence.d.ts.map +1 -1
  19. package/dist/candidateEvidence.js.map +1 -1
  20. package/dist/candidateTerminals.d.ts.map +1 -1
  21. package/dist/candidateTerminals.js +1 -0
  22. package/dist/candidateTerminals.js.map +1 -1
  23. package/dist/credential-differential.d.ts +1 -1
  24. package/dist/credential-differential.d.ts.map +1 -1
  25. package/dist/credential-differential.js +5 -2
  26. package/dist/credential-differential.js.map +1 -1
  27. package/dist/credential-profile-rotation.d.ts +4 -0
  28. package/dist/credential-profile-rotation.d.ts.map +1 -1
  29. package/dist/credential-profile-rotation.js +12 -13
  30. package/dist/credential-profile-rotation.js.map +1 -1
  31. package/dist/credential-profiles.d.ts +15 -2
  32. package/dist/credential-profiles.d.ts.map +1 -1
  33. package/dist/credential-profiles.js +19 -5
  34. package/dist/credential-profiles.js.map +1 -1
  35. package/dist/effortGovernance.d.ts +4 -40
  36. package/dist/effortGovernance.d.ts.map +1 -1
  37. package/dist/effortGovernance.js +4 -32
  38. package/dist/effortGovernance.js.map +1 -1
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/laneStreamEvents.d.ts +4 -1
  44. package/dist/laneStreamEvents.d.ts.map +1 -1
  45. package/dist/laneStreamEvents.js +9 -0
  46. package/dist/laneStreamEvents.js.map +1 -1
  47. package/dist/modelGovernance.d.ts.map +1 -1
  48. package/dist/modelGovernance.js +4 -1
  49. package/dist/modelGovernance.js.map +1 -1
  50. package/dist/orchestrator-credentials.d.ts +42 -3
  51. package/dist/orchestrator-credentials.d.ts.map +1 -1
  52. package/dist/orchestrator-credentials.js +94 -5
  53. package/dist/orchestrator-credentials.js.map +1 -1
  54. package/dist/orchestrator.d.ts +3 -8
  55. package/dist/orchestrator.d.ts.map +1 -1
  56. package/dist/orchestrator.js +88 -97
  57. package/dist/orchestrator.js.map +1 -1
  58. package/dist/planRun.d.ts +7 -16
  59. package/dist/planRun.d.ts.map +1 -1
  60. package/dist/planRun.js +21 -23
  61. package/dist/planRun.js.map +1 -1
  62. package/dist/pre-progress-refusal.d.ts +61 -0
  63. package/dist/pre-progress-refusal.d.ts.map +1 -0
  64. package/dist/pre-progress-refusal.js +67 -0
  65. package/dist/pre-progress-refusal.js.map +1 -0
  66. package/dist/prompt-framing.d.ts +8 -0
  67. package/dist/prompt-framing.d.ts.map +1 -0
  68. package/dist/prompt-framing.js +33 -0
  69. package/dist/prompt-framing.js.map +1 -0
  70. package/dist/reviewerPanel.d.ts.map +1 -1
  71. package/dist/reviewerPanel.js +18 -17
  72. package/dist/reviewerPanel.js.map +1 -1
  73. package/dist/rotation-predicate.d.ts +3 -0
  74. package/dist/rotation-predicate.d.ts.map +1 -1
  75. package/dist/rotation-predicate.js +3 -0
  76. package/dist/rotation-predicate.js.map +1 -1
  77. package/dist/runSupport.d.ts.map +1 -1
  78. package/dist/runSupport.js +2 -4
  79. package/dist/runSupport.js.map +1 -1
  80. package/dist/runTerminalResults.d.ts +4 -1
  81. package/dist/runTerminalResults.d.ts.map +1 -1
  82. package/dist/runTerminalResults.js +14 -2
  83. package/dist/runTerminalResults.js.map +1 -1
  84. package/package.json +17 -17
@@ -1,8 +1,10 @@
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
4
  import { bindProcessingAdmission, 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";
7
+ import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, rotateSpecOnTypedLimit, } from "./credential-profiles.js";
6
8
  import { OrchestratorCredentials, reviewerProfileResolver, rotatedSpecInLaneHome, } from "./orchestrator-credentials.js";
7
9
  import { accountPoolRows } from "./account-pool.js";
8
10
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
@@ -11,7 +13,7 @@ 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";
@@ -39,7 +41,7 @@ import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, wr
39
41
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
40
42
  import { finalizeStructuredOutput } from "./structuredOutput.js";
41
43
  import { admitRun } from "./runAdmission.js";
42
- import { dropDeltaPastBudget, emitPlanProgress, emitTransientExhausted, emitTransientRetryPlan, observeReadonlySpend, } from "./laneStreamEvents.js";
44
+ import { dropDeltaPastBudget, emitPlanProgress, emitTransientExhausted, emitTransientRetryPlan, observeReadonlySpend, recordAdapterThrow, } from "./laneStreamEvents.js";
43
45
  import { promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, winnerNeedsHuman, writeRaceDeliveryDecision, } from "./runSupport.js";
44
46
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
45
47
  import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
@@ -48,7 +50,7 @@ import { buildContinuation, } from "./continuity.js";
48
50
  import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from "./continuity-facts.js";
49
51
  import { runDiffReview } from "./diffReview.js";
50
52
  import { rawScoutBundle, resolveDeepScanSynthesis, } from "./deepScanReducer.js";
51
- import { classifyAdapterThrow, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
53
+ import { createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
52
54
  import { newAttemptOutputMarkers } from "./attemptOutputMarkers.js";
53
55
  import * as delegateFailure from "./delegationFailure.js";
54
56
  import * as secretDiff from "./secretDiff.js";
@@ -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
  /**
@@ -554,43 +557,19 @@ export class Orchestrator {
554
557
  // profile-admitted route joins even an AUTO pool (the run spawns with
555
558
  // the profile's transport, so the default store's state is not the
556
559
  // routing truth). Capability/manifest gating above still applies.
557
- let profileAdmitted = false;
558
560
  const profileAdapter = this.deps.registry.get(id);
559
- const profileProbe = profileAdapter?.probeCredentialProfile?.bind(profileAdapter);
560
561
  const explicitPin = this.credentials.effectiveProfileId(input, id);
561
- const model = input.models?.[id] ?? cfgEntry?.default_model ?? null;
562
562
  // 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
- }
563
+ const rows = await this.credentials.admitRouteRows({
564
+ input,
565
+ harnessId: id,
566
+ defaultReady: status.status === "ok",
567
+ model: input.models?.[id] ?? cfgEntry?.default_model ?? null,
568
+ quota: vendorQuota,
569
+ unusable: liveUnusable,
570
+ probe: profileAdapter?.probeCredentialProfile?.bind(profileAdapter),
571
+ });
572
+ const profileAdmitted = rows.admitted;
594
573
  if (profileAdmitted) {
595
574
  // A valid account row restores manifest intent truth when the default store failed.
596
575
  if (status.status !== "ok") {
@@ -603,14 +582,19 @@ export class Orchestrator {
603
582
  }
604
583
  }
605
584
  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}`;
585
+ // An explicit pin that is not ready refuses/drops the lane; not-ready
586
+ // UNPINNED rows are named below, and the default doctor verdict speaks
587
+ // only when neither a row nor a default login exists.
588
+ const why = `${id} credential profile is not ready: ${rows.pinVerdict}`;
611
589
  dropLane(id, "credential", why);
612
590
  continue;
613
591
  }
592
+ // #363: registered rows own this lane, so their observed states — not
593
+ // the default login's doctor advice — explain why none was admitted.
594
+ if (rows.unreadyRows && (status.status === "unavailable" || !explicitPool)) {
595
+ dropLane(id, "credential", rows.unreadyRows);
596
+ continue;
597
+ }
614
598
  if (status.status === "unavailable" && !profileAdmitted) {
615
599
  const why = `${id} is unavailable${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`;
616
600
  dropLane(id, "doctor", why);
@@ -1135,25 +1119,6 @@ export class Orchestrator {
1135
1119
  callerSchema: contract.output_schema ?? null,
1136
1120
  });
1137
1121
  }
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
1122
  routeSpecKnobs(routed, contract, overrideModel, effortHint) {
1158
1123
  const s = routed.settings;
1159
1124
  const contractPolicy = contract.external_context.policy;
@@ -1286,7 +1251,7 @@ export class Orchestrator {
1286
1251
  const briefPath = join(paths.contextDir, "THREAD.md");
1287
1252
  store.writeText(briefPath, result.packetMarkdown);
1288
1253
  return {
1289
- pointerLine: `Earlier conversation context for this thread is at: ${briefPath} — read it before answering.`,
1254
+ pointerLine: threadContextPointer(briefPath),
1290
1255
  };
1291
1256
  }
1292
1257
  catch (err) {
@@ -1405,6 +1370,8 @@ export class Orchestrator {
1405
1370
  raw_context_packet: rawContextPacket,
1406
1371
  stream_deltas: streamDeltas,
1407
1372
  });
1373
+ if (runInput)
1374
+ this.credentials.stampProfileSelection(spec, runInput, adapter.id);
1408
1375
  bindProcessingAdmission(spec, ledger, processingLease.id, adapter.id, attemptId, processingLease.onDenied, processingAdmission);
1409
1376
  const billingInput = runInput ?? { repoRoot: contract.repo.root };
1410
1377
  // Keep billing route-bound until admission observes the actual prepared
@@ -1418,7 +1385,7 @@ export class Orchestrator {
1418
1385
  if (interaction)
1419
1386
  spec.extra["interactionChannel"] = interaction;
1420
1387
  const workEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(interaction));
1421
- const workReportMode = this.applyWorkEnvelope(spec, workEnvelope);
1388
+ const workReportMode = applyWorkEnvelope(spec, workEnvelope);
1422
1389
  const inactivityMs = harnessInactivityTimeoutMs(this.config(contract.repo.root));
1423
1390
  // Named once: the attempt record, the CandidateRun and the terminal gate all
1424
1391
  // read the SAME applied facts rather than three re-derivations. Derived
@@ -1482,6 +1449,8 @@ export class Orchestrator {
1482
1449
  ? AbortSignal.any([signal, attemptAbort.signal])
1483
1450
  : attemptAbort.signal;
1484
1451
  activeSessionId = runSpec.session_id;
1452
+ // #363: bound before the spawn — a credential change mid-try voids it.
1453
+ const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
1485
1454
  const transientStart = telemetry.transientFailures.length;
1486
1455
  const rateLimitStart = telemetry.rateLimits.length;
1487
1456
  let rawPatch = null;
@@ -1589,11 +1558,14 @@ export class Orchestrator {
1589
1558
  processingRefusal = err;
1590
1559
  break;
1591
1560
  }
1592
- telemetry.transientFailures.push(classifyAdapterThrow({ errorName: err instanceof Error ? err.name : null }));
1561
+ if (!recordAdapterThrow(telemetry, err))
1562
+ break;
1593
1563
  }
1594
1564
  finally {
1595
1565
  clearFileBackedContext();
1596
1566
  }
1567
+ if (telemetry.requestRefusal)
1568
+ break;
1597
1569
  const newTransients = telemetry.transientFailures.slice(transientStart);
1598
1570
  // #31: the centralized retry gate reads the classified `retryable` verdict.
1599
1571
  const sawRetryable = newTransients.some((f) => f.retryable);
@@ -1605,6 +1577,8 @@ export class Orchestrator {
1605
1577
  ? directoryCapture.files?.noChanges === true
1606
1578
  : currentDiff.trim().length === 0;
1607
1579
  const deliverableEmpty = workspaceUnchanged && answer.text().length === 0;
1580
+ const delivered = !harnessErrored && !deliverableEmpty && !signal?.aborted;
1581
+ this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
1608
1582
  // W5.4 + A2 failover: a typed-limit hit OR a structural pre-progress
1609
1583
  // death rebuilds the spec on a NEW session under the next profile.
1610
1584
  if (harnessErrored && runInput && !signal?.aborted) {
@@ -1616,9 +1590,10 @@ export class Orchestrator {
1616
1590
  registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
1617
1591
  snapshots: this.deps.quotaSnapshots?.() ?? [],
1618
1592
  probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
1619
- ...this.credentials.rotationObservations(adapter, spec, newTransients),
1593
+ ...this.credentials.rotationObservations(adapter, spec, newTransients, refusal),
1620
1594
  triedProfiles,
1621
1595
  markers: telemetry.outputMarkers,
1596
+ requestRefused: Boolean(telemetry.requestRefusal),
1622
1597
  sawTypedLimit,
1623
1598
  sawRetryable,
1624
1599
  attemptErrored: harnessErrored,
@@ -1670,7 +1645,7 @@ export class Orchestrator {
1670
1645
  }
1671
1646
  // A pool-exhausted terminal is rotation's verdict, not the transient
1672
1647
  // machinery's — no `route.transient.exhausted` rides along with it.
1673
- if (harnessErrored && !poolExhausted && !processingRefusal) {
1648
+ if (harnessErrored && !poolExhausted && !processingRefusal && !telemetry.requestRefusal) {
1674
1649
  emitTransientExhausted((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
1675
1650
  }
1676
1651
  const attemptStreamEndedMs = Date.now();
@@ -1821,9 +1796,11 @@ export class Orchestrator {
1821
1796
  telemetry,
1822
1797
  ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1823
1798
  // A5: the typed refusal survives NORMAL attempt finalization (no throw).
1824
- ...(poolExhausted || processingRefusal
1825
- ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
1826
- : {}),
1799
+ ...(telemetry.requestRefusal
1800
+ ? { declaredFailure: requestRefusalFailure(telemetry.requestRefusal) }
1801
+ : poolExhausted || processingRefusal
1802
+ ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
1803
+ : {}),
1827
1804
  outcomeClass: finalized.outcomeClass,
1828
1805
  applied,
1829
1806
  };
@@ -3326,6 +3303,9 @@ export class Orchestrator {
3326
3303
  // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3327
3304
  if (input.signal?.aborted || processingBudgetDenial)
3328
3305
  break;
3306
+ if (run.telemetry.requestRefusal ||
3307
+ run.telemetry.effortResolution?.resolution === "rejected")
3308
+ break;
3329
3309
  if (delegateFailure.candidateFailureKind(run)) {
3330
3310
  const failure = delegateFailure.candidateFailureTerminal(run, "convergence");
3331
3311
  if (run.files) {
@@ -3699,6 +3679,7 @@ export class Orchestrator {
3699
3679
  : (convDeclared?.category ?? "internal"),
3700
3680
  code: processingBudgetMapping?.code ?? convDeclared?.code ?? null,
3701
3681
  resetsAt: convDeclared?.resetsAt ?? null,
3682
+ requestRefusal: convDeclared?.requestRefusal,
3702
3683
  safeMessage: convNeedsDecision
3703
3684
  ? `review escalated to a human decision after ${attempt} attempt(s)`
3704
3685
  : facts.reason === "stuck_no_progress"
@@ -3843,6 +3824,7 @@ export class Orchestrator {
3843
3824
  ? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
3844
3825
  : null) ?? args.fallbackHome,
3845
3826
  });
3827
+ this.credentials.stampProfileSelection(spec, input, adapter.id);
3846
3828
  const plannerAbort = new AbortController();
3847
3829
  spec.extra["abortSignal"] = input.signal
3848
3830
  ? AbortSignal.any([input.signal, plannerAbort.signal])
@@ -3850,7 +3832,7 @@ export class Orchestrator {
3850
3832
  const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
3851
3833
  if (planInteraction)
3852
3834
  spec.extra["interactionChannel"] = planInteraction;
3853
- const planWorkMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
3835
+ const planWorkMode = applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
3854
3836
  return {
3855
3837
  knobs,
3856
3838
  effectiveWeb,
@@ -4036,6 +4018,8 @@ export class Orchestrator {
4036
4018
  error: outcome.error,
4037
4019
  });
4038
4020
  if (outcome.status !== "success") {
4021
+ if (outcome.telemetry?.effortResolution?.resolution === "rejected")
4022
+ break;
4039
4023
  const next = adapters[idx + 1];
4040
4024
  if (next && !input.signal?.aborted) {
4041
4025
  fallbackFrom = outcome.harnessId;
@@ -4211,10 +4195,11 @@ export class Orchestrator {
4211
4195
  env_inheritance: envInheritance(this.config(input.repoRoot)),
4212
4196
  env: homeEnv,
4213
4197
  });
4198
+ this.credentials.stampProfileSelection(spec, input, routed.adapter.id);
4214
4199
  // D-16: compile the WorkReport transport onto the reducer spec (the
4215
4200
  // reducer is non-interactive) so its output is unwrapped + finalized
4216
4201
  // through the shared attempt contract, not a fourth deliverable predicate.
4217
- const workReportMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, false));
4202
+ const workReportMode = applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, false));
4218
4203
  return {
4219
4204
  spec,
4220
4205
  webPolicy: knobs.webPolicy,
@@ -4417,13 +4402,7 @@ export class Orchestrator {
4417
4402
  : null;
4418
4403
  // D-16d: the continuation packet pointer rides after the lane pointer so
4419
4404
  // the fresh session is re-grounded in the exhausted attempt's work.
4420
- const promptWithPointers = [
4421
- explorerPrompt,
4422
- laneContinuity?.pointerLine,
4423
- continuationPointer,
4424
- ]
4425
- .filter((p) => Boolean(p))
4426
- .join("\n\n");
4405
+ const promptWithPointers = renderPromptWithPointers(explorerPrompt, laneContinuity?.pointerLine, continuationPointer);
4427
4406
  const spec = HarnessRunSpec.parse({
4428
4407
  session_id: newId("ses"),
4429
4408
  intent: opts.intent,
@@ -4446,6 +4425,7 @@ export class Orchestrator {
4446
4425
  ? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
4447
4426
  : null) ?? roHome.env,
4448
4427
  });
4428
+ this.credentials.stampProfileSelection(spec, input, adapter.id);
4449
4429
  const reportAbort = new AbortController();
4450
4430
  spec.extra["abortSignal"] = input.signal
4451
4431
  ? AbortSignal.any([input.signal, reportAbort.signal])
@@ -4455,7 +4435,7 @@ export class Orchestrator {
4455
4435
  spec.extra["interactionChannel"] = reportInteraction;
4456
4436
  // D-16: compile the WorkReport envelope for the read-only lane.
4457
4437
  const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4458
- const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
4438
+ const readonlyWorkMode = applyWorkEnvelope(spec, readonlyWorkEnvelope);
4459
4439
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4460
4440
  const answer = new AnswerAssembly();
4461
4441
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [],
@@ -4548,6 +4528,8 @@ export class Orchestrator {
4548
4528
  extra: { ...spec.extra },
4549
4529
  });
4550
4530
  activeSessionId = runSpec.session_id;
4531
+ // #363: bound before the spawn — a credential change mid-try voids it.
4532
+ const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
4551
4533
  const transientStart = telemetry.transientFailures.length;
4552
4534
  const rateLimitStart = telemetry.rateLimits.length;
4553
4535
  log.emit("harness.started", {
@@ -4618,14 +4600,19 @@ export class Orchestrator {
4618
4600
  break;
4619
4601
  }
4620
4602
  // #31: classify the throw (watchdog timeout vs process crash) as typed.
4621
- telemetry.transientFailures.push(classifyAdapterThrow({ errorName: err instanceof Error ? err.name : null }));
4603
+ if (!recordAdapterThrow(telemetry, err))
4604
+ break;
4622
4605
  }
4623
4606
  if (streamBudgetDenied)
4624
4607
  break;
4608
+ if (telemetry.requestRefusal)
4609
+ break;
4625
4610
  const newTransients = telemetry.transientFailures.slice(transientStart);
4626
4611
  const sawRetryable = newTransients.some((f) => f.retryable);
4627
4612
  const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
4628
4613
  const reportSoFar = answer.text();
4614
+ const delivered = !harnessError && reportSoFar.length > 0 && !input.signal?.aborted;
4615
+ this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
4629
4616
  // W5.4 + A2 reactive failover, READ-ONLY lane (same contract as the
4630
4617
  // candidate lane: typed limit or structural pre-progress death).
4631
4618
  if (harnessError && !input.signal?.aborted) {
@@ -4637,9 +4624,10 @@ export class Orchestrator {
4637
4624
  registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
4638
4625
  snapshots: this.deps.quotaSnapshots?.() ?? [],
4639
4626
  probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
4640
- ...this.credentials.rotationObservations(adapter, spec, newTransients),
4627
+ ...this.credentials.rotationObservations(adapter, spec, newTransients, refusal),
4641
4628
  triedProfiles,
4642
4629
  markers: telemetry.outputMarkers,
4630
+ requestRefused: Boolean(telemetry.requestRefusal),
4643
4631
  sawTypedLimit,
4644
4632
  sawRetryable,
4645
4633
  attemptErrored: harnessError !== null,
@@ -4698,7 +4686,7 @@ export class Orchestrator {
4698
4686
  preStreamFailureSource: "readonly-pre-stream",
4699
4687
  });
4700
4688
  }
4701
- if (harnessError && !poolExhausted && !processingRefusal) {
4689
+ if (harnessError && !poolExhausted && !processingRefusal && !telemetry.requestRefusal) {
4702
4690
  emitTransientExhausted((t, p) => log.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
4703
4691
  }
4704
4692
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
@@ -4759,9 +4747,11 @@ export class Orchestrator {
4759
4747
  report,
4760
4748
  error: harnessError,
4761
4749
  telemetry,
4762
- ...(poolExhausted || processingRefusal
4763
- ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
4764
- : {}),
4750
+ ...(telemetry.requestRefusal
4751
+ ? { declaredFailure: requestRefusalFailure(telemetry.requestRefusal) }
4752
+ : poolExhausted || processingRefusal
4753
+ ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
4754
+ : {}),
4765
4755
  });
4766
4756
  if (opts.deepScan) {
4767
4757
  store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
@@ -4868,12 +4858,15 @@ export class Orchestrator {
4868
4858
  }
4869
4859
  }
4870
4860
  }
4861
+ if (last?.telemetry.effortResolution?.resolution === "rejected")
4862
+ break;
4871
4863
  // Per-harness fallback_model: one same-harness retry on FAILURE (not
4872
4864
  // policy blocks) before falling through to the next harness.
4873
4865
  const fallbackModel = routed.settings?.fallbackModel;
4874
4866
  const firstModel = contract.routing_models[routed.adapter.id] ?? routed.settings?.defaultModel ?? null;
4875
4867
  if (last &&
4876
4868
  last.status === "failed" &&
4869
+ !last.telemetry.requestRefusal &&
4877
4870
  fallbackModel &&
4878
4871
  fallbackModel !== firstModel &&
4879
4872
  !budgetStopped &&
@@ -5000,6 +4993,7 @@ export class Orchestrator {
5000
4993
  eventRefs: roEventRefs,
5001
4994
  runDir: paths.root,
5002
4995
  resetsAt: roDeclared?.resetsAt ?? null,
4996
+ requestRefusal: roDeclared?.requestRefusal,
5003
4997
  vendorFailure: attemptVendorFailure(attemptTelemetries, last?.attemptId),
5004
4998
  nextActions: harnessFailureNextActions(roCategory),
5005
4999
  });
@@ -5060,10 +5054,7 @@ export class Orchestrator {
5060
5054
  const succeeded = succeededReadonly;
5061
5055
  if (opts.deepScan && succeeded.length === 0) {
5062
5056
  const blocked = attempts.some((a) => a.status === "blocked");
5063
- // QA-050/QA-019: an all-denied scan (finite-zero, or every scout refused
5064
- // before spawn) is a BUDGET failure, not harness_error — route it through
5065
- // the shared classifier. Only a pure-denial scan (no scout actually errored
5066
- // in the harness) qualifies, so a real explorer failure is never masked.
5057
+ // QA-050/QA-019: only a pure-denial scan may claim a budget failure.
5067
5058
  const scanBudgetMapping = budgetStopped && !blocked && attempts.every((a) => a.budgetDenied === true)
5068
5059
  ? classifyBudgetFailure({ denial: budgetDenial, terminal: ledger.terminal() })
5069
5060
  : null;
@@ -5085,12 +5076,15 @@ export class Orchestrator {
5085
5076
  }));
5086
5077
  }
5087
5078
  else {
5088
- // #31: classify the scout failures; keep the scan-specific width hint but
5089
- // drop the unconditional auth line unless the cause was a real auth failure.
5079
+ // Mixed causes stay generic; only unanimous evidence may name the scan's cause.
5080
+ const declared = blocked ? null : unanimousDeclaredFailure(attempts);
5090
5081
  const scanCategory = dominantHarnessFailureCategory(attemptTelemetries.flatMap((a) => a.telemetry.transientFailures));
5091
5082
  writeFailure(store, paths, {
5092
5083
  phase: "harness",
5093
- category: blocked ? "policy" : "harness_error",
5084
+ category: blocked ? "policy" : (declared?.category ?? "harness_error"),
5085
+ code: declared?.code,
5086
+ resetsAt: declared?.resetsAt,
5087
+ requestRefusal: declared?.requestRefusal,
5094
5088
  safeMessage: message || "all explorers failed",
5095
5089
  eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
5096
5090
  runDir: paths.root,
@@ -5101,10 +5095,7 @@ export class Orchestrator {
5101
5095
  ],
5102
5096
  });
5103
5097
  }
5104
- // QA-036: with ZERO successful explorers there is no synthesizable
5105
- // deliverable, so a blocked scan can no longer read as a succeeded
5106
- // "needs review" run (exit 0). An empty scan is a failure whether the
5107
- // explorers were blocked or errored.
5098
+ // QA-036: no successful explorer means no synthesizable deliverable.
5108
5099
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Lifecycle: ${scanFailFacts.lifecycle}${scanFailFacts.reason ? ` (${scanFailFacts.reason})` : ""}\n\n${message}\n`);
5109
5100
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
5110
5101
  log.emit("run.failed", {