@claudexor/orchestrator 4.1.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) 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 +3 -0
  6. package/dist/account-resolution.d.ts.map +1 -1
  7. package/dist/account-resolution.js +1 -0
  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 +1 -1
  12. package/dist/attemptTelemetry.js.map +1 -1
  13. package/dist/budgetFailure.d.ts +4 -1
  14. package/dist/budgetFailure.d.ts.map +1 -1
  15. package/dist/budgetFailure.js +4 -2
  16. package/dist/budgetFailure.js.map +1 -1
  17. package/dist/candidateEvidence.d.ts.map +1 -1
  18. package/dist/candidateEvidence.js +7 -2
  19. package/dist/candidateEvidence.js.map +1 -1
  20. package/dist/candidateOutputs.d.ts.map +1 -1
  21. package/dist/candidateOutputs.js +6 -0
  22. package/dist/candidateOutputs.js.map +1 -1
  23. package/dist/candidateTerminals.d.ts +12 -1
  24. package/dist/candidateTerminals.d.ts.map +1 -1
  25. package/dist/candidateTerminals.js +51 -21
  26. package/dist/candidateTerminals.js.map +1 -1
  27. package/dist/captureRefusal.d.ts +28 -0
  28. package/dist/captureRefusal.d.ts.map +1 -0
  29. package/dist/captureRefusal.js +54 -0
  30. package/dist/captureRefusal.js.map +1 -0
  31. package/dist/continuity-terminal.d.ts +3 -1
  32. package/dist/continuity-terminal.d.ts.map +1 -1
  33. package/dist/continuity-terminal.js.map +1 -1
  34. package/dist/credential-profile-rotation.js +2 -3
  35. package/dist/credential-profile-rotation.js.map +1 -1
  36. package/dist/delegationFailure.d.ts +104 -7
  37. package/dist/delegationFailure.d.ts.map +1 -1
  38. package/dist/delegationFailure.js +47 -31
  39. package/dist/delegationFailure.js.map +1 -1
  40. package/dist/inrun-continuity-types.d.ts +13 -1
  41. package/dist/inrun-continuity-types.d.ts.map +1 -1
  42. package/dist/inrun-continuity.d.ts +9 -2
  43. package/dist/inrun-continuity.d.ts.map +1 -1
  44. package/dist/inrun-continuity.js +43 -23
  45. package/dist/inrun-continuity.js.map +1 -1
  46. package/dist/inrun-continuity.test-support.d.ts +6 -2
  47. package/dist/inrun-continuity.test-support.d.ts.map +1 -1
  48. package/dist/inrun-continuity.test-support.js +4 -1
  49. package/dist/inrun-continuity.test-support.js.map +1 -1
  50. package/dist/orchestrator-credentials.d.ts +5 -4
  51. package/dist/orchestrator-credentials.d.ts.map +1 -1
  52. package/dist/orchestrator-credentials.js +7 -4
  53. package/dist/orchestrator-credentials.js.map +1 -1
  54. package/dist/orchestrator.d.ts.map +1 -1
  55. package/dist/orchestrator.js +277 -335
  56. package/dist/orchestrator.js.map +1 -1
  57. package/dist/persistedPatch.js +2 -2
  58. package/dist/persistedPatch.js.map +1 -1
  59. package/dist/pool-attempt-evidence.d.ts +62 -0
  60. package/dist/pool-attempt-evidence.d.ts.map +1 -0
  61. package/dist/pool-attempt-evidence.js +153 -0
  62. package/dist/pool-attempt-evidence.js.map +1 -0
  63. package/dist/pre-progress-refusal.d.ts +43 -6
  64. package/dist/pre-progress-refusal.d.ts.map +1 -1
  65. package/dist/pre-progress-refusal.js +53 -8
  66. package/dist/pre-progress-refusal.js.map +1 -1
  67. package/dist/readonlyTerminals.d.ts +55 -0
  68. package/dist/readonlyTerminals.d.ts.map +1 -0
  69. package/dist/readonlyTerminals.js +205 -0
  70. package/dist/readonlyTerminals.js.map +1 -0
  71. package/dist/rotation-predicate.d.ts +7 -23
  72. package/dist/rotation-predicate.d.ts.map +1 -1
  73. package/dist/rotation-predicate.js +45 -0
  74. package/dist/rotation-predicate.js.map +1 -1
  75. package/dist/runEventLog.d.ts +1 -0
  76. package/dist/runEventLog.d.ts.map +1 -1
  77. package/dist/runEventLog.js +3 -2
  78. package/dist/runEventLog.js.map +1 -1
  79. package/dist/runTerminalContext.d.ts +4 -0
  80. package/dist/runTerminalContext.d.ts.map +1 -1
  81. package/dist/runTerminalContext.js.map +1 -1
  82. package/dist/runTerminalHelpers.d.ts +25 -1
  83. package/dist/runTerminalHelpers.d.ts.map +1 -1
  84. package/dist/runTerminalHelpers.js +69 -6
  85. package/dist/runTerminalHelpers.js.map +1 -1
  86. package/dist/runTerminalResults.d.ts +25 -3
  87. package/dist/runTerminalResults.d.ts.map +1 -1
  88. package/dist/runTerminalResults.js +61 -29
  89. package/dist/runTerminalResults.js.map +1 -1
  90. package/dist/runTerminals.d.ts +0 -1
  91. package/dist/runTerminals.d.ts.map +1 -1
  92. package/dist/runTerminals.js +47 -11
  93. package/dist/runTerminals.js.map +1 -1
  94. package/dist/secretDiff.d.ts +14 -11
  95. package/dist/secretDiff.d.ts.map +1 -1
  96. package/dist/secretDiff.js +21 -28
  97. package/dist/secretDiff.js.map +1 -1
  98. package/dist/terminalOutcome.d.ts +24 -4
  99. package/dist/terminalOutcome.d.ts.map +1 -1
  100. package/dist/terminalOutcome.js +32 -5
  101. package/dist/terminalOutcome.js.map +1 -1
  102. 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,8 +58,7 @@ 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";
@@ -1312,7 +1313,10 @@ export class Orchestrator {
1312
1313
  * must record what it ran under, not just why it stopped. REQUIRED (no
1313
1314
  * default): a silent fallback here would spawn a delegated attempt on the
1314
1315
  * operator's real home while the record still claimed scoped state. */
1315
- harnessHome, observedPaths = new Set()) {
1316
+ harnessHome, observedPaths = new Set(),
1317
+ /** The caller's, like `harnessHome`: its catch keeps the tries' accounts.
1318
+ * REQUIRED, so no caller can drop them by omission again. */
1319
+ poolEvidence) {
1316
1320
  const adapter = routed.adapter;
1317
1321
  const knobs = this.routeSpecKnobs(routed, contract, modelHint, effortHint);
1318
1322
  // Isolated scoped-home sessions are never retained after disposal.
@@ -1418,6 +1422,7 @@ export class Orchestrator {
1418
1422
  let beltFailureDisclosed = false;
1419
1423
  const errors = [];
1420
1424
  let answer = new secretDiff.CountedAnswerAssembly();
1425
+ let currentCapture;
1421
1426
  const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
1422
1427
  // QA-024: the delegation belt is the ONLY engine-owned extra MCP server
1423
1428
  // injected into an agent lane (the browser MCP rides its own field), so its
@@ -1444,9 +1449,11 @@ export class Orchestrator {
1444
1449
  harnessHome,
1445
1450
  workOrder: prompt,
1446
1451
  firstPrompt: spec.prompt,
1452
+ poolEvidence,
1447
1453
  }), { spec, continuation: runInput?.continuation, store, telemetry }));
