@claudexor/orchestrator 4.1.1 → 4.3.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 (170) hide show
  1. package/dist/account-pool.d.ts +11 -0
  2. package/dist/account-pool.d.ts.map +1 -1
  3. package/dist/account-pool.js +9 -0
  4. package/dist/account-pool.js.map +1 -1
  5. package/dist/account-resolution.d.ts +5 -13
  6. package/dist/account-resolution.d.ts.map +1 -1
  7. package/dist/account-resolution.js +14 -15
  8. package/dist/account-resolution.js.map +1 -1
  9. package/dist/attemptTelemetry.d.ts +4 -6
  10. package/dist/attemptTelemetry.d.ts.map +1 -1
  11. package/dist/attemptTelemetry.js +6 -10
  12. package/dist/attemptTelemetry.js.map +1 -1
  13. package/dist/attemptUsageCost.d.ts +6 -1
  14. package/dist/attemptUsageCost.d.ts.map +1 -1
  15. package/dist/attemptUsageCost.js +16 -1
  16. package/dist/attemptUsageCost.js.map +1 -1
  17. package/dist/budgetFailure.d.ts +4 -1
  18. package/dist/budgetFailure.d.ts.map +1 -1
  19. package/dist/budgetFailure.js +4 -2
  20. package/dist/budgetFailure.js.map +1 -1
  21. package/dist/candidateEvidence.d.ts.map +1 -1
  22. package/dist/candidateEvidence.js +7 -2
  23. package/dist/candidateEvidence.js.map +1 -1
  24. package/dist/candidateOutputs.d.ts.map +1 -1
  25. package/dist/candidateOutputs.js +6 -0
  26. package/dist/candidateOutputs.js.map +1 -1
  27. package/dist/candidateReview.d.ts.map +1 -1
  28. package/dist/candidateReview.js +1 -0
  29. package/dist/candidateReview.js.map +1 -1
  30. package/dist/candidateTerminals.d.ts +12 -1
  31. package/dist/candidateTerminals.d.ts.map +1 -1
  32. package/dist/candidateTerminals.js +51 -21
  33. package/dist/candidateTerminals.js.map +1 -1
  34. package/dist/captureRefusal.d.ts +28 -0
  35. package/dist/captureRefusal.d.ts.map +1 -0
  36. package/dist/captureRefusal.js +54 -0
  37. package/dist/captureRefusal.js.map +1 -0
  38. package/dist/continuation-evidence.d.ts.map +1 -1
  39. package/dist/continuation-evidence.js +1 -0
  40. package/dist/continuation-evidence.js.map +1 -1
  41. package/dist/continuity-notice.d.ts.map +1 -1
  42. package/dist/continuity-notice.js +1 -0
  43. package/dist/continuity-notice.js.map +1 -1
  44. package/dist/continuity-terminal.d.ts +3 -1
  45. package/dist/continuity-terminal.d.ts.map +1 -1
  46. package/dist/continuity-terminal.js.map +1 -1
  47. package/dist/credential-cooldown.d.ts +2 -0
  48. package/dist/credential-cooldown.d.ts.map +1 -1
  49. package/dist/credential-cooldown.js +7 -1
  50. package/dist/credential-cooldown.js.map +1 -1
  51. package/dist/credential-preflight.d.ts.map +1 -1
  52. package/dist/credential-preflight.js +1 -0
  53. package/dist/credential-preflight.js.map +1 -1
  54. package/dist/credential-profile-rotation.d.ts.map +1 -1
  55. package/dist/credential-profile-rotation.js +4 -3
  56. package/dist/credential-profile-rotation.js.map +1 -1
  57. package/dist/decisionBudget.d.ts +17 -5
  58. package/dist/decisionBudget.d.ts.map +1 -1
  59. package/dist/decisionBudget.js +28 -6
  60. package/dist/decisionBudget.js.map +1 -1
  61. package/dist/deepScanReducer.d.ts +1 -1
  62. package/dist/deepScanReducer.d.ts.map +1 -1
  63. package/dist/delegationBudgetAuthority.d.ts +2 -2
  64. package/dist/delegationBudgetAuthority.d.ts.map +1 -1
  65. package/dist/delegationBudgetAuthority.js.map +1 -1
  66. package/dist/delegationFailure.d.ts +104 -7
  67. package/dist/delegationFailure.d.ts.map +1 -1
  68. package/dist/delegationFailure.js +47 -31
  69. package/dist/delegationFailure.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +1 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/inrun-continuity-types.d.ts +13 -1
  75. package/dist/inrun-continuity-types.d.ts.map +1 -1
  76. package/dist/inrun-continuity.d.ts +10 -2
  77. package/dist/inrun-continuity.d.ts.map +1 -1
  78. package/dist/inrun-continuity.js +69 -50
  79. package/dist/inrun-continuity.js.map +1 -1
  80. package/dist/inrun-continuity.test-support.d.ts +17 -3
  81. package/dist/inrun-continuity.test-support.d.ts.map +1 -1
  82. package/dist/inrun-continuity.test-support.js +4 -1
  83. package/dist/inrun-continuity.test-support.js.map +1 -1
  84. package/dist/interaction.d.ts +5 -5
  85. package/dist/interaction.d.ts.map +1 -1
  86. package/dist/interaction.js +58 -26
  87. package/dist/interaction.js.map +1 -1
  88. package/dist/modelGovernance.d.ts +3 -1
  89. package/dist/modelGovernance.d.ts.map +1 -1
  90. package/dist/modelGovernance.js +4 -0
  91. package/dist/modelGovernance.js.map +1 -1
  92. package/dist/orchestrator-credentials.d.ts +5 -4
  93. package/dist/orchestrator-credentials.d.ts.map +1 -1
  94. package/dist/orchestrator-credentials.js +7 -4
  95. package/dist/orchestrator-credentials.js.map +1 -1
  96. package/dist/orchestrator.d.ts +10 -1
  97. package/dist/orchestrator.d.ts.map +1 -1
  98. package/dist/orchestrator.js +283 -335
  99. package/dist/orchestrator.js.map +1 -1
  100. package/dist/persistedPatch.js +2 -2
  101. package/dist/persistedPatch.js.map +1 -1
  102. package/dist/plannerAttempt.d.ts +1 -1
  103. package/dist/plannerAttempt.d.ts.map +1 -1
  104. package/dist/pool-attempt-evidence.d.ts +62 -0
  105. package/dist/pool-attempt-evidence.d.ts.map +1 -0
  106. package/dist/pool-attempt-evidence.js +153 -0
  107. package/dist/pool-attempt-evidence.js.map +1 -0
  108. package/dist/pre-progress-refusal.d.ts +43 -6
  109. package/dist/pre-progress-refusal.d.ts.map +1 -1
  110. package/dist/pre-progress-refusal.js +53 -8
  111. package/dist/pre-progress-refusal.js.map +1 -1
  112. package/dist/processing-routing.d.ts.map +1 -1
  113. package/dist/processing-routing.js +9 -0
  114. package/dist/processing-routing.js.map +1 -1
  115. package/dist/quota-refusal-scope.d.ts +11 -0
  116. package/dist/quota-refusal-scope.d.ts.map +1 -0
  117. package/dist/quota-refusal-scope.js +21 -0
  118. package/dist/quota-refusal-scope.js.map +1 -0
  119. package/dist/readonlyTerminals.d.ts +55 -0
  120. package/dist/readonlyTerminals.d.ts.map +1 -0
  121. package/dist/readonlyTerminals.js +205 -0
  122. package/dist/readonlyTerminals.js.map +1 -0
  123. package/dist/retry-processing-intent.d.ts +19 -0
  124. package/dist/retry-processing-intent.d.ts.map +1 -0
  125. package/dist/retry-processing-intent.js +32 -0
  126. package/dist/retry-processing-intent.js.map +1 -0
  127. package/dist/root-ledger.d.ts.map +1 -1
  128. package/dist/root-ledger.js +3 -1
  129. package/dist/root-ledger.js.map +1 -1
  130. package/dist/rotation-predicate.d.ts +7 -23
  131. package/dist/rotation-predicate.d.ts.map +1 -1
  132. package/dist/rotation-predicate.js +45 -0
  133. package/dist/rotation-predicate.js.map +1 -1
  134. package/dist/runEventLog.d.ts +1 -0
  135. package/dist/runEventLog.d.ts.map +1 -1
  136. package/dist/runEventLog.js +3 -2
  137. package/dist/runEventLog.js.map +1 -1
  138. package/dist/runSupport.d.ts +2 -2
  139. package/dist/runSupport.d.ts.map +1 -1
  140. package/dist/runSupport.js +3 -4
  141. package/dist/runSupport.js.map +1 -1
  142. package/dist/runTerminalContext.d.ts +7 -1
  143. package/dist/runTerminalContext.d.ts.map +1 -1
  144. package/dist/runTerminalContext.js +1 -0
  145. package/dist/runTerminalContext.js.map +1 -1
  146. package/dist/runTerminalHelpers.d.ts +25 -1
  147. package/dist/runTerminalHelpers.d.ts.map +1 -1
  148. package/dist/runTerminalHelpers.js +69 -6
  149. package/dist/runTerminalHelpers.js.map +1 -1
  150. package/dist/runTerminalResults.d.ts +25 -3
  151. package/dist/runTerminalResults.d.ts.map +1 -1
  152. package/dist/runTerminalResults.js +61 -29
  153. package/dist/runTerminalResults.js.map +1 -1
  154. package/dist/runTerminals.d.ts +0 -1
  155. package/dist/runTerminals.d.ts.map +1 -1
  156. package/dist/runTerminals.js +47 -11
  157. package/dist/runTerminals.js.map +1 -1
  158. package/dist/secretDiff.d.ts +14 -11
  159. package/dist/secretDiff.d.ts.map +1 -1
  160. package/dist/secretDiff.js +21 -28
  161. package/dist/secretDiff.js.map +1 -1
  162. package/dist/terminalOutcome.d.ts +24 -4
  163. package/dist/terminalOutcome.d.ts.map +1 -1
  164. package/dist/terminalOutcome.js +32 -5
  165. package/dist/terminalOutcome.js.map +1 -1
  166. package/dist/transientClassify.d.ts +1 -0
  167. package/dist/transientClassify.d.ts.map +1 -1
  168. package/dist/transientClassify.js +3 -0
  169. package/dist/transientClassify.js.map +1 -1
  170. package/package.json +17 -17
