@claudexor/orchestrator 3.19.0 → 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 (223) hide show
  1. package/dist/account-evidence.d.ts +13 -0
  2. package/dist/account-evidence.d.ts.map +1 -0
  3. package/dist/account-evidence.js +55 -0
  4. package/dist/account-evidence.js.map +1 -0
  5. package/dist/account-resolution.d.ts.map +1 -1
  6. package/dist/account-resolution.js +22 -5
  7. package/dist/account-resolution.js.map +1 -1
  8. package/dist/attemptFinalize.d.ts +14 -101
  9. package/dist/attemptFinalize.d.ts.map +1 -1
  10. package/dist/attemptFinalize.js +7 -328
  11. package/dist/attemptFinalize.js.map +1 -1
  12. package/dist/candidateEvidence.d.ts +15 -6
  13. package/dist/candidateEvidence.d.ts.map +1 -1
  14. package/dist/candidateEvidence.js +1 -1
  15. package/dist/candidateEvidence.js.map +1 -1
  16. package/dist/candidateOutputs.d.ts +7 -3
  17. package/dist/candidateOutputs.d.ts.map +1 -1
  18. package/dist/candidateOutputs.js +4 -1
  19. package/dist/candidateOutputs.js.map +1 -1
  20. package/dist/candidateReview.d.ts.map +1 -1
  21. package/dist/candidateReview.js +3 -1
  22. package/dist/candidateReview.js.map +1 -1
  23. package/dist/candidateTerminals.d.ts +10 -0
  24. package/dist/candidateTerminals.d.ts.map +1 -1
  25. package/dist/candidateTerminals.js +44 -5
  26. package/dist/candidateTerminals.js.map +1 -1
  27. package/dist/candidateWorkProduct.d.ts +21 -0
  28. package/dist/candidateWorkProduct.d.ts.map +1 -0
  29. package/dist/candidateWorkProduct.js +43 -0
  30. package/dist/candidateWorkProduct.js.map +1 -0
  31. package/dist/carrier-planner.d.ts +94 -0
  32. package/dist/carrier-planner.d.ts.map +1 -0
  33. package/dist/carrier-planner.js +111 -0
  34. package/dist/carrier-planner.js.map +1 -0
  35. package/dist/continuation-custody.d.ts +57 -0
  36. package/dist/continuation-custody.d.ts.map +1 -0
  37. package/dist/continuation-custody.js +151 -0
  38. package/dist/continuation-custody.js.map +1 -0
  39. package/dist/continuation-evidence-io.d.ts +18 -0
  40. package/dist/continuation-evidence-io.d.ts.map +1 -0
  41. package/dist/continuation-evidence-io.js +167 -0
  42. package/dist/continuation-evidence-io.js.map +1 -0
  43. package/dist/continuation-evidence.d.ts +81 -0
  44. package/dist/continuation-evidence.d.ts.map +1 -0
  45. package/dist/continuation-evidence.js +122 -0
  46. package/dist/continuation-evidence.js.map +1 -0
  47. package/dist/continuation-input.d.ts +10 -0
  48. package/dist/continuation-input.d.ts.map +1 -0
  49. package/dist/continuation-input.js +69 -0
  50. package/dist/continuation-input.js.map +1 -0
  51. package/dist/continue-from.d.ts +41 -0
  52. package/dist/continue-from.d.ts.map +1 -0
  53. package/dist/continue-from.js +292 -0
  54. package/dist/continue-from.js.map +1 -0
  55. package/dist/continuity-facts.d.ts +1 -0
  56. package/dist/continuity-facts.d.ts.map +1 -1
  57. package/dist/continuity-facts.js +1 -0
  58. package/dist/continuity-facts.js.map +1 -1
  59. package/dist/continuity-notice.d.ts +42 -0
  60. package/dist/continuity-notice.d.ts.map +1 -0
  61. package/dist/continuity-notice.js +42 -0
  62. package/dist/continuity-notice.js.map +1 -0
  63. package/dist/continuity-summary.d.ts +2 -1
  64. package/dist/continuity-summary.d.ts.map +1 -1
  65. package/dist/continuity-summary.js +2 -2
  66. package/dist/continuity-summary.js.map +1 -1
  67. package/dist/continuity-terminal.d.ts +19 -0
  68. package/dist/continuity-terminal.d.ts.map +1 -0
  69. package/dist/continuity-terminal.js +34 -0
  70. package/dist/continuity-terminal.js.map +1 -0
  71. package/dist/credential-cooldown.d.ts +0 -10
  72. package/dist/credential-cooldown.d.ts.map +1 -1
  73. package/dist/credential-cooldown.js +8 -27
  74. package/dist/credential-cooldown.js.map +1 -1
  75. package/dist/credential-differential.d.ts +13 -1
  76. package/dist/credential-differential.d.ts.map +1 -1
  77. package/dist/credential-differential.js +71 -14
  78. package/dist/credential-differential.js.map +1 -1
  79. package/dist/credential-execution.d.ts +4 -0
  80. package/dist/credential-execution.d.ts.map +1 -0
  81. package/dist/credential-execution.js +101 -0
  82. package/dist/credential-execution.js.map +1 -0
  83. package/dist/credential-profile-rotation.d.ts.map +1 -1
  84. package/dist/credential-profile-rotation.js +8 -2
  85. package/dist/credential-profile-rotation.js.map +1 -1
  86. package/dist/credential-profiles.d.ts +3 -4
  87. package/dist/credential-profiles.d.ts.map +1 -1
  88. package/dist/credential-profiles.js +28 -6
  89. package/dist/credential-profiles.js.map +1 -1
  90. package/dist/deepScanReducer.d.ts +4 -1
  91. package/dist/deepScanReducer.d.ts.map +1 -1
  92. package/dist/deepScanReducer.js +14 -8
  93. package/dist/deepScanReducer.js.map +1 -1
  94. package/dist/delegationFailure.d.ts +4 -3
  95. package/dist/delegationFailure.d.ts.map +1 -1
  96. package/dist/delegationFailure.js +32 -56
  97. package/dist/delegationFailure.js.map +1 -1
  98. package/dist/directoryCandidate.d.ts +2 -2
  99. package/dist/directoryCandidate.d.ts.map +1 -1
  100. package/dist/directoryCandidate.js +3 -0
  101. package/dist/directoryCandidate.js.map +1 -1
  102. package/dist/index.d.ts +4 -3
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +3 -2
  105. package/dist/index.js.map +1 -1
  106. package/dist/inrun-continuity-carrier.d.ts +76 -0
  107. package/dist/inrun-continuity-carrier.d.ts.map +1 -0
  108. package/dist/inrun-continuity-carrier.js +218 -0
  109. package/dist/inrun-continuity-carrier.js.map +1 -0
  110. package/dist/inrun-continuity-thread.d.ts +8 -0
  111. package/dist/inrun-continuity-thread.d.ts.map +1 -0
  112. package/dist/inrun-continuity-thread.js +23 -0
  113. package/dist/inrun-continuity-thread.js.map +1 -0
  114. package/dist/inrun-continuity-types.d.ts +105 -0
  115. package/dist/inrun-continuity-types.d.ts.map +1 -0
  116. package/dist/inrun-continuity-types.js +2 -0
  117. package/dist/inrun-continuity-types.js.map +1 -0
  118. package/dist/inrun-continuity.d.ts +79 -0
  119. package/dist/inrun-continuity.d.ts.map +1 -0
  120. package/dist/inrun-continuity.js +465 -0
  121. package/dist/inrun-continuity.js.map +1 -0
  122. package/dist/inrun-continuity.test-support.d.ts +374 -0
  123. package/dist/inrun-continuity.test-support.d.ts.map +1 -0
  124. package/dist/inrun-continuity.test-support.js +198 -0
  125. package/dist/inrun-continuity.test-support.js.map +1 -0
  126. package/dist/inrun-retained-output.d.ts +9 -0
  127. package/dist/inrun-retained-output.d.ts.map +1 -0
  128. package/dist/inrun-retained-output.js +16 -0
  129. package/dist/inrun-retained-output.js.map +1 -0
  130. package/dist/interaction.d.ts +1 -1
  131. package/dist/interaction.d.ts.map +1 -1
  132. package/dist/interaction.js +6 -1
  133. package/dist/interaction.js.map +1 -1
  134. package/dist/laneStreamEvents.d.ts +2 -4
  135. package/dist/laneStreamEvents.d.ts.map +1 -1
  136. package/dist/laneStreamEvents.js +3 -5
  137. package/dist/laneStreamEvents.js.map +1 -1
  138. package/dist/modelGovernance.d.ts +4 -0
  139. package/dist/modelGovernance.d.ts.map +1 -1
  140. package/dist/modelGovernance.js +29 -3
  141. package/dist/modelGovernance.js.map +1 -1
  142. package/dist/orchestrator-credentials.d.ts +5 -1
  143. package/dist/orchestrator-credentials.d.ts.map +1 -1
  144. package/dist/orchestrator-credentials.js +32 -8
  145. package/dist/orchestrator-credentials.js.map +1 -1
  146. package/dist/orchestrator.d.ts +7 -5
  147. package/dist/orchestrator.d.ts.map +1 -1
  148. package/dist/orchestrator.js +297 -267
  149. package/dist/orchestrator.js.map +1 -1
  150. package/dist/persistedPatch.d.ts +55 -0
  151. package/dist/persistedPatch.d.ts.map +1 -0
  152. package/dist/persistedPatch.js +132 -0
  153. package/dist/persistedPatch.js.map +1 -0
  154. package/dist/planFinalize.d.ts +37 -0
  155. package/dist/planFinalize.d.ts.map +1 -0
  156. package/dist/planFinalize.js +94 -0
  157. package/dist/planFinalize.js.map +1 -0
  158. package/dist/planRun.d.ts +3 -30
  159. package/dist/planRun.d.ts.map +1 -1
  160. package/dist/planRun.js +9 -88
  161. package/dist/planRun.js.map +1 -1
  162. package/dist/plannerAttempt.d.ts +4 -1
  163. package/dist/plannerAttempt.d.ts.map +1 -1
  164. package/dist/plannerAttempt.js +14 -8
  165. package/dist/plannerAttempt.js.map +1 -1
  166. package/dist/prompt-framing.d.ts +1 -1
  167. package/dist/prompt-framing.d.ts.map +1 -1
  168. package/dist/prompt-framing.js.map +1 -1
  169. package/dist/retainedOutput.d.ts +4 -0
  170. package/dist/retainedOutput.d.ts.map +1 -0
  171. package/dist/retainedOutput.js +31 -0
  172. package/dist/retainedOutput.js.map +1 -0
  173. package/dist/reviewerEffort.d.ts +41 -0
  174. package/dist/reviewerEffort.d.ts.map +1 -0
  175. package/dist/reviewerEffort.js +37 -0
  176. package/dist/reviewerEffort.js.map +1 -0
  177. package/dist/reviewerPanel.d.ts +5 -5
  178. package/dist/reviewerPanel.d.ts.map +1 -1
  179. package/dist/reviewerPanel.js +22 -50
  180. package/dist/reviewerPanel.js.map +1 -1
  181. package/dist/run-input-resolution.d.ts.map +1 -1
  182. package/dist/run-input-resolution.js +10 -0
  183. package/dist/run-input-resolution.js.map +1 -1
  184. package/dist/runEventLog.d.ts.map +1 -1
  185. package/dist/runEventLog.js +5 -0
  186. package/dist/runEventLog.js.map +1 -1
  187. package/dist/runFactsBuilder.d.ts.map +1 -1
  188. package/dist/runFactsBuilder.js +11 -2
  189. package/dist/runFactsBuilder.js.map +1 -1
  190. package/dist/runFactsPresentation.d.ts.map +1 -1
  191. package/dist/runFactsPresentation.js +6 -1
  192. package/dist/runFactsPresentation.js.map +1 -1
  193. package/dist/runSupport.d.ts +2 -1
  194. package/dist/runSupport.d.ts.map +1 -1
  195. package/dist/runSupport.js +2 -1
  196. package/dist/runSupport.js.map +1 -1
  197. package/dist/runTelemetryWriter.d.ts.map +1 -1
  198. package/dist/runTelemetryWriter.js +7 -2
  199. package/dist/runTelemetryWriter.js.map +1 -1
  200. package/dist/runTerminalResults.d.ts +6 -2
  201. package/dist/runTerminalResults.d.ts.map +1 -1
  202. package/dist/runTerminalResults.js +28 -3
  203. package/dist/runTerminalResults.js.map +1 -1
  204. package/dist/runTerminals.d.ts.map +1 -1
  205. package/dist/runTerminals.js +15 -7
  206. package/dist/runTerminals.js.map +1 -1
  207. package/dist/secretDiff.d.ts +67 -27
  208. package/dist/secretDiff.d.ts.map +1 -1
  209. package/dist/secretDiff.js +143 -113
  210. package/dist/secretDiff.js.map +1 -1
  211. package/dist/session-capsule.d.ts +29 -0
  212. package/dist/session-capsule.d.ts.map +1 -0
  213. package/dist/session-capsule.js +86 -0
  214. package/dist/session-capsule.js.map +1 -0
  215. package/dist/workReportEnvelope.d.ts +123 -0
  216. package/dist/workReportEnvelope.d.ts.map +1 -0
  217. package/dist/workReportEnvelope.js +362 -0
  218. package/dist/workReportEnvelope.js.map +1 -0
  219. package/package.json +17 -17
  220. package/dist/effortGovernance.d.ts +0 -13
  221. package/dist/effortGovernance.d.ts.map +0 -1
  222. package/dist/effortGovernance.js +0 -7
  223. 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) {
@@ -368,6 +370,8 @@ export class Orchestrator {
368
370
  quotaAbsences: () => this.deps.quotaAbsences?.() ?? [],
369
371
  credentialUnusable: () => this.deps.credentialUnusable?.() ?? [],
370
372
  recordCredentialUnusable: (obs) => this.deps.recordCredentialUnusable?.(obs),
373
+ credentialObserverFactory: () => this.deps.credentialObserverFactory,
374
+ credentialEvidence: () => this.deps.credentialEvidence,
371
375
  preProgressRefusals: () => this.deps.preProgressRefusals,
372
376
  authPreferenceForHarness: (repoRoot, harnessId, runPreference) => this.authPreferenceForHarness(repoRoot, harnessId, runPreference),
373
377
  });
