@claudexor/orchestrator 3.20.1 → 3.22.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 (180) hide show
  1. package/dist/attemptFinalize.d.ts +14 -101
  2. package/dist/attemptFinalize.d.ts.map +1 -1
  3. package/dist/attemptFinalize.js +7 -328
  4. package/dist/attemptFinalize.js.map +1 -1
  5. package/dist/candidateEvidence.d.ts +15 -6
  6. package/dist/candidateEvidence.d.ts.map +1 -1
  7. package/dist/candidateEvidence.js +1 -1
  8. package/dist/candidateEvidence.js.map +1 -1
  9. package/dist/candidateOutputs.d.ts +7 -3
  10. package/dist/candidateOutputs.d.ts.map +1 -1
  11. package/dist/candidateOutputs.js +4 -1
  12. package/dist/candidateOutputs.js.map +1 -1
  13. package/dist/candidateReview.d.ts.map +1 -1
  14. package/dist/candidateReview.js +3 -1
  15. package/dist/candidateReview.js.map +1 -1
  16. package/dist/candidateTerminals.d.ts +4 -0
  17. package/dist/candidateTerminals.d.ts.map +1 -1
  18. package/dist/candidateTerminals.js +18 -7
  19. package/dist/candidateTerminals.js.map +1 -1
  20. package/dist/candidateWorkProduct.d.ts.map +1 -1
  21. package/dist/candidateWorkProduct.js +6 -6
  22. package/dist/candidateWorkProduct.js.map +1 -1
  23. package/dist/carrier-planner.d.ts +94 -0
  24. package/dist/carrier-planner.d.ts.map +1 -0
  25. package/dist/carrier-planner.js +111 -0
  26. package/dist/carrier-planner.js.map +1 -0
  27. package/dist/continuation-custody.d.ts +57 -0
  28. package/dist/continuation-custody.d.ts.map +1 -0
  29. package/dist/continuation-custody.js +151 -0
  30. package/dist/continuation-custody.js.map +1 -0
  31. package/dist/continuation-evidence-io.d.ts +18 -0
  32. package/dist/continuation-evidence-io.d.ts.map +1 -0
  33. package/dist/continuation-evidence-io.js +167 -0
  34. package/dist/continuation-evidence-io.js.map +1 -0
  35. package/dist/continuation-evidence.d.ts +81 -0
  36. package/dist/continuation-evidence.d.ts.map +1 -0
  37. package/dist/continuation-evidence.js +122 -0
  38. package/dist/continuation-evidence.js.map +1 -0
  39. package/dist/continuation-input.d.ts +10 -0
  40. package/dist/continuation-input.d.ts.map +1 -0
  41. package/dist/continuation-input.js +69 -0
  42. package/dist/continuation-input.js.map +1 -0
  43. package/dist/continue-from.d.ts +41 -0
  44. package/dist/continue-from.d.ts.map +1 -0
  45. package/dist/continue-from.js +292 -0
  46. package/dist/continue-from.js.map +1 -0
  47. package/dist/continuity-notice.d.ts +42 -0
  48. package/dist/continuity-notice.d.ts.map +1 -0
  49. package/dist/continuity-notice.js +42 -0
  50. package/dist/continuity-notice.js.map +1 -0
  51. package/dist/continuity-terminal.d.ts +19 -0
  52. package/dist/continuity-terminal.d.ts.map +1 -0
  53. package/dist/continuity-terminal.js +34 -0
  54. package/dist/continuity-terminal.js.map +1 -0
  55. package/dist/credential-cooldown.d.ts.map +1 -1
  56. package/dist/credential-cooldown.js +8 -3
  57. package/dist/credential-cooldown.js.map +1 -1
  58. package/dist/credential-differential.d.ts +1 -0
  59. package/dist/credential-differential.d.ts.map +1 -1
  60. package/dist/credential-differential.js +32 -4
  61. package/dist/credential-differential.js.map +1 -1
  62. package/dist/deepScanReducer.d.ts +4 -1
  63. package/dist/deepScanReducer.d.ts.map +1 -1
  64. package/dist/deepScanReducer.js +10 -5
  65. package/dist/deepScanReducer.js.map +1 -1
  66. package/dist/delegationFailure.d.ts +4 -3
  67. package/dist/delegationFailure.d.ts.map +1 -1
  68. package/dist/delegationFailure.js +25 -24
  69. package/dist/delegationFailure.js.map +1 -1
  70. package/dist/directoryCandidate.d.ts +2 -2
  71. package/dist/directoryCandidate.d.ts.map +1 -1
  72. package/dist/directoryCandidate.js +3 -0
  73. package/dist/directoryCandidate.js.map +1 -1
  74. package/dist/inrun-continuity-carrier.d.ts +76 -0
  75. package/dist/inrun-continuity-carrier.d.ts.map +1 -0
  76. package/dist/inrun-continuity-carrier.js +218 -0
  77. package/dist/inrun-continuity-carrier.js.map +1 -0
  78. package/dist/inrun-continuity-thread.d.ts +8 -0
  79. package/dist/inrun-continuity-thread.d.ts.map +1 -0
  80. package/dist/inrun-continuity-thread.js +23 -0
  81. package/dist/inrun-continuity-thread.js.map +1 -0
  82. package/dist/inrun-continuity-types.d.ts +105 -0
  83. package/dist/inrun-continuity-types.d.ts.map +1 -0
  84. package/dist/inrun-continuity-types.js +2 -0
  85. package/dist/inrun-continuity-types.js.map +1 -0
  86. package/dist/inrun-continuity.d.ts +79 -0
  87. package/dist/inrun-continuity.d.ts.map +1 -0
  88. package/dist/inrun-continuity.js +465 -0
  89. package/dist/inrun-continuity.js.map +1 -0
  90. package/dist/inrun-continuity.test-support.d.ts +374 -0
  91. package/dist/inrun-continuity.test-support.d.ts.map +1 -0
  92. package/dist/inrun-continuity.test-support.js +198 -0
  93. package/dist/inrun-continuity.test-support.js.map +1 -0
  94. package/dist/inrun-retained-output.d.ts +9 -0
  95. package/dist/inrun-retained-output.d.ts.map +1 -0
  96. package/dist/inrun-retained-output.js +16 -0
  97. package/dist/inrun-retained-output.js.map +1 -0
  98. package/dist/interaction.d.ts +1 -1
  99. package/dist/interaction.d.ts.map +1 -1
  100. package/dist/interaction.js +6 -1
  101. package/dist/interaction.js.map +1 -1
  102. package/dist/modelGovernance.d.ts +4 -0
  103. package/dist/modelGovernance.d.ts.map +1 -1
  104. package/dist/modelGovernance.js +27 -1
  105. package/dist/modelGovernance.js.map +1 -1
  106. package/dist/orchestrator-credentials.d.ts +1 -1
  107. package/dist/orchestrator-credentials.d.ts.map +1 -1
  108. package/dist/orchestrator-credentials.js +4 -3
  109. package/dist/orchestrator-credentials.js.map +1 -1
  110. package/dist/orchestrator.d.ts +4 -4
  111. package/dist/orchestrator.d.ts.map +1 -1
  112. package/dist/orchestrator.js +252 -226
  113. package/dist/orchestrator.js.map +1 -1
  114. package/dist/persistedPatch.d.ts +55 -0
  115. package/dist/persistedPatch.d.ts.map +1 -0
  116. package/dist/persistedPatch.js +132 -0
  117. package/dist/persistedPatch.js.map +1 -0
  118. package/dist/planFinalize.d.ts +37 -0
  119. package/dist/planFinalize.d.ts.map +1 -0
  120. package/dist/planFinalize.js +94 -0
  121. package/dist/planFinalize.js.map +1 -0
  122. package/dist/planRun.d.ts +2 -28
  123. package/dist/planRun.d.ts.map +1 -1
  124. package/dist/planRun.js +5 -88
  125. package/dist/planRun.js.map +1 -1
  126. package/dist/plannerAttempt.d.ts +4 -1
  127. package/dist/plannerAttempt.d.ts.map +1 -1
  128. package/dist/plannerAttempt.js +9 -4
  129. package/dist/plannerAttempt.js.map +1 -1
  130. package/dist/prompt-framing.d.ts +1 -1
  131. package/dist/prompt-framing.d.ts.map +1 -1
  132. package/dist/prompt-framing.js.map +1 -1
  133. package/dist/reviewerEffort.d.ts +41 -0
  134. package/dist/reviewerEffort.d.ts.map +1 -0
  135. package/dist/reviewerEffort.js +37 -0
  136. package/dist/reviewerEffort.js.map +1 -0
  137. package/dist/reviewerPanel.d.ts +5 -5
  138. package/dist/reviewerPanel.d.ts.map +1 -1
  139. package/dist/reviewerPanel.js +22 -50
  140. package/dist/reviewerPanel.js.map +1 -1
  141. package/dist/run-input-resolution.d.ts.map +1 -1
  142. package/dist/run-input-resolution.js +10 -0
  143. package/dist/run-input-resolution.js.map +1 -1
  144. package/dist/runEventLog.d.ts.map +1 -1
  145. package/dist/runEventLog.js +5 -0
  146. package/dist/runEventLog.js.map +1 -1
  147. package/dist/runFactsBuilder.d.ts.map +1 -1
  148. package/dist/runFactsBuilder.js +11 -2
  149. package/dist/runFactsBuilder.js.map +1 -1
  150. package/dist/runSupport.d.ts +2 -1
  151. package/dist/runSupport.d.ts.map +1 -1
  152. package/dist/runSupport.js +2 -1
  153. package/dist/runSupport.js.map +1 -1
  154. package/dist/runTelemetryWriter.d.ts.map +1 -1
  155. package/dist/runTelemetryWriter.js +7 -2
  156. package/dist/runTelemetryWriter.js.map +1 -1
  157. package/dist/runTerminalResults.d.ts +6 -2
  158. package/dist/runTerminalResults.d.ts.map +1 -1
  159. package/dist/runTerminalResults.js +8 -1
  160. package/dist/runTerminalResults.js.map +1 -1
  161. package/dist/runTerminals.d.ts.map +1 -1
  162. package/dist/runTerminals.js +6 -5
  163. package/dist/runTerminals.js.map +1 -1
  164. package/dist/secretDiff.d.ts +67 -27
  165. package/dist/secretDiff.d.ts.map +1 -1
  166. package/dist/secretDiff.js +143 -113
  167. package/dist/secretDiff.js.map +1 -1
  168. package/dist/session-capsule.d.ts +29 -0
  169. package/dist/session-capsule.d.ts.map +1 -0
  170. package/dist/session-capsule.js +86 -0
  171. package/dist/session-capsule.js.map +1 -0
  172. package/dist/workReportEnvelope.d.ts +123 -0
  173. package/dist/workReportEnvelope.d.ts.map +1 -0
  174. package/dist/workReportEnvelope.js +362 -0
  175. package/dist/workReportEnvelope.js.map +1 -0
  176. package/package.json +17 -17
  177. package/dist/effortGovernance.d.ts +0 -13
  178. package/dist/effortGovernance.d.ts.map +0 -1
  179. package/dist/effortGovernance.js +0 -7
  180. package/dist/effortGovernance.js.map +0 -1