@@ -4,11 +4,13 @@ import { openRunScopeCustody, scopedRouteContext, scopedSpecEnv, } from "./runSc
4
4
  import { delegationBeltFor } from "./delegationBelt.js";
5
5
  import { bindProcessingAdmission, processingAttemptCostEvidence, processingAdmissionForLease, updateProcessingStreamHold, ProcessingBudgetAdmissionError, } from "./processing-dispatch.js";
6
6
  import { cancelledCandidatesResult, emptyCandidateResult, failedCandidatesResult, failAfterCandidates, } from "./candidateTerminals.js";
7
+ import { deepScanFailureResult, readOnlyChainFailureResult, } from "./readonlyTerminals.js";
7
8
  import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, } from "./credential-profiles.js";
8
9
  import { AdmissionProfileProbes, OrchestratorCredentials, reviewerProfileResolver, } from "./orchestrator-credentials.js";
9
10
  import { continuityWorkspace, resumableOf, resumableTerminal, } from "./continuity-terminal.js";
10
11
  import { openContinuity } from "./continue-from.js";
11
12
  import { accountPoolRows } from "./account-pool.js";
13
+ import { PoolAttemptEvidence, terminalPoolAttempts, withPoolAttempts, } from "./pool-attempt-evidence.js";
12
14
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
13
15
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
14
16
  import { processAttemptUsage } from "./attemptUsage.js";
@@ -16,7 +18,7 @@ import { captureDirectoryCandidate, directoryHasOutput, directoryCandidateStable
16
18
  import { appliedAttemptFacts, assertDelegatedEvidence, isMutatingAccess, outerBoundaryNotice, scopedHarnessHome, selectLaneHome, followSpecHome, } from "./delegatedHome.js";
17
19
  import * as AC from "./attemptUsageCost.js";
18
20
  import * as custody from "./continuation-custody.js";
19
- import { candidateRoster, candidateStatuses, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, unanimousDeclaredFailure, } from "./candidateEvidence.js";
21
+ import { candidateRoster, candidateStatuses, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
20
22
  import { capabilityIntents } from "@claudexor/gateway";
21
23
  import { reviewCandidateRuns, resolveEngineReview, unreviewedCandidateEvidence, evaluateUnreviewedConvergence, } from "./candidateReview.js";
22
24
  import { join } from "node:path";
@@ -39,7 +41,7 @@ import { resolveRunInputDefaults } from "./run-input-resolution.js";
39
41
  import { beginAnnouncedRun } from "./runEventLog.js";
40
42
  import { arbitrationBudgetOptions } from "./decisionBudget.js";
41
43
  import { buildRevisePrompt } from "./revisePrompt.js";
42
- import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, writeFailure, attemptVendorFailure, cancelReasonFromSignalToken, } from "./runTerminals.js";
44
+ import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, writeFailure, cancelReasonFromSignalToken, } from "./runTerminals.js";
43
45
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
44
46
  import { finalizeStructuredOutput } from "./structuredOutput.js";
45
47
  import { admitRun } from "./runAdmission.js";
@@ -56,14 +58,14 @@ import { createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, tel
56
58
  import { newAttemptOutputMarkers } from "./attemptOutputMarkers.js";
57
59
  import * as delegateFailure from "./delegationFailure.js";
58
60
  import * as secretDiff from "./secretDiff.js";
59
- import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
60
- import { finalizeAttempt, readOnlyNoSuccessTerminal, unrecoveredToolErrorFailure, webEvidenceFailure, } from "./attemptFinalize.js";
61
+ import { finalizeAttempt, unrecoveredToolErrorFailure, webEvidenceFailure, } from "./attemptFinalize.js";
61
62
  import { resolveWorkReportEnvelope, unwrapWorkReportEnvelope, } from "./workReportEnvelope.js";
62
63
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
63
64
  import { interactionChannelFor } from "./interaction.js";
64
65
  import { liveAttempt } from "./live-input.js";
65
66
  import { finalVerifyRun, gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
66
67
  import { buildTaskContract } from "./task-contract-builder.js";
68
+ import { recordProcessingIntent } from "./retry-processing-intent.js";
67
69
  import { prepareReviewEvidenceDir, recordReviewEvidenceCleanup } from "./reviewEvidence.js";
68
70
  import { ArtifactStore } from "@claudexor/artifact-store";
69
71
  import { buildContextPack, rawContextForEnvelope, writeEvidencePacket } from "@claudexor/context";
@@ -809,6 +811,11 @@ export class Orchestrator {
809
811
  effectiveN: out.length,
810
812
  droppedLanes,
811
813
  });
814
+ if (runId) {
815
+ // Freeze each selected lane's inherited speed for Exact Retry (first routing wins).
816
+ const store = this.artifactStore(input);
817
+ recordProcessingIntent(store, store.runPaths(runId).contextDir, Object.fromEntries(out.flatMap((lane) => lane.processingIntent ? [[lane.adapter.id, lane.processingIntent]] : [])));
818
+ }
812
819
  this.requestRequirements.requireEffectiveBrowser(input.browser === true, out.map((lane) => lane.browserRequirement));
813
820
  // Owner decision (2026-07-26): known PRE-START belt unavailability does
814
821
  // not discard the requested Agent work. Continue without Delegate and emit
@@ -1312,7 +1319,10 @@ export class Orchestrator {
1312
1319
  * must record what it ran under, not just why it stopped. REQUIRED (no
1313
1320
  * default): a silent fallback here would spawn a delegated attempt on the
1314
1321
  * operator's real home while the record still claimed scoped state. */
1315
- harnessHome, observedPaths = new Set()) {
1322
+ harnessHome, observedPaths = new Set(),
1323
+ /** The caller's, like `harnessHome`: its catch keeps the tries' accounts.
1324
+ * REQUIRED, so no caller can drop them by omission again. */
1325
+ poolEvidence) {
1316
1326
  const adapter = routed.adapter;
1317
1327
  const knobs = this.routeSpecKnobs(routed, contract, modelHint, effortHint);
1318
1328
  // Isolated scoped-home sessions are never retained after disposal.
@@ -1418,6 +1428,7 @@ export class Orchestrator {
1418
1428
  let beltFailureDisclosed = false;
1419
1429
  const errors = [];
1420
1430
  let answer = new secretDiff.CountedAnswerAssembly();
1431
+ let currentCapture;
1421
1432
  const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
1422
1433
  // QA-024: the delegation belt is the ONLY engine-owned extra MCP server
1423
1434
  // injected into an agent lane (the browser MCP rides its own field), so its
@@ -1444,9 +1455,11 @@ export class Orchestrator {
1444
1455
  harnessHome,
1445
1456
  workOrder: prompt,
1446
1457
  firstPrompt: spec.prompt,
1458
+ poolEvidence,
1447
1459
  }), { spec, continuation: runInput?.continuation, store, telemetry }));