@@ -657,7 +661,6 @@ export class Orchestrator {
657
661
  requiresFullAccess: manifest.capability_profile.mcp_injection_requires_full_access,
658
662
  fullAccess: isFullAccess(requiredAccess),
659
663
  }),
660
- effortLevels: manifest.capabilities.effort_levels,
661
664
  knownModels: manifest.capabilities.known_models,
662
665
  modelInventory: manifest.capabilities,
663
666
  // A selected profile's credential_kind IS the route (round-18 #2);
@@ -817,7 +820,7 @@ export class Orchestrator {
817
820
  // settles clean), so a schema run keeps its interaction channel — since
818
821
  // the daemon always arms one, the refusal was denying claude every
819
822
  // daemon/CLI structured-output run. The WorkReport side_tool envelope
820
- // stays interactive-gated separately (attemptFinalize) as a deliberate
823
+ // stays interactive-gated separately (workReportEnvelope) as a deliberate
821
824
  // scope choice; the caller schema itself rides regardless.
822
825
  if (input.outputSchema !== undefined && input.outputSchema !== null) {
823
826
  const incapable = out.filter((lane) => !lane.supportsJsonSchemaOutput);
@@ -1139,16 +1142,14 @@ export class Orchestrator {
1139
1142
  // per-harness map, which beats the per-harness settings default. There is
1140
1143
  // no run-global model.
1141
1144
  const model = overrideModel ?? contract.routing_models[routed.adapter.id] ?? s?.defaultModel ?? null;
1142
- // Effort disclosure (INV-105) against the harness's advertised ladder. This
1143
- // gate only DISCLOSES an unplaceable level; the clamp belongs to the adapter,
1144
- // which resolves against the catalog for the profile env the child runs in
1145
- // (the manifest here is the DEFAULT account's — see effortGovernance.ts). The
1146
- // contract's FROZEN per-lane effort (QA-035) wins so Exact Retry replays it
1147
- // without re-reading settings; `effortHint`/settings apply only to an unfrozen lane.
1148
- const governed = governRouteEffort(contract.routing_efforts[routed.adapter.id] ?? effortHint ?? s?.effort ?? null, { id: routed.adapter.id, ...routed });
1149
- const effort = governed.effort;
1150
- if (governed.ignored)
1151
- 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;
1152
1153
  return {
1153
1154
  model,
1154
1155
  effort,
@@ -1222,6 +1223,7 @@ export class Orchestrator {
1222
1223
  processing,
1223
1224
  processingAdmission,
1224
1225
  physicalDispatchStarted,
1226
+ credentialObserverFactory: this.deps.credentialObserverFactory,
1225
1227
  billingVerificationForProfile: (profile) => this.credentials.selectedProfileBillingVerification(profile),
1226
1228
  paidFallback: this.config(runInput.repoRoot).global.routing.paid_fallback,
1227
1229
  });
@@ -1382,17 +1384,17 @@ export class Orchestrator {
1382
1384
  let applied = appliedNow();
1383
1385
  const attemptStartedMs = Date.now();
1384
1386
  const budgetSignalState = { quotaPressureDisclosed: false };
1385
- const triedProfiles = new Set(); // W5.4 failover: each profile at most once
1386
1387
  let cost = 0;
1387
1388
  let costEstimated = false;
1388
1389
  let harnessErrored = false;
1390
+ let streamBudgetDenied = false;
1389
1391
  let processingRefusal = null;
1390
1392
  let poolExhausted = null; // A5: typed pool-exhausted refusal
1391
1393
  const deltaFlood = { count: 0, disclosed: false }; // W-C4 per-attempt delta budget
1392
1394
  // QA-024: emit the belt-failure disclosure event at most once per attempt.
1393
1395
  let beltFailureDisclosed = false;
1394
1396
  const errors = [];
1395
- let answer = new AnswerAssembly();
1397
+ let answer = new secretDiff.CountedAnswerAssembly();
1396
1398
  const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
1397
1399
  // QA-024: the delegation belt is the ONLY engine-owned extra MCP server
1398
1400
  // injected into an agent lane (the browser MCP rides its own field), so its
@@ -1413,28 +1415,31 @@ export class Orchestrator {
1413
1415
  onAbort();
1414
1416
  else
1415
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 }));
1416
1425
  try {
1417
1426
  for (let nativeTry = 0; !signal?.aborted; nativeTry += 1) {
1418
1427
  // A3 per-try isolation: neither output nor progress markers leak across tries.
1419
1428
  if (nativeTry > 0) {
1420
- answer = new AnswerAssembly();
1429
+ answer = new secretDiff.CountedAnswerAssembly();
1421
1430
  telemetry.outputMarkers = newAttemptOutputMarkers();
1422
1431
  }
1423
1432
  const clearFileBackedContext = stageFileBackedContext(envelope.worktree_path, fileBackedContext);
1424
1433
  const runSpec = nativeTry === 0
1425
1434
  ? spec
1426
1435
  : HarnessRunSpec.parse({ ...spec, session_id: newId("ses"), extra: { ...spec.extra } });
1427
- // Per-TRY abort controller: the inactivity watchdog aborts THIS try's
1428
- // stream (killing the process group through the existing abort
1429
- // plumbing) without touching the run-level cancel signal, so a timeout
1430
- // and a user cancel stay distinguishable. Recreated per
1431
- // nativeTry — a transient retry must get a LIVE signal, not the
1432
- // previous try's already-aborted composite.
1436
+ // The watchdog aborts only this try; run cancellation also prevents successors.
1433
1437
  const attemptAbort = new AbortController();
1434
1438
  runSpec.extra["abortSignal"] = signal
1435
1439
  ? AbortSignal.any([signal, attemptAbort.signal])
1436
1440
  : attemptAbort.signal;
1437
1441
  activeSessionId = runSpec.session_id;
1442
+ continuity.beginTry(runSpec, nativeTry, attemptAbort);
1438
1443
  // #363: bound before the spawn — a credential change mid-try voids it.
1439
1444
  const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
1440
1445
  const transientStart = telemetry.transientFailures.length;
@@ -1455,26 +1460,29 @@ export class Orchestrator {
1455
1460
  suspensionVersion: () => interaction?.suspensionVersion?.() ?? 0,
1456
1461
  });
1457
1462
  for await (const ev of watched) {
1458
- if (signal?.aborted)
1459
- break;
1460
1463
  rawPatch = captureRawPatchEnvelope(rawContextPacket !== null, rawPatch, ev);
1461
1464
  if (ev.type === "patch_produced")
1462
1465
  continue;
1463
1466
  const safeEv = redactHarnessEvent(ev);
1464
1467
  if (directory)
1465
1468
  observeDirectoryPaths(observedPaths, safeEv, envelope.worktree_path);
1466
- if (dropDeltaPastBudget(safeEv, deltaFlood, Orchestrator.MAX_DELTAS_PER_ATTEMPT, (t, p) => log?.emit(t, p), adapter.id, attemptId))
1467
- continue;
1468
- safeInvoke(onHarnessEvent, safeEv);
1469
- // In-place turns run in the live tree under the native environment, so
1470
- // the session they emit IS reachable for the next turn: record it. An
1471
- // ISOLATED envelope-born session lives in the scoped home that dispose()
1472
- // deletes, so observing it would poison the thread resume map with
1473
- // unreachable ids — skip it there.
1474
- if (inPlaceEnvelope)
1475
- observeNativeSessionEvent(runInput, adapter.id, safeEv);
1469
+ const capped = dropDeltaPastBudget(safeEv, deltaFlood, Orchestrator.MAX_DELTAS_PER_ATTEMPT, (t, p) => log?.emit(t, p), adapter.id, attemptId);
1470
+ const display = !capped && safeEv.payload?.["buffered"] !== true;
1471
+ log?.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv), display);
1472
+ if (signal?.aborted)
1473
+ break;
1474
+ if (display)
1475
+ safeInvoke(onHarnessEvent, safeEv);
1476
1476
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
1477
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);
1478
1486
  // QA-024: the injected delegation belt's MCP server reported a
1479
1487
  // terminal startup failure. Disclose it ONCE while live; recoverable
1480
1488
  // exact tool-result failures are evaluated at attempt finalization.
@@ -1507,7 +1515,7 @@ export class Orchestrator {
1507
1515
  cost = usage.cost;
1508
1516
  costEstimated = usage.costEstimated;
1509
1517
  if (usage.hardCapReached) {
1510
- harnessErrored = true;
1518
+ harnessErrored = streamBudgetDenied = true;
1511
1519
  errors.push("budget hard cap reached mid-attempt; stream aborted");
1512
1520
  break;
1513
1521
  }
@@ -1518,7 +1526,7 @@ export class Orchestrator {
1518
1526
  }
1519
1527
  // Capture assistant prose so an answer-only turn (no file changes) still
1520
1528
  // has an honest output artifact; a TYPED final message wins verbatim.
1521
- answer.observe(safeEv);
1529
+ answer.observeCounted(ev, safeEv);
1522
1530
  // Observe ALL budget/quota signals (one codex usage event carries
1523
1531
  // BOTH spend and quota); pressure disclosed once per attempt.
1524
1532
  observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
@@ -1566,64 +1574,37 @@ export class Orchestrator {
1566
1574
  const deliverableEmpty = workspaceUnchanged && answer.text().length === 0;
1567
1575
  const delivered = !harnessErrored && !deliverableEmpty && !signal?.aborted;
1568
1576
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
1569
- // W5.4 + A2 failover: a typed-limit hit OR a structural pre-progress
1570
- // death rebuilds the spec on a NEW session under the next profile.
1571
- if (harnessErrored && runInput && !signal?.aborted) {
1572
- const rotated = await rotateSpecOnTypedLimit({
1573
- spec,
1574
- harnessId: adapter.id,
1575
- attemptId,
1576
- policy: this.credentials.profilePolicy(contract.repo.root, adapter.id),
1577
- registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
1578
- snapshots: this.deps.quotaSnapshots?.() ?? [],
1579
- probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
1580
- ...this.credentials.rotationObservations(adapter, spec, newTransients, refusal),
1581
- triedProfiles,
1582
- markers: telemetry.outputMarkers,
1583
- requestRefused: Boolean(telemetry.requestRefusal),
1584
- sawTypedLimit,
1585
- sawRetryable,
1586
- attemptErrored: harnessErrored,
1587
- // Rotation evidence reads the POLICY-accepted try output: refusal
1588
- // prose arriving as mid-stream MESSAGE events (claude org-disabled)
1589
- // is no deliverable; the transient gate keeps RAW deliverableEmpty.
1590
- deliverableEmpty: workspaceUnchanged && acceptedTryOutput(answer, harnessErrored).length === 0,
1591
- workspaceDiffNonEmpty: !workspaceUnchanged,
1592
- lastLimit: telemetry.rateLimits.at(-1) ?? null,
1593
- emit: (type, payload) => log?.emit(type, payload),
1594
- newSessionId: () => newId("ses"),
1595
- defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
1596
- // D-U6: an explicit pin never rotates; pool-selected rows do.
1597
- pinned: runInput.credentialProfileId != null,
1598
- });
1599
- // A5 ordering: an exhausted pool terminalizes TYPED here, BEFORE the
1600
- // transient gate below burns same-profile retries (limits classify
1601
- // as retryable) on the already-refused subject.
1602
- if (rotated && "poolExhausted" in rotated) {
1603
- poolExhausted = rotated.poolExhausted;
1604
- errors.push(safeErrorMessage(poolExhausted));
1605
- break;
1606
- }
1607
- if (rotated) {
1608
- // INV-137: the rotated row runs in ITS OWN lane home (no-op for
1609
- // isolated envelopes and native-env in-place turns).
1610
- spec = rotatedSpecInLaneHome(spec, rotated, (id) => this.laneHomeEnvFor(runInput, adapter.id, id), runInput.credentialProfileId ?? null);
1611
- applied = appliedNow();
1612
- errors.length = 0;
1613
- harnessErrored = false;
1614
- continue;
1615
- }
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;
1616
1600
  }
1617
- if (!harnessErrored ||
1618
- !sawRetryable ||
1619
- !deliverableEmpty ||
1620
- nativeTry >= retryPolicy.maxRetries ||
1621
- signal?.aborted)
1601
+ if (verdict.kind === "break")
1622
1602
  break;
1623
- const delayMs = emitTransientRetryPlan((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, nativeTry, retryPolicy);
1603
+ spec = verdict.spec;
1604
+ applied = appliedNow();
1624
1605
  errors.length = 0;
1625
1606
  harnessErrored = false;
1626
- await sleep(delayMs);
1607
+ await sleep(verdict.delayMs);
1627
1608
  }
1628
1609
  }
1629
1610
  finally {
@@ -1643,19 +1624,23 @@ export class Orchestrator {
1643
1624
  // with no typed final contributes no answer material (acceptedTryOutput).
1644
1625
  const unwrapped = unwrapWorkReportEnvelope(acceptedTryOutput(answer, harnessErrored), workReportMode, { sideToolReport: telemetry.sideToolWorkReport ?? undefined });
1645
1626
  const redacted = redactSecrets(unwrapped.deliverable);
1646
- 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;
1647
1629
  if (directory)
1648
1630
  await captureDirectory();
1649
- const { diff, refusal: secretDiffRefusal } = directory
1650
- ? { diff: "", refusal: directoryCapture.refusal }
1651
- : await secretDiff.quarantineCandidateWorkspace(wsm, envelope, inPlaceEnvelope, candidateAnswer);
1652
- harnessErrored = secretDiff.recordSecretDiffRefusal(secretDiffRefusal, errors, harnessErrored);
1653
- 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);
1654
1639
  const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText) || directoryHasOutput(directoryCapture.files);
1655
1640
  // Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
1656
1641
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
1657
1642
  // still land, so partial work stays inspectable.
1658
- const gateSignalAborted = signal?.aborted === true || secretDiffRefusal !== undefined;
1643
+ const gateSignalAborted = signal?.aborted === true || captureRefusal !== undefined;
1659
1644
  if (!gateSignalAborted) {
1660
1645
  log?.emit("gate.started", {
1661
1646
  attempt_id: attemptId,
@@ -1702,6 +1687,7 @@ export class Orchestrator {
1702
1687
  workReport: unwrapped.workReport,
1703
1688
  workReportSource: unwrapped.source,
1704
1689
  workReportViolation: unwrapped.contractViolation,
1690
+ workReportUnverified: unwrapped.unverified ?? null,
1705
1691
  contextTerminalExhausted: telemetry.contextExhausted,
1706
1692
  });
1707
1693
  harnessErrored = finalized.harnessErrored;
@@ -1709,6 +1695,14 @@ export class Orchestrator {
1709
1695
  errors.push(`work_report contract: ${unwrapped.contractViolation}`);
1710
1696
  const deliverablePresent = finalized.deliverablePresent;
1711
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
+ });
1712
1706
  setAttemptOutcome(telemetry, {
1713
1707
  deliverablePresent,
1714
1708
  gatesPassed: gates.length > 0 ? gatesPassed(gates) : null,
@@ -1729,9 +1723,9 @@ export class Orchestrator {
1729
1723
  attemptDir,
1730
1724
  worktreePath: envelope.worktree_path,
1731
1725
  artifactRelativeDir,
1732
- diff,
1733
- persistPatch: !directory && secretDiffRefusal === undefined && isMutatingAccess(access),
1734
- persistProducedMedia: !directory && secretDiffRefusal === undefined,
1726
+ ...{ diff, persistedDiff: captured.persistedDiff },
1727
+ persistPatch: !directory && captureRefusal === undefined && isMutatingAccess(access),
1728
+ persistProducedMedia: !directory && captureRefusal === undefined,
1735
1729
  answerText,
1736
1730
  record: {
1737
1731
  attempt_id: attemptId,
@@ -1743,7 +1737,7 @@ export class Orchestrator {
1743
1737
  errors: errors.slice(0, 5),
1744
1738
  ...telemetrySummary(telemetry),
1745
1739
  outcome: telemetry.outcome,
1746
- ...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
1740
+ ...secretDiff.attemptDisclosure(captured),
1747
1741
  gates: gates.map((g) => ({ id: g.id, status: g.status })),
1748
1742
  branch: envelope.branch_name,
1749
1743
  ...(directoryCapture.files
@@ -1765,11 +1759,17 @@ export class Orchestrator {
1765
1759
  estimated: costEstimated,
1766
1760
  settlement: attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost),
1767
1761
  });
1762
+ for (const path of producedFiles)
1763
+ log?.emit("output.ready", {
1764
+ kind: "artifact",
1765
+ path: `attempts/${attemptId}/produced/${path}`,
1766
+ state: "diagnostic",
1767
+ });
1768
1768
  return {
1769
1769
  attemptId,
1770
1770
  harnessId: adapter.id,
1771
1771
  label,
1772
- diff,
1772
+ ...captured,
1773
1773
  ...(directoryCapture.files ? { files: directoryCapture.files } : {}),
1774
1774
  answerText,
1775
1775
  reviewCwd: envelope.worktree_path,
@@ -1781,7 +1781,6 @@ export class Orchestrator {
1781
1781
  costEstimated,
1782
1782
  errors: errors.slice(0, 8),
1783
1783
  telemetry,
1784
- ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1785
1784
  // A5: the typed refusal survives NORMAL attempt finalization (no throw).
1786
1785
  ...(telemetry.requestRefusal
1787
1786
  ? { declaredFailure: requestRefusalFailure(telemetry.requestRefusal) }
@@ -1790,14 +1789,47 @@ export class Orchestrator {
1790
1789
  : {}),
1791
1790
  outcomeClass: finalized.outcomeClass,
1792
1791
  applied,
1792
+ ...continuity.terminalFacts(resumable),
1793
1793
  };
1794
1794
  }
1795
- interactionChannelFor(input, log, runId, taskId, attemptId, harnessId,
1796
- // REQUIRED (no default): every call site must state the routed manifest's
1797
- // `interactive` capability, or a future site would silently bypass the gate.
1798
- supportsInteractive) {
1799
- // Thin delegate — the channel mechanics live in interaction.ts.
1800
- 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
+ };
1801
1833
  }
1802
1834
  async runRace(input, mode, announce) {
1803
1835
  const taskId = input.taskId ?? newId("task");
@@ -1917,7 +1949,7 @@ export class Orchestrator {
1917
1949
  const disposeReviewEnvelopes = async () => {
1918
1950
  const envelopes = reviewEnvelopes.splice(0);
1919
1951
  for (const env of envelopes)
1920
- await wsm.dispose(env);
1952
+ await kept.settle(wsm, env, runsBySlot);
1921
1953
  };
1922
1954
  const candidateAccess = contract.access.effective_profile;
1923
1955
  let budgetStopped = false;
@@ -1926,6 +1958,7 @@ export class Orchestrator {
1926
1958
  let budgetDenial = null;
1927
1959
  let softWarned = false;
1928
1960
  const requestedSingleCandidate = adapters.length === 1;
1961
+ const kept = custody.forRun(input, requestedSingleCandidate && mutatingRun, runId, paths, log);
1929
1962
  const slots = [];
1930
1963
  for (let i = 0; i < adapters.length; i++) {
1931
1964
  const routed = adapters[i];
@@ -2025,7 +2058,7 @@ export class Orchestrator {
2025
2058
  external_context_policy: knobs.webPolicy,
2026
2059
  ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
2027
2060
  });
2028
- envelope = await wsm.create({
2061
+ envelope = await kept.envelope(wsm, {
2029
2062
  taskId,
2030
2063
  attemptId: slot.attemptId,
2031
2064
  baseRef: contract.repo.base_ref,
@@ -2047,11 +2080,7 @@ export class Orchestrator {
2047
2080
  budgetStopped = true;
2048
2081
  budgetDenial ??= denial;
2049
2082
  },
2050
- }, candidateAccess, (ev) => {
2051
- const safeEv = redactHarnessEvent(ev);
2052
- safeInvoke(input.onHarnessEvent, safeEv);
2053
- log.emit("harness.event", harnessEventPayload(adapter.id, slot.attemptId, safeEv));
2054
- }, 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) => {
2055
2084
  ledger.updateHold(slot.lease.lease_id, streamedUsd);
2056
2085
  if (ledger.tier() !== "hard")
2057
2086
  return false;
@@ -2094,7 +2123,7 @@ export class Orchestrator {
2094
2123
  harness: adapter.id,
2095
2124
  profileId: input.credentialProfileId ?? null,
2096
2125
  priorPrompt: input.prompt,
2097
- priorOutput: run.answerText ?? run.diff ?? "",
2126
+ priorOutput: run.answerText ?? run.persistedDiff ?? run.diff ?? "",
2098
2127
  }));
2099
2128
  // Reserve the continuation lease BEFORE any disclosure: a denied lease
2100
2129
  // must never emit run.continuation (which claims a continuation
@@ -2130,11 +2159,7 @@ export class Orchestrator {
2130
2159
  budgetStopped = true;
2131
2160
  budgetDenial ??= denial;
2132
2161
  },
2133
- }, candidateAccess, (ev) => {
2134
- const safeEv = redactHarnessEvent(ev);
2135
- safeInvoke(input.onHarnessEvent, safeEv);
2136
- log.emit("harness.event", harnessEventPayload(adapter.id, contAttemptId, safeEv));
2137
- }, 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) => {
2138
2163
  ledger.updateHold(contLeaseId, streamedUsd);
2139
2164
  if (ledger.tier() !== "hard")
2140
2165
  return false;
@@ -2221,7 +2246,7 @@ export class Orchestrator {
2221
2246
  }
2222
2247
  finally {
2223
2248
  if (envelope)
2224
- 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
2225
2250
  }
2226
2251
  };
2227
2252
  await runParallelCandidates(slots, this.deps.runtimeConcurrencyCaps, runSlot);
@@ -2238,6 +2263,16 @@ export class Orchestrator {
2238
2263
  mode,
2239
2264
  ledger,
2240
2265
  runs,
2266
+ git: input.workspaceKind !== "directory"
2267
+ ? {
2268
+ live: input.inPlace === true &&
2269
+ requestedSingleCandidate &&
2270
+ runs[0]?.reviewCwd === execRoot,
2271
+ execRoot,
2272
+ preTurnSha,
2273
+ postTurnSha: earlyPostTurnSha,
2274
+ }
2275
+ : undefined,
2241
2276
  signal: input.signal,
2242
2277
  writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2243
2278
  });
@@ -2252,7 +2287,7 @@ export class Orchestrator {
2252
2287
  input.workspaceKind !== "directory" &&
2253
2288
  input.inPlace &&
2254
2289
  requestedSingleCandidate &&
2255
- runs.every((run) => !run.secretDiffRefusal)) {
2290
+ runs.every((run) => !run.captureRefusal)) {
2256
2291
  try {
2257
2292
  earlyPostTurnSha = await snapshotTree(execRoot);
2258
2293
  }
@@ -2365,7 +2400,7 @@ export class Orchestrator {
2365
2400
  catch (err) {
2366
2401
  // Review preflight/evidence failures end TERMINALLY with artifacts —
2367
2402
  // never as an escaped throw that orphans the run dir.
2368
- 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);
2369
2404
  }
2370
2405
  finally {
2371
2406
  // Review preflight failures must not leak candidate worktrees.
@@ -2423,11 +2458,7 @@ export class Orchestrator {
2423
2458
  budgetStopped = true;
2424
2459
  budgetDenial ??= denial;
2425
2460
  },
2426
- }, candidateAccess, (ev) => {
2427
- const safeEv = redactHarnessEvent(ev);
2428
- safeInvoke(input.onHarnessEvent, safeEv);
2429
- log.emit("harness.event", harnessEventPayload(synthAdapter.id, "synth", safeEv));
2430
- }, 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);
2431
2462
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2432
2463
  reviewEnvelopes.push(envelope);
2433
2464
  envelope = undefined;
@@ -2474,7 +2505,7 @@ export class Orchestrator {
2474
2505
  }
2475
2506
  catch (err) {
2476
2507
  // Arbitration throws end terminally with artifacts, never as an orphan.
2477
- 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);
2478
2509
  }
2479
2510
  log.emit("arbitration.completed", {
2480
2511
  winner: result.decision.winner,
@@ -2620,9 +2651,8 @@ export class Orchestrator {
2620
2651
  });
2621
2652
  }
2622
2653
  else if (mutatingRun) {
2623
- secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2624
- const patchSha256 = sha256(winnerRun.diff);
2625
- store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
2654
+ const savedPatch = secretDiff.persistFinalPatch(store, paths.finalDir, winnerRun);
2655
+ const patchSha256 = savedPatch.patchSha256;
2626
2656
  const wstats = diffStats(winnerRun.diff);
2627
2657
  const hasDiff = winnerRun.diff.trim().length > 0;
2628
2658
  const blockers = winnerEvidence
@@ -2718,7 +2748,7 @@ export class Orchestrator {
2718
2748
  outcome_facts: facts,
2719
2749
  review_verified: actualReviewVerified,
2720
2750
  budget_stopped: budgetStopped,
2721
- patch_sha256: patchSha256,
2751
+ ...{ patch_sha256: patchSha256, ...savedPatch.meta },
2722
2752
  result_kind: resultKind,
2723
2753
  diffstat: {
2724
2754
  files: wstats.paths.length,
@@ -2731,10 +2761,11 @@ export class Orchestrator {
2731
2761
  pre_turn_sha: preTurnSha,
2732
2762
  post_turn_sha: postTurnSha,
2733
2763
  revert_anchor_id: revertAnchorId,
2764
+ execution_root: adopted === true ? execRoot : null,
2734
2765
  },
2735
2766
  });
2736
2767
  }
2737
- 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)));
2738
2769
  // summary.md is a DIAGNOSTIC artifact only (V8/PLAN addendum 2): it no
2739
2770
  // longer carries primary-output authority. A clean applyable success or a
2740
2771
  // winner answer still marks it ready for legacy INV-116 ordering; any
@@ -2839,7 +2870,12 @@ export class Orchestrator {
2839
2870
  log.emit("work_product.emitted", { winner: result.decision.winner });
2840
2871
  }
2841
2872
  if (!isFailureTerminal) {
2842
- 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
+ });
2843
2879
  }
2844
2880
  else if (facts.lifecycle === "succeeded") {
2845
2881
  // needsDecision at terminal — the event's phase must agree with
@@ -2847,6 +2883,7 @@ export class Orchestrator {
2847
2883
  log.emit("run.blocked", {
2848
2884
  lifecycle: facts.lifecycle,
2849
2885
  facts,
2886
+ ...resumableTerminal(store, paths, runs, isFailureTerminal),
2850
2887
  phase: deliveryFailureReason && !finalVerifyFailed
2851
2888
  ? "delivery"
2852
2889
  : finalVerifyFailed
@@ -2862,6 +2899,7 @@ export class Orchestrator {
2862
2899
  reason: facts.reason,
2863
2900
  phase: "arbitration",
2864
2901
  failure_ref: "final/failure.yaml",
2902
+ ...resumableTerminal(store, paths, runs, isFailureTerminal),
2865
2903
  });
2866
2904
  }
2867
2905
  return {
@@ -2910,6 +2948,7 @@ export class Orchestrator {
2910
2948
  reviewScoped(input) {
2911
2949
  return reviewCandidate({
2912
2950
  ...input,
2951
+ credentialObserverFactory: this.deps.credentialObserverFactory,
2913
2952
  reviewerTimeoutMs: input.reviewerTimeoutMs ?? reviewerTimeoutMs(this.config(input.cwd)),
2914
2953
  transientRetryPolicy: input.transientRetryPolicy ?? transientRetryPolicy(this.config(input.cwd)),
2915
2954
  });
@@ -3222,11 +3261,7 @@ export class Orchestrator {
3222
3261
  exhausted = true;
3223
3262
  processingBudgetDenial ??= denial;
3224
3263
  },
3225
- }, convergenceAccess, (ev) => {
3226
- const safeEv = redactHarnessEvent(ev);
3227
- safeInvoke(input.onHarnessEvent, safeEv);
3228
- log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
3229
- }, 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) => {
3230
3265
  ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
3231
3266
  return ledger.tier() === "hard";
3232
3267
  }, input, true, // convergence runs one candidate: live deltas on (W-C4)
@@ -3279,7 +3314,7 @@ export class Orchestrator {
3279
3314
  observedPaths: [...observedPaths],
3280
3315
  });
3281
3316
  run.files = captured.files;
3282
- run.secretDiffRefusal = captured.refusal;
3317
+ run.captureRefusal = captured.refusal;
3283
3318
  }
3284
3319
  }
3285
3320
  lastRun = run;
@@ -3287,6 +3322,18 @@ export class Orchestrator {
3287
3322
  // per attempt, so the proof is spent per attempt.
3288
3323
  assertDelegatedEvidence(input.delegated === true, convergenceAccess, [run]);
3289
3324
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry: run.telemetry });
3325
+ lastPostTurnSha = null;
3326
+ // Post-mutation fence for in-place: snapshot the live tree NOW (after the
3327
+ // harness mutated it, before this attempt's review). The last attempt's
3328
+ // value is the revert target persisted into work_product.yaml.
3329
+ if (input.inPlace === true && input.workspaceKind !== "directory" && !run.captureRefusal) {
3330
+ try {
3331
+ lastPostTurnSha = await snapshotTree(execRoot);
3332
+ }
3333
+ catch {
3334
+ lastPostTurnSha = null;
3335
+ }
3336
+ }
3290
3337
  // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3291
3338
  if (input.signal?.aborted || processingBudgetDenial)
3292
3339
  break;
@@ -3319,7 +3366,7 @@ export class Orchestrator {
3319
3366
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null);
3320
3367
  return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
3321
3368
  }