@@ -2,10 +2,12 @@ import { applyWorkEnvelope, promptWithPointers as renderPromptWithPointers, thre
2
2
  import { requestRefusalFailure } from "./runTerminalResults.js";
3
3
  import { delegationBeltFor } from "./delegationBelt.js";
4
4
  import { bindProcessingAdmission, processingAttemptCostEvidence, processingAdmissionForLease, updateProcessingStreamHold, ProcessingBudgetAdmissionError, } from "./processing-dispatch.js";
5
- import { cancelledCandidatesResult, emptyCandidateResult, failedCandidatesResult, } from "./candidateTerminals.js";
5
+ import { cancelledCandidatesResult, emptyCandidateResult, failedCandidatesResult, failAfterCandidates, } from "./candidateTerminals.js";
6
6
  import { existsSync } from "node:fs";
7
- import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, rotateSpecOnTypedLimit, } from "./credential-profiles.js";
8
- import { AdmissionProfileProbes, OrchestratorCredentials, reviewerProfileResolver, rotatedSpecInLaneHome, } from "./orchestrator-credentials.js";
7
+ import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, } from "./credential-profiles.js";
8
+ import { AdmissionProfileProbes, OrchestratorCredentials, reviewerProfileResolver, } from "./orchestrator-credentials.js";
9
+ import { continuityWorkspace, resumableOf, resumableTerminal, } from "./continuity-terminal.js";
10
+ import { openContinuity } from "./continue-from.js";
9
11
  import { accountPoolRows } from "./account-pool.js";
10
12
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
11
13
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
@@ -13,6 +15,7 @@ import { processAttemptUsage } from "./attemptUsage.js";
13
15
  import { captureDirectoryCandidate, directoryHasOutput, directoryCandidateStable, observeDirectoryPaths, publishDirectoryCandidate, } from "./directoryCandidate.js";
14
16
  import { appliedAttemptFacts, assertDelegatedEvidence, isMutatingAccess, outerBoundaryNotice, scopedHarnessHome, } from "./delegatedHome.js";
15
17
  import * as AC from "./attemptUsageCost.js";
18
+ import * as custody from "./continuation-custody.js";
16
19
  import { candidateRoster, candidateStatuses, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, unanimousDeclaredFailure, } from "./candidateEvidence.js";
17
20
  import { capabilityIntents } from "@claudexor/gateway";
18
21
  import { reviewCandidateRuns, resolveEngineReview, unreviewedCandidateEvidence, evaluateUnreviewedConvergence, } from "./candidateReview.js";