1448
1454
  try {
1449
1455
  for (let nativeTry = 0; !signal?.aborted; nativeTry += 1) {
1456
+ currentCapture = undefined;
1450
1457
  // A3 per-try isolation: neither output nor progress markers leak across tries.
1451
1458
  if (nativeTry > 0) {
1452
1459
  answer = new secretDiff.CountedAnswerAssembly();
@@ -1462,9 +1469,8 @@ export class Orchestrator {
1462
1469
  ? AbortSignal.any([signal, attemptAbort.signal])
1463
1470
  : attemptAbort.signal;
1464
1471
  activeSessionId = runSpec.session_id;
1465
- continuity.beginTry(runSpec, nativeTry, attemptAbort);
1466
1472
  // #363: bound before the spawn — a credential change mid-try voids it.
1467
- const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
1473
+ const refusal = continuity.beginTry(runSpec, nativeTry, attemptAbort);
1468
1474
  const transientStart = telemetry.transientFailures.length;
1469
1475
  const rateLimitStart = telemetry.rateLimits.length;
1470
1476
  let rawPatch = null;
@@ -1588,30 +1594,42 @@ export class Orchestrator {
1588
1594
  // #31: the centralized retry gate reads the classified `retryable` verdict.
1589
1595
  const sawRetryable = newTransients.some((f) => f.retryable);
1590
1596
  const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
1597
+ const tryFacts = {
1598
+ harnessErrored,
1599
+ originalError: errors[0],
1600
+ budgetStopped: streamBudgetDenied,
1601
+ aborted: signal?.aborted === true,
1602
+ requestRefused: Boolean(telemetry.requestRefusal),
1603
+ newTransients,
1604
+ markers: telemetry.outputMarkers,
1605
+ };
1606
+ // Its account row first: an infrastructure failure below never erases it.
1607
+ continuity.settleTry(tryFacts);
1591
1608
  if (directory)
1592
1609
  await captureDirectory();
1593
- const currentDiff = directory ? "" : await wsm.diff(envelope);
1610
+ currentCapture = directory
1611
+ ? undefined
1612
+ : await secretDiff.captureCandidateDiff(wsm, envelope);
1613
+ const currentDiff = secretDiff.capturedDiff(currentCapture);
1594
1614
  const workspaceUnchanged = directory
1595
1615
  ? directoryCapture.files?.noChanges === true
1596
- : currentDiff.trim().length === 0;
1616
+ : currentDiff?.trim().length === 0;
1597
1617
  const deliverableEmpty = workspaceUnchanged && answer.text().length === 0;
1598
1618
  const delivered = !harnessErrored && !deliverableEmpty && !signal?.aborted;
1599
1619
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
1600
1620
  const verdict = await continuity.afterTry({
1621
+ ...tryFacts,
1622
+ // A cancel or deadline that landed during capture still stops the
1623
+ // attempt; the try's own facts above stay its evidence.
1624
+ stopped: signal?.aborted === true,
1601
1625
  runSpec,
1602
1626
  nativeTry,
1603
- harnessErrored,
1604
- budgetStopped: streamBudgetDenied,
1605
- aborted: signal?.aborted === true,
1606
- requestRefused: Boolean(telemetry.requestRefusal),
1607
- newTransients,
1608
1627
  sawTypedLimit,
1609
1628
  sawRetryable,
1610
1629
  answer,
1611
1630
  rawDeliverableEmpty: deliverableEmpty,
1612
1631
  workspaceDiffNonEmpty: !workspaceUnchanged,
1613
- currentDiff,
1614
- markers: telemetry.outputMarkers,
1632
+ ...(currentDiff === undefined ? {} : { currentDiff }),
1615
1633
  lastLimit: telemetry.rateLimits.at(-1) ?? null,
1616
1634
  refusal,
1617
1635
  telemetry,
@@ -1635,8 +1653,13 @@ export class Orchestrator {
1635
1653
  signal?.removeEventListener("abort", onAbort);
1636
1654
  }
1637
1655
  // 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) {
1656
+ // machinery's, and a stopped run exhausted no retries: no
1657
+ // `route.transient.exhausted` rides along with either.
1658
+ if (harnessErrored &&
1659
+ !poolExhausted &&
1660
+ !processingRefusal &&
1661
+ !telemetry.requestRefusal &&
1662
+ !signal?.aborted) {
1640
1663
  emitTransientExhausted((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
1641
1664
  }
1642
1665
  const attemptStreamEndedMs = Date.now();
@@ -1656,9 +1679,10 @@ export class Orchestrator {
1656
1679
  : await secretDiff.captureCandidateWorkspace({
1657
1680
  ...{ wsm, envelope, inPlace: inPlaceEnvelope, projectRoot: store.repoRoot },
1658
1681
  ...{ answerText, answerMatches: answer.secretLikeMatches() },
1682
+ holder: { runId: paths.runId, runDir: paths.root },
1683
+ incompleteCapture: currentCapture?.captureIncomplete ? currentCapture : undefined,
1659
1684
  });
1660
1685
  const { diff, captureRefusal } = captured;
1661
- harnessErrored = secretDiff.recordCaptureRefusal(captureRefusal, errors, harnessErrored);
1662
1686
  const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText) || directoryHasOutput(directoryCapture.files);
1663
1687
  // Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
1664
1688
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
@@ -1717,7 +1741,9 @@ export class Orchestrator {
1717
1741
  if (finalized.outcomeClass === "contract_failure" && unwrapped.contractViolation)
1718
1742
  errors.push(`work_report contract: ${unwrapped.contractViolation}`);
1719
1743
  const deliverablePresent = finalized.deliverablePresent;
1720
- const errored = harnessErrored || webBlocked;
1744
+ if (captureRefusal)
1745
+ errors.push(captureRefusal.detail);
1746
+ const errored = harnessErrored || webBlocked || captureRefusal !== undefined;
1721
1747
  const resumable = await continuity.finish({
1722
1748
  runSpec: spec,
1723
1749
  errored,
@@ -1727,6 +1753,7 @@ export class Orchestrator {
1727
1753
  workState: finalized.workState,
1728
1754
  });
1729
1755
  setAttemptOutcome(telemetry, {
1756
+ captureFailed: captureRefusal !== undefined,
1730
1757
  deliverablePresent,
1731
1758
  gatesPassed: gates.length > 0 ? gatesPassed(gates) : null,
1732
1759
  harnessErrored,
@@ -1840,9 +1867,11 @@ export class Orchestrator {
1840
1867
  probeReadyProfiles: (spec, tried) => runInput
1841
1868
  ? this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, tried, spec.model_hint ?? null)
1842
1869
  : Promise.resolve(new Set()),
1870
+ bindRefusal: (spec, producer) => this.credentials.bindPreProgressRefusal(adapter.id, spec, producer),
1843
1871
  rotationObservations: (spec, transients, refusal) => this.credentials.rotationObservations(adapter, spec, transients, refusal),
1844
1872
  emit: (type, payload) => log?.emit(type, payload),
1845
1873
  route: routed,
1874
+ poolEvidence: lane.poolEvidence,
1846
1875
  newSessionId: () => newId("ses"),
1847
1876
  thread: runInput?.threadId
1848
1877
  ? {
@@ -1864,7 +1893,7 @@ export class Orchestrator {
1864
1893
  const mutatingRun = isMutatingAccess(contract.access.effective_profile);
1865
1894
  const planBrief = verifiedPlanBrief(input);
1866
1895
  const quotaSnapshots = this.quotaSnapshotPreflight();
1867
- const { store, paths, log, ledger } = beginAnnouncedRun({
1896
+ const { store, paths, log, ledger, runContext } = beginAnnouncedRun({
1868
1897
  input,
1869
1898
  contract,
1870
1899
  quotaSnapshots,
@@ -2020,6 +2049,7 @@ export class Orchestrator {
2020
2049
  });
2021
2050
  }
2022
2051
  const runsBySlot = new Array(slots.length);
2052
+ runContext.attempts = () => runsBySlot.filter((run) => run !== undefined);
2023
2053
  // D-16d: one-shot continuation budget shared across the concurrent candidate
2024
2054
  // slots (parity with the read-only chain's single counter). Claimed
2025
2055
  // synchronously (check-then-increment with no await between), so two slots
@@ -2072,8 +2102,9 @@ export class Orchestrator {
2072
2102
  const effectiveWeb = this.discloseWebUpgrade(log, slot.routed, knobs.webPolicy, slot.attemptId);
2073
2103
  let envelope;
2074
2104
  // Declared OUTSIDE the try so the catch below can state what the attempt
2075
- // actually ran under, not merely that it stopped.
2105
+ // actually ran under, not merely that it stopped — and on which accounts.
2076
2106
  let harnessHome;
2107
+ const tried = new PoolAttemptEvidence(adapter.id, slot.attemptId);
2077
2108
  try {
2078
2109
  log.emit("harness.started", {
2079
2110
  harness_id: adapter.id,
@@ -2110,7 +2141,7 @@ export class Orchestrator {
2110
2141
  budgetStopped = true;
2111
2142
  return true;
2112
2143
  }, input, requestedSingleCandidate, // W-C4 deltas: single-candidate chat lane only (racing = noise x N)
2113
- undefined, undefined, harnessHome);
2144
+ undefined, undefined, harnessHome, undefined, tried);
2114
2145
  ledger.settle(slot.lease.lease_id, attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2115
2146
  log.emit("harness.completed", {
2116
2147
  harness_id: adapter.id,
@@ -2175,6 +2206,7 @@ export class Orchestrator {
2175
2206
  continuation_of: run.attemptId,
2176
2207
  });
2177
2208
  const contLeaseId = contLease.lease?.lease_id ?? "";
2209
+ const contTried = new PoolAttemptEvidence(adapter.id, contAttemptId);
2178
2210
  try {
2179
2211
  const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, {
2180
2212
  lease: contLease.lease,
@@ -2188,7 +2220,7 @@ export class Orchestrator {
2188
2220
  return false;
2189
2221
  budgetStopped = true;
2190
2222
  return true;
2191
- }, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined, harnessHome);
2223
+ }, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined, harnessHome, undefined, contTried);
2192
2224
  ledger.settle(contLeaseId, attemptUsageCostSettlement(contRun.cost, contRun.costEstimated, contRun.attemptId, contRun.harnessId, contRun.telemetry.authMode, contRun.telemetry.usageCost));
2193
2225
  log.emit("harness.completed", {
2194
2226
  harness_id: adapter.id,
@@ -2215,6 +2247,8 @@ export class Orchestrator {
2215
2247
  error: safeErrorMessage(err),
2216
2248
  });
2217
2249
  }
2250
+ // Whichever attempt speaks names both attempts' failed tries, in order.
2251
+ effectiveRun.poolAttempts = [...(run.poolAttempts ?? []), ...contTried.snapshot()];
2218
2252
  }
2219
2253
  else {
2220
2254
  ledger.cancel(contLease.lease?.lease_id ?? "");
@@ -2260,6 +2294,7 @@ export class Orchestrator {
2260
2294
  slot.routed.delegationRequirement,
2261
2295
  ], knobs.model),
2262
2296
  infraPhase,
2297
+ poolAttempts: tried.snapshot(),
2263
2298
  // Keep a TYPED pre-spawn refusal (spent quota window + its reset)
2264
2299
  // alive past this catch; `message` alone would force the terminal to
2265
2300
  // read prose back out.
@@ -2274,10 +2309,17 @@ export class Orchestrator {
2274
2309
  };
2275
2310
  await runParallelCandidates(slots, this.deps.runtimeConcurrencyCaps, runSlot);
2276
2311
  const runs = runsBySlot.filter((r) => r !== undefined);
2312
+ // From here `runs` is the lane's one attempt collection (a synthesis that
2313
+ // returns joins it); a synthesis that threw first leaves only its tries.
2314
+ let thrownSynthesis = null;
2315
+ const history = () => thrownSynthesis ? [...runs, thrownSynthesis] : runs;
2316
+ runContext.attempts = history;
2277
2317
  // Fail-closed terminal: a delegated mutating run whose attempts state
2278
2318
  // neither historical proof nor deliberate absence refuses instead of passing.
2279
2319
  assertDelegatedEvidence(input.delegated === true, candidateAccess, runs);
2280
- const cancelledRaceResult = () => cancelledCandidatesResult({
2320
+ // One candidate-terminal context for every race terminal below; its
2321
+ // telemetry writer reads the live roster, so a later synth attempt counts.
2322
+ const raceTerminal = {
2281
2323
  store,
2282
2324
  paths,
2283
2325
  log,
@@ -2286,6 +2328,11 @@ export class Orchestrator {
2286
2328
  mode,
2287
2329
  ledger,
2288
2330
  runs,
2331
+ writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2332
+ };
2333
+ const cancelledRaceResult = () => cancelledCandidatesResult({
2334
+ ...raceTerminal,
2335
+ attempts: history(),
2289
2336
  git: input.workspaceKind !== "directory"
2290
2337
  ? {
2291
2338
  live: input.inPlace === true &&
@@ -2297,7 +2344,6 @@ export class Orchestrator {
2297
2344
  }
2298
2345
  : undefined,
2299
2346
  signal: input.signal,
2300
- writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2301
2347
  });
2302
2348
  // Revert divergence fence for the single-candidate in-place path: the
2303
2349
  // candidate mutated the LIVE tree during execution above, so the post-turn
@@ -2324,31 +2370,34 @@ export class Orchestrator {
2324
2370
  }
2325
2371
  const failedDelegation = delegateFailure.dominantRaceCandidateFailure(runs);
2326
2372
  if (failedDelegation) {
2327
- const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2328
2373
  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",
2374
+ const failedOutput = {
2375
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode, run: failedDelegation },
2376
+ live: input.inPlace === true &&
2377
+ input.workspaceKind !== "directory" &&
2378
+ failedDelegation.reviewCwd === execRoot,
2379
+ postTurnSha: earlyPostTurnSha,
2380
+ kind: input.create === true ? "new_repo" : "patch",
2381
+ };
2382
+ // A live candidate whose capture failed (no Delegate failure) ends in the
2383
+ // shared no-working-candidate terminal, as in the isolated race and
2384
+ // convergence: a recorded budget denial, a budget terminal the ledger
2385
+ // settled (stream hard cap, exact overshoot, unverifiable cash) or a
2386
+ // unanimous typed refusal leads, its live receipt carries the same facts,
2387
+ // and the capture facts and inspection actions follow. Any other dominant
2388
+ // capture failure joins it only under a settled budget terminal.
2389
+ if (delegateFailure.candidateFailureKind(failedDelegation) === "capture" &&
2390
+ (failedOutput.live || ledger.terminal() !== null))
2391
+ return failedCandidatesResult({
2392
+ ...raceTerminal,
2393
+ budgetDenial,
2394
+ ...(mutatingRun ? { liveCapture: failedOutput } : {}),
2348
2395
  });
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);
2396
+ const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2397
+ if (mutatingRun)
2398
+ await delegateFailure.persistFailedCandidateOutput(failedOutput);
2399
+ raceTerminal.writeTelemetry();
2400
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), { ...failure.metadata, attempts: runs });
2352
2401
  }
2353
2402
  if (runs.length === 0)
2354
2403
  return emptyCandidateResult({
@@ -2370,18 +2419,7 @@ export class Orchestrator {
2370
2419
  const workingRuns = partitionCandidates(runs).working;
2371
2420
  if (workingRuns.length === 0) {
2372
2421
  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
- });
2422
+ return failedCandidatesResult({ ...raceTerminal, budgetDenial });
2385
2423
  }
2386
2424
  // QA-025: only announce that review STARTED when the panel will actually
2387
2425
  // run. A candidate that changed no files is skipped inside reviewRuns; a
@@ -2423,7 +2461,7 @@ export class Orchestrator {
2423
2461
  catch (err) {
2424
2462
  // Review preflight/evidence failures end TERMINALLY with artifacts —
2425
2463
  // never as an escaped throw that orphans the run dir.
2426
- return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "review", err);
2464
+ return failAfterCandidates(raceTerminal, "review", err);
2427
2465
  }
2428
2466
  finally {
2429
2467
  // Review preflight failures must not leak candidate worktrees.
@@ -2453,6 +2491,7 @@ export class Orchestrator {
2453
2491
  });
2454
2492
  if (lease.granted) {
2455
2493
  let envelope;
2494
+ const synthTried = new PoolAttemptEvidence(synthRouted.adapter.id, "synth");
2456
2495
  try {
2457
2496
  const plan = buildSynthesisPlan(evidences);
2458
2497
  const synthesisInput = buildFileBackedSynthesisInput({
@@ -2481,7 +2520,7 @@ export class Orchestrator {
2481
2520
  budgetStopped = true;
2482
2521
  budgetDenial ??= denial;
2483
2522
  },
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);
2523
+ }, 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
2524
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2486
2525
  reviewEnvelopes.push(envelope);
2487
2526
  envelope = undefined;
@@ -2495,7 +2534,7 @@ export class Orchestrator {
2495
2534
  workingRuns.push(run);
2496
2535
  }
2497
2536
  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);
2537
+ return cancelledResult(log, runId, taskId, mode, paths.root, candidateStatuses(runs), raceTerminal.writeTelemetry, ledger.spend(), input.signal, store, undefined, undefined, history());
2499
2538
  }
2500
2539
  }
2501
2540
  finally {
@@ -2503,6 +2542,9 @@ export class Orchestrator {
2503
2542
  }
2504
2543
  }
2505
2544
  catch (err) {
2545
+ // Not yet in `runs`: the run goes on, so its tries' accounts stay in the history.
2546
+ if (!runs.some((run) => run.attemptId === "synth"))
2547
+ thrownSynthesis = { poolAttempts: synthTried.snapshot() };
2506
2548
  ledger.settle(lease.lease?.lease_id ?? "", AC.attemptFailureCost(err, "synthesis-error").settlement);
2507
2549
  log.emit("harness.completed", {
2508
2550
  attempt_id: "synth",
@@ -2528,7 +2570,7 @@ export class Orchestrator {
2528
2570
  }
2529
2571
  catch (err) {
2530
2572
  // Arbitration throws end terminally with artifacts, never as an orphan.
2531
- return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "arbitration", err);
2573
+ return failAfterCandidates({ ...raceTerminal, attempts: history() }, "arbitration", err);
2532
2574
  }
2533
2575
  log.emit("arbitration.completed", {
2534
2576
  winner: result.decision.winner,
@@ -2608,6 +2650,9 @@ export class Orchestrator {
2608
2650
  });
2609
2651
  store.writeYaml(join(paths.arbitrationDir, "pairwise.yaml"), result.pairwise);
2610
2652
  const decisionPath = join(paths.arbitrationDir, "decision.yaml");
2653
+ // Every attempt's failed tries by the FINAL outcome (delivery below can
2654
+ // still fail the checks); a review or check block is not an account failure.
2655
+ const racePool = () => terminalPoolAttempts(facts, history());
2611
2656
  if (winnerRun) {
2612
2657
  for (const path of materializeWinnerOutputs({
2613
2658
  attemptDir: join(paths.attemptsDir, winnerRun.attemptId),
@@ -2786,7 +2831,7 @@ export class Orchestrator {
2786
2831
  },
2787
2832
  });
2788
2833
  }
2789
- store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified, secretDiff.secretLikeSummaryLine(winnerRun.secretLike)));
2834
+ store.writeText(join(paths.finalDir, "summary.md"), withPoolAttempts(renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified, secretDiff.secretLikeSummaryLine(winnerRun.secretLike)), racePool()));
2790
2835
  // summary.md is a DIAGNOSTIC artifact only (V8/PLAN addendum 2): it no
2791
2836
  // longer carries primary-output authority. A clean applyable success or a
2792
2837
  // winner answer still marks it ready for legacy INV-116 ordering; any
@@ -2812,8 +2857,14 @@ export class Orchestrator {
2812
2857
  // (applyable, no_changes, or not-verified) is an honest completion.
2813
2858
  const needsDecisionTerminal = facts.review === "blocked" || facts.checks === "failed";
2814
2859
  const isFailureTerminal = facts.lifecycle !== "succeeded" || needsDecisionTerminal;
2860
+ const pool = racePool();
2815
2861
  if (deliveryFailureReason && !finalVerifyFailed) {
2816
- writeFailure(store, paths, deliveryRefusalFailure(deliveryFailureReason, paths.root));
2862
+ const refused = deliveryRefusalFailure(deliveryFailureReason, paths.root);
2863
+ writeFailure(store, paths, {
2864
+ ...refused,
2865
+ safeMessage: withPoolAttempts(refused.safeMessage, pool),
2866
+ poolAttempts: pool,
2867
+ });
2817
2868
  }
2818
2869
  else if (finalVerifyFailed) {
2819
2870
  writeFailure(store, paths, {
@@ -2821,9 +2872,10 @@ export class Orchestrator {
2821
2872
  // RunFailure.category is a closed enum; "validation" is the honest
2822
2873
  // bucket (the winner failed re-validation on a fresh base).
2823
2874
  category: "validation",
2824
- safeMessage: finalVerify?.applied_cleanly === null
2875
+ safeMessage: withPoolAttempts(finalVerify?.applied_cleanly === null
2825
2876
  ? `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")}`,
2877
+ : `final verify failed: ${finalVerify?.reason ?? (finalVerify?.gates_passed === false ? "deterministic gates failed on the fresh verify tree" : "unknown")}`, pool),
2878
+ poolAttempts: pool,
2827
2879
  runDir: paths.root,
2828
2880
  nextActions: finalVerify?.applied_cleanly === null
2829
2881
  ? [
@@ -2855,11 +2907,12 @@ export class Orchestrator {
2855
2907
  : "internal",
2856
2908
  harnessId: winnerRun?.errored ? winnerRun.harnessId : undefined,
2857
2909
  attemptId: winnerRun?.errored ? winnerRun.attemptId : undefined,
2858
- safeMessage: needsHuman
2910
+ safeMessage: withPoolAttempts(needsHuman
2859
2911
  ? `review escalated to a human decision: ${result.decision.why_winner}`
2860
2912
  : needsDecisionTerminal
2861
2913
  ? `review blocked before apply: ${result.decision.why_winner}`
2862
- : result.decision.why_winner,
2914
+ : result.decision.why_winner, pool),
2915
+ poolAttempts: pool,
2863
2916
  rawDetailRef: winnerRun?.errored
2864
2917
  ? `attempts/${winnerRun.attemptId}/attempt.yaml`
2865
2918
  : undefined,
@@ -2879,7 +2932,7 @@ export class Orchestrator {
2879
2932
  ],
2880
2933
  });
2881
2934
  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`);
2935
+ 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
2936
  log.emit("output.ready", {
2884
2937
  kind: "summary",
2885
2938
  path: "final/summary.md",
@@ -2895,6 +2948,8 @@ export class Orchestrator {
2895
2948
  lifecycle: facts.lifecycle,
2896
2949
  facts,
2897
2950
  reason: facts.reason,
2951
+ // A work-state veto is still a block: it keeps the earlier refusals.
2952
+ ...(pool.length > 0 ? { pool_attempts: pool } : {}),
2898
2953
  ...resumableTerminal(store, paths, winnerRun ? [winnerRun] : runs, isFailureTerminal),
2899
2954
  });
2900
2955
  }
@@ -2904,6 +2959,7 @@ export class Orchestrator {
2904
2959
  log.emit("run.blocked", {
2905
2960
  lifecycle: facts.lifecycle,
2906
2961
  facts,
2962
+ pool_attempts: pool,
2907
2963
  ...resumableTerminal(store, paths, runs, isFailureTerminal),
2908
2964
  phase: deliveryFailureReason && !finalVerifyFailed
2909
2965
  ? "delivery"
@@ -2919,6 +2975,7 @@ export class Orchestrator {
2919
2975
  facts,
2920
2976
  reason: facts.reason,
2921
2977
  phase: "arbitration",
2978
+ pool_attempts: pool,
2922
2979
  failure_ref: "final/failure.yaml",
2923
2980
  ...resumableTerminal(store, paths, runs, isFailureTerminal),
2924
2981
  });
@@ -2931,7 +2988,7 @@ export class Orchestrator {
2931
2988
  facts,
2932
2989
  winner: result.decision.winner,
2933
2990
  runDir: paths.root,
2934
- summary: result.decision.why_winner,
2991
+ summary: withPoolAttempts(result.decision.why_winner, pool),
2935
2992
  candidates: candidateStatuses(runs),
2936
2993
  decisionPath,
2937
2994
  reviewVerified: actualReviewVerified,
@@ -3009,7 +3066,7 @@ export class Orchestrator {
3009
3066
  const contract = this.buildContract(input, taskId, mode);
3010
3067
  const planBrief = verifiedPlanBrief(input);
3011
3068
  const quotaSnapshots = this.quotaSnapshotPreflight();
3012
- const { store, paths, log, ledger } = beginAnnouncedRun({
3069
+ const { store, paths, log, ledger, runContext } = beginAnnouncedRun({
3013
3070
  input,
3014
3071
  contract,
3015
3072
  quotaSnapshots,
@@ -3146,6 +3203,8 @@ export class Orchestrator {
3146
3203
  let interrupted = false; // D-16 r8: terminalizes the run interrupted
3147
3204
  let lastFindings = [];
3148
3205
  let lastRun = null;
3206
+ const convergenceRuns = [];
3207
+ runContext.attempts = () => convergenceRuns;
3149
3208
  let actualReviewVerified = false;
3150
3209
  let lastFinalReviewClean = false;
3151
3210
  // Honest apply-state for in-place convergence: the attempts mutate the LIVE
@@ -3235,6 +3294,7 @@ export class Orchestrator {
3235
3294
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
3236
3295
  let run;
3237
3296
  let harnessHome;
3297
+ const tried = new PoolAttemptEvidence(adapter.id, attemptId);
3238
3298
  try {
3239
3299
  harnessHome = this.harnessHomeFor(wsm, envelope, routed, input);
3240
3300
  log.emit("harness.started", {
@@ -3253,7 +3313,7 @@ export class Orchestrator {
3253
3313
  ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
3254
3314
  return ledger.tier() === "hard";
3255
3315
  }, input, true, // convergence runs one candidate: live deltas on (W-C4)
3256
- undefined, undefined, harnessHome, observedPaths);
3316
+ undefined, undefined, harnessHome, observedPaths, tried);
3257
3317
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
3258
3318
  log.emit("harness.completed", {
3259
3319
  harness_id: adapter.id,
@@ -3290,6 +3350,7 @@ export class Orchestrator {
3290
3350
  errored: true,
3291
3351
  costEstimated: failureCost.estimated,
3292
3352
  errors: [message],
3353
+ poolAttempts: tried.snapshot(),
3293
3354
  ...(declared.code ? { declaredFailure: declared } : {}),
3294
3355
  telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model),
3295
3356
  };
@@ -3305,6 +3366,7 @@ export class Orchestrator {
3305
3366
  }
3306
3367
  }
3307
3368
  lastRun = run;
3369
+ convergenceRuns.push(run);
3308
3370
  // Fail-closed twin of the candidate lane's gate: this loop terminalizes
3309
3371
  // per attempt, so the proof is spent per attempt.
3310
3372
  assertDelegatedEvidence(input.delegated === true, convergenceAccess, [run]);
@@ -3324,34 +3386,28 @@ export class Orchestrator {
3324
3386
  // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3325
3387
  if (input.signal?.aborted || processingBudgetDenial)
3326
3388
  break;
3389
+ // When this attempt's capture failed (and no Delegate failure leads), a
3390
+ // budget terminal the ledger settled on it (stream hard cap, exact overshoot,
3391
+ // unverifiable cash) outranks that capture failure and its typed refusal
3392
+ // (race parity): the budget-aware finalizer below names the budget cause and
3393
+ // remedy first, with the capture facts, source and inspection actions after.
3394
+ if (delegateFailure.candidateFailureKind(run) === "capture" && ledger.terminal()) {
3395
+ exhausted = true;
3396
+ break;
3397
+ }
3327
3398
  if (run.telemetry.requestRefusal ||
3328
3399
  run.telemetry.effortResolution?.resolution === "rejected")
3329
3400
  break;
3330
3401
  if (delegateFailure.candidateFailureKind(run)) {
3331
3402
  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
- }
3403
+ await delegateFailure.persistFailedCandidateOutput({
3404
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode, run },
3405
+ live: input.inPlace === true && input.workspaceKind !== "directory",
3406
+ kind: input.create === true ? "new_repo" : "patch",
3407
+ attempts: attempt,
3408
+ });
3353
3409
  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);
3410
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), { ...failure.metadata, attempts: convergenceRuns });
3355
3411
  }
3356
3412
  if (input.workspaceKind === "directory" && run.captureRefusal) {
3357
3413
  return failedCandidatesResult({
@@ -3363,6 +3419,7 @@ export class Orchestrator {
3363
3419
  runId,
3364
3420
  taskId,
3365
3421
  runs: [run],
3422
+ attempts: convergenceRuns,
3366
3423
  writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null),
3367
3424
  });
3368
3425
  }
@@ -3422,7 +3479,17 @@ export class Orchestrator {
3422
3479
  facts: makeOutcomeFacts("failed", { noChanges: run.files.noChanges }),
3423
3480
  log,
3424
3481
  });
3425
- return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs: [run] }, "review", err);
3482
+ return failAfterCandidates({
3483
+ log,
3484
+ store,
3485
+ paths,
3486
+ runId,
3487
+ taskId,
3488
+ mode,
3489
+ ledger,
3490
+ runs: [run],
3491
+ attempts: convergenceRuns,
3492
+ }, "review", err);
3426
3493
  }
3427
3494
  if (conv.converged) {
3428
3495
  converged = true;
@@ -3541,11 +3608,12 @@ export class Orchestrator {
3541
3608
  facts = decision.facts;
3542
3609
  }
3543
3610
  }
3544
- if (lastRun?.files)
3611
+ // Captured files decide noChanges; an incomplete capture leaves it unknown.
3612
+ if (lastRun?.files || lastRun?.captureRefusal)
3545
3613
  facts = {
3546
3614
  ...facts,
3547
- noChanges: lastRun.files.noChanges,
3548
- reason: facts.reason === "no_changes" && lastRun.files.noChanges !== true ? null : facts.reason,
3615
+ noChanges: lastRun.files?.noChanges ?? null,
3616
+ reason: facts.reason === "no_changes" && lastRun.files?.noChanges !== true ? null : facts.reason,
3549
3617
  };
3550
3618
  // A budget terminal turns a succeeded lifecycle into a failed one (D8).
3551
3619
  const convBudgetTerminal = ledger.terminal();
@@ -3556,6 +3624,19 @@ export class Orchestrator {
3556
3624
  review_requested: contract.review_requested,
3557
3625
  });
3558
3626
  }
3627
+ // The shared classifier owns a budget terminal's cause and remedy: a
3628
+ // processing re-admission denial, or the ledger's settled terminal once it
3629
+ // stopped the loop over an incomplete capture (the capture facts and
3630
+ // inspection actions follow it in the failure record).
3631
+ const convBudgetMapping = processingBudgetDenial ||
3632
+ (convBudgetTerminal && lastRun?.captureRefusal && isBudgetTerminal(facts.reason))
3633
+ ? classifyBudgetFailure({ denial: processingBudgetDenial, terminal: convBudgetTerminal })
3634
+ : null;
3635
+ if (convBudgetMapping &&
3636
+ facts.lifecycle === "failed" &&
3637
+ isBudgetTerminal(facts.reason) &&
3638
+ facts.reason !== convBudgetMapping.reason)
3639
+ facts = { ...facts, reason: convBudgetMapping.reason };
3559
3640
  // A reviewer escalation to a human forces the REVIEW axis to blocked.
3560
3641
  const needsHuman = lastFindings.some((f) => f.severity === "NEEDS_HUMAN" && isBlocking(f));
3561
3642
  if (needsHuman && facts.lifecycle === "succeeded" && facts.review !== "blocked") {
@@ -3579,6 +3660,8 @@ export class Orchestrator {
3579
3660
  facts = { ...facts, checks: "failed", reason: "checks_failed" };
3580
3661
  }
3581
3662
  const convNeedsDecision = facts.review === "blocked" || facts.checks === "failed";
3663
+ // Every attempt's failed tries by the actual outcome; a block is not an account failure.
3664
+ const convPool = terminalPoolAttempts(facts, convergenceRuns);
3582
3665
  if (decision) {
3583
3666
  // Shared honesty owner (same as the race path): a needs-decision terminal
3584
3667
  // overrides the persisted decision; final_verify is recorded either way.
@@ -3600,16 +3683,13 @@ export class Orchestrator {
3600
3683
  reviewAllowsApply(facts) &&
3601
3684
  facts.checks !== "failed" &&
3602
3685
  !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,
3686
+ if (lastRun.files || lastRun.captureRefusal) {
3687
+ // An incomplete capture has no exact patch: never publish an empty one.
3688
+ await delegateFailure.persistFailedCandidateOutput({
3689
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode, facts, run: lastRun },
3690
+ live: input.inPlace === true && input.workspaceKind !== "directory",
3691
+ kind: input.create === true ? "new_repo" : "patch",
3692
+ attempts: attempt,
3613
3693
  });
3614
3694
  }
3615
3695
  else {
@@ -3656,7 +3736,7 @@ export class Orchestrator {
3656
3736
  store.writeText(join(paths.finalDir, "answer.md"), lastRun.answerText);
3657
3737
  log.emit("output.ready", { kind: "answer", path: "final/answer.md" });
3658
3738
  }
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`);
3739
+ 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
3740
  // Lifecycle invariant (all modes): output.ready precedes the terminal
3661
3741
  // event so a client that applied the terminal event has the output.
3662
3742
  log.emit("output.ready", {
@@ -3675,9 +3755,7 @@ export class Orchestrator {
3675
3755
  const convDeclared = !convNeedsDecision && facts.lifecycle === "failed" && !isBudgetTerminal(facts.reason)
3676
3756
  ? lastRun?.declaredFailure
3677
3757
  : undefined;
3678
- const processingBudgetMapping = processingBudgetDenial
3679
- ? classifyBudgetFailure({ denial: processingBudgetDenial, terminal: ledger.terminal() })
3680
- : null;
3758
+ const inspect = secretDiff.captureInspectionActions(lastRun?.captureRefusal);
3681
3759
  writeFailure(store, paths, {
3682
3760
  phase: convNeedsDecision ? "review" : "convergence",
3683
3761
  category: isBudgetTerminal(facts.reason)
@@ -3687,50 +3765,58 @@ export class Orchestrator {
3687
3765
  : convNeedsDecision
3688
3766
  ? "policy"
3689
3767
  : (convDeclared?.category ?? "internal"),
3690
- code: processingBudgetMapping?.code ?? convDeclared?.code ?? null,
3768
+ code: convBudgetMapping?.code ?? convDeclared?.code ?? null,
3691
3769
  resetsAt: convDeclared?.resetsAt ?? null,
3692
3770
  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
3771
  harnessId: lastRun?.harnessId,
3699
3772
  attemptId: lastRun?.attemptId,
3700
3773
  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"
3774
+ captureFailure: lastRun?.captureRefusal?.failure,
3775
+ nextActions: [
3776
+ ...(facts.lifecycle === "cancelled"
3777
+ ? facts.reason === "wall_clock_exceeded"
3714
3778
  ? [
3715
- "Inspect the stable patch",
3716
- "Inspect the failing gate output",
3717
- "Fix the gate or provide a different repair instruction",
3779
+ "Inspect the partial work kept from before the deadline",
3780
+ "Increase --max-seconds or narrow the scope, then re-run",
3718
3781
  ]
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 })
3782
+ : ["Retry if cancellation was accidental"]
3783
+ : convNeedsDecision
3784
+ ? [
3785
+ "Review the blocking findings on the run's turn",
3786
+ "Accept the risk to apply this exact patch, or discard the change",
3787
+ ]
3788
+ : facts.reason === "stuck_no_progress"
3789
+ ? [
3790
+ "Inspect the stable patch",
3791
+ "Inspect the failing gate output",
3792
+ "Fix the gate or provide a different repair instruction",
3793
+ ]
3794
+ : lastRun?.captureRefusal // no latest patch exists to inspect
3795
+ ? secretDiff.captureRefusalNextActions(lastRun.captureRefusal)
3796
+ : [
3797
+ "Open diagnostics",
3798
+ "Inspect latest patch and review findings",
3799
+ "Retry with more attempts or a narrower prompt",
3800
+ ]),
3801
+ ...inspect,
3802
+ ],
3803
+ ...(convBudgetMapping
3804
+ ? budgetFailureRecord(convBudgetMapping, { runDir: paths.root, followUps: inspect })
3726
3805
  : {}),
3806
+ safeMessage: withPoolAttempts(convBudgetMapping?.safeMessage ??
3807
+ (convNeedsDecision
3808
+ ? `review escalated to a human decision after ${attempt} attempt(s)`
3809
+ : facts.reason === "stuck_no_progress"
3810
+ ? (stuckNoProgressReason ?? `stuck_no_progress after ${attempt} attempt(s)`)
3811
+ : `${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""} after ${attempt} attempt(s)${lastDiffStable ? "" : " (diff changed after review; review is stale)"}`), convPool),
3812
+ poolAttempts: convPool,
3727
3813
  });
3728
3814
  // D-16 r8/r9: an INTERRUPTED envelope run still gets its diagnostic
3729
3815
  // summary + output.ready (only patch/work_product are withheld) — the
3730
3816
  // ARCHITECTURE event contract guarantees output.ready precedes the
3731
3817
  // terminal in every mode.
3732
3818
  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`);
3819
+ 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
3820
  log.emit("output.ready", {
3735
3821
  kind: "summary",
3736
3822
  path: "final/summary.md",
@@ -3738,8 +3824,9 @@ export class Orchestrator {
3738
3824
  });
3739
3825
  }
3740
3826
  }
3741
- // work_product.emitted only when a product was actually written (r9).
3742
- if (lastRun && (lastRun.files || !interrupted || input.inPlace === true)) {
3827
+ // work_product.emitted only when a product was actually written (r9); a
3828
+ // capture refusal's live record announces itself.
3829
+ if (lastRun && !lastRun.captureRefusal && (lastRun.files || !interrupted || input.inPlace)) {
3743
3830
  log.emit("work_product.emitted", { winner: lastRun.attemptId });
3744
3831
  }
3745
3832
  if (!convIsFailureTerminal) {
@@ -3749,6 +3836,8 @@ export class Orchestrator {
3749
3836
  facts,
3750
3837
  reason: facts.reason,
3751
3838
  attempts: attempt,
3839
+ // A work-state veto is still a block: it keeps the earlier refusals.
3840
+ ...(convPool.length > 0 ? { pool_attempts: convPool } : {}),
3752
3841
  });
3753
3842
  }
3754
3843
  else if (facts.lifecycle === "succeeded") {
@@ -3758,6 +3847,7 @@ export class Orchestrator {
3758
3847
  facts,
3759
3848
  attempts: attempt,
3760
3849
  phase: "review",
3850
+ pool_attempts: convPool,
3761
3851
  failure_ref: "final/failure.yaml",
3762
3852
  });
3763
3853
  }
@@ -3769,6 +3859,7 @@ export class Orchestrator {
3769
3859
  reason: facts.reason,
3770
3860
  attempts: attempt,
3771
3861
  phase: "convergence",
3862
+ pool_attempts: convPool,
3772
3863
  failure_ref: "final/failure.yaml",
3773
3864
  // QA-041: surface the typed deadline reason on the terminal event so the
3774
3865
  // daemon job result / Control API can distinguish it from a user cancel.
@@ -3791,9 +3882,9 @@ export class Orchestrator {
3791
3882
  ...(facts.lifecycle === "cancelled" && convAbortReason
3792
3883
  ? { cancelReason: convAbortReason }
3793
3884
  : {}),
3794
- summary: converged
3885
+ summary: withPoolAttempts(converged
3795
3886
  ? `converged in ${attempt} attempt(s)`
3796
- : `${facts.lifecycle} after ${attempt} attempt(s)`,
3887
+ : `${facts.lifecycle} after ${attempt} attempt(s)`, convPool),
3797
3888
  candidates: lastRun
3798
3889
  ? [
3799
3890
  {
@@ -4227,7 +4318,7 @@ export class Orchestrator {
4227
4318
  // recorded user intent is the CALLER's goal.
4228
4319
  const contract = this.buildContract({ ...input, prompt: opts.contractIntent ?? prompt }, taskId, opts.mode);
4229
4320
  const quotaSnapshots = this.quotaSnapshotPreflight();
4230
- const { store, paths, log, ledger } = beginAnnouncedRun({
4321
+ const { store, paths, log, ledger, runContext } = beginAnnouncedRun({
4231
4322
  input,
4232
4323
  contract,
4233
4324
  quotaSnapshots,
@@ -4287,6 +4378,14 @@ export class Orchestrator {
4287
4378
  };
4288
4379
  }
4289
4380
  const attempts = [];
4381
+ // A launched attempt's tries' accounts, owned here until its record joins
4382
+ // `attempts` (as an Agent slot owns them): a throw after its tries settled
4383
+ // still names them on the run's terminal, and never twice.
4384
+ const launched = new Set();
4385
+ runContext.attempts = () => [
4386
+ ...attempts,
4387
+ ...[...launched].map((tried) => ({ poolAttempts: tried.snapshot() })),
4388
+ ];
4290
4389
  const attemptTelemetries = [];
4291
4390
  let fallbackOpen = false;
4292
4391
  let budgetStopped = false;
@@ -4498,12 +4597,15 @@ export class Orchestrator {
4498
4597
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4499
4598
  let activeSessionId = spec.session_id;
4500
4599
  const live = liveAttempt(input, routed, paths, contract, attemptId, () => activeSessionId);
4600
+ const tried = new PoolAttemptEvidence(adapter.id, attemptId);
4601
+ launched.add(tried);
4501
4602
  let continuity;
4502
4603
  ({ continuity, spec } = await openContinuity(this.continuityDeps(adapter, routed, contract, attemptId, paths, input, log, {
4503
4604
  envelope: null,
4504
4605
  harnessHome: null,
4505
4606
  workOrder: input.prompt,
4506
4607
  firstPrompt: spec.prompt,
4608
+ poolEvidence: tried,
4507
4609
  }), { spec, continuation: input.continuation, store, telemetry }));
4508
4610
  const onAbort = () => void adapter.cancel?.(activeSessionId)?.catch(() => { });
4509
4611
  if (input.signal) {
@@ -4538,9 +4640,8 @@ export class Orchestrator {
4538
4640
  runSpec.extra["abortSignal"] = input.signal
4539
4641
  ? AbortSignal.any([input.signal, tryAbort.signal])
4540
4642
  : tryAbort.signal;
4541
- continuity.beginTry(runSpec, nativeTry, tryAbort);
4542
4643
  // #363: bound before the spawn — a credential change mid-try voids it.
4543
- const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
4644
+ const refusal = continuity.beginTry(runSpec, nativeTry, tryAbort);
4544
4645
  const transientStart = telemetry.transientFailures.length;
4545
4646
  const rateLimitStart = telemetry.rateLimits.length;
4546
4647
  log.emit("harness.started", {
@@ -4615,8 +4716,6 @@ export class Orchestrator {
4615
4716
  if (!recordAdapterThrow(telemetry, err))
4616
4717
  break;
4617
4718
  }
4618
- if (streamBudgetDenied)
4619
- break;
4620
4719
  if (telemetry.requestRefusal)
4621
4720
  break;
4622
4721
  const newTransients = telemetry.transientFailures.slice(transientStart);
@@ -4625,10 +4724,14 @@ export class Orchestrator {
4625
4724
  const reportSoFar = answer.text();
4626
4725
  const delivered = !harnessError && reportSoFar.length > 0 && !input.signal?.aborted;
4627
4726
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
4727
+ // A budget stop ends the attempt; typed progress before it still served the account.
4728
+ if (streamBudgetDenied)
4729
+ break;
4628
4730
  const verdict = await continuity.afterTry({
4629
4731
  runSpec,
4630
4732
  nativeTry,
4631
4733
  harnessErrored: harnessError !== null,
4734
+ originalError: harnessError,
4632
4735
  aborted: input.signal?.aborted === true,
4633
4736
  requestRefused: Boolean(telemetry.requestRefusal),
4634
4737
  newTransients,
@@ -4746,6 +4849,7 @@ export class Orchestrator {
4746
4849
  : {}),
4747
4850
  ...continuity.terminalFacts(roResumable),
4748
4851
  });
4852
+ launched.delete(tried);
4749
4853
  if (opts.deepScan) {
4750
4854
  store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
4751
4855
  }
@@ -4771,6 +4875,7 @@ export class Orchestrator {
4771
4875
  ...(scoutInterrupted ? { interrupted: true } : {}),
4772
4876
  ...continuity.terminalFacts(roResumable),
4773
4877
  });
4878
+ launched.delete(tried);
4774
4879
  if (opts.deepScan) {
4775
4880
  const warningNote = toolWarnings(telemetry).length
4776
4881
  ? `\n\n> Tool warnings: ${toolWarnings(telemetry)
@@ -4931,188 +5036,20 @@ export class Orchestrator {
4931
5036
  harnessId: a.harnessId,
4932
5037
  status: a.status,
4933
5038
  }));
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"));
5039
+ 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
5040
  if (input.signal?.aborted)
4936
5041
  return cancelledTerminal();
4937
5042
  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
- }
5043
+ const readonlyTerminal = {
5044
+ ...{ store, paths, log, runId, taskId, mode: opts.mode, ledger, attempts },
5045
+ ...{ attemptTelemetries, budgetStopped, budgetDenial },
5046
+ writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null),
5047
+ };
5048
+ if (!opts.deepScan && succeededReadonly.length === 0)
5049
+ return readOnlyChainFailureResult({ ...readonlyTerminal, fallbackOpen });
5043
5050
  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
- }
5051
+ if (opts.deepScan && succeeded.length === 0)
5052
+ return deepScanFailureResult(readonlyTerminal);
5116
5053
  const unsuccessful = attempts.filter((a) => a.status !== "success");
5117
5054
  // #27 / D-6: a multi-scout deep scan runs ONE bounded synthesis reducer over
5118
5055
  // the raw scout reports so the final artifact is a real merge, not a
@@ -5216,7 +5153,9 @@ export class Orchestrator {
5216
5153
  .map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
5217
5154
  .join(", ");
5218
5155
  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`);
5156
+ // A delivered report that still ends unsuccessfully or blocked keeps it and names every failed try.
5157
+ const reportPool = terminalPoolAttempts(terminalFacts, attempts);
5158
+ 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
5159
  const reportProducerAttemptId = opts.deepScan &&
5221
5160
  deepScanSynthesis?.status === "succeeded" &&
5222
5161
  deepScanSynthesis.reducer_attempt_id
@@ -5243,7 +5182,8 @@ export class Orchestrator {
5243
5182
  writeFailure(store, paths, {
5244
5183
  phase: "executor",
5245
5184
  category: terminalFacts.reason === "context_capacity_exhausted" ? "harness_error" : "budget",
5246
- safeMessage: `read-only report ended ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason.replaceAll("_", " ")})` : ""}`,
5185
+ safeMessage: withPoolAttempts(`read-only report ended ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason.replaceAll("_", " ")})` : ""}`, reportPool),
5186
+ poolAttempts: reportPool,
5247
5187
  runDir: paths.root,
5248
5188
  nextActions: terminalFacts.reason === "context_capacity_exhausted"
5249
5189
  ? ["Inspect the partial report", "Re-run with a narrower scope"]
@@ -5254,6 +5194,7 @@ export class Orchestrator {
5254
5194
  facts: terminalFacts,
5255
5195
  reason: terminalFacts.reason,
5256
5196
  phase: "executor",
5197
+ pool_attempts: reportPool,
5257
5198
  failure_ref: "final/failure.yaml",
5258
5199
  ...resumableTerminal(store, paths, attempts, true),
5259
5200
  });
@@ -5263,6 +5204,7 @@ export class Orchestrator {
5263
5204
  lifecycle: terminalFacts.lifecycle,
5264
5205
  facts: terminalFacts,
5265
5206
  reason: terminalFacts.reason,
5207
+ pool_attempts: reportPool,
5266
5208
  ...resumableTerminal(store, paths, attempts, true),
5267
5209
  });
5268
5210
  }
@@ -5283,7 +5225,7 @@ export class Orchestrator {
5283
5225
  facts: terminalFacts,
5284
5226
  winner: null,
5285
5227
  runDir: paths.root,
5286
- summary: redactSecrets(report).slice(0, 400),
5228
+ summary: withPoolAttempts(redactSecrets(report).slice(0, 400), reportPool),
5287
5229
  candidates: candidateSummaries,
5288
5230
  };
5289
5231
  }