3322
- if (input.workspaceKind === "directory" && run.secretDiffRefusal) {
3369
+ if (input.workspaceKind === "directory" && run.captureRefusal) {
3323
3370
  return failedCandidatesResult({
3324
3371
  ledger,
3325
3372
  mode,
@@ -3338,17 +3385,6 @@ export class Orchestrator {
3338
3385
  interrupted = true;
3339
3386
  break;
3340
3387
  }
3341
- // Post-mutation fence for in-place: snapshot the live tree NOW (after the
3342
- // harness mutated it, before this attempt's review). The last attempt's
3343
- // value is the revert target persisted into work_product.yaml.
3344
- if (input.inPlace === true && input.workspaceKind !== "directory") {
3345
- try {
3346
- lastPostTurnSha = await snapshotTree(execRoot);
3347
- }
3348
- catch {
3349
- lastPostTurnSha = null;
3350
- }
3351
- }
3352
3388
  // The review round is wrapped so a preflight/revalidation throw ends the
3353
3389
  // run TERMINALLY with artifacts instead of orphaning the run dir.
3354
3390
  let conv;
@@ -3399,7 +3435,7 @@ export class Orchestrator {
3399
3435
  facts: makeOutcomeFacts("failed", { noChanges: run.files.noChanges }),
3400
3436
  log,
3401
3437
  });
3402
- 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);
3403
3439
  }
3404
3440
  if (conv.converged) {
3405
3441
  converged = true;
@@ -3590,15 +3626,10 @@ export class Orchestrator {
3590
3626
  facts,
3591
3627
  log,
3592
3628
  });
3593
- if (lastRun.answerText) {
3594
- store.writeText(join(paths.finalDir, "answer.md"), lastRun.answerText);
3595
- log.emit("output.ready", { kind: "answer", path: "final/answer.md" });
3596
- }
3597
3629
  }
