@claudexor/orchestrator 3.20.1 → 3.23.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 (186) 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/delegatedHome.d.ts +30 -1
  67. package/dist/delegatedHome.d.ts.map +1 -1
  68. package/dist/delegatedHome.js +37 -2
  69. package/dist/delegatedHome.js.map +1 -1
  70. package/dist/delegationFailure.d.ts +4 -3
  71. package/dist/delegationFailure.d.ts.map +1 -1
  72. package/dist/delegationFailure.js +25 -24
  73. package/dist/delegationFailure.js.map +1 -1
  74. package/dist/directoryCandidate.d.ts +2 -3
  75. package/dist/directoryCandidate.d.ts.map +1 -1
  76. package/dist/directoryCandidate.js +3 -2
  77. package/dist/directoryCandidate.js.map +1 -1
  78. package/dist/inrun-continuity-carrier.d.ts +76 -0
  79. package/dist/inrun-continuity-carrier.d.ts.map +1 -0
  80. package/dist/inrun-continuity-carrier.js +218 -0
  81. package/dist/inrun-continuity-carrier.js.map +1 -0
  82. package/dist/inrun-continuity-thread.d.ts +8 -0
  83. package/dist/inrun-continuity-thread.d.ts.map +1 -0
  84. package/dist/inrun-continuity-thread.js +23 -0
  85. package/dist/inrun-continuity-thread.js.map +1 -0
  86. package/dist/inrun-continuity-types.d.ts +105 -0
  87. package/dist/inrun-continuity-types.d.ts.map +1 -0
  88. package/dist/inrun-continuity-types.js +2 -0
  89. package/dist/inrun-continuity-types.js.map +1 -0
  90. package/dist/inrun-continuity.d.ts +79 -0
  91. package/dist/inrun-continuity.d.ts.map +1 -0
  92. package/dist/inrun-continuity.js +465 -0
  93. package/dist/inrun-continuity.js.map +1 -0
  94. package/dist/inrun-continuity.test-support.d.ts +374 -0
  95. package/dist/inrun-continuity.test-support.d.ts.map +1 -0
  96. package/dist/inrun-continuity.test-support.js +198 -0
  97. package/dist/inrun-continuity.test-support.js.map +1 -0
  98. package/dist/inrun-retained-output.d.ts +9 -0
  99. package/dist/inrun-retained-output.d.ts.map +1 -0
  100. package/dist/inrun-retained-output.js +16 -0
  101. package/dist/inrun-retained-output.js.map +1 -0
  102. package/dist/interaction.d.ts +1 -1
  103. package/dist/interaction.d.ts.map +1 -1
  104. package/dist/interaction.js +6 -1
  105. package/dist/interaction.js.map +1 -1
  106. package/dist/modelGovernance.d.ts +4 -0
  107. package/dist/modelGovernance.d.ts.map +1 -1
  108. package/dist/modelGovernance.js +27 -1
  109. package/dist/modelGovernance.js.map +1 -1
  110. package/dist/orchestrator-credentials.d.ts +1 -1
  111. package/dist/orchestrator-credentials.d.ts.map +1 -1
  112. package/dist/orchestrator-credentials.js +4 -3
  113. package/dist/orchestrator-credentials.js.map +1 -1
  114. package/dist/orchestrator.d.ts +4 -4
  115. package/dist/orchestrator.d.ts.map +1 -1
  116. package/dist/orchestrator.js +261 -235
  117. package/dist/orchestrator.js.map +1 -1
  118. package/dist/persistedPatch.d.ts +55 -0
  119. package/dist/persistedPatch.d.ts.map +1 -0
  120. package/dist/persistedPatch.js +132 -0
  121. package/dist/persistedPatch.js.map +1 -0
  122. package/dist/planFinalize.d.ts +37 -0
  123. package/dist/planFinalize.d.ts.map +1 -0
  124. package/dist/planFinalize.js +94 -0
  125. package/dist/planFinalize.js.map +1 -0
  126. package/dist/planRun.d.ts +2 -28
  127. package/dist/planRun.d.ts.map +1 -1
  128. package/dist/planRun.js +5 -88
  129. package/dist/planRun.js.map +1 -1
  130. package/dist/plannerAttempt.d.ts +4 -1
  131. package/dist/plannerAttempt.d.ts.map +1 -1
  132. package/dist/plannerAttempt.js +9 -4
  133. package/dist/plannerAttempt.js.map +1 -1
  134. package/dist/prompt-framing.d.ts +1 -1
  135. package/dist/prompt-framing.d.ts.map +1 -1
  136. package/dist/prompt-framing.js.map +1 -1
  137. package/dist/reviewerEffort.d.ts +41 -0
  138. package/dist/reviewerEffort.d.ts.map +1 -0
  139. package/dist/reviewerEffort.js +37 -0
  140. package/dist/reviewerEffort.js.map +1 -0
  141. package/dist/reviewerPanel.d.ts +5 -5
  142. package/dist/reviewerPanel.d.ts.map +1 -1
  143. package/dist/reviewerPanel.js +22 -50
  144. package/dist/reviewerPanel.js.map +1 -1
  145. package/dist/run-input-resolution.d.ts.map +1 -1
  146. package/dist/run-input-resolution.js +10 -0
  147. package/dist/run-input-resolution.js.map +1 -1
  148. package/dist/runAdmission.js +1 -1
  149. package/dist/runAdmission.js.map +1 -1
  150. package/dist/runEventLog.d.ts.map +1 -1
  151. package/dist/runEventLog.js +5 -0
  152. package/dist/runEventLog.js.map +1 -1
  153. package/dist/runFactsBuilder.d.ts.map +1 -1
  154. package/dist/runFactsBuilder.js +11 -2
  155. package/dist/runFactsBuilder.js.map +1 -1
  156. package/dist/runSupport.d.ts +2 -1
  157. package/dist/runSupport.d.ts.map +1 -1
  158. package/dist/runSupport.js +2 -1
  159. package/dist/runSupport.js.map +1 -1
  160. package/dist/runTelemetryWriter.d.ts.map +1 -1
  161. package/dist/runTelemetryWriter.js +7 -2
  162. package/dist/runTelemetryWriter.js.map +1 -1
  163. package/dist/runTerminalResults.d.ts +6 -2
  164. package/dist/runTerminalResults.d.ts.map +1 -1
  165. package/dist/runTerminalResults.js +8 -1
  166. package/dist/runTerminalResults.js.map +1 -1
  167. package/dist/runTerminals.d.ts.map +1 -1
  168. package/dist/runTerminals.js +6 -5
  169. package/dist/runTerminals.js.map +1 -1
  170. package/dist/secretDiff.d.ts +67 -27
  171. package/dist/secretDiff.d.ts.map +1 -1
  172. package/dist/secretDiff.js +143 -113
  173. package/dist/secretDiff.js.map +1 -1
  174. package/dist/session-capsule.d.ts +29 -0
  175. package/dist/session-capsule.d.ts.map +1 -0
  176. package/dist/session-capsule.js +86 -0
  177. package/dist/session-capsule.js.map +1 -0
  178. package/dist/workReportEnvelope.d.ts +123 -0
  179. package/dist/workReportEnvelope.d.ts.map +1 -0
  180. package/dist/workReportEnvelope.js +362 -0
  181. package/dist/workReportEnvelope.js.map +1 -0
  182. package/package.json +17 -17
  183. package/dist/effortGovernance.d.ts +0 -13
  184. package/dist/effortGovernance.d.ts.map +0 -1
  185. package/dist/effortGovernance.js +0 -7
  186. package/dist/effortGovernance.js.map +0 -1