1448
1460
  try {
1449
1461
  for (let nativeTry = 0; !signal?.aborted; nativeTry += 1) {
1462
+ currentCapture = undefined;
1450
1463
  // A3 per-try isolation: neither output nor progress markers leak across tries.
1451
1464
  if (nativeTry > 0) {
1452
1465
  answer = new secretDiff.CountedAnswerAssembly();
@@ -1462,9 +1475,8 @@ export class Orchestrator {
1462
1475
  ? AbortSignal.any([signal, attemptAbort.signal])
1463
1476
  : attemptAbort.signal;
1464
1477
  activeSessionId = runSpec.session_id;
1465
- continuity.beginTry(runSpec, nativeTry, attemptAbort);
1466
1478
  // #363: bound before the spawn — a credential change mid-try voids it.
1467
- const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
1479
+ const refusal = continuity.beginTry(runSpec, nativeTry, attemptAbort);
1468
1480
  const transientStart = telemetry.transientFailures.length;
1469
1481
  const rateLimitStart = telemetry.rateLimits.length;
1470
1482
  let rawPatch = null;
@@ -1588,30 +1600,42 @@ export class Orchestrator {
1588
1600
  // #31: the centralized retry gate reads the classified `retryable` verdict.
1589
1601
  const sawRetryable = newTransients.some((f) => f.retryable);
1590
1602
  const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
1603
+ const tryFacts = {
1604
+ harnessErrored,
1605
+ originalError: errors[0],
1606
+ budgetStopped: streamBudgetDenied,
1607
+ aborted: signal?.aborted === true,
1608
+ requestRefused: Boolean(telemetry.requestRefusal),
1609
+ newTransients,
1610
+ markers: telemetry.outputMarkers,
1611
+ };
1612
+ // Its account row first: an infrastructure failure below never erases it.
1613
+ continuity.settleTry(tryFacts);
1591
1614
  if (directory)
1592
1615
  await captureDirectory();
1593
- const currentDiff = directory ? "" : await wsm.diff(envelope);
1616
+ currentCapture = directory
1617
+ ? undefined
1618
+ : await secretDiff.captureCandidateDiff(wsm, envelope);
1619
+ const currentDiff = secretDiff.capturedDiff(currentCapture);
1594
1620
  const workspaceUnchanged = directory
1595
1621
  ? directoryCapture.files?.noChanges === true
1596
- : currentDiff.trim().length === 0;
1622
+ : currentDiff?.trim().length === 0;
1597
1623
  const deliverableEmpty = workspaceUnchanged && answer.text().length === 0;
1598
1624
  const delivered = !harnessErrored && !deliverableEmpty && !signal?.aborted;
1599
1625
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
1600
1626
  const verdict = await continuity.afterTry({
1627
+ ...tryFacts,
1628
+ // A cancel or deadline that landed during capture still stops the
1629
+ // attempt; the try's own facts above stay its evidence.
1630
+ stopped: signal?.aborted === true,
1601
1631
  runSpec,
1602
1632
  nativeTry,
1603
- harnessErrored,
1604
- budgetStopped: streamBudgetDenied,
1605
- aborted: signal?.aborted === true,
1606
- requestRefused: Boolean(telemetry.requestRefusal),
1607
- newTransients,
1608
1633
  sawTypedLimit,
1609
1634
  sawRetryable,
1610
1635
  answer,
1611
1636
  rawDeliverableEmpty: deliverableEmpty,
1612
1637
  workspaceDiffNonEmpty: !workspaceUnchanged,
1613
- currentDiff,
1614
- markers: telemetry.outputMarkers,
1638
+ ...(currentDiff === undefined ? {} : { currentDiff }),
1615
1639
  lastLimit: telemetry.rateLimits.at(-1) ?? null,
1616
1640
  refusal,
1617
1641
  telemetry,
@@ -1635,8 +1659,13 @@ export class Orchestrator {
1635
1659
  signal?.removeEventListener("abort", onAbort);
1636
1660
  }
1637
1661
  // A pool-exhausted terminal is rotation's verdict, not the transient
1638
- // machinery's — no `route.transient.exhausted` rides along with it.
1639
- if (harnessErrored && !poolExhausted && !processingRefusal && !telemetry.requestRefusal) {
1662
+ // machinery's, and a stopped run exhausted no retries: no
1663
+ // `route.transient.exhausted` rides along with either.
1664
+ if (harnessErrored &&
1665
+ !poolExhausted &&
1666
+ !processingRefusal &&
1667
+ !telemetry.requestRefusal &&
1668
+ !signal?.aborted) {
1640
1669
  emitTransientExhausted((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
1641
1670
  }
1642
1671
  const attemptStreamEndedMs = Date.now();
@@ -1656,9 +1685,10 @@ export class Orchestrator {
1656
1685
  : await secretDiff.captureCandidateWorkspace({
1657
1686
  ...{ wsm, envelope, inPlace: inPlaceEnvelope, projectRoot: store.repoRoot },
1658
1687
  ...{ answerText, answerMatches: answer.secretLikeMatches() },
1688
+ holder: { runId: paths.runId, runDir: paths.root },
1689
+ incompleteCapture: currentCapture?.captureIncomplete ? currentCapture : undefined,
1659
1690
  });
1660
1691
  const { diff, captureRefusal } = captured;
1661
- harnessErrored = secretDiff.recordCaptureRefusal(captureRefusal, errors, harnessErrored);
1662
1692
  const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText) || directoryHasOutput(directoryCapture.files);
1663
1693
  // Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
1664
1694
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
@@ -1717,7 +1747,9 @@ export class Orchestrator {
1717
1747
  if (finalized.outcomeClass === "contract_failure" && unwrapped.contractViolation)
1718
1748
  errors.push(`work_report contract: ${unwrapped.contractViolation}`);
1719
1749
  const deliverablePresent = finalized.deliverablePresent;
1720
- const errored = harnessErrored || webBlocked;
1750
+ if (captureRefusal)
1751
+ errors.push(captureRefusal.detail);
1752
+ const errored = harnessErrored || webBlocked || captureRefusal !== undefined;
1721
1753
  const resumable = await continuity.finish({
1722
1754
  runSpec: spec,
1723
1755
  errored,
@@ -1727,6 +1759,7 @@ export class Orchestrator {
1727
1759
  workState: finalized.workState,
1728
1760
  });
1729
1761
  setAttemptOutcome(telemetry, {
1762
+ captureFailed: captureRefusal !== undefined,
1730
1763
  deliverablePresent,
1731
1764
  gatesPassed: gates.length > 0 ? gatesPassed(gates) : null,
1732
1765
  harnessErrored,
@@ -1840,9 +1873,11 @@ export class Orchestrator {
1840
1873
  probeReadyProfiles: (spec, tried) => runInput
1841
1874
  ? this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, tried, spec.model_hint ?? null)
1842
1875
  : Promise.resolve(new Set()),
1876
+ bindRefusal: (spec, producer) => this.credentials.bindPreProgressRefusal(adapter.id, spec, producer),
1843
1877
  rotationObservations: (spec, transients, refusal) => this.credentials.rotationObservations(adapter, spec, transients, refusal),
1844
1878
  emit: (type, payload) => log?.emit(type, payload),
1845
1879
  route: routed,
1880
+ poolEvidence: lane.poolEvidence,
1846
1881
  newSessionId: () => newId("ses"),
1847
1882
  thread: runInput?.threadId
1848
1883
  ? {
@@ -1864,7 +1899,7 @@ export class Orchestrator {
1864
1899
  const mutatingRun = isMutatingAccess(contract.access.effective_profile);
1865
1900
  const planBrief = verifiedPlanBrief(input);
1866
1901
  const quotaSnapshots = this.quotaSnapshotPreflight();
1867
- const { store, paths, log, ledger } = beginAnnouncedRun({
1902
+ const { store, paths, log, ledger, runContext } = beginAnnouncedRun({
1868
1903
  input,
1869
1904
  contract,
1870
1905
  quotaSnapshots,
@@ -2020,6 +2055,7 @@ export class Orchestrator {
2020
2055
  });
2021
2056
  }
2022
2057
  const runsBySlot = new Array(slots.length);
2058
+ runContext.attempts = () => runsBySlot.filter((run) => run !== undefined);
2023
2059
  // D-16d: one-shot continuation budget shared across the concurrent candidate
2024
2060
  // slots (parity with the read-only chain's single counter). Claimed
2025
2061
  // synchronously (check-then-increment with no await between), so two slots
@@ -2072,8 +2108,9 @@ export class Orchestrator {
2072
2108
  const effectiveWeb = this.discloseWebUpgrade(log, slot.routed, knobs.webPolicy, slot.attemptId);
2073
2109
  let envelope;
2074
2110
  // Declared OUTSIDE the try so the catch below can state what the attempt
2075
- // actually ran under, not merely that it stopped.
2111
+ // actually ran under, not merely that it stopped — and on which accounts.
2076
2112
  let harnessHome;
2113
+ const tried = new PoolAttemptEvidence(adapter.id, slot.attemptId);
2077
2114
  try {
2078
2115
  log.emit("harness.started", {
2079
2116
  harness_id: adapter.id,
@@ -2110,7 +2147,7 @@ export class Orchestrator {
2110
2147
  budgetStopped = true;
2111
2148
  return true;
2112
2149
  }, input, requestedSingleCandidate, // W-C4 deltas: single-candidate chat lane only (racing = noise x N)
2113
- undefined, undefined, harnessHome);
2150
+ undefined, undefined, harnessHome, undefined, tried);
2114
2151
  ledger.settle(slot.lease.lease_id, attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2115
2152
  log.emit("harness.completed", {
2116
2153
  harness_id: adapter.id,
@@ -2175,6 +2212,7 @@ export class Orchestrator {
2175
2212
  continuation_of: run.attemptId,
2176
2213
  });
2177
2214
  const contLeaseId = contLease.lease?.lease_id ?? "";
2215
+ const contTried = new PoolAttemptEvidence(adapter.id, contAttemptId);
2178
2216
  try {
2179
2217
  const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, {
2180
2218
  lease: contLease.lease,
@@ -2188,7 +2226,7 @@ export class Orchestrator {
2188
2226
  return false;
2189
2227
  budgetStopped = true;
2190
2228
  return true;
2191
- }, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined, harnessHome);
2229
+ }, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined, harnessHome, undefined, contTried);
2192
2230
  ledger.settle(contLeaseId, attemptUsageCostSettlement(contRun.cost, contRun.costEstimated, contRun.attemptId, contRun.harnessId, contRun.telemetry.authMode, contRun.telemetry.usageCost));
2193
2231
  log.emit("harness.completed", {
2194
2232
  harness_id: adapter.id,
@@ -2215,6 +2253,8 @@ export class Orchestrator {
2215
2253
  error: safeErrorMessage(err),
2216
2254
  });
2217
2255
  }
2256
+ // Whichever attempt speaks names both attempts' failed tries, in order.
2257
+ effectiveRun.poolAttempts = [...(run.poolAttempts ?? []), ...contTried.snapshot()];
2218
2258
  }
2219
2259
  else {
2220
2260
  ledger.cancel(contLease.lease?.lease_id ?? "");
@@ -2260,6 +2300,7 @@ export class Orchestrator {
2260
2300
  slot.routed.delegationRequirement,
2261
2301
  ], knobs.model),
2262
2302
  infraPhase,
2303
+ poolAttempts: tried.snapshot(),
2263
2304
  // Keep a TYPED pre-spawn refusal (spent quota window + its reset)
2264
2305
  // alive past this catch; `message` alone would force the terminal to
2265
2306
  // read prose back out.
@@ -2274,10 +2315,17 @@ export class Orchestrator {
2274
2315
  };
2275
2316
  await runParallelCandidates(slots, this.deps.runtimeConcurrencyCaps, runSlot);
2276
2317
  const runs = runsBySlot.filter((r) => r !== undefined);
2318
+ // From here `runs` is the lane's one attempt collection (a synthesis that
2319
+ // returns joins it); a synthesis that threw first leaves only its tries.
2320
+ let thrownSynthesis = null;
2321
+ const history = () => thrownSynthesis ? [...runs, thrownSynthesis] : runs;
2322
+ runContext.attempts = history;
2277
2323
  // Fail-closed terminal: a delegated mutating run whose attempts state
2278
2324
  // neither historical proof nor deliberate absence refuses instead of passing.
2279
2325
  assertDelegatedEvidence(input.delegated === true, candidateAccess, runs);
2280
- const cancelledRaceResult = () => cancelledCandidatesResult({
2326
+ // One candidate-terminal context for every race terminal below; its
2327
+ // telemetry writer reads the live roster, so a later synth attempt counts.
2328
+ const raceTerminal = {
2281
2329
  store,
2282
2330
  paths,
2283
2331
  log,
@@ -2286,6 +2334,11 @@ export class Orchestrator {
2286
2334
  mode,
2287
2335
  ledger,
2288
2336
  runs,
2337
+ writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2338
+ };
2339
+ const cancelledRaceResult = () => cancelledCandidatesResult({
2340
+ ...raceTerminal,
2341
+ attempts: history(),
2289
2342
  git: input.workspaceKind !== "directory"
2290
2343
  ? {
2291
2344
  live: input.inPlace === true &&
@@ -2297,7 +2350,6 @@ export class Orchestrator {
2297
2350
  }
2298
2351
  : undefined,
2299
2352
  signal: input.signal,
2300
- writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2301
2353
  });
2302
2354
  // Revert divergence fence for the single-candidate in-place path: the
2303
2355
  // candidate mutated the LIVE tree during execution above, so the post-turn
@@ -2324,31 +2376,34 @@ export class Orchestrator {
2324
2376
  }
2325
2377
  const failedDelegation = delegateFailure.dominantRaceCandidateFailure(runs);
2326
2378
  if (failedDelegation) {
2327
- const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2328
2379
  await disposeReviewEnvelopes();
2329
- if (mutatingRun && failedDelegation.files) {
2330
- await publishDirectoryCandidate({
2331
- files: failedDelegation.files,
2332
- store,
2333
- paths,
2334
- taskId,
2335
- attemptId: failedDelegation.attemptId,
2336
- harnessId: failedDelegation.harnessId,
2337
- facts: makeOutcomeFacts("failed", { noChanges: failedDelegation.files.noChanges }),
2338
- log,
2339
- });
2340
- }
2341
- else if (mutatingRun && input.workspaceKind !== "directory") {
2342
- await delegateFailure.persistFailedInPlaceWorkProduct({
2343
- ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
2344
- live: input.inPlace === true && failedDelegation.reviewCwd === execRoot,
2345
- run: failedDelegation,
2346
- postTurnSha: earlyPostTurnSha,
2347
- kind: input.create === true ? "new_repo" : "patch",
2380
+ const failedOutput = {
2381
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode, run: failedDelegation },
2382
+ live: input.inPlace === true &&
2383
+ input.workspaceKind !== "directory" &&
2384
+ failedDelegation.reviewCwd === execRoot,
2385
+ postTurnSha: earlyPostTurnSha,
2386
+ kind: input.create === true ? "new_repo" : "patch",
2387
+ };
2388
+ // A live candidate whose capture failed (no Delegate failure) ends in the
2389
+ // shared no-working-candidate terminal, as in the isolated race and
2390
+ // convergence: a recorded budget denial, a budget terminal the ledger
2391
+ // settled (stream hard cap, exact overshoot, unverifiable cash) or a
2392
+ // unanimous typed refusal leads, its live receipt carries the same facts,
2393
+ // and the capture facts and inspection actions follow. Any other dominant
2394
+ // capture failure joins it only under a settled budget terminal.
2395
+ if (delegateFailure.candidateFailureKind(failedDelegation) === "capture" &&
2396
+ (failedOutput.live || ledger.terminal() !== null))
2397
+ return failedCandidatesResult({
2398
+ ...raceTerminal,
2399
+ budgetDenial,
2400
+ ...(mutatingRun ? { liveCapture: failedOutput } : {}),
2348
2401
  });
2349
- }
2350
- this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2351
- return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
2402
+ const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2403
+ if (mutatingRun)
2404
+ await delegateFailure.persistFailedCandidateOutput(failedOutput);
2405
+ raceTerminal.writeTelemetry();
2406
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), { ...failure.metadata, attempts: runs });
2352
2407
  }
2353
2408
  if (runs.length === 0)
2354
2409
  return emptyCandidateResult({
@@ -2370,18 +2425,7 @@ export class Orchestrator {
2370
2425
  const workingRuns = partitionCandidates(runs).working;
2371
2426
  if (workingRuns.length === 0) {
2372
2427
  await disposeReviewEnvelopes();
2373
- return failedCandidatesResult({
2374
- store,
2375
- paths,
2376
- log,
2377
- runId,
2378
- taskId,
2379
- mode,
2380
- ledger,
2381
- runs,
2382
- budgetDenial,
2383
- writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2384
- });
2428
+ return failedCandidatesResult({ ...raceTerminal, budgetDenial });
2385
2429
  }
2386
2430
  // QA-025: only announce that review STARTED when the panel will actually
2387
2431
  // run. A candidate that changed no files is skipped inside reviewRuns; a
@@ -2423,7 +2467,7 @@ export class Orchestrator {
2423
2467
  catch (err) {
2424
2468
  // Review preflight/evidence failures end TERMINALLY with artifacts —
2425
2469
  // never as an escaped throw that orphans the run dir.
2426
- return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "review", err);
2470
+ return failAfterCandidates(raceTerminal, "review", err);
2427
2471
  }
2428
2472
  finally {
2429
2473
  // Review preflight failures must not leak candidate worktrees.
@@ -2453,6 +2497,7 @@ export class Orchestrator {
2453
2497
  });
2454
2498
  if (lease.granted) {
2455
2499
  let envelope;
2500
+ const synthTried = new PoolAttemptEvidence(synthRouted.adapter.id, "synth");
2456
2501
  try {
2457
2502
  const plan = buildSynthesisPlan(evidences);
2458
2503
  const synthesisInput = buildFileBackedSynthesisInput({
@@ -2481,7 +2526,7 @@ export class Orchestrator {
2481
2526
  budgetStopped = true;
2482
2527
  budgetDenial ??= denial;
2483
2528
  },
2484
- }, candidateAccess, input.onHarnessEvent, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content, undefined, synthHome);
2529
+ }, candidateAccess, input.onHarnessEvent, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content, undefined, synthHome, undefined, synthTried);
2485
2530
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2486
2531
  reviewEnvelopes.push(envelope);
2487
2532
  envelope = undefined;
@@ -2495,7 +2540,7 @@ export class Orchestrator {
2495
2540
  workingRuns.push(run);
2496
2541
  }
2497
2542
  if (input.signal?.aborted) {
2498
- return cancelledResult(log, runId, taskId, mode, paths.root, candidateStatuses(runs), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
2543
+ return cancelledResult(log, runId, taskId, mode, paths.root, candidateStatuses(runs), raceTerminal.writeTelemetry, ledger.spend(), input.signal, store, undefined, undefined, history());
2499
2544
  }
2500
2545
  }
2501
2546
  finally {
@@ -2503,6 +2548,9 @@ export class Orchestrator {
2503
2548
  }
2504
2549
  }
2505
2550
  catch (err) {
2551
+ // Not yet in `runs`: the run goes on, so its tries' accounts stay in the history.
2552
+ if (!runs.some((run) => run.attemptId === "synth"))
2553
+ thrownSynthesis = { poolAttempts: synthTried.snapshot() };
2506
2554
  ledger.settle(lease.lease?.lease_id ?? "", AC.attemptFailureCost(err, "synthesis-error").settlement);
2507
2555
  log.emit("harness.completed", {
2508
2556
  attempt_id: "synth",
@@ -2528,7 +2576,7 @@ export class Orchestrator {
2528
2576
  }
2529
2577
  catch (err) {
2530
2578
  // Arbitration throws end terminally with artifacts, never as an orphan.
2531
- return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "arbitration", err);
2579
+ return failAfterCandidates({ ...raceTerminal, attempts: history() }, "arbitration", err);
2532
2580
  }
2533
2581
  log.emit("arbitration.completed", {
2534
2582
  winner: result.decision.winner,
@@ -2608,6 +2656,9 @@ export class Orchestrator {
2608
2656
  });
2609
2657
  store.writeYaml(join(paths.arbitrationDir, "pairwise.yaml"), result.pairwise);
2610
2658
  const decisionPath = join(paths.arbitrationDir, "decision.yaml");
2659
+ // Every attempt's failed tries by the FINAL outcome (delivery below can
2660
+ // still fail the checks); a review or check block is not an account failure.
2661
+ const racePool = () => terminalPoolAttempts(facts, history());
2611
2662
  if (winnerRun) {
2612
2663
  for (const path of materializeWinnerOutputs({
2613
2664
  attemptDir: join(paths.attemptsDir, winnerRun.attemptId),
@@ -2786,7 +2837,7 @@ export class Orchestrator {
2786
2837
  },
2787
2838
  });
2788
2839
  }
2789
- store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified, secretDiff.secretLikeSummaryLine(winnerRun.secretLike)));
2840
+ store.writeText(join(paths.finalDir, "summary.md"), withPoolAttempts(renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified, secretDiff.secretLikeSummaryLine(winnerRun.secretLike)), racePool()));
2790
2841
  // summary.md is a DIAGNOSTIC artifact only (V8/PLAN addendum 2): it no
2791
2842
  // longer carries primary-output authority. A clean applyable success or a
2792
2843
  // winner answer still marks it ready for legacy INV-116 ordering; any
@@ -2812,8 +2863,14 @@ export class Orchestrator {
2812
2863
  // (applyable, no_changes, or not-verified) is an honest completion.
2813
2864
  const needsDecisionTerminal = facts.review === "blocked" || facts.checks === "failed";
2814
2865
  const isFailureTerminal = facts.lifecycle !== "succeeded" || needsDecisionTerminal;
2866
+ const pool = racePool();
2815
2867
  if (deliveryFailureReason && !finalVerifyFailed) {
2816
- writeFailure(store, paths, deliveryRefusalFailure(deliveryFailureReason, paths.root));
2868
+ const refused = deliveryRefusalFailure(deliveryFailureReason, paths.root);
2869
+ writeFailure(store, paths, {
2870
+ ...refused,
2871
+ safeMessage: withPoolAttempts(refused.safeMessage, pool),
2872
+ poolAttempts: pool,
2873
+ });
2817
2874
  }
2818
2875
  else if (finalVerifyFailed) {
2819
2876
  writeFailure(store, paths, {
@@ -2821,9 +2878,10 @@ export class Orchestrator {
2821
2878
  // RunFailure.category is a closed enum; "validation" is the honest
2822
2879
  // bucket (the winner failed re-validation on a fresh base).
2823
2880
  category: "validation",
2824
- safeMessage: finalVerify?.applied_cleanly === null
2881
+ safeMessage: withPoolAttempts(finalVerify?.applied_cleanly === null
2825
2882
  ? `final verify ERRORED before proving the patch against a clean base: ${finalVerify?.reason ?? "verify infrastructure error"}`
2826
- : `final verify failed: ${finalVerify?.reason ?? (finalVerify?.gates_passed === false ? "deterministic gates failed on the fresh verify tree" : "unknown")}`,
2883
+ : `final verify failed: ${finalVerify?.reason ?? (finalVerify?.gates_passed === false ? "deterministic gates failed on the fresh verify tree" : "unknown")}`, pool),
2884
+ poolAttempts: pool,
2827
2885
  runDir: paths.root,
2828
2886
  nextActions: finalVerify?.applied_cleanly === null
2829
2887
  ? [
@@ -2855,11 +2913,12 @@ export class Orchestrator {
2855
2913
  : "internal",
2856
2914
  harnessId: winnerRun?.errored ? winnerRun.harnessId : undefined,
2857
2915
  attemptId: winnerRun?.errored ? winnerRun.attemptId : undefined,
2858
- safeMessage: needsHuman
2916
+ safeMessage: withPoolAttempts(needsHuman
2859
2917
  ? `review escalated to a human decision: ${result.decision.why_winner}`
2860
2918
  : needsDecisionTerminal
2861
2919
  ? `review blocked before apply: ${result.decision.why_winner}`
2862
- : result.decision.why_winner,
2920
+ : result.decision.why_winner, pool),
2921
+ poolAttempts: pool,
2863
2922
  rawDetailRef: winnerRun?.errored
2864
2923
  ? `attempts/${winnerRun.attemptId}/attempt.yaml`
2865
2924
  : undefined,
@@ -2879,7 +2938,7 @@ export class Orchestrator {
2879
2938
  ],
2880
2939
  });
2881
2940
  if (!winnerRun) {
2882
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: arbitration\n\n${result.decision.why_winner}\n`);
2941
+ store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: arbitration\n\n${withPoolAttempts(result.decision.why_winner, pool)}\n`);
2883
2942
  log.emit("output.ready", {
2884
2943
  kind: "summary",
2885
2944
  path: "final/summary.md",
@@ -2895,6 +2954,8 @@ export class Orchestrator {
2895
2954
  lifecycle: facts.lifecycle,
2896
2955
  facts,
2897
2956
  reason: facts.reason,
2957
+ // A work-state veto is still a block: it keeps the earlier refusals.
2958
+ ...(pool.length > 0 ? { pool_attempts: pool } : {}),
2898
2959
  ...resumableTerminal(store, paths, winnerRun ? [winnerRun] : runs, isFailureTerminal),
2899
2960
  });
2900
2961
  }
@@ -2904,6 +2965,7 @@ export class Orchestrator {
2904
2965
  log.emit("run.blocked", {
2905
2966
  lifecycle: facts.lifecycle,
2906
2967
  facts,
2968
+ pool_attempts: pool,
2907
2969
  ...resumableTerminal(store, paths, runs, isFailureTerminal),
2908
2970
  phase: deliveryFailureReason && !finalVerifyFailed
2909
2971
  ? "delivery"
@@ -2919,6 +2981,7 @@ export class Orchestrator {
2919
2981
  facts,
2920
2982
  reason: facts.reason,
2921
2983
  phase: "arbitration",
2984
+ pool_attempts: pool,
2922
2985
  failure_ref: "final/failure.yaml",
2923
2986
  ...resumableTerminal(store, paths, runs, isFailureTerminal),
2924
2987
  });
@@ -2931,7 +2994,7 @@ export class Orchestrator {
2931
2994
  facts,
2932
2995
  winner: result.decision.winner,
2933
2996
  runDir: paths.root,
2934
- summary: result.decision.why_winner,
2997
+ summary: withPoolAttempts(result.decision.why_winner, pool),
2935
2998
  candidates: candidateStatuses(runs),
2936
2999
  decisionPath,
2937
3000
  reviewVerified: actualReviewVerified,
@@ -3009,7 +3072,7 @@ export class Orchestrator {
3009
3072
  const contract = this.buildContract(input, taskId, mode);
3010
3073
  const planBrief = verifiedPlanBrief(input);
3011
3074
  const quotaSnapshots = this.quotaSnapshotPreflight();
3012
- const { store, paths, log, ledger } = beginAnnouncedRun({
3075
+ const { store, paths, log, ledger, runContext } = beginAnnouncedRun({
3013
3076
  input,
3014
3077
  contract,
3015
3078
  quotaSnapshots,
@@ -3146,6 +3209,8 @@ export class Orchestrator {
3146
3209
  let interrupted = false; // D-16 r8: terminalizes the run interrupted
3147
3210
  let lastFindings = [];
3148
3211
  let lastRun = null;
3212
+ const convergenceRuns = [];
3213
+ runContext.attempts = () => convergenceRuns;
3149
3214
  let actualReviewVerified = false;
3150
3215
  let lastFinalReviewClean = false;
3151
3216
  // Honest apply-state for in-place convergence: the attempts mutate the LIVE
@@ -3235,6 +3300,7 @@ export class Orchestrator {
3235
3300
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
3236
3301
  let run;
3237
3302
  let harnessHome;
3303
+ const tried = new PoolAttemptEvidence(adapter.id, attemptId);
3238
3304
  try {
3239
3305
  harnessHome = this.harnessHomeFor(wsm, envelope, routed, input);
3240
3306
  log.emit("harness.started", {
@@ -3253,7 +3319,7 @@ export class Orchestrator {
3253
3319
  ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
3254
3320
  return ledger.tier() === "hard";
3255
3321
  }, input, true, // convergence runs one candidate: live deltas on (W-C4)
3256
- undefined, undefined, harnessHome, observedPaths);
3322
+ undefined, undefined, harnessHome, observedPaths, tried);
3257
3323
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
3258
3324
  log.emit("harness.completed", {
3259
3325
  harness_id: adapter.id,
@@ -3290,6 +3356,7 @@ export class Orchestrator {
3290
3356
  errored: true,
3291
3357
  costEstimated: failureCost.estimated,
3292
3358
  errors: [message],
3359
+ poolAttempts: tried.snapshot(),
3293
3360
  ...(declared.code ? { declaredFailure: declared } : {}),
3294
3361
  telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model),
3295
3362
  };
@@ -3305,6 +3372,7 @@ export class Orchestrator {
3305
3372
  }
3306
3373
  }
3307
3374
  lastRun = run;
3375
+ convergenceRuns.push(run);
3308
3376
  // Fail-closed twin of the candidate lane's gate: this loop terminalizes
3309
3377
  // per attempt, so the proof is spent per attempt.
3310
3378
  assertDelegatedEvidence(input.delegated === true, convergenceAccess, [run]);
@@ -3324,34 +3392,28 @@ export class Orchestrator {
3324
3392
  // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3325
3393
  if (input.signal?.aborted || processingBudgetDenial)
3326
3394
  break;
3395
+ // When this attempt's capture failed (and no Delegate failure leads), a
3396
+ // budget terminal the ledger settled on it (stream hard cap, exact overshoot,
3397
+ // unverifiable cash) outranks that capture failure and its typed refusal
3398
+ // (race parity): the budget-aware finalizer below names the budget cause and
3399
+ // remedy first, with the capture facts, source and inspection actions after.
3400
+ if (delegateFailure.candidateFailureKind(run) === "capture" && ledger.terminal()) {
3401
+ exhausted = true;
3402
+ break;
3403
+ }
3327
3404
  if (run.telemetry.requestRefusal ||
3328
3405
  run.telemetry.effortResolution?.resolution === "rejected")
3329
3406
  break;
3330
3407
  if (delegateFailure.candidateFailureKind(run)) {
3331
3408
  const failure = delegateFailure.candidateFailureTerminal(run, "convergence");
3332
- if (run.files) {
3333
- await publishDirectoryCandidate({
3334
- files: run.files,
3335
- store,
3336
- paths,
3337
- taskId,
3338
- attemptId,
3339
- harnessId: run.harnessId,
3340
- facts: makeOutcomeFacts("failed", { noChanges: run.files.noChanges }),
3341
- log,
3342
- });
3343
- }
3344
- else if (input.workspaceKind !== "directory") {
3345
- await delegateFailure.persistFailedInPlaceWorkProduct({
3346
- ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
3347
- live: input.inPlace === true,
3348
- run,
3349
- kind: input.create === true ? "new_repo" : "patch",
3350
- attempts: attempt,
3351
- });
3352
- }
3409
+ await delegateFailure.persistFailedCandidateOutput({
3410
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode, run },
3411
+ live: input.inPlace === true && input.workspaceKind !== "directory",
3412
+ kind: input.create === true ? "new_repo" : "patch",
3413
+ attempts: attempt,
3414
+ });
3353
3415
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null);
3354
- return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
3416
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), { ...failure.metadata, attempts: convergenceRuns });
3355
3417
  }
3356
3418
  if (input.workspaceKind === "directory" && run.captureRefusal) {
3357
3419
  return failedCandidatesResult({
@@ -3363,6 +3425,7 @@ export class Orchestrator {
3363
3425
  runId,
3364
3426
  taskId,
3365
3427
  runs: [run],
3428
+ attempts: convergenceRuns,
3366
3429
  writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null),
3367
3430
  });
3368
3431
  }
@@ -3422,7 +3485,17 @@ export class Orchestrator {
3422
3485
  facts: makeOutcomeFacts("failed", { noChanges: run.files.noChanges }),
3423
3486
  log,
3424
3487
  });
3425
- return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs: [run] }, "review", err);
3488
+ return failAfterCandidates({
3489
+ log,
3490
+ store,
3491
+ paths,
3492
+ runId,
3493
+ taskId,
3494
+ mode,
3495
+ ledger,
3496
+ runs: [run],
3497
+ attempts: convergenceRuns,
3498
+ }, "review", err);
3426
3499
  }
3427
3500
  if (conv.converged) {
3428
3501
  converged = true;
@@ -3541,11 +3614,12 @@ export class Orchestrator {
3541
3614
  facts = decision.facts;
3542
3615
  }
3543
3616
  }
3544
- if (lastRun?.files)
3617
+ // Captured files decide noChanges; an incomplete capture leaves it unknown.
3618
+ if (lastRun?.files || lastRun?.captureRefusal)
3545
3619
  facts = {
3546
3620
  ...facts,
3547
- noChanges: lastRun.files.noChanges,
3548
- reason: facts.reason === "no_changes" && lastRun.files.noChanges !== true ? null : facts.reason,
3621
+ noChanges: lastRun.files?.noChanges ?? null,
3622
+ reason: facts.reason === "no_changes" && lastRun.files?.noChanges !== true ? null : facts.reason,
3549
3623
  };
3550
3624
  // A budget terminal turns a succeeded lifecycle into a failed one (D8).
3551
3625
  const convBudgetTerminal = ledger.terminal();
@@ -3556,6 +3630,19 @@ export class Orchestrator {
3556
3630
  review_requested: contract.review_requested,
3557
3631
  });
3558
3632
  }
3633
+ // The shared classifier owns a budget terminal's cause and remedy: a
3634
+ // processing re-admission denial, or the ledger's settled terminal once it
3635
+ // stopped the loop over an incomplete capture (the capture facts and
3636
+ // inspection actions follow it in the failure record).
3637
+ const convBudgetMapping = processingBudgetDenial ||
3638
+ (convBudgetTerminal && lastRun?.captureRefusal && isBudgetTerminal(facts.reason))
3639
+ ? classifyBudgetFailure({ denial: processingBudgetDenial, terminal: convBudgetTerminal })
3640
+ : null;
3641
+ if (convBudgetMapping &&
3642
+ facts.lifecycle === "failed" &&
3643
+ isBudgetTerminal(facts.reason) &&
3644
+ facts.reason !== convBudgetMapping.reason)
3645
+ facts = { ...facts, reason: convBudgetMapping.reason };
3559
3646
  // A reviewer escalation to a human forces the REVIEW axis to blocked.
3560
3647
  const needsHuman = lastFindings.some((f) => f.severity === "NEEDS_HUMAN" && isBlocking(f));
3561
3648
  if (needsHuman && facts.lifecycle === "succeeded" && facts.review !== "blocked") {
@@ -3579,6 +3666,8 @@ export class Orchestrator {
3579
3666
  facts = { ...facts, checks: "failed", reason: "checks_failed" };
3580
3667
  }
3581
3668
  const convNeedsDecision = facts.review === "blocked" || facts.checks === "failed";
3669
+ // Every attempt's failed tries by the actual outcome; a block is not an account failure.
3670
+ const convPool = terminalPoolAttempts(facts, convergenceRuns);
3582
3671
  if (decision) {
3583
3672
  // Shared honesty owner (same as the race path): a needs-decision terminal
3584
3673
  // overrides the persisted decision; final_verify is recorded either way.
@@ -3600,16 +3689,13 @@ export class Orchestrator {
3600
3689
  reviewAllowsApply(facts) &&
3601
3690
  facts.checks !== "failed" &&
3602
3691
  !workStateVetoes(facts);
3603
- if (lastRun.files) {
3604
- await publishDirectoryCandidate({
3605
- files: lastRun.files,
3606
- store,
3607
- paths,
3608
- taskId,
3609
- attemptId: lastRun.attemptId,
3610
- harnessId: lastRun.harnessId,
3611
- facts,
3612
- log,
3692
+ if (lastRun.files || lastRun.captureRefusal) {
3693
+ // An incomplete capture has no exact patch: never publish an empty one.
3694
+ await delegateFailure.persistFailedCandidateOutput({
3695
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode, facts, run: lastRun },
3696
+ live: input.inPlace === true && input.workspaceKind !== "directory",
3697
+ kind: input.create === true ? "new_repo" : "patch",
3698
+ attempts: attempt,
3613
3699
  });
3614
3700
  }
3615
3701
  else {
@@ -3656,7 +3742,7 @@ export class Orchestrator {
3656
3742
  store.writeText(join(paths.finalDir, "answer.md"), lastRun.answerText);
3657
3743
  log.emit("output.ready", { kind: "answer", path: "final/answer.md" });
3658
3744
  }
3659
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Attempts: ${attempt}\n- Winner: ${lastRun.attemptId}\n- Review verified (cross-family): ${actualReviewVerified}\n- Apply recommendation: ${decision?.apply_recommendation ?? "inspect"}${stuckNoProgressReason ? `\n- No-progress reason: ${stuckNoProgressReason}` : ""}${secretDiff.summaryDisclosure(lastRun.secretLike)}\n`);
3745
+ store.writeText(join(paths.finalDir, "summary.md"), `${withPoolAttempts(`# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Attempts: ${attempt}\n- Winner: ${lastRun.attemptId}\n- Review verified (cross-family): ${actualReviewVerified}\n- Apply recommendation: ${decision?.apply_recommendation ?? "inspect"}${stuckNoProgressReason ? `\n- No-progress reason: ${stuckNoProgressReason}` : ""}${secretDiff.summaryDisclosure(lastRun.secretLike)}`, convPool)}\n`);
3660
3746
  // Lifecycle invariant (all modes): output.ready precedes the terminal
3661
3747
  // event so a client that applied the terminal event has the output.
3662
3748
  log.emit("output.ready", {
@@ -3675,9 +3761,7 @@ export class Orchestrator {
3675
3761
  const convDeclared = !convNeedsDecision && facts.lifecycle === "failed" && !isBudgetTerminal(facts.reason)
3676
3762
  ? lastRun?.declaredFailure
3677
3763
  : undefined;
3678
- const processingBudgetMapping = processingBudgetDenial
3679
- ? classifyBudgetFailure({ denial: processingBudgetDenial, terminal: ledger.terminal() })
3680
- : null;
3764
+ const inspect = secretDiff.captureInspectionActions(lastRun?.captureRefusal);
3681
3765
  writeFailure(store, paths, {
3682
3766
  phase: convNeedsDecision ? "review" : "convergence",
3683
3767
  category: isBudgetTerminal(facts.reason)
@@ -3687,50 +3771,58 @@ export class Orchestrator {
3687
3771
  : convNeedsDecision
3688
3772
  ? "policy"
3689
3773
  : (convDeclared?.category ?? "internal"),
3690
- code: processingBudgetMapping?.code ?? convDeclared?.code ?? null,
3774
+ code: convBudgetMapping?.code ?? convDeclared?.code ?? null,
3691
3775
  resetsAt: convDeclared?.resetsAt ?? null,
3692
3776
  requestRefusal: convDeclared?.requestRefusal,
3693
- safeMessage: convNeedsDecision
3694
- ? `review escalated to a human decision after ${attempt} attempt(s)`
3695
- : facts.reason === "stuck_no_progress"
3696
- ? (stuckNoProgressReason ?? `stuck_no_progress after ${attempt} attempt(s)`)
3697
- : `${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""} after ${attempt} attempt(s)${lastDiffStable ? "" : " (diff changed after review; review is stale)"}`,
3698
3777
  harnessId: lastRun?.harnessId,
3699
3778
  attemptId: lastRun?.attemptId,
3700
3779
  runDir: paths.root,
3701
- nextActions: facts.lifecycle === "cancelled"
3702
- ? facts.reason === "wall_clock_exceeded"
3703
- ? [
3704
- "Inspect the partial work kept from before the deadline",
3705
- "Increase --max-seconds or narrow the scope, then re-run",
3706
- ]
3707
- : ["Retry if cancellation was accidental"]
3708
- : convNeedsDecision
3709
- ? [
3710
- "Review the blocking findings on the run's turn",
3711
- "Accept the risk to apply this exact patch, or discard the change",
3712
- ]
3713
- : facts.reason === "stuck_no_progress"
3780
+ captureFailure: lastRun?.captureRefusal?.failure,
3781
+ nextActions: [
3782
+ ...(facts.lifecycle === "cancelled"
3783
+ ? facts.reason === "wall_clock_exceeded"
3714
3784
  ? [
3715
- "Inspect the stable patch",
3716
- "Inspect the failing gate output",
3717
- "Fix the gate or provide a different repair instruction",
3785
+ "Inspect the partial work kept from before the deadline",
3786
+ "Increase --max-seconds or narrow the scope, then re-run",
3718
3787
  ]
3719
- : [
3720
- "Open diagnostics",
3721
- "Inspect latest patch and review findings",
3722
- "Retry with more attempts or a narrower prompt",
3723
- ],
3724
- ...(processingBudgetMapping
3725
- ? budgetFailureRecord(processingBudgetMapping, { runDir: paths.root })
3788
+ : ["Retry if cancellation was accidental"]
3789
+ : convNeedsDecision
3790
+ ? [
3791
+ "Review the blocking findings on the run's turn",
3792
+ "Accept the risk to apply this exact patch, or discard the change",
3793
+ ]
3794
+ : facts.reason === "stuck_no_progress"
3795
+ ? [
3796
+ "Inspect the stable patch",
3797
+ "Inspect the failing gate output",
3798
+ "Fix the gate or provide a different repair instruction",
3799
+ ]
3800
+ : lastRun?.captureRefusal // no latest patch exists to inspect
3801
+ ? secretDiff.captureRefusalNextActions(lastRun.captureRefusal)
3802
+ : [
3803
+ "Open diagnostics",
3804
+ "Inspect latest patch and review findings",
3805
+ "Retry with more attempts or a narrower prompt",
3806
+ ]),
3807
+ ...inspect,
3808
+ ],
3809
+ ...(convBudgetMapping
3810
+ ? budgetFailureRecord(convBudgetMapping, { runDir: paths.root, followUps: inspect })
3726
3811
  : {}),
3812
+ safeMessage: withPoolAttempts(convBudgetMapping?.safeMessage ??
3813
+ (convNeedsDecision
3814
+ ? `review escalated to a human decision after ${attempt} attempt(s)`
3815
+ : facts.reason === "stuck_no_progress"
3816
+ ? (stuckNoProgressReason ?? `stuck_no_progress after ${attempt} attempt(s)`)
3817
+ : `${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""} after ${attempt} attempt(s)${lastDiffStable ? "" : " (diff changed after review; review is stale)"}`), convPool),
3818
+ poolAttempts: convPool,
3727
3819
  });
3728
3820
  // D-16 r8/r9: an INTERRUPTED envelope run still gets its diagnostic
3729
3821
  // summary + output.ready (only patch/work_product are withheld) — the
3730
3822
  // ARCHITECTURE event contract guarantees output.ready precedes the
3731
3823
  // terminal in every mode.
3732
3824
  if (!lastRun || (interrupted && input.inPlace !== true && !lastRun.files)) {
3733
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Attempts: ${attempt}\n`);
3825
+ store.writeText(join(paths.finalDir, "summary.md"), `${withPoolAttempts(`# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Attempts: ${attempt}`, convPool)}\n`);
3734
3826
  log.emit("output.ready", {
3735
3827
  kind: "summary",
3736
3828
  path: "final/summary.md",
@@ -3738,8 +3830,9 @@ export class Orchestrator {
3738
3830
  });
3739
3831
  }
3740
3832
  }
3741
- // work_product.emitted only when a product was actually written (r9).
3742
- if (lastRun && (lastRun.files || !interrupted || input.inPlace === true)) {
3833
+ // work_product.emitted only when a product was actually written (r9); a
3834
+ // capture refusal's live record announces itself.
3835
+ if (lastRun && !lastRun.captureRefusal && (lastRun.files || !interrupted || input.inPlace)) {
3743
3836
  log.emit("work_product.emitted", { winner: lastRun.attemptId });
3744
3837
  }
3745
3838
  if (!convIsFailureTerminal) {
@@ -3749,6 +3842,8 @@ export class Orchestrator {
3749
3842
  facts,
3750
3843
  reason: facts.reason,
3751
3844
  attempts: attempt,
3845
+ // A work-state veto is still a block: it keeps the earlier refusals.
3846
+ ...(convPool.length > 0 ? { pool_attempts: convPool } : {}),
3752
3847
  });
3753
3848
  }
3754
3849
  else if (facts.lifecycle === "succeeded") {
@@ -3758,6 +3853,7 @@ export class Orchestrator {
3758
3853
  facts,
3759
3854
  attempts: attempt,
3760
3855
  phase: "review",
3856
+ pool_attempts: convPool,
3761
3857
  failure_ref: "final/failure.yaml",
3762
3858
  });
3763
3859
  }
@@ -3769,6 +3865,7 @@ export class Orchestrator {
3769
3865
  reason: facts.reason,
3770
3866
  attempts: attempt,
3771
3867
  phase: "convergence",
3868
+ pool_attempts: convPool,
3772
3869
  failure_ref: "final/failure.yaml",
3773
3870
  // QA-041: surface the typed deadline reason on the terminal event so the
3774
3871
  // daemon job result / Control API can distinguish it from a user cancel.
@@ -3791,9 +3888,9 @@ export class Orchestrator {
3791
3888
  ...(facts.lifecycle === "cancelled" && convAbortReason
3792
3889
  ? { cancelReason: convAbortReason }
3793
3890
  : {}),
3794
- summary: converged
3891
+ summary: withPoolAttempts(converged
3795
3892
  ? `converged in ${attempt} attempt(s)`
3796
- : `${facts.lifecycle} after ${attempt} attempt(s)`,
3893
+ : `${facts.lifecycle} after ${attempt} attempt(s)`, convPool),
3797
3894
  candidates: lastRun
3798
3895
  ? [
3799
3896
  {
@@ -4227,7 +4324,7 @@ export class Orchestrator {
4227
4324
  // recorded user intent is the CALLER's goal.
4228
4325
  const contract = this.buildContract({ ...input, prompt: opts.contractIntent ?? prompt }, taskId, opts.mode);
4229
4326
  const quotaSnapshots = this.quotaSnapshotPreflight();
4230
- const { store, paths, log, ledger } = beginAnnouncedRun({
4327
+ const { store, paths, log, ledger, runContext } = beginAnnouncedRun({
4231
4328
  input,
4232
4329
  contract,
4233
4330
  quotaSnapshots,
@@ -4287,6 +4384,14 @@ export class Orchestrator {
4287
4384
  };
4288
4385
  }
4289
4386
  const attempts = [];
4387
+ // A launched attempt's tries' accounts, owned here until its record joins
4388
+ // `attempts` (as an Agent slot owns them): a throw after its tries settled
4389
+ // still names them on the run's terminal, and never twice.
4390
+ const launched = new Set();
4391
+ runContext.attempts = () => [
4392
+ ...attempts,
4393
+ ...[...launched].map((tried) => ({ poolAttempts: tried.snapshot() })),
4394
+ ];
4290
4395
  const attemptTelemetries = [];
4291
4396
  let fallbackOpen = false;
4292
4397
  let budgetStopped = false;
@@ -4498,12 +4603,15 @@ export class Orchestrator {
4498
4603
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4499
4604
  let activeSessionId = spec.session_id;
4500
4605
  const live = liveAttempt(input, routed, paths, contract, attemptId, () => activeSessionId);
4606
+ const tried = new PoolAttemptEvidence(adapter.id, attemptId);
4607
+ launched.add(tried);
4501
4608
  let continuity;
4502
4609
  ({ continuity, spec } = await openContinuity(this.continuityDeps(adapter, routed, contract, attemptId, paths, input, log, {
4503
4610
  envelope: null,
4504
4611
  harnessHome: null,
4505
4612
  workOrder: input.prompt,
4506
4613
  firstPrompt: spec.prompt,
4614
+ poolEvidence: tried,
4507
4615
  }), { spec, continuation: input.continuation, store, telemetry }));
4508
4616
  const onAbort = () => void adapter.cancel?.(activeSessionId)?.catch(() => { });
4509
4617
  if (input.signal) {
@@ -4538,9 +4646,8 @@ export class Orchestrator {
4538
4646
  runSpec.extra["abortSignal"] = input.signal
4539
4647
  ? AbortSignal.any([input.signal, tryAbort.signal])
4540
4648
  : tryAbort.signal;
4541
- continuity.beginTry(runSpec, nativeTry, tryAbort);
4542
4649
  // #363: bound before the spawn — a credential change mid-try voids it.
4543
- const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
4650
+ const refusal = continuity.beginTry(runSpec, nativeTry, tryAbort);
4544
4651
  const transientStart = telemetry.transientFailures.length;
4545
4652
  const rateLimitStart = telemetry.rateLimits.length;
4546
4653
  log.emit("harness.started", {
@@ -4615,8 +4722,6 @@ export class Orchestrator {
4615
4722
  if (!recordAdapterThrow(telemetry, err))
4616
4723
  break;
4617
4724
  }
4618
- if (streamBudgetDenied)
4619
- break;
4620
4725
  if (telemetry.requestRefusal)
4621
4726
  break;
4622
4727
  const newTransients = telemetry.transientFailures.slice(transientStart);
@@ -4625,10 +4730,14 @@ export class Orchestrator {
4625
4730
  const reportSoFar = answer.text();
4626
4731
  const delivered = !harnessError && reportSoFar.length > 0 && !input.signal?.aborted;
4627
4732
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
4733
+ // A budget stop ends the attempt; typed progress before it still served the account.
4734
+ if (streamBudgetDenied)
4735
+ break;
4628
4736
  const verdict = await continuity.afterTry({
4629
4737
  runSpec,
4630
4738
  nativeTry,
4631
4739
  harnessErrored: harnessError !== null,
4740
+ originalError: harnessError,
4632
4741
  aborted: input.signal?.aborted === true,
4633
4742
  requestRefused: Boolean(telemetry.requestRefusal),
4634
4743
  newTransients,
@@ -4746,6 +4855,7 @@ export class Orchestrator {
4746
4855
  : {}),
4747
4856
  ...continuity.terminalFacts(roResumable),
4748
4857
  });
4858
+ launched.delete(tried);
4749
4859
  if (opts.deepScan) {
4750
4860
  store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
4751
4861
  }
@@ -4771,6 +4881,7 @@ export class Orchestrator {
4771
4881
  ...(scoutInterrupted ? { interrupted: true } : {}),
4772
4882
  ...continuity.terminalFacts(roResumable),
4773
4883
  });
4884
+ launched.delete(tried);
4774
4885
  if (opts.deepScan) {
4775
4886
  const warningNote = toolWarnings(telemetry).length
4776
4887
  ? `\n\n> Tool warnings: ${toolWarnings(telemetry)
@@ -4931,188 +5042,20 @@ export class Orchestrator {
4931
5042
  harnessId: a.harnessId,
4932
5043
  status: a.status,
4933
5044
  }));
4934
- const cancelledTerminal = () => cancelledResult(log, runId, taskId, opts.mode, paths.root, candidateSummaries, () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null), ledger.spend(), input.signal, store, undefined, resumableOf(attempts, "cancelled"));
5045
+ const cancelledTerminal = () => cancelledResult(log, runId, taskId, opts.mode, paths.root, candidateSummaries, () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null), ledger.spend(), input.signal, store, undefined, resumableOf(attempts, "cancelled"), attempts);
4935
5046
  if (input.signal?.aborted)
4936
5047
  return cancelledTerminal();
4937
5048
  const succeededReadonly = attempts.filter((a) => a.status === "success");
4938
- if (!opts.deepScan && succeededReadonly.length === 0) {
4939
- const last = attempts[attempts.length - 1];
4940
- const webBlocked = attempts.some((a) => a.status === "blocked");
4941
- // QA-050: a budget refusal is a BUDGET failure, not a harness one — route
4942
- // it through the shared classifier so phase/category/code/route and the
4943
- // remediation are budget-typed (never auth/setup) across every mode.
4944
- const budgetMapping = budgetStopped && !webBlocked
4945
- ? classifyBudgetFailure({ denial: budgetDenial, terminal: ledger.terminal() })
4946
- : null;
4947
- const singleError = budgetMapping?.safeMessage ??
4948
- last?.error ??
4949
- (budgetStopped ? "budget exhausted before any attempt" : "harness failed");
4950
- if (fallbackOpen || webBlocked) {
4951
- log.emit("route.fallback.exhausted", {
4952
- harness_id: last?.harnessId ?? null,
4953
- attempt_id: last?.attemptId ?? null,
4954
- reason: "web_evidence_unsatisfied",
4955
- error: singleError,
4956
- });
4957
- fallbackOpen = false;
4958
- }
4959
- const partialReport = [...attempts].reverse().find((a) => a.report)?.report ?? "";
4960
- this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null);
4961
- store.writeText(join(paths.contextDir, "context_error.md"), `# ${budgetMapping ? "Budget Denied" : "Harness Error"}\n\n${singleError}\n`);
4962
- const roEventRefs = attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`);
4963
- if (budgetMapping) {
4964
- writeFailure(store, paths, budgetFailureRecord(budgetMapping, { eventRefs: roEventRefs, runDir: paths.root }));
4965
- }
4966
- else {
4967
- // #31: classify the harness cause across the read-only attempts so auth
4968
- // guidance appears only on a real auth failure.
4969
- const roCategory = dominantHarnessFailureCategory(attemptTelemetries.flatMap((a) => a.telemetry.transientFailures));
4970
- // A5: the terminal already speaks with the LAST attempt's message; its
4971
- // typed refusal rides along machine-readably instead of reducing to it.
4972
- const roDeclared = webBlocked ? undefined : last?.declaredFailure;
4973
- writeFailure(store, paths, {
4974
- phase: "harness",
4975
- category: webBlocked ? "policy" : (roDeclared?.category ?? "harness_error"),
4976
- code: roDeclared?.code ?? null,
4977
- harnessId: last?.harnessId,
4978
- attemptId: last?.attemptId,
4979
- safeMessage: singleError,
4980
- eventRefs: roEventRefs,
4981
- runDir: paths.root,
4982
- resetsAt: roDeclared?.resetsAt ?? null,
4983
- requestRefusal: roDeclared?.requestRefusal,
4984
- vendorFailure: attemptVendorFailure(attemptTelemetries, last?.attemptId),
4985
- nextActions: harnessFailureNextActions(roCategory),
4986
- });
4987
- }
4988
- // QA-036: re-check the DELIVERABLE through the shared finalizer helper —
4989
- // a blocked Ask that produced NO answer can no longer read as a succeeded
4990
- // "Needs review" run (exit 0); it is an honest failure (exit 1).
4991
- const roTerminal = readOnlyNoSuccessTerminal({
4992
- webBlocked,
4993
- hasDeliverable: partialReport.trim().length > 0,
4994
- budgetStopped,
4995
- attemptsCount: attempts.length,
4996
- });
4997
- const terminalFacts = makeOutcomeFacts(roTerminal.lifecycle, {
4998
- ...(roTerminal.review ? { review: roTerminal.review } : {}),
4999
- reason: budgetMapping?.reason ?? roTerminal.reason,
5000
- });
5001
- const terminalHarnessId = budgetMapping?.harnessId ?? last?.harnessId;
5002
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harness: ${terminalHarnessId ?? "none"}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}\n\n${singleError}\n`);
5003
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
5004
- if (terminalFacts.lifecycle === "succeeded") {
5005
- log.emit("run.blocked", {
5006
- lifecycle: terminalFacts.lifecycle,
5007
- facts: terminalFacts,
5008
- harness_id: terminalHarnessId,
5009
- error: singleError,
5010
- failure_ref: "final/failure.yaml",
5011
- ...resumableTerminal(store, paths, attempts, true),
5012
- });
5013
- }
5014
- else {
5015
- log.emit("run.failed", {
5016
- lifecycle: terminalFacts.lifecycle,
5017
- facts: terminalFacts,
5018
- reason: terminalFacts.reason,
5019
- phase: budgetMapping?.phase,
5020
- harness_id: terminalHarnessId,
5021
- error: singleError,
5022
- failure_ref: "final/failure.yaml",
5023
- ...resumableTerminal(store, paths, attempts, true),
5024
- });
5025
- }
5026
- return {
5027
- spendUsd: ledger.spend(),
5028
- runId,
5029
- taskId,
5030
- mode: opts.mode,
5031
- lifecycle: terminalFacts.lifecycle,
5032
- facts: terminalFacts,
5033
- winner: null,
5034
- runDir: paths.root,
5035
- summary: singleError,
5036
- candidates: attempts.map((a) => ({
5037
- attemptId: a.attemptId,
5038
- harnessId: a.harnessId,
5039
- status: a.status,
5040
- })),
5041
- };
5042
- }
5049
+ const readonlyTerminal = {
5050
+ ...{ store, paths, log, runId, taskId, mode: opts.mode, ledger, attempts },
5051
+ ...{ attemptTelemetries, budgetStopped, budgetDenial },
5052
+ writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null),
5053
+ };
5054
+ if (!opts.deepScan && succeededReadonly.length === 0)
5055
+ return readOnlyChainFailureResult({ ...readonlyTerminal, fallbackOpen });
5043
5056
  const succeeded = succeededReadonly;
5044
- if (opts.deepScan && succeeded.length === 0) {
5045
- const blocked = attempts.some((a) => a.status === "blocked");
5046
- // QA-050/QA-019: only a pure-denial scan may claim a budget failure.
5047
- const scanBudgetMapping = budgetStopped && !blocked && attempts.every((a) => a.budgetDenied === true)
5048
- ? classifyBudgetFailure({ denial: budgetDenial, terminal: ledger.terminal() })
5049
- : null;
5050
- // D-16 r8: ALL scouts out of context → the aggregate is interrupted.
5051
- const allInterrupted = attempts.length > 0 && attempts.every((a) => a.interrupted === true);
5052
- const scanFailFacts = allInterrupted
5053
- ? makeOutcomeFacts("interrupted", { reason: "context_capacity_exhausted" })
5054
- : makeOutcomeFacts("failed", {
5055
- reason: scanBudgetMapping ? scanBudgetMapping.reason : "harness_failed",
5056
- });
5057
- const message = scanBudgetMapping
5058
- ? scanBudgetMapping.safeMessage
5059
- : attempts.map((a) => `${a.attemptId}/${a.harnessId}: ${a.error ?? "failed"}`).join("\n");
5060
- this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null);
5061
- if (scanBudgetMapping) {
5062
- writeFailure(store, paths, budgetFailureRecord(scanBudgetMapping, {
5063
- eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
5064
- runDir: paths.root,
5065
- }));
5066
- }
5067
- else {
5068
- // Mixed causes stay generic; only unanimous evidence may name the scan's cause.
5069
- const declared = blocked ? null : unanimousDeclaredFailure(attempts);
5070
- const scanCategory = dominantHarnessFailureCategory(attemptTelemetries.flatMap((a) => a.telemetry.transientFailures));
5071
- writeFailure(store, paths, {
5072
- phase: "harness",
5073
- category: blocked ? "policy" : (declared?.category ?? "harness_error"),
5074
- code: declared?.code,
5075
- resetsAt: declared?.resetsAt,
5076
- requestRefusal: declared?.requestRefusal,
5077
- safeMessage: message || "all explorers failed",
5078
- eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
5079
- runDir: paths.root,
5080
- nextActions: [
5081
- ...harnessFailureNextActions(scanCategory).filter((a) => !a.startsWith("Retry")),
5082
- "Reduce explore width",
5083
- "Retry after setup",
5084
- ],
5085
- });
5086
- }
5087
- // QA-036: no successful explorer means no synthesizable deliverable.
5088
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Lifecycle: ${scanFailFacts.lifecycle}${scanFailFacts.reason ? ` (${scanFailFacts.reason})` : ""}\n\n${message}\n`);
5089
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
5090
- log.emit("run.failed", {
5091
- lifecycle: scanFailFacts.lifecycle,
5092
- facts: scanFailFacts,
5093
- reason: scanFailFacts.reason,
5094
- phase: scanBudgetMapping ? scanBudgetMapping.phase : "harness",
5095
- error: message,
5096
- failure_ref: "final/failure.yaml",
5097
- ...resumableTerminal(store, paths, attempts, true),
5098
- });
5099
- return {
5100
- spendUsd: ledger.spend(),
5101
- runId,
5102
- taskId,
5103
- mode: opts.mode,
5104
- lifecycle: scanFailFacts.lifecycle,
5105
- facts: scanFailFacts,
5106
- winner: null,
5107
- runDir: paths.root,
5108
- summary: message,
5109
- candidates: attempts.map((a) => ({
5110
- attemptId: a.attemptId,
5111
- harnessId: a.harnessId,
5112
- status: a.status,
5113
- })),
5114
- };
5115
- }
5057
+ if (opts.deepScan && succeeded.length === 0)
5058
+ return deepScanFailureResult(readonlyTerminal);
5116
5059
  const unsuccessful = attempts.filter((a) => a.status !== "success");
5117
5060
  // #27 / D-6: a multi-scout deep scan runs ONE bounded synthesis reducer over
5118
5061
  // the raw scout reports so the final artifact is a real merge, not a
@@ -5216,7 +5159,9 @@ export class Orchestrator {
5216
5159
  .map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
5217
5160
  .join(", ");
5218
5161
  const secretLike = secretDiff.answerSecretLikeFinding(answerMatches);
5219
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harnesses: ${harnessLabel}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}${secretDiff.summaryDisclosure(secretLike)}\n\n${report}\n`);
5162
+ // A delivered report that still ends unsuccessfully or blocked keeps it and names every failed try.
5163
+ const reportPool = terminalPoolAttempts(terminalFacts, attempts);
5164
+ store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harnesses: ${harnessLabel}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}${secretDiff.summaryDisclosure(secretLike)}\n\n${withPoolAttempts(report, reportPool)}\n`);
5220
5165
  const reportProducerAttemptId = opts.deepScan &&
5221
5166
  deepScanSynthesis?.status === "succeeded" &&
5222
5167
  deepScanSynthesis.reducer_attempt_id
@@ -5243,7 +5188,8 @@ export class Orchestrator {
5243
5188
  writeFailure(store, paths, {
5244
5189
  phase: "executor",
5245
5190
  category: terminalFacts.reason === "context_capacity_exhausted" ? "harness_error" : "budget",
5246
- safeMessage: `read-only report ended ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason.replaceAll("_", " ")})` : ""}`,
5191
+ safeMessage: withPoolAttempts(`read-only report ended ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason.replaceAll("_", " ")})` : ""}`, reportPool),
5192
+ poolAttempts: reportPool,
5247
5193
  runDir: paths.root,
5248
5194
  nextActions: terminalFacts.reason === "context_capacity_exhausted"
5249
5195
  ? ["Inspect the partial report", "Re-run with a narrower scope"]
@@ -5254,6 +5200,7 @@ export class Orchestrator {
5254
5200
  facts: terminalFacts,
5255
5201
  reason: terminalFacts.reason,
5256
5202
  phase: "executor",
5203
+ pool_attempts: reportPool,
5257
5204
  failure_ref: "final/failure.yaml",
5258
5205
  ...resumableTerminal(store, paths, attempts, true),
5259
5206
  });
@@ -5263,6 +5210,7 @@ export class Orchestrator {
5263
5210
  lifecycle: terminalFacts.lifecycle,
5264
5211
  facts: terminalFacts,
5265
5212
  reason: terminalFacts.reason,
5213
+ pool_attempts: reportPool,
5266
5214
  ...resumableTerminal(store, paths, attempts, true),
5267
5215
  });
5268
5216
  }
@@ -5283,7 +5231,7 @@ export class Orchestrator {
5283
5231
  facts: terminalFacts,
5284
5232
  winner: null,
5285
5233
  runDir: paths.root,
5286
- summary: redactSecrets(report).slice(0, 400),
5234
+ summary: withPoolAttempts(redactSecrets(report).slice(0, 400), reportPool),
5287
5235
  candidates: candidateSummaries,
5288
5236
  };
5289
5237
  }