3598
3630
  else {
3599
- secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3600
- const patchSha256 = sha256(lastRun.diff);
3601
- store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
3631
+ const savedPatch = secretDiff.persistFinalPatch(store, paths.finalDir, lastRun);
3632
+ const patchSha256 = savedPatch.patchSha256;
3602
3633
  // Honest apply-state (parity with runRace single-candidate in-place): a
3603
3634
  // convergence run with inPlace mutated the live tree directly across its
3604
3635
  // attempts, so it is "applied" even when review blocked (Revert offered).
@@ -3625,16 +3656,22 @@ export class Orchestrator {
3625
3656
  lifecycle: facts.lifecycle,
3626
3657
  outcome_facts: facts,
3627
3658
  review_verified: actualReviewVerified,
3628
- patch_sha256: patchSha256,
3659
+ ...{ patch_sha256: patchSha256, ...savedPatch.meta },
3629
3660
  adopted: convAdopted,
3630
3661
  apply_state: convApplyState,
3631
3662
  pre_turn_sha: convAdopted === true ? preTurnSha : null,
3632
3663
  post_turn_sha: convAdopted === true ? lastPostTurnSha : null,
3633
3664
  revert_anchor_id: revertAnchorId,
3665
+ execution_root: convAdopted === true ? execRoot : null,
3634
3666
  },
3635
3667
  });