@@ -22,10 +25,9 @@ import { runPlannerAttempt as executePlannerAttempt, plannerAttemptSummary, } fr
22
25
  import { HarnessRunSpec, MAX_COUNCIL_MEMBERS_DEFAULT, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, reviewAllowsApply, workStateVetoes, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
23
26
  import { globalConfigDir, loadConfig } from "@claudexor/config";
24
27
  import { prepareRoutedProcessing, processingCostEvidence, prepareReviewerProcessing, } from "./processing-routing.js";
25
- import { acceptedTryOutput, AccessProfileIncompatibleError, AnswerAssembly, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
28
+ import { acceptedTryOutput, AccessProfileIncompatibleError, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
26
29
  import { assertRouteModelsAllowed, runModelGovernedRoute } from "./modelGovernance.js";
27
30
  import { authModeForCredentialRoute, authModeForPreference, authRouteEvidenceFor, } from "./auth-route-classification.js";
28
- import { governRouteEffort } from "./effortGovernance.js";
29
31
  import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
30
32
  import { activateDelegationParent } from "./delegation-parent-activation.js";
31
33
  import { writeRoutingFailureTerminal } from "./routing-failure.js";
@@ -41,7 +43,7 @@ import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, wr
41
43
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
42
44
  import { finalizeStructuredOutput } from "./structuredOutput.js";
43
45
  import { admitRun } from "./runAdmission.js";
44
- import { dropDeltaPastBudget, emitPlanProgress, emitTransientExhausted, emitTransientRetryPlan, observeReadonlySpend, recordAdapterThrow, } from "./laneStreamEvents.js";
46
+ import { dropDeltaPastBudget, emitPlanProgress, emitTransientExhausted, observeReadonlySpend, recordAdapterThrow, } from "./laneStreamEvents.js";
45
47
  import { promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, winnerNeedsHuman, writeRaceDeliveryDecision, } from "./runSupport.js";
46
48
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
47
49
  import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
@@ -55,7 +57,8 @@ import { newAttemptOutputMarkers } from "./attemptOutputMarkers.js";
55
57
  import * as delegateFailure from "./delegationFailure.js";
56
58
  import * as secretDiff from "./secretDiff.js";
57
59
  import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
58
- import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
60
+ import { finalizeAttempt, readOnlyNoSuccessTerminal, unrecoveredToolErrorFailure, webEvidenceFailure, } from "./attemptFinalize.js";
61
+ import { resolveWorkReportEnvelope, unwrapWorkReportEnvelope, } from "./workReportEnvelope.js";
59
62
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
60
63
  import { interactionChannelFor } from "./interaction.js";
61
64
  import { liveAttempt } from "./live-input.js";
@@ -73,8 +76,6 @@ import { billingKnowledgeForAuthRoute, attemptUsageCostSettlement, BudgetLedger,
73
76
  import { readTextSafe, appendLine, assertNoInlineSecretValues, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, } from "@claudexor/util";
74
77
  const LABELS = "ABCDEFGHIJ".split("");
75
78
  const NO_PROJECT_ROOT = noProjectRepoRoot();
76
- /** Default wait for one interactive answer before a benign decline. */
77
- const DEFAULT_INTERACTION_TIMEOUT_MS = 900_000;
78
79
  export class Orchestrator {
79
80
  deps;
80
81
  gateway;
@@ -210,7 +211,7 @@ export class Orchestrator {
210
211
  if (this.deps.reviewers)
211
212
  return this.deps.reviewers;
212
213
  if (this.deps.reviewerPanel && this.deps.reviewerPanel.length > 0) {
213
- return this.resolveExplicitReviewerPanel(cwd, this.deps.reviewerPanel, runAuthPreference);
214
+ return this.resolveExplicitReviewerPanel(cwd, this.deps.reviewerPanel, runAuthPreference, onIgnoredSetting);
214
215
  }
215
216
  return resolveAutoReviewerPanel({
216
217
  cwd,
@@ -232,7 +233,7 @@ export class Orchestrator {
232
233
  if (input.review === false)
233
234
  return { reviewers: [] };
234
235
  try {
235
- // Auto-panel dropped knobs (reviewerEfforts) → ignored-settings channel (QA-070):
236
+ // Reviewer knobs a panel omitted or dropped → ignored-settings channel (QA-070):
236
237
  const warn = (d) => void log.emit("review.preflight", { ignored_settings: [d] });
237
238
  const config = this.config(input.repoRoot).global;
238
239
  const reviewers = await this.resolveReviewers(input.repoRoot, input.authPreference, warn);
@@ -274,13 +275,14 @@ export class Orchestrator {
274
275
  };
275
276
  }
276
277
  }
277
- async resolveExplicitReviewerPanel(cwd, panel, runAuthPreference) {
278
+ async resolveExplicitReviewerPanel(cwd, panel, runAuthPreference, onIgnoredSetting) {
278
279
  return resolveExplicitReviewerPanel({
279
280
  cwd,
280
281
  registry: this.deps.registry,
281
282
  harnessSettings: this.config(cwd)?.global.harnesses ?? {},
282
283
  authPreferenceFor: (id) => this.authPreferenceForHarness(cwd, id, runAuthPreference),
283
284
  resolveReviewerProfile: reviewerProfileResolver(this.credentials, cwd),
285
+ onIgnoredSetting,
284
286
  }, panel);
285
287
  }
286
288
  authPreferenceForHarness(repoRoot, harnessId, runAuthPreference) {
@@ -659,7 +661,6 @@ export class Orchestrator {
659
661
  requiresFullAccess: manifest.capability_profile.mcp_injection_requires_full_access,
660
662
  fullAccess: isFullAccess(requiredAccess),
661
663
  }),
662
- effortLevels: manifest.capabilities.effort_levels,
663
664
  knownModels: manifest.capabilities.known_models,
664
665
  modelInventory: manifest.capabilities,
665
666
  // A selected profile's credential_kind IS the route (round-18 #2);
@@ -819,7 +820,7 @@ export class Orchestrator {
819
820
  // settles clean), so a schema run keeps its interaction channel — since
820
821
  // the daemon always arms one, the refusal was denying claude every
821
822
  // daemon/CLI structured-output run. The WorkReport side_tool envelope
822
- // stays interactive-gated separately (attemptFinalize) as a deliberate
823
+ // stays interactive-gated separately (workReportEnvelope) as a deliberate
823
824
  // scope choice; the caller schema itself rides regardless.
824
825
  if (input.outputSchema !== undefined && input.outputSchema !== null) {
825
826
  const incapable = out.filter((lane) => !lane.supportsJsonSchemaOutput);
@@ -1141,16 +1142,14 @@ export class Orchestrator {
1141
1142
  // per-harness map, which beats the per-harness settings default. There is
1142
1143
  // no run-global model.
1143
1144
  const model = overrideModel ?? contract.routing_models[routed.adapter.id] ?? s?.defaultModel ?? null;
1144
- // Effort disclosure (INV-105) against the harness's advertised ladder. This
1145
- // gate only DISCLOSES an unplaceable level; the clamp belongs to the adapter,
1146
- // which resolves against the catalog for the profile env the child runs in
1147
- // (the manifest here is the DEFAULT account's — see effortGovernance.ts). The
1148
- // contract's FROZEN per-lane effort (QA-035) wins so Exact Retry replays it
1149
- // without re-reading settings; `effortHint`/settings apply only to an unfrozen lane.
1150
- const governed = governRouteEffort(contract.routing_efforts[routed.adapter.id] ?? effortHint ?? s?.effort ?? null, { id: routed.adapter.id, ...routed });
1151
- const effort = governed.effort;
1152
- if (governed.ignored)
1153
- ignored.push(governed.ignored);
1145
+ // The effort PREFERENCE travels to the route unchanged (INV-105): discovery
1146
+ // describes the DEFAULT account, so only the final route may resolve it — a
1147
+ // knob adapter against the catalog of the profile env its child runs in, the
1148
+ // engine as `omitted` for a route without a knob — and that one resolution is
1149
+ // what the receipt and the disclosure record. The contract's FROZEN per-lane
1150
+ // effort (QA-035) wins so Exact Retry replays it without re-reading settings;
1151
+ // `effortHint`/settings apply only to an unfrozen lane.
1152
+ const effort = contract.routing_efforts[routed.adapter.id] ?? effortHint ?? s?.effort ?? null;
1154
1153
  return {
1155
1154
  model,
1156
1155
  effort,
@@ -1385,17 +1384,17 @@ export class Orchestrator {
1385
1384
  let applied = appliedNow();
1386
1385
  const attemptStartedMs = Date.now();
1387
1386
  const budgetSignalState = { quotaPressureDisclosed: false };
1388
- const triedProfiles = new Set(); // W5.4 failover: each profile at most once
1389
1387
  let cost = 0;
1390
1388
  let costEstimated = false;
1391
1389
  let harnessErrored = false;
1390
+ let streamBudgetDenied = false;
1392
1391
  let processingRefusal = null;
1393
1392
  let poolExhausted = null; // A5: typed pool-exhausted refusal
1394
1393
  const deltaFlood = { count: 0, disclosed: false }; // W-C4 per-attempt delta budget
1395
1394
  // QA-024: emit the belt-failure disclosure event at most once per attempt.
1396
1395
  let beltFailureDisclosed = false;
1397
1396
  const errors = [];
1398
- let answer = new AnswerAssembly();
1397
+ let answer = new secretDiff.CountedAnswerAssembly();
1399
1398
  const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
1400
1399
  // QA-024: the delegation belt is the ONLY engine-owned extra MCP server
1401
1400
  // injected into an agent lane (the browser MCP rides its own field), so its
@@ -1416,28 +1415,31 @@ export class Orchestrator {
1416
1415
  onAbort();
1417
1416
  else
1418
1417
  signal?.addEventListener("abort", onAbort, { once: true });
1418
+ let continuity;
1419
+ ({ continuity, spec } = await openContinuity(this.continuityDeps(adapter, routed, contract, attemptId, paths, runInput, log, {
1420
+ envelope,
1421
+ harnessHome,
1422
+ workOrder: prompt,
1423
+ firstPrompt: spec.prompt,
1424
+ }), { spec, continuation: runInput?.continuation, store, telemetry }));
1419
1425
  try {
1420
1426
  for (let nativeTry = 0; !signal?.aborted; nativeTry += 1) {
1421
1427
  // A3 per-try isolation: neither output nor progress markers leak across tries.
1422
1428
  if (nativeTry > 0) {
1423
- answer = new AnswerAssembly();
1429
+ answer = new secretDiff.CountedAnswerAssembly();
1424
1430
  telemetry.outputMarkers = newAttemptOutputMarkers();
1425
1431
  }
1426
1432
  const clearFileBackedContext = stageFileBackedContext(envelope.worktree_path, fileBackedContext);
1427
1433
  const runSpec = nativeTry === 0
1428
1434
  ? spec
1429
1435
  : HarnessRunSpec.parse({ ...spec, session_id: newId("ses"), extra: { ...spec.extra } });
1430
- // Per-TRY abort controller: the inactivity watchdog aborts THIS try's
1431
- // stream (killing the process group through the existing abort
1432
- // plumbing) without touching the run-level cancel signal, so a timeout
1433
- // and a user cancel stay distinguishable. Recreated per
1434
- // nativeTry — a transient retry must get a LIVE signal, not the
1435
- // previous try's already-aborted composite.
1436
+ // The watchdog aborts only this try; run cancellation also prevents successors.
1436
1437
  const attemptAbort = new AbortController();
1437
1438
  runSpec.extra["abortSignal"] = signal
1438
1439
  ? AbortSignal.any([signal, attemptAbort.signal])
1439
1440
  : attemptAbort.signal;
1440
1441
  activeSessionId = runSpec.session_id;
1442
+ continuity.beginTry(runSpec, nativeTry, attemptAbort);
1441
1443
  // #363: bound before the spawn — a credential change mid-try voids it.
1442
1444
  const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
1443
1445
  const transientStart = telemetry.transientFailures.length;
@@ -1471,15 +1473,16 @@ export class Orchestrator {
1471
1473
  break;
1472
1474
  if (display)
1473
1475
  safeInvoke(onHarnessEvent, safeEv);
1474
- // In-place turns run in the live tree under the native environment, so
1475
- // the session they emit IS reachable for the next turn: record it. An
1476
- // ISOLATED envelope-born session lives in the scoped home that dispose()
1477
- // deletes, so observing it would poison the thread resume map with
1478
- // unreachable ids — skip it there.
1479
- if (inPlaceEnvelope)
1480
- observeNativeSessionEvent(runInput, adapter.id, safeEv);
1481
1476
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
1482
1477
  observeAttemptTelemetry(telemetry, safeEv);
1478
+ const mismatch = continuity.observe(safeEv, runSpec, telemetry.outputMarkers);
1479
+ if (mismatch) {
1480
+ harnessErrored = true;
1481
+ errors.push(mismatch);
1482
+ break;
1483
+ }
1484
+ if (inPlaceEnvelope && continuity.mayPublishSession)
1485
+ observeNativeSessionEvent(runInput, adapter.id, safeEv);
1483
1486
  // QA-024: the injected delegation belt's MCP server reported a
1484
1487
  // terminal startup failure. Disclose it ONCE while live; recoverable
1485
1488
  // exact tool-result failures are evaluated at attempt finalization.
@@ -1512,7 +1515,7 @@ export class Orchestrator {
1512
1515
  cost = usage.cost;
1513
1516
  costEstimated = usage.costEstimated;
1514
1517
  if (usage.hardCapReached) {
1515
- harnessErrored = true;
1518
+ harnessErrored = streamBudgetDenied = true;
1516
1519
  errors.push("budget hard cap reached mid-attempt; stream aborted");
1517
1520
  break;
1518
1521
  }
@@ -1523,7 +1526,7 @@ export class Orchestrator {
1523
1526
  }
1524
1527
  // Capture assistant prose so an answer-only turn (no file changes) still
1525
1528
  // has an honest output artifact; a TYPED final message wins verbatim.
1526
- answer.observe(safeEv);
1529
+ answer.observeCounted(ev, safeEv);
1527
1530
  // Observe ALL budget/quota signals (one codex usage event carries
1528
1531
  // BOTH spend and quota); pressure disclosed once per attempt.
1529
1532
  observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
@@ -1571,64 +1574,37 @@ export class Orchestrator {
1571
1574
  const deliverableEmpty = workspaceUnchanged && answer.text().length === 0;
1572
1575
  const delivered = !harnessErrored && !deliverableEmpty && !signal?.aborted;
1573
1576
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
1574
- // W5.4 + A2 failover: a typed-limit hit OR a structural pre-progress
1575
- // death rebuilds the spec on a NEW session under the next profile.
1576
- if (harnessErrored && runInput && !signal?.aborted) {
1577
- const rotated = await rotateSpecOnTypedLimit({
1578
- spec,
1579
- harnessId: adapter.id,
1580
- attemptId,
1581
- policy: this.credentials.profilePolicy(contract.repo.root, adapter.id),
1582
- registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
1583
- snapshots: this.deps.quotaSnapshots?.() ?? [],
1584
- probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
1585
- ...this.credentials.rotationObservations(adapter, spec, newTransients, refusal),
1586
- triedProfiles,
1587
- markers: telemetry.outputMarkers,
1588
- requestRefused: Boolean(telemetry.requestRefusal),
1589
- sawTypedLimit,
1590
- sawRetryable,
1591
- attemptErrored: harnessErrored,
1592
- // Rotation evidence reads the POLICY-accepted try output: refusal
1593
- // prose arriving as mid-stream MESSAGE events (claude org-disabled)
1594
- // is no deliverable; the transient gate keeps RAW deliverableEmpty.
1595
- deliverableEmpty: workspaceUnchanged && acceptedTryOutput(answer, harnessErrored).length === 0,
1596
- workspaceDiffNonEmpty: !workspaceUnchanged,
1597
- lastLimit: telemetry.rateLimits.at(-1) ?? null,
1598
- emit: (type, payload) => log?.emit(type, payload),
1599
- newSessionId: () => newId("ses"),
1600
- defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
1601
- // D-U6: an explicit pin never rotates; pool-selected rows do.
1602
- pinned: runInput.credentialProfileId != null,
1603
- });
1604
- // A5 ordering: an exhausted pool terminalizes TYPED here, BEFORE the
1605
- // transient gate below burns same-profile retries (limits classify
1606
- // as retryable) on the already-refused subject.
1607
- if (rotated && "poolExhausted" in rotated) {
1608
- poolExhausted = rotated.poolExhausted;
1609
- errors.push(safeErrorMessage(poolExhausted));
1610
- break;
1611
- }
1612
- if (rotated) {
1613
- // INV-137: the rotated row runs in ITS OWN lane home (no-op for
1614
- // isolated envelopes and native-env in-place turns).
1615
- spec = rotatedSpecInLaneHome(spec, rotated, (id) => this.laneHomeEnvFor(runInput, adapter.id, id), runInput.credentialProfileId ?? null);
1616
- applied = appliedNow();
1617
- errors.length = 0;
1618
- harnessErrored = false;
1619
- continue;
1620
- }
1577
+ const verdict = await continuity.afterTry({
1578
+ runSpec,
1579
+ nativeTry,
1580
+ harnessErrored,
1581
+ budgetStopped: streamBudgetDenied,
1582
+ aborted: signal?.aborted === true,
1583
+ requestRefused: Boolean(telemetry.requestRefusal),
1584
+ newTransients,
1585
+ sawTypedLimit,
1586
+ sawRetryable,
1587
+ answer,
1588
+ rawDeliverableEmpty: deliverableEmpty,
1589
+ workspaceDiffNonEmpty: !workspaceUnchanged,
1590
+ currentDiff,
1591
+ markers: telemetry.outputMarkers,
1592
+ lastLimit: telemetry.rateLimits.at(-1) ?? null,
1593
+ refusal,
1594
+ telemetry,
1595
+ });
1596
+ if (verdict.kind === "terminal") {
1597
+ poolExhausted = verdict.error;
1598
+ errors.push(safeErrorMessage(poolExhausted));
1599
+ break;
1621
1600
  }
1622
- if (!harnessErrored ||
1623
- !sawRetryable ||
1624
- !deliverableEmpty ||
1625
- nativeTry >= retryPolicy.maxRetries ||
1626
- signal?.aborted)
1601
+ if (verdict.kind === "break")
1627
1602
  break;
1628
- const delayMs = emitTransientRetryPlan((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, nativeTry, retryPolicy);
1603
+ spec = verdict.spec;
1604
+ applied = appliedNow();
1629
1605
  errors.length = 0;
1630
1606
  harnessErrored = false;
1631
- await sleep(delayMs);
1607
+ await sleep(verdict.delayMs);
1632
1608
  }
1633
1609
  }
1634
1610
  finally {
@@ -1648,19 +1624,23 @@ export class Orchestrator {
1648
1624
  // with no typed final contributes no answer material (acceptedTryOutput).
1649
1625
  const unwrapped = unwrapWorkReportEnvelope(acceptedTryOutput(answer, harnessErrored), workReportMode, { sideToolReport: telemetry.sideToolWorkReport ?? undefined });
1650
1626
  const redacted = redactSecrets(unwrapped.deliverable);
1651
- const candidateAnswer = redacted.trim().length > 0 ? redacted : undefined;
1627
+ // INV-062: the (already redacted) answer is kept whatever the patch holds.
1628
+ const answerText = redacted.trim().length > 0 ? redacted : undefined;
1652
1629
  if (directory)
1653
1630
  await captureDirectory();
1654
- const { diff, refusal: secretDiffRefusal } = directory
1655
- ? { diff: "", refusal: directoryCapture.refusal }
1656
- : await secretDiff.quarantineCandidateWorkspace(wsm, envelope, inPlaceEnvelope, candidateAnswer);
1657
- harnessErrored = secretDiff.recordSecretDiffRefusal(secretDiffRefusal, errors, harnessErrored);
1658
- const answerText = secretDiffRefusal ? undefined : candidateAnswer;
1631
+ const captured = directory
1632
+ ? { diff: "", captureRefusal: directoryCapture.refusal }
1633
+ : await secretDiff.captureCandidateWorkspace({
1634
+ ...{ wsm, envelope, inPlace: inPlaceEnvelope, projectRoot: store.repoRoot },
1635
+ ...{ answerText, answerMatches: answer.secretLikeMatches() },
1636
+ });
1637
+ const { diff, captureRefusal } = captured;
1638
+ harnessErrored = secretDiff.recordCaptureRefusal(captureRefusal, errors, harnessErrored);
1659
1639
  const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText) || directoryHasOutput(directoryCapture.files);
1660
1640
  // Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
1661
1641
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
1662
1642
  // still land, so partial work stays inspectable.
1663
- const gateSignalAborted = signal?.aborted === true || secretDiffRefusal !== undefined;
1643
+ const gateSignalAborted = signal?.aborted === true || captureRefusal !== undefined;
1664
1644
  if (!gateSignalAborted) {
1665
1645
  log?.emit("gate.started", {
1666
1646
  attempt_id: attemptId,
@@ -1707,6 +1687,7 @@ export class Orchestrator {
1707
1687
  workReport: unwrapped.workReport,
1708
1688
  workReportSource: unwrapped.source,
1709
1689
  workReportViolation: unwrapped.contractViolation,
1690
+ workReportUnverified: unwrapped.unverified ?? null,
1710
1691
  contextTerminalExhausted: telemetry.contextExhausted,
1711
1692
  });
1712
1693
  harnessErrored = finalized.harnessErrored;
@@ -1714,6 +1695,14 @@ export class Orchestrator {
1714
1695
  errors.push(`work_report contract: ${unwrapped.contractViolation}`);
1715
1696
  const deliverablePresent = finalized.deliverablePresent;
1716
1697
  const errored = harnessErrored || webBlocked;
1698
+ const resumable = await continuity.finish({
1699
+ runSpec: spec,
1700
+ errored,
1701
+ nonSuccess: !gatesPassed(gates),
1702
+ aborted: signal?.aborted === true,
1703
+ cancelReason: typeof signal?.reason === "string" ? signal.reason : null,
1704
+ workState: finalized.workState,
1705
+ });
1717
1706
  setAttemptOutcome(telemetry, {
1718
1707
  deliverablePresent,
1719
1708
  gatesPassed: gates.length > 0 ? gatesPassed(gates) : null,
@@ -1734,9 +1723,9 @@ export class Orchestrator {
1734
1723
  attemptDir,
1735
1724
  worktreePath: envelope.worktree_path,
1736
1725
  artifactRelativeDir,
1737
- diff,
1738
- persistPatch: !directory && secretDiffRefusal === undefined && isMutatingAccess(access),
1739
- persistProducedMedia: !directory && secretDiffRefusal === undefined,
1726
+ ...{ diff, persistedDiff: captured.persistedDiff },
1727
+ persistPatch: !directory && captureRefusal === undefined && isMutatingAccess(access),
1728
+ persistProducedMedia: !directory && captureRefusal === undefined,
1740
1729
  answerText,
1741
1730
  record: {
1742
1731
  attempt_id: attemptId,
@@ -1748,7 +1737,7 @@ export class Orchestrator {
1748
1737
  errors: errors.slice(0, 5),
1749
1738
  ...telemetrySummary(telemetry),
1750
1739
  outcome: telemetry.outcome,
1751
- ...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
1740
+ ...secretDiff.attemptDisclosure(captured),
1752
1741
  gates: gates.map((g) => ({ id: g.id, status: g.status })),
1753
1742
  branch: envelope.branch_name,
1754
1743
  ...(directoryCapture.files
@@ -1780,7 +1769,7 @@ export class Orchestrator {
1780
1769
  attemptId,
1781
1770
  harnessId: adapter.id,
1782
1771
  label,
1783
- diff,
1772
+ ...captured,
1784
1773
  ...(directoryCapture.files ? { files: directoryCapture.files } : {}),
1785
1774
  answerText,
1786
1775
  reviewCwd: envelope.worktree_path,
@@ -1792,7 +1781,6 @@ export class Orchestrator {
1792
1781
  costEstimated,
1793
1782
  errors: errors.slice(0, 8),
1794
1783
  telemetry,
1795
- ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1796
1784
  // A5: the typed refusal survives NORMAL attempt finalization (no throw).
1797
1785
  ...(telemetry.requestRefusal
1798
1786
  ? { declaredFailure: requestRefusalFailure(telemetry.requestRefusal) }
@@ -1801,14 +1789,47 @@ export class Orchestrator {
1801
1789
  : {}),
1802
1790
  outcomeClass: finalized.outcomeClass,
1803
1791
  applied,
1792
+ ...continuity.terminalFacts(resumable),
1804
1793
  };
1805
1794
  }
1806
- interactionChannelFor(input, log, runId, taskId, attemptId, harnessId,
1807
- // REQUIRED (no default): every call site must state the routed manifest's
1808
- // `interactive` capability, or a future site would silently bypass the gate.
1809
- supportsInteractive) {
1810
- // Thin delegate — the channel mechanics live in interaction.ts.
1811
- return interactionChannelFor(input, log, runId, taskId, attemptId, harnessId, supportsInteractive, DEFAULT_INTERACTION_TIMEOUT_MS);
1795
+ continuityDeps(adapter, routed, contract, attemptId, paths, runInput, log, lane) {
1796
+ const repoRoot = contract.repo.root;
1797
+ const cwd = lane.envelope?.worktree_path ?? (runInput ? this.execRootOf(runInput) : repoRoot);
1798
+ return {
1799
+ adapter,
1800
+ runId: paths.runId,
1801
+ attemptId,
1802
+ runDir: paths.root,
1803
+ attemptDir: join(paths.attemptsDir, attemptId),
1804
+ cwd,
1805
+ workspace: continuityWorkspace(lane.envelope, cwd),
1806
+ workOrder: lane.workOrder,
1807
+ firstPrompt: lane.firstPrompt,
1808
+ registry: this.config(repoRoot)?.global.credential_profiles ?? [],
1809
+ policy: this.credentials.profilePolicy(repoRoot, adapter.id),
1810
+ snapshots: this.deps.quotaSnapshots?.() ?? [],
1811
+ retryPolicy: transientRetryPolicy(this.config(repoRoot)),
1812
+ pinned: runInput?.credentialProfileId != null, // D-U6: a pin never rotates
1813
+ defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
1814
+ requestedProfileId: runInput?.credentialProfileId ?? null,
1815
+ rotationEnabled: runInput !== undefined,
1816
+ laneEnvFor: (id) => (runInput ? this.laneHomeEnvFor(runInput, adapter.id, id) : null),
1817
+ probeReadyProfiles: (spec, tried) => runInput
1818
+ ? this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, tried, spec.model_hint ?? null)
1819
+ : Promise.resolve(new Set()),
1820
+ rotationObservations: (spec, transients, refusal) => this.credentials.rotationObservations(adapter, spec, transients, refusal),
1821
+ emit: (type, payload) => log?.emit(type, payload),
1822
+ route: routed,
1823
+ newSessionId: () => newId("ses"),
1824
+ thread: runInput?.threadId
1825
+ ? {
1826
+ threadId: runInput.threadId,
1827
+ turnId: runInput.threadContinuity?.turnId ?? null,
1828
+ onSessionObserved: runInput.onSessionObserved,
1829
+ onContinuityResolved: runInput.onContinuityResolved,
1830
+ }
1831
+ : null,
1832
+ };
1812
1833
  }
1813
1834
  async runRace(input, mode, announce) {
1814
1835
  const taskId = input.taskId ?? newId("task");
@@ -1928,7 +1949,7 @@ export class Orchestrator {
1928
1949
  const disposeReviewEnvelopes = async () => {
1929
1950
  const envelopes = reviewEnvelopes.splice(0);
1930
1951
  for (const env of envelopes)
1931
- await wsm.dispose(env);
1952
+ await kept.settle(wsm, env, runsBySlot);
1932
1953
  };
1933
1954
  const candidateAccess = contract.access.effective_profile;
1934
1955
  let budgetStopped = false;
@@ -1937,6 +1958,7 @@ export class Orchestrator {
1937
1958
  let budgetDenial = null;
1938
1959
  let softWarned = false;
1939
1960
  const requestedSingleCandidate = adapters.length === 1;
1961
+ const kept = custody.forRun(input, requestedSingleCandidate && mutatingRun, runId, paths, log);
1940
1962
  const slots = [];
1941
1963
  for (let i = 0; i < adapters.length; i++) {
1942
1964
  const routed = adapters[i];
@@ -2036,7 +2058,7 @@ export class Orchestrator {
2036
2058
  external_context_policy: knobs.webPolicy,
2037
2059
  ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
2038
2060
  });
2039
- envelope = await wsm.create({
2061
+ envelope = await kept.envelope(wsm, {
2040
2062
  taskId,
2041
2063
  attemptId: slot.attemptId,
2042
2064
  baseRef: contract.repo.base_ref,
@@ -2058,7 +2080,7 @@ export class Orchestrator {
2058
2080
  budgetStopped = true;
2059
2081
  budgetDenial ??= denial;
2060
2082
  },
2061
- }, candidateAccess, input.onHarnessEvent, input.signal, downgradeModel ?? undefined, input.effort, this.candidateIntent(input), log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, slot.attemptId, adapter.id, slot.routed.supportsInteractive), (streamedUsd) => {
2083
+ }, candidateAccess, input.onHarnessEvent, input.signal, downgradeModel ?? undefined, input.effort, this.candidateIntent(input), log, effectiveWeb, interactionChannelFor(input, log, runId, taskId, slot.attemptId, adapter.id, slot.routed.supportsInteractive), (streamedUsd) => {
2062
2084
  ledger.updateHold(slot.lease.lease_id, streamedUsd);
2063
2085
  if (ledger.tier() !== "hard")
2064
2086
  return false;
@@ -2101,7 +2123,7 @@ export class Orchestrator {
2101
2123
  harness: adapter.id,
2102
2124
  profileId: input.credentialProfileId ?? null,
2103
2125
  priorPrompt: input.prompt,
2104
- priorOutput: run.answerText ?? run.diff ?? "",
2126
+ priorOutput: run.answerText ?? run.persistedDiff ?? run.diff ?? "",
2105
2127
  }));
2106
2128
  // Reserve the continuation lease BEFORE any disclosure: a denied lease
2107
2129
  // must never emit run.continuation (which claims a continuation
@@ -2137,7 +2159,7 @@ export class Orchestrator {
2137
2159
  budgetStopped = true;
2138
2160
  budgetDenial ??= denial;
2139
2161
  },
2140
- }, candidateAccess, input.onHarnessEvent, input.signal, downgradeModel ?? undefined, input.effort, this.candidateIntent(input), log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, contAttemptId, adapter.id, slot.routed.supportsInteractive), (streamedUsd) => {
2162
+ }, candidateAccess, input.onHarnessEvent, input.signal, downgradeModel ?? undefined, input.effort, this.candidateIntent(input), log, effectiveWeb, interactionChannelFor(input, log, runId, taskId, contAttemptId, adapter.id, slot.routed.supportsInteractive), (streamedUsd) => {
2141
2163
  ledger.updateHold(contLeaseId, streamedUsd);
2142
2164
  if (ledger.tier() !== "hard")
2143
2165
  return false;
@@ -2224,7 +2246,7 @@ export class Orchestrator {
2224
2246
  }
2225
2247
  finally {
2226
2248
  if (envelope)
2227
- await wsm.dispose(envelope); // no worktree leak even on create/run error
2249
+ await kept.settle(wsm, envelope, runsBySlot); // no leak on create/run error
2228
2250
  }
2229
2251
  };
2230
2252
  await runParallelCandidates(slots, this.deps.runtimeConcurrencyCaps, runSlot);
@@ -2265,7 +2287,7 @@ export class Orchestrator {
2265
2287
  input.workspaceKind !== "directory" &&
2266
2288
  input.inPlace &&
2267
2289
  requestedSingleCandidate &&
2268
- runs.every((run) => !run.secretDiffRefusal)) {
2290
+ runs.every((run) => !run.captureRefusal)) {
2269
2291
  try {
2270
2292
  earlyPostTurnSha = await snapshotTree(execRoot);
2271
2293
  }
@@ -2378,7 +2400,7 @@ export class Orchestrator {
2378
2400
  catch (err) {
2379
2401
  // Review preflight/evidence failures end TERMINALLY with artifacts —
2380
2402
  // never as an escaped throw that orphans the run dir.
2381
- return failTerminally(log, store, paths, runId, taskId, mode, "review", err, ledger.spend());
2403
+ return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "review", err);
2382
2404
  }
2383
2405
  finally {
2384
2406
  // Review preflight failures must not leak candidate worktrees.
@@ -2436,7 +2458,7 @@ export class Orchestrator {
2436
2458
  budgetStopped = true;
2437
2459
  budgetDenial ??= denial;
2438
2460
  },
2439
- }, candidateAccess, input.onHarnessEvent, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content, undefined, synthHome);
2461
+ }, 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);
2440
2462
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2441
2463
  reviewEnvelopes.push(envelope);
2442
2464
  envelope = undefined;
@@ -2483,7 +2505,7 @@ export class Orchestrator {
2483
2505
  }
2484
2506
  catch (err) {
2485
2507
  // Arbitration throws end terminally with artifacts, never as an orphan.
2486
- return failTerminally(log, store, paths, runId, taskId, mode, "arbitration", err, ledger.spend());
2508
+ return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "arbitration", err);
2487
2509
  }
2488
2510
  log.emit("arbitration.completed", {
2489
2511
  winner: result.decision.winner,
@@ -2629,9 +2651,8 @@ export class Orchestrator {
2629
2651
  });
2630
2652
  }
2631
2653
  else if (mutatingRun) {
2632
- secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2633
- const patchSha256 = sha256(winnerRun.diff);
2634
- store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
2654
+ const savedPatch = secretDiff.persistFinalPatch(store, paths.finalDir, winnerRun);
2655
+ const patchSha256 = savedPatch.patchSha256;
2635
2656
  const wstats = diffStats(winnerRun.diff);
2636
2657
  const hasDiff = winnerRun.diff.trim().length > 0;
2637
2658
  const blockers = winnerEvidence
@@ -2727,7 +2748,7 @@ export class Orchestrator {
2727
2748
  outcome_facts: facts,
2728
2749
  review_verified: actualReviewVerified,
2729
2750
  budget_stopped: budgetStopped,
2730
- patch_sha256: patchSha256,
2751
+ ...{ patch_sha256: patchSha256, ...savedPatch.meta },
2731
2752
  result_kind: resultKind,
2732
2753
  diffstat: {
2733
2754
  files: wstats.paths.length,
@@ -2744,7 +2765,7 @@ export class Orchestrator {
2744
2765
  },
2745
2766
  });
2746
2767
  }
2747
- store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified));
2768
+ store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified, secretDiff.secretLikeSummaryLine(winnerRun.secretLike)));
2748
2769
  // summary.md is a DIAGNOSTIC artifact only (V8/PLAN addendum 2): it no
2749
2770
  // longer carries primary-output authority. A clean applyable success or a
2750
2771
  // winner answer still marks it ready for legacy INV-116 ordering; any
@@ -2849,7 +2870,12 @@ export class Orchestrator {
2849
2870
  log.emit("work_product.emitted", { winner: result.decision.winner });
2850
2871
  }
2851
2872
  if (!isFailureTerminal) {
2852
- log.emit("run.completed", { lifecycle: facts.lifecycle, facts, reason: facts.reason });
2873
+ log.emit("run.completed", {
2874
+ lifecycle: facts.lifecycle,
2875
+ facts,
2876
+ reason: facts.reason,
2877
+ ...resumableTerminal(store, paths, winnerRun ? [winnerRun] : runs, isFailureTerminal),
2878
+ });
2853
2879
  }
2854
2880
  else if (facts.lifecycle === "succeeded") {
2855
2881
  // needsDecision at terminal — the event's phase must agree with
@@ -2857,6 +2883,7 @@ export class Orchestrator {
2857
2883
  log.emit("run.blocked", {
2858
2884
  lifecycle: facts.lifecycle,
2859
2885
  facts,
2886
+ ...resumableTerminal(store, paths, runs, isFailureTerminal),
2860
2887
  phase: deliveryFailureReason && !finalVerifyFailed
2861
2888
  ? "delivery"
2862
2889
  : finalVerifyFailed
@@ -2872,6 +2899,7 @@ export class Orchestrator {
2872
2899
  reason: facts.reason,
2873
2900
  phase: "arbitration",
2874
2901
  failure_ref: "final/failure.yaml",
2902
+ ...resumableTerminal(store, paths, runs, isFailureTerminal),
2875
2903
  });
2876
2904
  }
2877
2905
  return {
@@ -3233,7 +3261,7 @@ export class Orchestrator {
3233
3261
  exhausted = true;
3234
3262
  processingBudgetDenial ??= denial;
3235
3263
  },
3236
- }, convergenceAccess, input.onHarnessEvent, input.signal, undefined, input.effort, "repair", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive), (streamedUsd) => {
3264
+ }, convergenceAccess, input.onHarnessEvent, input.signal, undefined, input.effort, "repair", log, effectiveWeb, interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive), (streamedUsd) => {
3237
3265
  ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
3238
3266
  return ledger.tier() === "hard";
3239
3267
  }, input, true, // convergence runs one candidate: live deltas on (W-C4)
@@ -3286,7 +3314,7 @@ export class Orchestrator {
3286
3314
  observedPaths: [...observedPaths],
3287
3315
  });
3288
3316
  run.files = captured.files;
3289
- run.secretDiffRefusal = captured.refusal;
3317
+ run.captureRefusal = captured.refusal;
3290
3318
  }
3291
3319
  }
3292
3320
  lastRun = run;
@@ -3298,9 +3326,7 @@ export class Orchestrator {
3298
3326
  // Post-mutation fence for in-place: snapshot the live tree NOW (after the
3299
3327
  // harness mutated it, before this attempt's review). The last attempt's
3300
3328
  // value is the revert target persisted into work_product.yaml.
3301
- if (input.inPlace === true &&
3302
- input.workspaceKind !== "directory" &&
3303
- !run.secretDiffRefusal) {
3329
+ if (input.inPlace === true && input.workspaceKind !== "directory" && !run.captureRefusal) {
3304
3330
  try {
3305
3331
  lastPostTurnSha = await snapshotTree(execRoot);
3306
3332
  }
@@ -3340,7 +3366,7 @@ export class Orchestrator {
3340
3366
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null);
3341
3367
  return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
3342
3368
  }
3343
- if (input.workspaceKind === "directory" && run.secretDiffRefusal) {
3369
+ if (input.workspaceKind === "directory" && run.captureRefusal) {
3344
3370
  return failedCandidatesResult({
3345
3371
  ledger,
3346
3372
  mode,
@@ -3409,7 +3435,7 @@ export class Orchestrator {
3409
3435
  facts: makeOutcomeFacts("failed", { noChanges: run.files.noChanges }),
3410
3436
  log,
3411
3437
  });
3412
- return failTerminally(log, store, paths, runId, taskId, mode, "review", err, ledger.spend());
3438
+ return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs: [run] }, "review", err);
3413
3439
  }
3414
3440
  if (conv.converged) {
3415
3441
  converged = true;
@@ -3600,15 +3626,10 @@ export class Orchestrator {
3600
3626
  facts,
3601
3627
  log,
3602
3628
  });
3603
- if (lastRun.answerText) {
3604
- store.writeText(join(paths.finalDir, "answer.md"), lastRun.answerText);
3605
- log.emit("output.ready", { kind: "answer", path: "final/answer.md" });
3606
- }
3607
3629
  }
3608
3630
  else {
3609
- secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3610
- const patchSha256 = sha256(lastRun.diff);
3611
- store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
3631
+ const savedPatch = secretDiff.persistFinalPatch(store, paths.finalDir, lastRun);
3632
+ const patchSha256 = savedPatch.patchSha256;
3612
3633
  // Honest apply-state (parity with runRace single-candidate in-place): a
3613
3634
  // convergence run with inPlace mutated the live tree directly across its
3614
3635
  // attempts, so it is "applied" even when review blocked (Revert offered).
@@ -3635,7 +3656,7 @@ export class Orchestrator {
3635
3656
  lifecycle: facts.lifecycle,
3636
3657
  outcome_facts: facts,
3637
3658
  review_verified: actualReviewVerified,
3638
- patch_sha256: patchSha256,
3659
+ ...{ patch_sha256: patchSha256, ...savedPatch.meta },
3639
3660
  adopted: convAdopted,
3640
3661
  apply_state: convApplyState,
3641
3662
  pre_turn_sha: convAdopted === true ? preTurnSha : null,
@@ -3645,7 +3666,12 @@ export class Orchestrator {
3645
3666
  },
3646
3667
  });
3647
3668
  }
3648
- 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}` : ""}\n`);
3669
+ // INV-062: the answer is kept for every convergence result, patch or files.
3670
+ if (lastRun.answerText) {
3671
+ store.writeText(join(paths.finalDir, "answer.md"), lastRun.answerText);
3672
+ log.emit("output.ready", { kind: "answer", path: "final/answer.md" });
3673
+ }
3674
+ 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`);
3649
3675
  // Lifecycle invariant (all modes): output.ready precedes the terminal
3650
3676
  // event so a client that applied the terminal event has the output.
3651
3677
  log.emit("output.ready", {
@@ -3657,6 +3683,7 @@ export class Orchestrator {
3657
3683
  // A needs-decision terminal (review blocked / checks failed) writes a
3658
3684
  // failure record + fires run.blocked even though the lifecycle succeeded.
3659
3685
  const convIsFailureTerminal = facts.lifecycle !== "succeeded" || convNeedsDecision;
3686
+ const continuation = resumableTerminal(store, paths, lastRun ? [lastRun] : [], convIsFailureTerminal);
3660
3687
  if (convIsFailureTerminal) {
3661
3688
  // A5 last-result lift: the LAST attempt's typed mid-attempt refusal
3662
3689
  // survives onto the terminal; budget/cancel/needs-decision outrank it.
@@ -3732,6 +3759,7 @@ export class Orchestrator {
3732
3759
  }
3733
3760
  if (!convIsFailureTerminal) {
3734
3761
  log.emit("run.completed", {
3762
+ ...continuation,
3735
3763
  lifecycle: facts.lifecycle,
3736
3764
  facts,
3737
3765
  reason: facts.reason,
@@ -3740,6 +3768,7 @@ export class Orchestrator {
3740
3768
  }
3741
3769
  else if (facts.lifecycle === "succeeded") {
3742
3770
  log.emit("run.blocked", {
3771
+ ...continuation,
3743
3772
  lifecycle: facts.lifecycle,
3744
3773
  facts,
3745
3774
  attempts: attempt,
@@ -3749,6 +3778,7 @@ export class Orchestrator {
3749
3778
  }
3750
3779
  else {
3751
3780
  log.emit("run.failed", {
3781
+ ...continuation,
3752
3782
  lifecycle: facts.lifecycle,
3753
3783
  facts,
3754
3784
  reason: facts.reason,
@@ -3827,7 +3857,7 @@ export class Orchestrator {
3827
3857
  spec.extra["abortSignal"] = input.signal
3828
3858
  ? AbortSignal.any([input.signal, plannerAbort.signal])
3829
3859
  : plannerAbort.signal;
3830
- const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
3860
+ const planInteraction = interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
3831
3861
  if (planInteraction)
3832
3862
  spec.extra["interactionChannel"] = planInteraction;
3833
3863
  const planWorkMode = applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
@@ -4420,18 +4450,14 @@ export class Orchestrator {
4420
4450
  : null) ?? roHome.env,
4421
4451
  });
4422
4452
  this.credentials.stampProfileSelection(spec, input, adapter.id);
4423
- const reportAbort = new AbortController();
4424
- spec.extra["abortSignal"] = input.signal
4425
- ? AbortSignal.any([input.signal, reportAbort.signal])
4426
- : reportAbort.signal;
4427
- const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4453
+ const reportInteraction = interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4428
4454
  if (reportInteraction)
4429
4455
  spec.extra["interactionChannel"] = reportInteraction;
4430
4456
  // D-16: compile the WorkReport envelope for the read-only lane.
4431
4457
  const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4432
4458
  const readonlyWorkMode = applyWorkEnvelope(spec, readonlyWorkEnvelope);
4433
4459
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4434
- const answer = new AnswerAssembly();
4460
+ const answer = new secretDiff.CountedAnswerAssembly();
4435
4461
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [],
4436
4462
  // Requested-model capture so ask/audit route receipts detect a silent
4437
4463
  // model downgrade (typed model_mismatch), not just agent runs.
@@ -4439,7 +4465,6 @@ export class Orchestrator {
4439
4465
  return {
4440
4466
  knobs,
4441
4467
  spec,
4442
- reportAbort,
4443
4468
  reportInteraction,
4444
4469
  readonlyWorkMode,
4445
4470
  attemptEventsPath,
@@ -4481,13 +4506,20 @@ export class Orchestrator {
4481
4506
  }
4482
4507
  return { status: "launched" };
4483
4508
  }
4484
- const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, telemetry, } = preparation.value;
4509
+ const { knobs, spec: preparedSpec, reportInteraction, readonlyWorkMode, attemptEventsPath, telemetry, } = preparation.value;
4485
4510
  let { answer } = preparation.value;
4486
4511
  let spec = preparedSpec;
4487
4512
  bindProcessingAdmission(spec, ledger, lease.lease, adapter.id, attemptId, undefined, processingAdmission);
4488
4513
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4489
4514
  let activeSessionId = spec.session_id;
4490
4515
  const live = liveAttempt(input, routed, paths, contract, attemptId, () => activeSessionId);
4516
+ let continuity;
4517
+ ({ continuity, spec } = await openContinuity(this.continuityDeps(adapter, routed, contract, attemptId, paths, input, log, {
4518
+ envelope: null,
4519
+ harnessHome: null,
4520
+ workOrder: input.prompt,
4521
+ firstPrompt: spec.prompt,
4522
+ }), { spec, continuation: input.continuation, store, telemetry }));
4491
4523
  const onAbort = () => void adapter.cancel?.(activeSessionId)?.catch(() => { });
4492
4524
  if (input.signal) {
4493
4525
  if (input.signal.aborted)
@@ -4502,12 +4534,11 @@ export class Orchestrator {
4502
4534
  let processingRefusal = null;
4503
4535
  let poolExhausted = null; // A5: typed pool-exhausted refusal
4504
4536
  try {
4505
- const triedProfiles = new Set(); // W5.4 failover: each profile at most once
4506
4537
  for (let nativeTry = 0; !input.signal?.aborted; nativeTry += 1) {
4507
4538
  // A3 per-try isolation (candidate-lane parity): neither a failed try's
4508
4539
  // output nor its progress markers leak into the next try's evidence.
4509
4540
  if (nativeTry > 0) {
4510
- answer = new AnswerAssembly();
4541
+ answer = new secretDiff.CountedAnswerAssembly();
4511
4542
  telemetry.outputMarkers = newAttemptOutputMarkers();
4512
4543
  }
4513
4544
  const runSpec = nativeTry === 0
@@ -4515,10 +4546,14 @@ export class Orchestrator {
4515
4546
  : HarnessRunSpec.parse({
4516
4547
  ...spec,
4517
4548
  session_id: newId("ses"),
4518
- resume_session_id: null,
4519
4549
  extra: { ...spec.extra },
4520
4550
  });
4521
4551
  activeSessionId = runSpec.session_id;
4552
+ const tryAbort = new AbortController();
4553
+ runSpec.extra["abortSignal"] = input.signal
4554
+ ? AbortSignal.any([input.signal, tryAbort.signal])
4555
+ : tryAbort.signal;
4556
+ continuity.beginTry(runSpec, nativeTry, tryAbort);
4522
4557
  // #363: bound before the spawn — a credential change mid-try voids it.
4523
4558
  const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
4524
4559
  const transientStart = telemetry.transientFailures.length;
@@ -4536,7 +4571,7 @@ export class Orchestrator {
4536
4571
  timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
4537
4572
  countsAsProgress: countsAsAgentProgress,
4538
4573
  onTimeout: () => {
4539
- reportAbort.abort();
4574
+ tryAbort.abort();
4540
4575
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
4541
4576
  },
4542
4577
  isSuspended: () => (reportInteraction?.pendingCount?.() ?? 0) > 0,
@@ -4546,20 +4581,19 @@ export class Orchestrator {
4546
4581
  const safeEv = redactHarnessEvent(ev);
4547
4582
  if (safeEv.payload?.["buffered"] !== true)
4548
4583
  safeInvoke(input.onHarnessEvent, safeEv);
4549
- // A thread ASK turn IS a chat turn now (INV-034): its native
4550
- // session lives in the DURABLE per-lane home, so record it for the
4551
- // next lane turn's resume. The read-only fallback chain is
4552
- // sequential (never the parallel deep-scan swarm, which is
4553
- // excluded from `laneRun`), so recordSession's upsert keeps the
4554
- // latest lane session without a race.
4555
- if (laneRun)
4556
- observeNativeSessionEvent(input, adapter.id, safeEv);
4557
4584
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
4558
4585
  log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv), safeEv.payload?.["buffered"] !== true);
4559
4586
  appendLine(attemptEventsPath, JSON.stringify(safeEv));
4560
4587
  if (input.signal?.aborted)
4561
4588
  break;
4562
4589
  observeAttemptTelemetry(telemetry, safeEv);
4590
+ const mismatch = continuity.observe(safeEv, runSpec, telemetry.outputMarkers);
4591
+ if (mismatch) {
4592
+ harnessError = mismatch;
4593
+ break;
4594
+ }
4595
+ if (laneRun && continuity.mayPublishSession)
4596
+ observeNativeSessionEvent(input, adapter.id, safeEv);
4563
4597
  emitPlanProgress((t, p) => log.emit(t, p), adapter.id, attemptId, safeEv);
4564
4598
  // read-only routes burn quota too (the orchestrate PLANNER is
4565
4599
  // the loudest) — same single owner as the agent loop.
@@ -4573,12 +4607,12 @@ export class Orchestrator {
4573
4607
  budgetStopped = streamBudgetDenied = true;
4574
4608
  budgetDenial ??= streamDenial;
4575
4609
  harnessError = streamDenial.reason;
4576
- reportAbort.abort();
4610
+ tryAbort.abort();
4577
4611
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
4578
4612
  break;
4579
4613
  }
4580
4614
  // A TYPED final message wins verbatim over joined narration.
4581
- answer.observe(safeEv);
4615
+ answer.observeCounted(ev, safeEv);
4582
4616
  if (safeEv.type === "error")
4583
4617
  harnessError = safeEv.error
4584
4618
  ? redactSecrets(safeEv.error)
@@ -4606,62 +4640,34 @@ export class Orchestrator {
4606
4640
  const reportSoFar = answer.text();
4607
4641
  const delivered = !harnessError && reportSoFar.length > 0 && !input.signal?.aborted;
4608
4642
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
4609
- // W5.4 + A2 reactive failover, READ-ONLY lane (same contract as the
4610
- // candidate lane: typed limit or structural pre-progress death).
4611
- if (harnessError && !input.signal?.aborted) {
4612
- const rotated = await rotateSpecOnTypedLimit({
4613
- spec,
4614
- harnessId: adapter.id,
4615
- attemptId,
4616
- policy: this.credentials.profilePolicy(input.repoRoot, adapter.id),
4617
- registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
4618
- snapshots: this.deps.quotaSnapshots?.() ?? [],
4619
- probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
4620
- ...this.credentials.rotationObservations(adapter, spec, newTransients, refusal),
4621
- triedProfiles,
4622
- markers: telemetry.outputMarkers,
4623
- requestRefused: Boolean(telemetry.requestRefusal),
4624
- sawTypedLimit,
4625
- sawRetryable,
4626
- attemptErrored: harnessError !== null,
4627
- // Same policy owner as the candidate lane: an errored try's
4628
- // narration is not a report; the transient gate keeps reportSoFar.
4629
- deliverableEmpty: acceptedTryOutput(answer, harnessError !== null).length === 0,
4630
- lastLimit: telemetry.rateLimits.at(-1) ?? null,
4631
- emit: (type, payload) => log.emit(type, payload),
4632
- newSessionId: () => newId("ses"),
4633
- defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
4634
- // D-U6: an explicit pin never rotates; pool-selected rows do.
4635
- pinned: input.credentialProfileId != null,
4636
- });
4637
- // A5 ordering: same as the candidate lane — terminalize typed
4638
- // before the transient gate burns same-profile retries. The pool
4639
- // refusal APPENDS to the original harness error (candidate-lane
4640
- // parity: errors.push) instead of erasing the true failure.
4641
- if (rotated && "poolExhausted" in rotated) {
4642
- poolExhausted = rotated.poolExhausted;
4643
- harnessError = harnessError
4644
- ? `${harnessError}; ${safeErrorMessage(poolExhausted)}`
4645
- : safeErrorMessage(poolExhausted);
4646
- break;
4647
- }
4648
- if (rotated) {
4649
- // INV-137: the rotated row's session must land in ITS OWN lane
4650
- // home — never the previous row's lane store.
4651
- spec = rotatedSpecInLaneHome(spec, rotated, (id) => this.laneHomeEnvFor(input, adapter.id, id), input.credentialProfileId ?? null);
4652
- harnessError = null;
4653
- continue;
4654
- }
4643
+ const verdict = await continuity.afterTry({
4644
+ runSpec,
4645
+ nativeTry,
4646
+ harnessErrored: harnessError !== null,
4647
+ aborted: input.signal?.aborted === true,
4648
+ requestRefused: Boolean(telemetry.requestRefusal),
4649
+ newTransients,
4650
+ sawTypedLimit,
4651
+ sawRetryable,
4652
+ answer,
4653
+ rawDeliverableEmpty: reportSoFar.length === 0,
4654
+ markers: telemetry.outputMarkers,
4655
+ lastLimit: telemetry.rateLimits.at(-1) ?? null,
4656
+ refusal,
4657
+ telemetry,
4658
+ });
4659
+ if (verdict.kind === "terminal") {
4660
+ poolExhausted = verdict.error;
4661
+ harnessError = harnessError
4662
+ ? `${harnessError}; ${safeErrorMessage(poolExhausted)}`
4663
+ : safeErrorMessage(poolExhausted);
4664
+ break;
4655
4665
  }
4656
- if (!harnessError ||
4657
- !sawRetryable ||
4658
- reportSoFar.length > 0 ||
4659
- nativeTry >= retryPolicy.maxRetries ||
4660
- input.signal?.aborted)
4666
+ if (verdict.kind === "break")
4661
4667
  break;
4662
- const delayMs = emitTransientRetryPlan((t, p) => log.emit(t, p), adapter.id, attemptId, telemetry, nativeTry, retryPolicy);
4668
+ spec = verdict.spec;
4663
4669
  harnessError = null;
4664
- await sleep(delayMs);
4670
+ await sleep(verdict.delayMs);
4665
4671
  }
4666
4672
  }
4667
4673
  finally {
@@ -4703,6 +4709,7 @@ export class Orchestrator {
4703
4709
  workReport: roUnwrapped.workReport,
4704
4710
  workReportSource: roUnwrapped.source,
4705
4711
  workReportViolation: roUnwrapped.contractViolation,
4712
+ workReportUnverified: roUnwrapped.unverified ?? null,
4706
4713
  contextTerminalExhausted: telemetry.contextExhausted,
4707
4714
  });
4708
4715
  // A broken WorkReport contract is a hard failure ONLY when the finalizer
@@ -4725,6 +4732,13 @@ export class Orchestrator {
4725
4732
  // interruptions keep the strict deliverable requirement.
4726
4733
  emptyDeliverableAllowed: roFinalized.outcomeClass === "clean",
4727
4734
  });
4735
+ const roResumable = await continuity.finish({
4736
+ runSpec: spec,
4737
+ errored: harnessError !== null,
4738
+ aborted: input.signal?.aborted === true,
4739
+ cancelReason: typeof input.signal?.reason === "string" ? input.signal.reason : null,
4740
+ workState: roFinalized.workState,
4741
+ });
4728
4742
  if (harnessError) {
4729
4743
  log.emit("harness.completed", {
4730
4744
  harness_id: adapter.id,
@@ -4745,6 +4759,7 @@ export class Orchestrator {
4745
4759
  : poolExhausted || processingRefusal
4746
4760
  ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
4747
4761
  : {}),
4762
+ ...continuity.terminalFacts(roResumable),
4748
4763
  });
4749
4764
  if (opts.deepScan) {
4750
4765
  store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
@@ -4765,9 +4780,11 @@ export class Orchestrator {
4765
4780
  harnessId: adapter.id,
4766
4781
  status: scoutInterrupted ? "failed" : "success",
4767
4782
  report: report || "(no output)",
4783
+ answerMatches: answer.secretLikeMatches(),
4768
4784
  error: scoutInterrupted ? "context capacity exhausted before the scout completed" : null,
4769
4785
  telemetry,
4770
4786
  ...(scoutInterrupted ? { interrupted: true } : {}),
4787
+ ...continuity.terminalFacts(roResumable),
4771
4788
  });
4772
4789
  if (opts.deepScan) {
4773
4790
  const warningNote = toolWarnings(telemetry).length
@@ -4929,7 +4946,7 @@ export class Orchestrator {
4929
4946
  harnessId: a.harnessId,
4930
4947
  status: a.status,
4931
4948
  }));
4932
- 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);
4949
+ 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"));
4933
4950
  if (input.signal?.aborted)
4934
4951
  return cancelledTerminal();
4935
4952
  const succeededReadonly = attempts.filter((a) => a.status === "success");
@@ -5006,6 +5023,7 @@ export class Orchestrator {
5006
5023
  harness_id: terminalHarnessId,
5007
5024
  error: singleError,
5008
5025
  failure_ref: "final/failure.yaml",
5026
+ ...resumableTerminal(store, paths, attempts, true),
5009
5027
  });
5010
5028
  }
5011
5029
  else {
@@ -5017,6 +5035,7 @@ export class Orchestrator {
5017
5035
  harness_id: terminalHarnessId,
5018
5036
  error: singleError,
5019
5037
  failure_ref: "final/failure.yaml",
5038
+ ...resumableTerminal(store, paths, attempts, true),
5020
5039
  });
5021
5040
  }
5022
5041
  return {
@@ -5090,6 +5109,7 @@ export class Orchestrator {
5090
5109
  phase: scanBudgetMapping ? scanBudgetMapping.phase : "harness",
5091
5110
  error: message,
5092
5111
  failure_ref: "final/failure.yaml",
5112
+ ...resumableTerminal(store, paths, attempts, true),
5093
5113
  });
5094
5114
  return {
5095
5115
  spendUsd: ledger.spend(),
@@ -5116,8 +5136,9 @@ export class Orchestrator {
5116
5136
  // whole decision + reducer spawn lives in deepScanReducer.ts (its owner).
5117
5137
  let deepScanSynthesis = null;
5118
5138
  let reducedReport = null;
5139
+ let answerMatches = succeeded.reduce((sum, a) => sum + (a.answerMatches ?? 0), 0);
5119
5140
  if (opts.deepScan) {
5120
- ({ deepScanSynthesis, reducedReport } = await resolveDeepScanSynthesis(this.deepScanReducerDeps(input, contract, log), {
5141
+ ({ deepScanSynthesis, reducedReport, answerMatches } = await resolveDeepScanSynthesis(this.deepScanReducerDeps(input, contract, log), {
5121
5142
  succeeded,
5122
5143
  adapters,
5123
5144
  budgetStopped,
@@ -5209,7 +5230,8 @@ export class Orchestrator {
5209
5230
  const harnessLabel = attempts
5210
5231
  .map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
5211
5232
  .join(", ");
5212
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harnesses: ${harnessLabel}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}\n\n${report}\n`);
5233
+ const secretLike = secretDiff.answerSecretLikeFinding(answerMatches);
5234
+ 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`);
5213
5235
  const reportProducerAttemptId = opts.deepScan &&
5214
5236
  deepScanSynthesis?.status === "succeeded" &&
5215
5237
  deepScanSynthesis.reducer_attempt_id
@@ -5226,6 +5248,7 @@ export class Orchestrator {
5226
5248
  mode: opts.mode,
5227
5249
  intent: opts.intent,
5228
5250
  read_only: true,
5251
+ ...(secretLike ? { secret_like: secretLike } : {}),
5229
5252
  },
5230
5253
  });
5231
5254
  log.emit("work_product.emitted", { kind: "report", winner: reportProducerAttemptId });
@@ -5247,6 +5270,7 @@ export class Orchestrator {
5247
5270
  reason: terminalFacts.reason,
5248
5271
  phase: "executor",
5249
5272
  failure_ref: "final/failure.yaml",
5273
+ ...resumableTerminal(store, paths, attempts, true),
5250
5274
  });
5251
5275
  }
5252
5276
  else if (workVetoed) {
@@ -5254,6 +5278,7 @@ export class Orchestrator {
5254
5278
  lifecycle: terminalFacts.lifecycle,
5255
5279
  facts: terminalFacts,
5256
5280
  reason: terminalFacts.reason,
5281
+ ...resumableTerminal(store, paths, attempts, true),
5257
5282
  });
5258
5283
  }
5259
5284
  else {
@@ -5261,6 +5286,7 @@ export class Orchestrator {
5261
5286
  lifecycle: terminalFacts.lifecycle,
5262
5287
  facts: terminalFacts,
5263
5288
  reason: terminalFacts.reason,
5289
+ ...resumableTerminal(store, paths, attempts),
5264
5290
  });
5265
5291
  }
5266
5292
  return {