@@ -2,17 +2,20 @@ 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";
12
14
  import { processAttemptUsage } from "./attemptUsage.js";
13
15
  import { captureDirectoryCandidate, directoryHasOutput, directoryCandidateStable, observeDirectoryPaths, publishDirectoryCandidate, } from "./directoryCandidate.js";
14
- import { appliedAttemptFacts, assertDelegatedEvidence, isMutatingAccess, outerBoundaryNotice, scopedHarnessHome, } from "./delegatedHome.js";
16
+ import { appliedAttemptFacts, assertDelegatedEvidence, isMutatingAccess, outerBoundaryNotice, scopedHarnessHome, selectLaneHome, followSpecHome, } 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);
@@ -992,10 +993,10 @@ export class Orchestrator {
992
993
  async lazyContextSection(input, contract, store, paths, log) {
993
994
  if (input.repoRoot === NO_PROJECT_ROOT || input.contextMode === "off")
994
995
  return "";
995
- // the versioned project config drives the context pack — mandatory files
996
- // (fail-closed when listed), plus include/exclude globs for the Scope Atlas.
996
+ // Project identity owns config; the execution tree supplies content.
997
+ // Mandatory files and include/exclude globs still come from project config.
997
998
  const projectCtx = this.projectConfig(input.repoRoot).context;
998
- const pack = await buildContextPack(input.repoRoot, contract, {
999
+ const pack = await buildContextPack(this.execRootOf(input), contract, {
999
1000
  mandatory: projectCtx.mandatory_files.length > 0 ? projectCtx.mandatory_files : undefined,
1000
1001
  include: projectCtx.include,
1001
1002
  exclude: projectCtx.exclude,
@@ -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,
@@ -1282,7 +1281,7 @@ export class Orchestrator {
1282
1281
  */
1283
1282
  harnessHomeFor(wsm, envelope, routed, runInput) {
1284
1283
  // Refuses (never silently degrades) when a delegated run cannot be confined.
1285
- return scopedHarnessHome(wsm, envelope, envelope.worktree_path === envelope.repo_root, runInput?.delegated === true, routed.adapterAccess);
1284
+ return scopedHarnessHome(wsm, envelope, envelope.worktree_path === envelope.repo_root, runInput?.delegated === true, routed.adapterAccess, runInput?.threadId ? (id) => this.laneHomeEnvFor(runInput, routed.adapter.id, id) : null);
1286
1285
  }
1287
1286
  async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, processingLease, access, onHarnessEvent, signal, modelHint, effortHint, intent, log, effectiveWebMode, interaction, budgetGuard, runInput, streamDeltas, fileBackedContext,
1288
1287
  /** D-16d: when set, the mechanical continuation checkpoint pointer for a
@@ -1305,13 +1304,14 @@ export class Orchestrator {
1305
1304
  manager: wsm,
1306
1305
  envelope,
1307
1306
  artifactRoot: join(paths.attemptsDir, attemptId),
1308
- sourceRoot: contract.repo.root,
1309
1307
  observedPaths: [...observedPaths],
1310
1308
  }));
1311
1309
  const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
1312
1310
  const sessionFields = runInput
1313
1311
  ? await this.sessionSpecFields(runInput, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission)
1314
1312
  : undefined;
1313
+ // A delegated thread's in-place lane home is keyed by the RESOLVED account.
1314
+ selectLaneHome(harnessHome, sessionFields?.credential_profile?.profile_id ?? runInput?.credentialProfileId ?? null);
1315
1315
  const processingAdmission = processingAdmissionForLease(ledger, processingLease.lease, adapter.id, attemptId, processingLease.onDenied);
1316
1316
  const capturedProcessing = {
1317
1317
  processing_preference: contract.processing_preference,
@@ -1381,21 +1381,21 @@ export class Orchestrator {
1381
1381
  // from the LIVE spec, so a W5.4 failover that rotates the credential
1382
1382
  // mid-attempt is re-recorded at the point it becomes true — the receipt
1383
1383
  // must name the profile that ran, never the one the request asked for.
1384
- const appliedNow = () => appliedAttemptFacts(harnessHome, spec.access, spec.credential_profile?.profile_id ?? runInput?.credentialProfileId ?? null);
1384
+ const appliedNow = () => appliedAttemptFacts(followSpecHome(harnessHome, spec, runInput?.credentialProfileId ?? null), spec.access, spec.credential_profile?.profile_id ?? runInput?.credentialProfileId ?? null);
1385
1385
  let applied = appliedNow();
1386
1386
  const attemptStartedMs = Date.now();
1387
1387
  const budgetSignalState = { quotaPressureDisclosed: false };
1388
- const triedProfiles = new Set(); // W5.4 failover: each profile at most once
1389
1388
  let cost = 0;
1390
1389
  let costEstimated = false;
1391
1390
  let harnessErrored = false;
1391
+ let streamBudgetDenied = false;
1392
1392
  let processingRefusal = null;
1393
1393
  let poolExhausted = null; // A5: typed pool-exhausted refusal
1394
1394
  const deltaFlood = { count: 0, disclosed: false }; // W-C4 per-attempt delta budget
1395
1395
  // QA-024: emit the belt-failure disclosure event at most once per attempt.
1396
1396
  let beltFailureDisclosed = false;
1397
1397
  const errors = [];
1398
- let answer = new AnswerAssembly();
1398
+ let answer = new secretDiff.CountedAnswerAssembly();
1399
1399
  const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
1400
1400
  // QA-024: the delegation belt is the ONLY engine-owned extra MCP server
1401
1401
  // injected into an agent lane (the browser MCP rides its own field), so its
@@ -1416,28 +1416,31 @@ export class Orchestrator {
1416
1416
  onAbort();
1417
1417
  else
1418
1418
  signal?.addEventListener("abort", onAbort, { once: true });
1419
+ let continuity;
1420
+ ({ continuity, spec } = await openContinuity(this.continuityDeps(adapter, routed, contract, attemptId, paths, runInput, log, {
1421
+ envelope,
1422
+ harnessHome,
1423
+ workOrder: prompt,
1424
+ firstPrompt: spec.prompt,
1425
+ }), { spec, continuation: runInput?.continuation, store, telemetry }));
1419
1426
  try {
1420
1427
  for (let nativeTry = 0; !signal?.aborted; nativeTry += 1) {
1421
1428
  // A3 per-try isolation: neither output nor progress markers leak across tries.
1422
1429
  if (nativeTry > 0) {
1423
- answer = new AnswerAssembly();
1430
+ answer = new secretDiff.CountedAnswerAssembly();
1424
1431
  telemetry.outputMarkers = newAttemptOutputMarkers();
1425
1432
  }
1426
1433
  const clearFileBackedContext = stageFileBackedContext(envelope.worktree_path, fileBackedContext);
1427
1434
  const runSpec = nativeTry === 0
1428
1435
  ? spec
1429
1436
  : 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.
1437
+ // The watchdog aborts only this try; run cancellation also prevents successors.
1436
1438
  const attemptAbort = new AbortController();
1437
1439
  runSpec.extra["abortSignal"] = signal
1438
1440
  ? AbortSignal.any([signal, attemptAbort.signal])
1439
1441
  : attemptAbort.signal;
1440
1442
  activeSessionId = runSpec.session_id;
1443
+ continuity.beginTry(runSpec, nativeTry, attemptAbort);
1441
1444
  // #363: bound before the spawn — a credential change mid-try voids it.
1442
1445
  const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
1443
1446
  const transientStart = telemetry.transientFailures.length;
@@ -1471,15 +1474,16 @@ export class Orchestrator {
1471
1474
  break;
1472
1475
  if (display)
1473
1476
  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
1477
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
1482
1478
  observeAttemptTelemetry(telemetry, safeEv);
1479
+ const mismatch = continuity.observe(safeEv, runSpec, telemetry.outputMarkers);
1480
+ if (mismatch) {
1481
+ harnessErrored = true;
1482
+ errors.push(mismatch);
1483
+ break;
1484
+ }
1485
+ if (inPlaceEnvelope && continuity.mayPublishSession)
1486
+ observeNativeSessionEvent(runInput, adapter.id, safeEv);
1483
1487
  // QA-024: the injected delegation belt's MCP server reported a
1484
1488
  // terminal startup failure. Disclose it ONCE while live; recoverable
1485
1489
  // exact tool-result failures are evaluated at attempt finalization.
@@ -1512,7 +1516,7 @@ export class Orchestrator {
1512
1516
  cost = usage.cost;
1513
1517
  costEstimated = usage.costEstimated;
1514
1518
  if (usage.hardCapReached) {
1515
- harnessErrored = true;
1519
+ harnessErrored = streamBudgetDenied = true;
1516
1520
  errors.push("budget hard cap reached mid-attempt; stream aborted");
1517
1521
  break;
1518
1522
  }
@@ -1523,7 +1527,7 @@ export class Orchestrator {
1523
1527
  }
1524
1528
  // Capture assistant prose so an answer-only turn (no file changes) still
1525
1529
  // has an honest output artifact; a TYPED final message wins verbatim.
1526
- answer.observe(safeEv);
1530
+ answer.observeCounted(ev, safeEv);
1527
1531
  // Observe ALL budget/quota signals (one codex usage event carries
1528
1532
  // BOTH spend and quota); pressure disclosed once per attempt.
1529
1533
  observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
@@ -1571,64 +1575,37 @@ export class Orchestrator {
1571
1575
  const deliverableEmpty = workspaceUnchanged && answer.text().length === 0;
1572
1576
  const delivered = !harnessErrored && !deliverableEmpty && !signal?.aborted;
1573
1577
  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
- }
1578
+ const verdict = await continuity.afterTry({
1579
+ runSpec,
1580
+ nativeTry,
1581
+ harnessErrored,
1582
+ budgetStopped: streamBudgetDenied,
1583
+ aborted: signal?.aborted === true,
1584
+ requestRefused: Boolean(telemetry.requestRefusal),
1585
+ newTransients,
1586
+ sawTypedLimit,
1587
+ sawRetryable,
1588
+ answer,
1589
+ rawDeliverableEmpty: deliverableEmpty,
1590
+ workspaceDiffNonEmpty: !workspaceUnchanged,
1591
+ currentDiff,
1592
+ markers: telemetry.outputMarkers,
1593
+ lastLimit: telemetry.rateLimits.at(-1) ?? null,
1594
+ refusal,
1595
+ telemetry,
1596
+ });
1597
+ if (verdict.kind === "terminal") {
1598
+ poolExhausted = verdict.error;
1599
+ errors.push(safeErrorMessage(poolExhausted));
1600
+ break;
1621
1601
  }
1622
- if (!harnessErrored ||
1623
- !sawRetryable ||
1624
- !deliverableEmpty ||
1625
- nativeTry >= retryPolicy.maxRetries ||
1626
- signal?.aborted)
1602
+ if (verdict.kind === "break")
1627
1603
  break;
1628
- const delayMs = emitTransientRetryPlan((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, nativeTry, retryPolicy);
1604
+ spec = verdict.spec;
1605
+ applied = appliedNow();
1629
1606
  errors.length = 0;
1630
1607
  harnessErrored = false;
1631
- await sleep(delayMs);
1608
+ await sleep(verdict.delayMs);
1632
1609
  }
1633
1610
  }
1634
1611
  finally {
@@ -1648,19 +1625,23 @@ export class Orchestrator {
1648
1625
  // with no typed final contributes no answer material (acceptedTryOutput).
1649
1626
  const unwrapped = unwrapWorkReportEnvelope(acceptedTryOutput(answer, harnessErrored), workReportMode, { sideToolReport: telemetry.sideToolWorkReport ?? undefined });
1650
1627
  const redacted = redactSecrets(unwrapped.deliverable);
1651
- const candidateAnswer = redacted.trim().length > 0 ? redacted : undefined;
1628
+ // INV-062: the (already redacted) answer is kept whatever the patch holds.
1629
+ const answerText = redacted.trim().length > 0 ? redacted : undefined;
1652
1630
  if (directory)
1653
1631
  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;
1632
+ const captured = directory
1633
+ ? { diff: "", captureRefusal: directoryCapture.refusal }
1634
+ : await secretDiff.captureCandidateWorkspace({
1635
+ ...{ wsm, envelope, inPlace: inPlaceEnvelope, projectRoot: store.repoRoot },
1636
+ ...{ answerText, answerMatches: answer.secretLikeMatches() },
1637
+ });
1638
+ const { diff, captureRefusal } = captured;
1639
+ harnessErrored = secretDiff.recordCaptureRefusal(captureRefusal, errors, harnessErrored);
1659
1640
  const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText) || directoryHasOutput(directoryCapture.files);
1660
1641
  // Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
1661
1642
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
1662
1643
  // still land, so partial work stays inspectable.
1663
- const gateSignalAborted = signal?.aborted === true || secretDiffRefusal !== undefined;
1644
+ const gateSignalAborted = signal?.aborted === true || captureRefusal !== undefined;
1664
1645
  if (!gateSignalAborted) {
1665
1646
  log?.emit("gate.started", {
1666
1647
  attempt_id: attemptId,
@@ -1707,6 +1688,7 @@ export class Orchestrator {
1707
1688
  workReport: unwrapped.workReport,
1708
1689
  workReportSource: unwrapped.source,
1709
1690
  workReportViolation: unwrapped.contractViolation,
1691
+ workReportUnverified: unwrapped.unverified ?? null,
1710
1692
  contextTerminalExhausted: telemetry.contextExhausted,
1711
1693
  });
1712
1694
  harnessErrored = finalized.harnessErrored;
@@ -1714,6 +1696,14 @@ export class Orchestrator {
1714
1696
  errors.push(`work_report contract: ${unwrapped.contractViolation}`);
1715
1697
  const deliverablePresent = finalized.deliverablePresent;
1716
1698
  const errored = harnessErrored || webBlocked;
1699
+ const resumable = await continuity.finish({
1700
+ runSpec: spec,
1701
+ errored,
1702
+ nonSuccess: !gatesPassed(gates),
1703
+ aborted: signal?.aborted === true,
1704
+ cancelReason: typeof signal?.reason === "string" ? signal.reason : null,
1705
+ workState: finalized.workState,
1706
+ });
1717
1707
  setAttemptOutcome(telemetry, {
1718
1708
  deliverablePresent,
1719
1709
  gatesPassed: gates.length > 0 ? gatesPassed(gates) : null,
@@ -1734,9 +1724,9 @@ export class Orchestrator {
1734
1724
  attemptDir,
1735
1725
  worktreePath: envelope.worktree_path,
1736
1726
  artifactRelativeDir,
1737
- diff,
1738
- persistPatch: !directory && secretDiffRefusal === undefined && isMutatingAccess(access),
1739
- persistProducedMedia: !directory && secretDiffRefusal === undefined,
1727
+ ...{ diff, persistedDiff: captured.persistedDiff },
1728
+ persistPatch: !directory && captureRefusal === undefined && isMutatingAccess(access),
1729
+ persistProducedMedia: !directory && captureRefusal === undefined,
1740
1730
  answerText,
1741
1731
  record: {
1742
1732
  attempt_id: attemptId,
@@ -1748,14 +1738,14 @@ export class Orchestrator {
1748
1738
  errors: errors.slice(0, 5),
1749
1739
  ...telemetrySummary(telemetry),
1750
1740
  outcome: telemetry.outcome,
1751
- ...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
1741
+ ...secretDiff.attemptDisclosure(captured),
1752
1742
  gates: gates.map((g) => ({ id: g.id, status: g.status })),
1753
1743
  branch: envelope.branch_name,
1754
1744
  ...(directoryCapture.files
1755
1745
  ? {
1756
1746
  files_manifest: directoryCapture.files.manifestPath,
1757
1747
  manifest_sha256: directoryCapture.files.manifestSha256,
1758
- source_root: contract.repo.root,
1748
+ source_root: directoryCapture.files.manifest.sourceRoot,
1759
1749
  execution_root: envelope.worktree_path,
1760
1750
  no_changes: directoryCapture.files.noChanges,
1761
1751
  }
@@ -1780,7 +1770,7 @@ export class Orchestrator {
1780
1770
  attemptId,
1781
1771
  harnessId: adapter.id,
1782
1772
  label,
1783
- diff,
1773
+ ...captured,
1784
1774
  ...(directoryCapture.files ? { files: directoryCapture.files } : {}),
1785
1775
  answerText,
1786
1776
  reviewCwd: envelope.worktree_path,
@@ -1792,7 +1782,6 @@ export class Orchestrator {
1792
1782
  costEstimated,
1793
1783
  errors: errors.slice(0, 8),
1794
1784
  telemetry,
1795
- ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1796
1785
  // A5: the typed refusal survives NORMAL attempt finalization (no throw).
1797
1786
  ...(telemetry.requestRefusal
1798
1787
  ? { declaredFailure: requestRefusalFailure(telemetry.requestRefusal) }
@@ -1801,14 +1790,47 @@ export class Orchestrator {
1801
1790
  : {}),
1802
1791
  outcomeClass: finalized.outcomeClass,
1803
1792
  applied,
1793
+ ...continuity.terminalFacts(resumable),
1804
1794
  };
1805
1795
  }
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);
1796
+ continuityDeps(adapter, routed, contract, attemptId, paths, runInput, log, lane) {
1797
+ const repoRoot = contract.repo.root;
1798
+ const cwd = lane.envelope?.worktree_path ?? (runInput ? this.execRootOf(runInput) : repoRoot);
1799
+ return {
1800
+ adapter,
1801
+ runId: paths.runId,
1802
+ attemptId,
1803
+ runDir: paths.root,
1804
+ attemptDir: join(paths.attemptsDir, attemptId),
1805
+ cwd,
1806
+ workspace: continuityWorkspace(lane.envelope, cwd),
1807
+ workOrder: lane.workOrder,
1808
+ firstPrompt: lane.firstPrompt,
1809
+ registry: this.config(repoRoot)?.global.credential_profiles ?? [],
1810
+ policy: this.credentials.profilePolicy(repoRoot, adapter.id),
1811
+ snapshots: this.deps.quotaSnapshots?.() ?? [],
1812
+ retryPolicy: transientRetryPolicy(this.config(repoRoot)),
1813
+ pinned: runInput?.credentialProfileId != null, // D-U6: a pin never rotates
1814
+ defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
1815
+ requestedProfileId: runInput?.credentialProfileId ?? null,
1816
+ rotationEnabled: runInput !== undefined,
1817
+ laneEnvFor: (id) => (runInput ? this.laneHomeEnvFor(runInput, adapter.id, id) : null),
1818
+ probeReadyProfiles: (spec, tried) => runInput
1819
+ ? this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, tried, spec.model_hint ?? null)
1820
+ : Promise.resolve(new Set()),
1821
+ rotationObservations: (spec, transients, refusal) => this.credentials.rotationObservations(adapter, spec, transients, refusal),
1822
+ emit: (type, payload) => log?.emit(type, payload),
1823
+ route: routed,
1824
+ newSessionId: () => newId("ses"),
1825
+ thread: runInput?.threadId
1826
+ ? {
1827
+ threadId: runInput.threadId,
1828
+ turnId: runInput.threadContinuity?.turnId ?? null,
1829
+ onSessionObserved: runInput.onSessionObserved,
1830
+ onContinuityResolved: runInput.onContinuityResolved,
1831
+ }
1832
+ : null,
1833
+ };
1812
1834
  }
1813
1835
  async runRace(input, mode, announce) {
1814
1836
  const taskId = input.taskId ?? newId("task");
@@ -1928,7 +1950,7 @@ export class Orchestrator {
1928
1950
  const disposeReviewEnvelopes = async () => {
1929
1951
  const envelopes = reviewEnvelopes.splice(0);
1930
1952
  for (const env of envelopes)
1931
- await wsm.dispose(env);
1953
+ await kept.settle(wsm, env, runsBySlot);
1932
1954
  };
1933
1955
  const candidateAccess = contract.access.effective_profile;
1934
1956
  let budgetStopped = false;
@@ -1937,6 +1959,7 @@ export class Orchestrator {
1937
1959
  let budgetDenial = null;
1938
1960
  let softWarned = false;
1939
1961
  const requestedSingleCandidate = adapters.length === 1;
1962
+ const kept = custody.forRun(input, requestedSingleCandidate && mutatingRun, runId, paths, log);
1940
1963
  const slots = [];
1941
1964
  for (let i = 0; i < adapters.length; i++) {
1942
1965
  const routed = adapters[i];
@@ -2036,7 +2059,7 @@ export class Orchestrator {
2036
2059
  external_context_policy: knobs.webPolicy,
2037
2060
  ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
2038
2061
  });
2039
- envelope = await wsm.create({
2062
+ envelope = await kept.envelope(wsm, {
2040
2063
  taskId,
2041
2064
  attemptId: slot.attemptId,
2042
2065
  baseRef: contract.repo.base_ref,
@@ -2058,7 +2081,7 @@ export class Orchestrator {
2058
2081
  budgetStopped = true;
2059
2082
  budgetDenial ??= denial;
2060
2083
  },
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) => {
2084
+ }, 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
2085
  ledger.updateHold(slot.lease.lease_id, streamedUsd);
2063
2086
  if (ledger.tier() !== "hard")
2064
2087
  return false;
@@ -2101,7 +2124,7 @@ export class Orchestrator {
2101
2124
  harness: adapter.id,
2102
2125
  profileId: input.credentialProfileId ?? null,
2103
2126
  priorPrompt: input.prompt,
2104
- priorOutput: run.answerText ?? run.diff ?? "",
2127
+ priorOutput: run.answerText ?? run.persistedDiff ?? run.diff ?? "",
2105
2128
  }));
2106
2129
  // Reserve the continuation lease BEFORE any disclosure: a denied lease
2107
2130
  // must never emit run.continuation (which claims a continuation
@@ -2137,7 +2160,7 @@ export class Orchestrator {
2137
2160
  budgetStopped = true;
2138
2161
  budgetDenial ??= denial;
2139
2162
  },
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) => {
2163
+ }, 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
2164
  ledger.updateHold(contLeaseId, streamedUsd);
2142
2165
  if (ledger.tier() !== "hard")
2143
2166
  return false;
@@ -2224,7 +2247,7 @@ export class Orchestrator {
2224
2247
  }
2225
2248
  finally {
2226
2249
  if (envelope)
2227
- await wsm.dispose(envelope); // no worktree leak even on create/run error
2250
+ await kept.settle(wsm, envelope, runsBySlot); // no leak on create/run error
2228
2251
  }
2229
2252
  };
2230
2253
  await runParallelCandidates(slots, this.deps.runtimeConcurrencyCaps, runSlot);
@@ -2265,7 +2288,7 @@ export class Orchestrator {
2265
2288
  input.workspaceKind !== "directory" &&
2266
2289
  input.inPlace &&
2267
2290
  requestedSingleCandidate &&
2268
- runs.every((run) => !run.secretDiffRefusal)) {
2291
+ runs.every((run) => !run.captureRefusal)) {
2269
2292
  try {
2270
2293
  earlyPostTurnSha = await snapshotTree(execRoot);
2271
2294
  }
@@ -2378,7 +2401,7 @@ export class Orchestrator {
2378
2401
  catch (err) {
2379
2402
  // Review preflight/evidence failures end TERMINALLY with artifacts —
2380
2403
  // never as an escaped throw that orphans the run dir.
2381
- return failTerminally(log, store, paths, runId, taskId, mode, "review", err, ledger.spend());
2404
+ return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "review", err);
2382
2405
  }
2383
2406
  finally {
2384
2407
  // Review preflight failures must not leak candidate worktrees.
@@ -2436,7 +2459,7 @@ export class Orchestrator {
2436
2459
  budgetStopped = true;
2437
2460
  budgetDenial ??= denial;
2438
2461
  },
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);
2462
+ }, 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
2463
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2441
2464
  reviewEnvelopes.push(envelope);
2442
2465
  envelope = undefined;
@@ -2483,7 +2506,7 @@ export class Orchestrator {
2483
2506
  }
2484
2507
  catch (err) {
2485
2508
  // Arbitration throws end terminally with artifacts, never as an orphan.
2486
- return failTerminally(log, store, paths, runId, taskId, mode, "arbitration", err, ledger.spend());
2509
+ return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "arbitration", err);
2487
2510
  }
2488
2511
  log.emit("arbitration.completed", {
2489
2512
  winner: result.decision.winner,
@@ -2629,9 +2652,8 @@ export class Orchestrator {
2629
2652
  });
2630
2653
  }
2631
2654
  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);
2655
+ const savedPatch = secretDiff.persistFinalPatch(store, paths.finalDir, winnerRun);
2656
+ const patchSha256 = savedPatch.patchSha256;
2635
2657
  const wstats = diffStats(winnerRun.diff);
2636
2658
  const hasDiff = winnerRun.diff.trim().length > 0;
2637
2659
  const blockers = winnerEvidence
@@ -2727,7 +2749,7 @@ export class Orchestrator {
2727
2749
  outcome_facts: facts,
2728
2750
  review_verified: actualReviewVerified,
2729
2751
  budget_stopped: budgetStopped,
2730
- patch_sha256: patchSha256,
2752
+ ...{ patch_sha256: patchSha256, ...savedPatch.meta },
2731
2753
  result_kind: resultKind,
2732
2754
  diffstat: {
2733
2755
  files: wstats.paths.length,
@@ -2744,7 +2766,7 @@ export class Orchestrator {
2744
2766
  },
2745
2767
  });
2746
2768
  }
2747
- store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified));
2769
+ store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified, secretDiff.secretLikeSummaryLine(winnerRun.secretLike)));
2748
2770
  // summary.md is a DIAGNOSTIC artifact only (V8/PLAN addendum 2): it no
2749
2771
  // longer carries primary-output authority. A clean applyable success or a
2750
2772
  // winner answer still marks it ready for legacy INV-116 ordering; any
@@ -2849,7 +2871,12 @@ export class Orchestrator {
2849
2871
  log.emit("work_product.emitted", { winner: result.decision.winner });
2850
2872
  }
2851
2873
  if (!isFailureTerminal) {
2852
- log.emit("run.completed", { lifecycle: facts.lifecycle, facts, reason: facts.reason });
2874
+ log.emit("run.completed", {
2875
+ lifecycle: facts.lifecycle,
2876
+ facts,
2877
+ reason: facts.reason,
2878
+ ...resumableTerminal(store, paths, winnerRun ? [winnerRun] : runs, isFailureTerminal),
2879
+ });
2853
2880
  }
2854
2881
  else if (facts.lifecycle === "succeeded") {
2855
2882
  // needsDecision at terminal — the event's phase must agree with
@@ -2857,6 +2884,7 @@ export class Orchestrator {
2857
2884
  log.emit("run.blocked", {
2858
2885
  lifecycle: facts.lifecycle,
2859
2886
  facts,
2887
+ ...resumableTerminal(store, paths, runs, isFailureTerminal),
2860
2888
  phase: deliveryFailureReason && !finalVerifyFailed
2861
2889
  ? "delivery"
2862
2890
  : finalVerifyFailed
@@ -2872,6 +2900,7 @@ export class Orchestrator {
2872
2900
  reason: facts.reason,
2873
2901
  phase: "arbitration",
2874
2902
  failure_ref: "final/failure.yaml",
2903
+ ...resumableTerminal(store, paths, runs, isFailureTerminal),
2875
2904
  });
2876
2905
  }
2877
2906
  return {
@@ -3233,7 +3262,7 @@ export class Orchestrator {
3233
3262
  exhausted = true;
3234
3263
  processingBudgetDenial ??= denial;
3235
3264
  },
3236
- }, convergenceAccess, input.onHarnessEvent, input.signal, undefined, input.effort, "repair", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive), (streamedUsd) => {
3265
+ }, convergenceAccess, input.onHarnessEvent, input.signal, undefined, input.effort, "repair", log, effectiveWeb, interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive), (streamedUsd) => {
3237
3266
  ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
3238
3267
  return ledger.tier() === "hard";
3239
3268
  }, input, true, // convergence runs one candidate: live deltas on (W-C4)
@@ -3282,11 +3311,10 @@ export class Orchestrator {
3282
3311
  manager: wsm,
3283
3312
  envelope,
3284
3313
  artifactRoot: join(paths.attemptsDir, attemptId),
3285
- sourceRoot: contract.repo.root,
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 {