3636
3668
  }
3637
- 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`);
3638
3675
  // Lifecycle invariant (all modes): output.ready precedes the terminal
3639
3676
  // event so a client that applied the terminal event has the output.
3640
3677
  log.emit("output.ready", {
@@ -3646,6 +3683,7 @@ export class Orchestrator {
3646
3683
  // A needs-decision terminal (review blocked / checks failed) writes a
3647
3684
  // failure record + fires run.blocked even though the lifecycle succeeded.
3648
3685
  const convIsFailureTerminal = facts.lifecycle !== "succeeded" || convNeedsDecision;
3686
+ const continuation = resumableTerminal(store, paths, lastRun ? [lastRun] : [], convIsFailureTerminal);
3649
3687
  if (convIsFailureTerminal) {
3650
3688
  // A5 last-result lift: the LAST attempt's typed mid-attempt refusal
3651
3689
  // survives onto the terminal; budget/cancel/needs-decision outrank it.
@@ -3721,6 +3759,7 @@ export class Orchestrator {
3721
3759
  }
3722
3760
  if (!convIsFailureTerminal) {
3723
3761
  log.emit("run.completed", {
3762
+ ...continuation,
3724
3763
  lifecycle: facts.lifecycle,
3725
3764
  facts,
3726
3765
  reason: facts.reason,
@@ -3729,6 +3768,7 @@ export class Orchestrator {
3729
3768
  }
3730
3769
  else if (facts.lifecycle === "succeeded") {
3731
3770
  log.emit("run.blocked", {
3771
+ ...continuation,
3732
3772
  lifecycle: facts.lifecycle,
3733
3773
  facts,
3734
3774
  attempts: attempt,
@@ -3738,6 +3778,7 @@ export class Orchestrator {
3738
3778
  }
3739
3779
  else {
3740
3780
  log.emit("run.failed", {
3781
+ ...continuation,
3741
3782
  lifecycle: facts.lifecycle,
3742
3783
  facts,
3743
3784
  reason: facts.reason,
@@ -3816,7 +3857,7 @@ export class Orchestrator {
3816
3857
  spec.extra["abortSignal"] = input.signal
3817
3858
  ? AbortSignal.any([input.signal, plannerAbort.signal])
3818
3859
  : plannerAbort.signal;
3819
- 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);
3820
3861
  if (planInteraction)
3821
3862
  spec.extra["interactionChannel"] = planInteraction;
3822
3863
  const planWorkMode = applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
@@ -4409,18 +4450,14 @@ export class Orchestrator {
4409
4450
  : null) ?? roHome.env,
4410
4451
  });
4411
4452
  this.credentials.stampProfileSelection(spec, input, adapter.id);
4412
- const reportAbort = new AbortController();
4413
- spec.extra["abortSignal"] = input.signal
4414
- ? AbortSignal.any([input.signal, reportAbort.signal])
4415
- : reportAbort.signal;
4416
- 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);
4417
4454
  if (reportInteraction)
4418
4455
  spec.extra["interactionChannel"] = reportInteraction;
4419
4456
  // D-16: compile the WorkReport envelope for the read-only lane.
4420
4457
  const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4421
4458
  const readonlyWorkMode = applyWorkEnvelope(spec, readonlyWorkEnvelope);
4422
4459
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4423
- const answer = new AnswerAssembly();
4460
+ const answer = new secretDiff.CountedAnswerAssembly();
4424
4461
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [],
4425
4462
  // Requested-model capture so ask/audit route receipts detect a silent
4426
4463
  // model downgrade (typed model_mismatch), not just agent runs.
@@ -4428,7 +4465,6 @@ export class Orchestrator {
4428
4465
  return {
4429
4466
  knobs,
4430
4467
  spec,
4431
- reportAbort,
4432
4468
  reportInteraction,
4433
4469
  readonlyWorkMode,
4434
4470
  attemptEventsPath,
@@ -4470,13 +4506,20 @@ export class Orchestrator {
4470
4506
  }
4471
4507
  return { status: "launched" };
4472
4508
  }
4473
- const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, telemetry, } = preparation.value;
4509
+ const { knobs, spec: preparedSpec, reportInteraction, readonlyWorkMode, attemptEventsPath, telemetry, } = preparation.value;
4474
4510
  let { answer } = preparation.value;
4475
4511
  let spec = preparedSpec;
4476
4512
  bindProcessingAdmission(spec, ledger, lease.lease, adapter.id, attemptId, undefined, processingAdmission);
4477
4513
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4478
4514
  let activeSessionId = spec.session_id;
4479
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 }));
4480
4523
  const onAbort = () => void adapter.cancel?.(activeSessionId)?.catch(() => { });
4481
4524
  if (input.signal) {
4482
4525
  if (input.signal.aborted)
@@ -4491,12 +4534,11 @@ export class Orchestrator {
4491
4534
  let processingRefusal = null;
4492
4535
  let poolExhausted = null; // A5: typed pool-exhausted refusal
4493
4536
  try {
4494
- const triedProfiles = new Set(); // W5.4 failover: each profile at most once
4495
4537
  for (let nativeTry = 0; !input.signal?.aborted; nativeTry += 1) {
4496
4538
  // A3 per-try isolation (candidate-lane parity): neither a failed try's
4497
4539
  // output nor its progress markers leak into the next try's evidence.
4498
4540
  if (nativeTry > 0) {
4499
- answer = new AnswerAssembly();
4541
+ answer = new secretDiff.CountedAnswerAssembly();
4500
4542
  telemetry.outputMarkers = newAttemptOutputMarkers();
4501
4543
  }
4502
4544
  const runSpec = nativeTry === 0
@@ -4504,10 +4546,14 @@ export class Orchestrator {
4504
4546
  : HarnessRunSpec.parse({
4505
4547
  ...spec,
4506
4548
  session_id: newId("ses"),
4507
- resume_session_id: null,
4508
4549
  extra: { ...spec.extra },
4509
4550
  });
4510
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);
4511
4557
  // #363: bound before the spawn — a credential change mid-try voids it.
4512
4558
  const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
4513
4559
  const transientStart = telemetry.transientFailures.length;
@@ -4525,29 +4571,29 @@ export class Orchestrator {
4525
4571
  timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
4526
4572
  countsAsProgress: countsAsAgentProgress,
4527
4573
  onTimeout: () => {
4528
- reportAbort.abort();
4574
+ tryAbort.abort();
4529
4575
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
4530
4576
  },
4531
4577
  isSuspended: () => (reportInteraction?.pendingCount?.() ?? 0) > 0,
4532
4578
  suspensionVersion: () => reportInteraction?.suspensionVersion?.() ?? 0,
4533
4579
  });
4534
4580
  for await (const ev of watchedReport) {
4535
- if (input.signal?.aborted)
4536
- break;
4537
4581
  const safeEv = redactHarnessEvent(ev);
4538
- safeInvoke(input.onHarnessEvent, safeEv);
4539
- // A thread ASK turn IS a chat turn now (INV-034): its native
4540
- // session lives in the DURABLE per-lane home, so record it for the
4541
- // next lane turn's resume. The read-only fallback chain is
4542
- // sequential (never the parallel deep-scan swarm, which is
4543
- // excluded from `laneRun`), so recordSession's upsert keeps the
4544
- // latest lane session without a race.
4545
- if (laneRun)
4546
- observeNativeSessionEvent(input, adapter.id, safeEv);
4582
+ if (safeEv.payload?.["buffered"] !== true)
4583
+ safeInvoke(input.onHarnessEvent, safeEv);
4547
4584
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
4548
- log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
4585
+ log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv), safeEv.payload?.["buffered"] !== true);
4549
4586
  appendLine(attemptEventsPath, JSON.stringify(safeEv));
4587
+ if (input.signal?.aborted)
4588
+ break;
4550
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);
4551
4597
  emitPlanProgress((t, p) => log.emit(t, p), adapter.id, attemptId, safeEv);
4552
4598
  // read-only routes burn quota too (the orchestrate PLANNER is
4553
4599
  // the loudest) — same single owner as the agent loop.
@@ -4561,12 +4607,12 @@ export class Orchestrator {
4561
4607
  budgetStopped = streamBudgetDenied = true;
4562
4608
  budgetDenial ??= streamDenial;
4563
4609
  harnessError = streamDenial.reason;
4564
- reportAbort.abort();
4610
+ tryAbort.abort();
4565
4611
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
4566
4612
  break;
4567
4613
  }
4568
4614
  // A TYPED final message wins verbatim over joined narration.
4569
- answer.observe(safeEv);
4615
+ answer.observeCounted(ev, safeEv);
4570
4616
  if (safeEv.type === "error")
4571
4617
  harnessError = safeEv.error
4572
4618
  ? redactSecrets(safeEv.error)
@@ -4594,62 +4640,34 @@ export class Orchestrator {
4594
4640
  const reportSoFar = answer.text();
4595
4641
  const delivered = !harnessError && reportSoFar.length > 0 && !input.signal?.aborted;
4596
4642
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
4597
- // W5.4 + A2 reactive failover, READ-ONLY lane (same contract as the
4598
- // candidate lane: typed limit or structural pre-progress death).
4599
- if (harnessError && !input.signal?.aborted) {
4600
- const rotated = await rotateSpecOnTypedLimit({
4601
- spec,
4602
- harnessId: adapter.id,
4603
- attemptId,
4604
- policy: this.credentials.profilePolicy(input.repoRoot, adapter.id),
4605
- registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
4606
- snapshots: this.deps.quotaSnapshots?.() ?? [],
4607
- probeReadyProfiles: () => this.credentials.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles, spec.model_hint ?? null),
4608
- ...this.credentials.rotationObservations(adapter, spec, newTransients, refusal),
4609
- triedProfiles,
4610
- markers: telemetry.outputMarkers,
4611
- requestRefused: Boolean(telemetry.requestRefusal),
4612
- sawTypedLimit,
4613
- sawRetryable,
4614
- attemptErrored: harnessError !== null,
4615
- // Same policy owner as the candidate lane: an errored try's
4616
- // narration is not a report; the transient gate keeps reportSoFar.
4617
- deliverableEmpty: acceptedTryOutput(answer, harnessError !== null).length === 0,
4618
- lastLimit: telemetry.rateLimits.at(-1) ?? null,
4619
- emit: (type, payload) => log.emit(type, payload),
4620
- newSessionId: () => newId("ses"),
4621
- defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
4622
- // D-U6: an explicit pin never rotates; pool-selected rows do.
4623
- pinned: input.credentialProfileId != null,
4624
- });
4625
- // A5 ordering: same as the candidate lane — terminalize typed
4626
- // before the transient gate burns same-profile retries. The pool
4627
- // refusal APPENDS to the original harness error (candidate-lane
4628
- // parity: errors.push) instead of erasing the true failure.
4629
- if (rotated && "poolExhausted" in rotated) {
4630
- poolExhausted = rotated.poolExhausted;
4631
- harnessError = harnessError
4632
- ? `${harnessError}; ${safeErrorMessage(poolExhausted)}`
4633
- : safeErrorMessage(poolExhausted);
4634
- break;
4635
- }
4636
- if (rotated) {
4637
- // INV-137: the rotated row's session must land in ITS OWN lane
4638
- // home — never the previous row's lane store.
4639
- spec = rotatedSpecInLaneHome(spec, rotated, (id) => this.laneHomeEnvFor(input, adapter.id, id), input.credentialProfileId ?? null);
4640
- harnessError = null;
4641
- continue;
4642
- }
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;
4643
4665
  }
4644
- if (!harnessError ||
4645
- !sawRetryable ||
4646
- reportSoFar.length > 0 ||
4647
- nativeTry >= retryPolicy.maxRetries ||
4648
- input.signal?.aborted)
4666
+ if (verdict.kind === "break")
4649
4667
  break;
4650
- const delayMs = emitTransientRetryPlan((t, p) => log.emit(t, p), adapter.id, attemptId, telemetry, nativeTry, retryPolicy);
4668
+ spec = verdict.spec;
4651
4669
  harnessError = null;
4652
- await sleep(delayMs);
4670
+ await sleep(verdict.delayMs);
4653
4671
  }
4654
4672
  }
4655
4673
  finally {
@@ -4691,6 +4709,7 @@ export class Orchestrator {
4691
4709
  workReport: roUnwrapped.workReport,
4692
4710
  workReportSource: roUnwrapped.source,
4693
4711
  workReportViolation: roUnwrapped.contractViolation,
4712
+ workReportUnverified: roUnwrapped.unverified ?? null,
4694
4713
  contextTerminalExhausted: telemetry.contextExhausted,
4695
4714
  });
4696
4715
  // A broken WorkReport contract is a hard failure ONLY when the finalizer
@@ -4713,6 +4732,13 @@ export class Orchestrator {
4713
4732
  // interruptions keep the strict deliverable requirement.
4714
4733
  emptyDeliverableAllowed: roFinalized.outcomeClass === "clean",
4715
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
+ });
4716
4742
  if (harnessError) {
4717
4743
  log.emit("harness.completed", {
4718
4744
  harness_id: adapter.id,
@@ -4733,6 +4759,7 @@ export class Orchestrator {
4733
4759
  : poolExhausted || processingRefusal
4734
4760
  ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
4735
4761
  : {}),
4762
+ ...continuity.terminalFacts(roResumable),
4736
4763
  });
4737
4764
  if (opts.deepScan) {
4738
4765
  store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
@@ -4753,9 +4780,11 @@ export class Orchestrator {
4753
4780
  harnessId: adapter.id,
4754
4781
  status: scoutInterrupted ? "failed" : "success",
4755
4782
  report: report || "(no output)",
4783
+ answerMatches: answer.secretLikeMatches(),
4756
4784
  error: scoutInterrupted ? "context capacity exhausted before the scout completed" : null,
4757
4785
  telemetry,
4758
4786
  ...(scoutInterrupted ? { interrupted: true } : {}),
4787
+ ...continuity.terminalFacts(roResumable),
4759
4788
  });
4760
4789
  if (opts.deepScan) {
4761
4790
  const warningNote = toolWarnings(telemetry).length
@@ -4917,7 +4946,7 @@ export class Orchestrator {
4917
4946
  harnessId: a.harnessId,
4918
4947
  status: a.status,
4919
4948
  }));
4920
- 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"));
4921
4950
  if (input.signal?.aborted)
4922
4951
  return cancelledTerminal();
4923
4952
  const succeededReadonly = attempts.filter((a) => a.status === "success");
@@ -4943,14 +4972,6 @@ export class Orchestrator {
4943
4972
  fallbackOpen = false;
4944
4973
  }
4945
4974
  const partialReport = [...attempts].reverse().find((a) => a.report)?.report ?? "";
4946
- if (partialReport) {
4947
- store.writeText(join(paths.finalDir, opts.artifactName), `# ${opts.title}\n\n> Unverified partial output. The run is ${webBlocked ? "blocked" : "failed"} because a required/attempted tool failed.\n\n${partialReport}\n`);
4948
- log.emit("output.ready", {
4949
- kind: opts.mode === "ask" ? "answer" : "report",
4950
- path: `final/${opts.artifactName}`,
4951
- state: "diagnostic",
4952
- });
4953
- }
4954
4975
  this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null);
4955
4976
  store.writeText(join(paths.contextDir, "context_error.md"), `# ${budgetMapping ? "Budget Denied" : "Harness Error"}\n\n${singleError}\n`);
4956
4977
  const roEventRefs = attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`);
@@ -5002,6 +5023,7 @@ export class Orchestrator {
5002
5023
  harness_id: terminalHarnessId,
5003
5024
  error: singleError,
5004
5025
  failure_ref: "final/failure.yaml",
5026
+ ...resumableTerminal(store, paths, attempts, true),
5005
5027
  });
5006
5028
  }
5007
5029
  else {
@@ -5013,6 +5035,7 @@ export class Orchestrator {
5013
5035
  harness_id: terminalHarnessId,
5014
5036
  error: singleError,
5015
5037
  failure_ref: "final/failure.yaml",
5038
+ ...resumableTerminal(store, paths, attempts, true),
5016
5039
  });
5017
5040
  }
5018
5041
  return {
@@ -5086,6 +5109,7 @@ export class Orchestrator {
5086
5109
  phase: scanBudgetMapping ? scanBudgetMapping.phase : "harness",
5087
5110
  error: message,
5088
5111
  failure_ref: "final/failure.yaml",
5112
+ ...resumableTerminal(store, paths, attempts, true),
5089
5113
  });
5090
5114
  return {
5091
5115
  spendUsd: ledger.spend(),
@@ -5112,8 +5136,9 @@ export class Orchestrator {
5112
5136
  // whole decision + reducer spawn lives in deepScanReducer.ts (its owner).
5113
5137
  let deepScanSynthesis = null;
5114
5138
  let reducedReport = null;
5139
+ let answerMatches = succeeded.reduce((sum, a) => sum + (a.answerMatches ?? 0), 0);
5115
5140
  if (opts.deepScan) {
5116
- ({ deepScanSynthesis, reducedReport } = await resolveDeepScanSynthesis(this.deepScanReducerDeps(input, contract, log), {
5141
+ ({ deepScanSynthesis, reducedReport, answerMatches } = await resolveDeepScanSynthesis(this.deepScanReducerDeps(input, contract, log), {
5117
5142
  succeeded,
5118
5143
  adapters,
5119
5144
  budgetStopped,
@@ -5205,7 +5230,8 @@ export class Orchestrator {
5205
5230
  const harnessLabel = attempts
5206
5231
  .map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
5207
5232
  .join(", ");
5208
- 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`);
5209
5235
  const reportProducerAttemptId = opts.deepScan &&
5210
5236
  deepScanSynthesis?.status === "succeeded" &&
5211
5237
  deepScanSynthesis.reducer_attempt_id
@@ -5222,6 +5248,7 @@ export class Orchestrator {
5222
5248
  mode: opts.mode,
5223
5249
  intent: opts.intent,
5224
5250
  read_only: true,
5251
+ ...(secretLike ? { secret_like: secretLike } : {}),
5225
5252
  },
5226
5253
  });
5227
5254
  log.emit("work_product.emitted", { kind: "report", winner: reportProducerAttemptId });
@@ -5243,6 +5270,7 @@ export class Orchestrator {
5243
5270
  reason: terminalFacts.reason,
5244
5271
  phase: "executor",
5245
5272
  failure_ref: "final/failure.yaml",
5273
+ ...resumableTerminal(store, paths, attempts, true),
5246
5274
  });
5247
5275
  }
5248
5276
  else if (workVetoed) {
@@ -5250,6 +5278,7 @@ export class Orchestrator {
5250
5278
  lifecycle: terminalFacts.lifecycle,
5251
5279
  facts: terminalFacts,
5252
5280
  reason: terminalFacts.reason,
5281
+ ...resumableTerminal(store, paths, attempts, true),
5253
5282
  });
5254
5283
  }
5255
5284
  else {
@@ -5257,6 +5286,7 @@ export class Orchestrator {
5257
5286
  lifecycle: terminalFacts.lifecycle,
5258
5287
  facts: terminalFacts,
5259
5288
  reason: terminalFacts.reason,
5289
+ ...resumableTerminal(store, paths, attempts),
5260
5290
  });
5261
5291
  }
5262
5292
  return {