ai-foreman 1.7.29 → 2.0.1

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 (156) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +231 -26
  4. package/dist/adapters/codex.d.ts +19 -7
  5. package/dist/adapters/codex.js +202 -19
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +85 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/git.js +3 -8
  18. package/dist/branch/planner.js +1 -4
  19. package/dist/branch/runner.d.ts +2 -1
  20. package/dist/branch/runner.js +10 -7
  21. package/dist/buildAdmission.js +4 -0
  22. package/dist/buildApproval.d.ts +15 -0
  23. package/dist/buildApproval.js +28 -9
  24. package/dist/buildAssignment.d.ts +1 -0
  25. package/dist/buildAssignment.js +82 -23
  26. package/dist/buildInterventions.d.ts +2 -2
  27. package/dist/buildInterventions.js +11 -4
  28. package/dist/buildLaunchGate.js +3 -2
  29. package/dist/buildRuns.d.ts +1 -0
  30. package/dist/buildRuns.js +6 -1
  31. package/dist/builderGuidanceFollowup.js +17 -6
  32. package/dist/cli/doctor.js +3 -1
  33. package/dist/cli/manager.js +22 -20
  34. package/dist/cli/start.js +85 -39
  35. package/dist/cli/tickets.d.ts +0 -8
  36. package/dist/cli/tickets.js +50 -60
  37. package/dist/continuity.d.ts +10 -16
  38. package/dist/continuity.js +223 -208
  39. package/dist/continuityOrigin.d.ts +13 -0
  40. package/dist/continuityOrigin.js +91 -0
  41. package/dist/dispatchNotSent.d.ts +4 -0
  42. package/dist/dispatchNotSent.js +10 -0
  43. package/dist/foreman.d.ts +15 -16
  44. package/dist/foreman.js +70 -278
  45. package/dist/gitExclude.d.ts +16 -0
  46. package/dist/gitExclude.js +46 -0
  47. package/dist/graph/boundary.d.ts +1 -1
  48. package/dist/graph/boundary.js +1 -1
  49. package/dist/graph/host.js +8 -19
  50. package/dist/graph/manager.d.ts +1 -1
  51. package/dist/graph/manager.js +1 -1
  52. package/dist/graph/preparation.d.ts +38 -0
  53. package/dist/graph/preparation.js +7 -6
  54. package/dist/graph/semantic.d.ts +1 -0
  55. package/dist/graph/semantic.js +4 -1
  56. package/dist/graph/session.js +7 -2
  57. package/dist/graph/turn.d.ts +3 -4
  58. package/dist/graph/turn.js +29 -41
  59. package/dist/handoffProtocol.js +2 -2
  60. package/dist/handoffs.d.ts +5 -3
  61. package/dist/handoffs.js +92 -88
  62. package/dist/hostGraphApplicability.d.ts +2 -1
  63. package/dist/hostGraphApplicability.js +17 -19
  64. package/dist/hostQaAugmentation.js +21 -4
  65. package/dist/hostQaComparison.d.ts +21 -2
  66. package/dist/hostQaComparison.js +43 -29
  67. package/dist/hostSourcePreservation.d.ts +38 -0
  68. package/dist/hostSourcePreservation.js +72 -0
  69. package/dist/humanDecision.d.ts +7 -0
  70. package/dist/humanDecision.js +11 -0
  71. package/dist/interviews.d.ts +7 -0
  72. package/dist/interviews.js +18 -1
  73. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  74. package/dist/legacyCommunicationDisplay.js +2 -0
  75. package/dist/managerPacket.js +3 -3
  76. package/dist/markers.d.ts +4 -3
  77. package/dist/markers.js +4 -20
  78. package/dist/observability.js +3 -8
  79. package/dist/probeIpc.d.ts +14 -0
  80. package/dist/probeIpc.js +29 -0
  81. package/dist/qaBuildGate.d.ts +21 -0
  82. package/dist/qaBuildGate.js +60 -13
  83. package/dist/qaBuilderCoverage.js +22 -13
  84. package/dist/qaBuilderEvidence.d.ts +16 -1
  85. package/dist/qaBuilderEvidence.js +48 -8
  86. package/dist/qaContractCoverage.d.ts +2 -2
  87. package/dist/qaContractCoverage.js +4 -9
  88. package/dist/qaContractDelivery.js +34 -20
  89. package/dist/qaCoverageAssembly.d.ts +22 -0
  90. package/dist/qaCoverageAssembly.js +16 -0
  91. package/dist/qaDeliveryJournal.d.ts +2 -0
  92. package/dist/qaDeliveryJournal.js +19 -0
  93. package/dist/qaFailureDelivery.js +97 -85
  94. package/dist/qaHostTransformation.d.ts +26 -0
  95. package/dist/qaHostTransformation.js +100 -0
  96. package/dist/qaPreparation.d.ts +8 -20
  97. package/dist/qaPreparation.js +153 -254
  98. package/dist/qaPreparationInputs.js +19 -0
  99. package/dist/qaPreparationStore.d.ts +11 -0
  100. package/dist/qaPreparationStore.js +44 -4
  101. package/dist/qaPrerequisites.d.ts +2 -1
  102. package/dist/qaPrerequisites.js +21 -1
  103. package/dist/qaProtocolV2.d.ts +15 -0
  104. package/dist/qaRecovery.d.ts +0 -3
  105. package/dist/qaRecovery.js +21 -27
  106. package/dist/qaReview.js +241 -406
  107. package/dist/qaReviewEvidence.d.ts +7 -3
  108. package/dist/qaReviewEvidence.js +15 -4
  109. package/dist/qaRuntime.js +1 -1
  110. package/dist/qaSnapshot.js +76 -21
  111. package/dist/qaStructuredResponse.d.ts +18 -0
  112. package/dist/qaStructuredResponse.js +81 -0
  113. package/dist/qaTimeline.js +2 -2
  114. package/dist/qaVerificationContract.d.ts +1 -2
  115. package/dist/qaVerificationContract.js +24 -35
  116. package/dist/readinessGate.js +5 -4
  117. package/dist/recoveryOwners.d.ts +32 -0
  118. package/dist/recoveryOwners.js +87 -0
  119. package/dist/runtimePreflight.d.ts +11 -0
  120. package/dist/runtimePreflight.js +77 -0
  121. package/dist/runtimeReadiness.js +23 -15
  122. package/dist/sources/sourceRegistry.d.ts +0 -3
  123. package/dist/sources/sourceRegistry.js +0 -42
  124. package/dist/statusReporter.d.ts +2 -0
  125. package/dist/statusReporter.js +2 -0
  126. package/dist/structuredCutover.d.ts +31 -0
  127. package/dist/structuredCutover.js +510 -0
  128. package/dist/structuredCutoverFiles.d.ts +10 -0
  129. package/dist/structuredCutoverFiles.js +181 -0
  130. package/dist/structuredDispatchAuthority.d.ts +22 -0
  131. package/dist/structuredDispatchAuthority.js +110 -0
  132. package/dist/structuredDispatchRecord.d.ts +29 -0
  133. package/dist/structuredDispatchRecord.js +36 -0
  134. package/dist/structuredLiveBudget.d.ts +2 -0
  135. package/dist/structuredLiveBudget.js +23 -0
  136. package/dist/structuredOperation.d.ts +18 -0
  137. package/dist/structuredOperation.js +279 -0
  138. package/dist/supervisedStart.js +35 -21
  139. package/dist/ticketPlanning.d.ts +1 -376
  140. package/dist/ticketPlanning.js +2 -49
  141. package/dist/ticketPopulation.d.ts +0 -3
  142. package/dist/ticketPopulation.js +4 -21
  143. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  144. package/dist/ticketPopulationPageAuthority.js +30 -0
  145. package/dist/ticketPopulationPaging.d.ts +27 -0
  146. package/dist/ticketPopulationPaging.js +127 -0
  147. package/dist/tickets/renderMarkdown.d.ts +14 -0
  148. package/dist/tickets/renderMarkdown.js +39 -5
  149. package/dist/tickets/ticketLoader.js +10 -0
  150. package/dist/tickets/ticketSchema.d.ts +107 -0
  151. package/dist/tickets/ticketSchema.js +4 -1
  152. package/dist/workflowDb.d.ts +116 -3
  153. package/dist/workflowDb.js +852 -61
  154. package/dist/workflowReader.d.ts +1 -0
  155. package/dist/workflowReader.js +9 -0
  156. package/package.json +23 -7
@@ -1,6 +1,6 @@
1
1
  import type { ContinuityDelta, ResolvedAgentSettings } from "rafi-spec";
2
2
  import type { BuilderAdapter, CompactResult, ContextUsage, NativeAutoCompactionPolicy, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, RuntimeFailure, TurnResult } from "./adapters/types.js";
3
- export declare const CONTINUITY_MARKER = "RAFI_CONTINUITY_DELTA:";
3
+ import type { BuilderHandoffRequest } from "./handoffs.js";
4
4
  export declare class ContinuityValidationError extends Error {
5
5
  readonly problems: string[];
6
6
  constructor(problems: string[]);
@@ -22,15 +22,10 @@ export declare class SessionUnavailableContinuityError extends Error {
22
22
  readonly guidedRecovery: boolean;
23
23
  constructor(runId: string, role: "builder" | "qa", failure: RuntimeFailure);
24
24
  }
25
+ export declare const CONTINUITY_REQUIRED_INSTRUCTION = "Include the required bounded continuity object in the operation response; continuity cannot be null for this session.";
25
26
  export declare function validateContinuityDelta(value: unknown): ContinuityDelta;
26
- export declare function parseContinuityDelta(text: string): {
27
- delta?: ContinuityDelta;
28
- cleanText: string;
29
- error?: ContinuityValidationError;
30
- };
31
27
  export declare function mergeContinuityDeltas(deltas: readonly ContinuityDelta[]): ContinuityDelta;
32
28
  export declare function baselineContinuityDelta(nextAction?: string): ContinuityDelta;
33
- export declare function continuityInstruction(role?: "builder" | "qa"): string;
34
29
  export interface ContinuityAdapterOptions {
35
30
  adapter: BuilderAdapter;
36
31
  projectDir: string;
@@ -53,7 +48,7 @@ export interface ContinuityAdapterOptions {
53
48
  /** Explicit compatibility recovery may replace a dead predecessor lease only after its first valid checkpoint. */
54
49
  replaceRecoveryLeaseAfterCheckpoint?: boolean;
55
50
  /** Host-owned validated Builder handoff request processing at a safe turn boundary. */
56
- handleHandoffRequest?: (text: string, predecessor: BuilderAdapter, frozenAction: string) => Promise<BuilderAdapter | undefined>;
51
+ handleHandoffRequest?: (text: string | BuilderHandoffRequest, predecessor: BuilderAdapter, frozenAction: string) => Promise<BuilderAdapter | undefined>;
57
52
  /** QA journals each provider turn separately; wrappers must not repair or redispatch inside one host turn. */
58
53
  durableSingleTurn?: boolean;
59
54
  }
@@ -79,15 +74,16 @@ export declare class ContinuityAdapter implements BuilderAdapter {
79
74
  /** Establish an explicitly requested fresh owner before context probes or work. */
80
75
  validateFreshRecovery(): Promise<void>;
81
76
  get agent(): "claude" | "codex";
82
- sendTurn(instruction: string, policy?: {
83
- handback?: boolean;
84
- responseOnly?: boolean;
85
- logicalActionId?: string;
86
- purpose?: import("./providerPhase.js").ProviderTurnPurpose;
87
- }): Promise<TurnResult>;
77
+ /** Every confirmed dispatch publishes a checkpoint, so continuity is mandatory. */
78
+ structuredDispatchRequirement(): import("./adapters/types.js").StructuredDispatchRequirement;
79
+ sendTurn(instruction: string, policy?: import("./adapters/types.js").TurnRequest): Promise<TurnResult>;
88
80
  private sendTurnOwned;
81
+ postConfirmedDispatch(reference: import("./adapters/types.js").ConfirmedDispatchReference): Promise<TurnResult>;
89
82
  observeEvents(listener: (event: import("./adapters/types.js").BuilderEvent) => void): () => void;
90
83
  acceptHandbackTurn(turn: TurnResult): void;
84
+ /** Explicit restart recovery of the already accepted action; never repeats acceptance. */
85
+ resumePlannedContinuation(): Promise<TurnResult | undefined>;
86
+ recoverConfirmedPublications(): Promise<void>;
91
87
  sessionId(): string | undefined;
92
88
  sessionRef(): import("rafi-spec").ProviderSessionRefV1 | undefined;
93
89
  prepareSession(): Promise<import("rafi-spec").ProviderSessionRefV1>;
@@ -109,9 +105,7 @@ export declare class ContinuityAdapter implements BuilderAdapter {
109
105
  /** Keep the observable wrapper and event stream stable while moving to an accepted successor. */
110
106
  adoptValidatedSuccessor(successor: BuilderAdapter): Promise<void>;
111
107
  private publish;
112
- private moveRecoveryLeaseAfterCheckpoint;
113
108
  private recordSessionUnavailable;
114
- private reconstructionHandoff;
115
109
  private revision;
116
110
  private pumpEvents;
117
111
  }
@@ -1,11 +1,15 @@
1
+ import { persistedProviderWireContract } from "rafi-spec";
2
+ import { markDispatchNotSent } from "./dispatchNotSent.js";
1
3
  import { dispatchWithGraphAccess, withGraphSessionAccess } from "./graph/session.js";
2
4
  import { localBuildAuthority } from "./buildAdmission.js";
3
5
  import { randomUUID, createHash } from "node:crypto";
4
- import { continuityDeltaV1Schema, zodIssues } from "rafi-spec";
6
+ import { continuityDeltaV1Schema, initializationOutput, zodIssues } from "rafi-spec";
5
7
  import { BuilderEventQueue } from "./activity.js";
6
8
  import { providerSessionKey } from "./sessionIdentity.js";
9
+ import { nativePolicy, operationInstruction, journalStructuredDispatch, continuationPolicy } from "./structuredOperation.js";
7
10
  import { WorkflowDb } from "./workflowDb.js";
8
- export const CONTINUITY_MARKER = "RAFI_CONTINUITY_DELTA:";
11
+ import { assertSealedDispatch, withFreshRecoveryValidation } from "./structuredDispatchAuthority.js";
12
+ import { verifyPersistedDispatch } from "./structuredDispatchRecord.js";
9
13
  export class ContinuityValidationError extends Error {
10
14
  problems;
11
15
  constructor(problems) {
@@ -44,6 +48,7 @@ export class SessionUnavailableContinuityError extends Error {
44
48
  this.guidedRecovery = guidedRecovery;
45
49
  }
46
50
  }
51
+ export const CONTINUITY_REQUIRED_INSTRUCTION = "Include the required bounded continuity object in the operation response; continuity cannot be null for this session.";
47
52
  export function validateContinuityDelta(value) {
48
53
  const problems = [];
49
54
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -74,20 +79,6 @@ export function validateContinuityDelta(value) {
74
79
  throw new ContinuityValidationError(problems);
75
80
  return output;
76
81
  }
77
- export function parseContinuityDelta(text) {
78
- const lines = text.split(/\r?\n/);
79
- const markerIndexes = lines.flatMap((line, index) => line.trimStart().startsWith(CONTINUITY_MARKER) ? [index] : []);
80
- const cleanText = lines.filter((_line, index) => !markerIndexes.includes(index)).join("\n").trimEnd();
81
- if (markerIndexes.length !== 1)
82
- return { cleanText, error: new ContinuityValidationError([markerIndexes.length ? "multiple continuity markers" : "missing continuity marker"]) };
83
- const raw = lines[markerIndexes[0]].trimStart().slice(CONTINUITY_MARKER.length).trim();
84
- try {
85
- return { delta: validateContinuityDelta(JSON.parse(raw)), cleanText };
86
- }
87
- catch (error) {
88
- return { cleanText, error: error instanceof ContinuityValidationError ? error : new ContinuityValidationError([`marker is not valid JSON: ${String(error)}`]) };
89
- }
90
- }
91
82
  export function mergeContinuityDeltas(deltas) {
92
83
  const result = baselineContinuityDelta();
93
84
  for (const delta of deltas) {
@@ -102,15 +93,6 @@ export function mergeContinuityDeltas(deltas) {
102
93
  export function baselineContinuityDelta(nextAction = "Await the first role action.") {
103
94
  return { version: 1, decisions: [], constraints: [], discoveries: [], completedActions: [], evidence: [], failures: [], blockers: [], openWork: [], nextAction };
104
95
  }
105
- export function continuityInstruction(role) {
106
- return [
107
- "Before your final STEP_STATUS line (or as the final line when STEP_STATUS is not requested), emit exactly one single-line continuity record:",
108
- `${CONTINUITY_MARKER} {"version":1,"decisions":[],"constraints":[],"discoveries":[],"completedActions":[],"evidence":[],"failures":[],"blockers":[],"openWork":[],"nextAction":"specific next action"}`,
109
- "The continuity record is JSON. A requested STEP_STATUS line is separate plain text: never add JSON braces, a trailing comma, or a code fence to that status line.",
110
- ...(role === "qa" ? ["QA protocol: this continuity record is required even when the review requests only a report or status. Put it before RAFI_QA_FAILURE_REPORT_START when returning a failure envelope, outside the report JSON. Rafi strips the record before validating the report, so keep the report envelope immediately followed by STEP_STATUS."] : []),
111
- "Keep it concise and cumulative for facts learned this turn. Do not include credentials, hidden reasoning, or raw transcripts.",
112
- ].join("\n");
113
- }
114
96
  /** Enforces a durable role checkpoint after every completed provider turn. */
115
97
  export class ContinuityAdapter {
116
98
  options;
@@ -136,7 +118,7 @@ export class ContinuityAdapter {
136
118
  db.ensureRun(options.runId);
137
119
  if (!db.continuityHead(options.runId, options.role)) {
138
120
  db.appendContinuityEvent({ runId: options.runId, role: "host", kind: "role_baseline", payload: { role: options.role, provider: options.settings.make, model: options.settings.model }, authoritativeStateRevision: this.revision() });
139
- db.publishContinuityCheckpoint({ runId: options.runId, role: options.role, delta: baselineContinuityDelta(), authoritativeStateRevision: this.revision() });
121
+ db.publishContinuityCheckpoint({ runId: options.runId, role: options.role, delta: baselineContinuityDelta(), hostEventKind: "role-baseline", inputCheckpointDigests: [], authoritativeStateRevision: this.revision() });
140
122
  }
141
123
  const initialSession = options.adapter.sessionRef?.() ?? options.adapter.sessionId();
142
124
  if (initialSession)
@@ -166,7 +148,7 @@ export class ContinuityAdapter {
166
148
  }
167
149
  this.validatingFreshRecovery = true;
168
150
  try {
169
- const result = await this.sendTurn("Fresh recovery validation only. Do not use tools, change files, implement work, or request a handoff. Acknowledge this new session and emit the required continuity record using only the supplied instructions.", { responseOnly: true });
151
+ const result = await withFreshRecoveryValidation(this.options.projectDir, this.options.runId, this.options.role, authorization => this.sendTurn("Fresh recovery validation only. Do not use tools, change files, implement work, or request a handoff. Acknowledge this new session and include the required continuity object. " + operationInstruction(initializationOutput), nativePolicy(initializationOutput, { responseOnly: true, journalRunId: this.options.runId, dispatchId: `fresh-recovery:${authorization.decisionDigest}` })));
170
152
  if (result.isError || result.failure || this.recoveryLeasePending)
171
153
  throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "fresh recovery did not establish a valid continuity checkpoint");
172
154
  }
@@ -179,47 +161,84 @@ export class ContinuityAdapter {
179
161
  }
180
162
  }
181
163
  get agent() { return this.adapter.agent; }
164
+ /** Every confirmed dispatch publishes a checkpoint, so continuity is mandatory. */
165
+ structuredDispatchRequirement() {
166
+ return { instruction: CONTINUITY_REQUIRED_INSTRUCTION, validate: response => { validateContinuityDelta(response.continuity); } };
167
+ }
182
168
  async sendTurn(instruction, policy) {
183
- return withGraphSessionAccess(this.options.projectDir, this.adapter, () => this.sendTurnOwned(instruction, policy));
169
+ if (!policy?.output)
170
+ throw new Error("Continuity dispatch requires a native operation contract");
171
+ if (policy.dispatchCapability) {
172
+ assertSealedDispatch(this.options.projectDir, policy.dispatchCapability, policy.selectedDispatch, instruction);
173
+ return this.sendTurnOwned(instruction, policy);
174
+ }
175
+ // The continuity requirement is applied by journalStructuredDispatch through
176
+ // structuredDispatchRequirement(), before sealing, on every dispatch path.
177
+ const hostInstruction = instruction;
178
+ return withGraphSessionAccess(this.options.projectDir, this.adapter, () => policy?.output
179
+ ? journalStructuredDispatch(this.options.projectDir, this, instruction, { ...policy, journalRunId: policy.journalRunId ?? this.options.runId }, (text, selected) => this.sendTurnOwned(text, selected))
180
+ : this.sendTurnOwned(instruction, policy)).then(result => {
181
+ if (result.failure?.category === "session-unavailable") {
182
+ this.recordSessionUnavailable(result.failure, hostInstruction);
183
+ throw new SessionUnavailableContinuityError(this.options.runId, this.options.role, result.failure);
184
+ }
185
+ return { ...result, hostInstruction };
186
+ });
184
187
  }
185
188
  async sendTurnOwned(instruction, policy) {
186
- const providerInstruction = `${instruction}\n\n${continuityInstruction(this.options.role)}`;
189
+ const providerInstruction = instruction;
187
190
  const journal = new WorkflowDb(this.options.projectDir);
188
- const dispatchId = `dispatch:${this.options.runId}:${this.options.role}:${randomUUID()}`;
191
+ const dispatchId = policy?.dispatchId ?? `dispatch:${this.options.runId}:${this.options.role}:${randomUUID()}`;
192
+ const beforeSession = this.adapter.sessionRef?.();
189
193
  let original;
190
194
  try {
191
- journal.atomic(() => {
192
- if (this.buildAuthority)
193
- journal.assertBuildAdmission(this.buildAuthority);
194
- else if (journal.buildAdmission())
195
- throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "dispatch requires original build admission authority");
196
- if (process.send && process.env.RAFI_BUILD_WORKER_RUN === this.options.runId) {
197
- const supervisor = journal.supervisorState(this.options.runId);
198
- if (!process.connected || supervisor?.status !== "running" || supervisor.workerGeneration !== Number(process.env.RAFI_BUILD_WORKER_GENERATION))
199
- throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "supervised worker no longer owns dispatch authority");
200
- }
201
- const owner = journal.roleMutationLease(this.options.runId, this.options.role);
202
- const ref = this.adapter.sessionRef?.();
203
- const session = ref?.sessionId ?? this.adapter.sessionId();
204
- if (!(this.validatingFreshRecovery && this.recoveryLeasePending && policy?.responseOnly) && owner && session && (owner.providerSessionId !== session || (owner.sessionKey && ref && owner.sessionKey !== providerSessionKey(ref)))) {
205
- throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "provider session no longer owns role dispatch authority");
206
- }
207
- if (journal.unresolvedRoleDispatches(this.options.runId, this.options.role).length)
208
- throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "a previous dispatched turn has no durable completion; reconcile it before sending more work");
209
- journal.planOperation({ runId: this.options.runId, idempotencyKey: dispatchId, kind: "provider-dispatch", intent: { role: this.options.role, sessionRef: this.adapter.sessionRef?.(), instructionDigest: sha(instruction), instructionBytes: Buffer.byteLength(instruction) } });
210
- journal.updateOperation(dispatchId, "in_progress");
211
- journal.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: "turn_started", payload: { role: this.options.role, dispatchId, instructionDigest: sha(instruction), instructionBytes: Buffer.byteLength(instruction) }, authoritativeStateRevision: this.revision() });
212
- });
195
+ try {
196
+ journal.atomic(() => {
197
+ if (this.buildAuthority)
198
+ journal.assertBuildAdmission(this.buildAuthority);
199
+ else if (journal.buildAdmission())
200
+ throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "dispatch requires original build admission authority");
201
+ if (process.send && process.env.RAFI_BUILD_WORKER_RUN === this.options.runId) {
202
+ const supervisor = journal.supervisorState(this.options.runId);
203
+ if (!process.connected || supervisor?.status !== "running" || supervisor.workerGeneration !== Number(process.env.RAFI_BUILD_WORKER_GENERATION))
204
+ throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "supervised worker no longer owns dispatch authority");
205
+ }
206
+ const owner = journal.roleMutationLease(this.options.runId, this.options.role);
207
+ const ref = this.adapter.sessionRef?.();
208
+ const session = ref?.sessionId ?? this.adapter.sessionId();
209
+ if (!(this.validatingFreshRecovery && this.recoveryLeasePending && policy?.responseOnly) && owner && session && (owner.providerSessionId !== session || (owner.sessionKey && ref && owner.sessionKey !== providerSessionKey(ref)))) {
210
+ throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "provider session no longer owns role dispatch authority");
211
+ }
212
+ if (journal.unresolvedRoleDispatches(this.options.runId, this.options.role).filter(item => item.idempotencyKey !== dispatchId).length)
213
+ throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "a previous dispatched turn has no durable completion; reconcile it before sending more work");
214
+ if (!policy?.selectedDispatch)
215
+ journal.planOperation({ runId: this.options.runId, idempotencyKey: dispatchId, kind: "provider-dispatch", intent: { role: this.options.role, sessionRef: this.adapter.sessionRef?.(), instructionDigest: sha(instruction), instructionBytes: Buffer.byteLength(instruction) } });
216
+ if (!policy?.selectedDispatch)
217
+ journal.updateOperation(dispatchId, "in_progress");
218
+ journal.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: "turn_started", payload: { role: this.options.role, dispatchId, instructionDigest: sha(instruction), instructionBytes: Buffer.byteLength(instruction) }, authoritativeStateRevision: this.revision() });
219
+ });
220
+ }
221
+ catch (error) {
222
+ throw markDispatchNotSent(error);
223
+ }
213
224
  try {
214
225
  original = await dispatchWithGraphAccess(this.options.projectDir, this.adapter, providerInstruction, policy, (text, p) => this.adapter.sendTurn(text, p));
215
226
  }
216
227
  catch (error) {
217
- journal.updateOperation(dispatchId, "uncertain", { error: String(error) });
228
+ if (!policy?.selectedDispatch)
229
+ journal.updateOperation(dispatchId, "uncertain", { error: String(error) });
218
230
  throw error;
219
231
  }
220
- const responseDigest = journal.putEvidence("handoff", original.rawResponse ?? original.text);
232
+ // Preserve the native transport result before interpreting it or publishing
233
+ // a checkpoint. The outer journal remains the sole completion owner.
234
+ // journalStructuredDispatch owns the one retained raw receipt for native
235
+ // turns. Continuity records retain only its durable reference/fact.
236
+ const responseDigest = policy?.selectedDispatch ? undefined : journal.putEvidence("handoff", Buffer.from(JSON.stringify(original)));
221
237
  journal.atomic(() => {
222
- journal.updateOperation(dispatchId, original.failure?.dispatchState === "unknown" ? "uncertain" : original.failure?.dispatchState === "not-sent" ? "failed" : "confirmed", { result: { responseDigest, isError: original.isError, failure: original.failure, turnId: original.turnId, sessionRef: this.adapter.sessionRef?.() } });
238
+ journal.appendContinuityEvent({ runId: this.options.runId, role: this.options.role, kind: "provider_response_received",
239
+ payload: { dispatchId, ...(responseDigest ? { responseDigest } : { journalOwnedEvidence: true }) }, authoritativeStateRevision: this.revision() });
240
+ if (!policy?.selectedDispatch)
241
+ journal.updateOperation(dispatchId, original.failure?.dispatchState === "unknown" ? "uncertain" : original.failure?.dispatchState === "not-sent" ? "failed" : "confirmed", { result: { responseDigest, isError: original.isError, failure: original.failure, turnId: original.turnId, sessionRef: this.adapter.sessionRef?.() } });
223
242
  if (original.failure?.dispatchState === "not-sent")
224
243
  journal.appendContinuityEvent({ runId: this.options.runId, role: this.options.role, kind: "turn_not_sent", payload: { dispatchId, role: this.options.role }, authoritativeStateRevision: this.revision() });
225
244
  });
@@ -227,152 +246,162 @@ export class ContinuityAdapter {
227
246
  finally {
228
247
  journal.close();
229
248
  }
230
- if (policy?.handback) {
231
- const parsed = parseContinuityDelta(original.rawResponse ?? original.text);
232
- // The delivery journal owns the sole repair allowance and validates execution
233
- // before a checkpoint is published. Never transfer/replay inside this turn.
234
- return { ...original, text: parsed.cleanText, hostInstruction: instruction,
235
- providerInstruction: original.providerInstruction ?? providerInstruction,
236
- rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText,
237
- continuityErrors: parsed.error?.problems ?? [] };
249
+ return original;
250
+ }
251
+ async postConfirmedDispatch(reference) {
252
+ const db = new WorkflowDb(this.options.projectDir);
253
+ let original;
254
+ let context;
255
+ let response;
256
+ try {
257
+ const operation = db.operation(reference.dispatchId);
258
+ if (operation?.runId !== this.options.runId || reference.runId !== this.options.runId || db.structuredOwnerDispatch(reference.dispatchId).tupleDigest !== reference.tupleDigest)
259
+ throw new Error("Continuity postprocessing belongs to a foreign dispatch");
260
+ original = db.retainedStructuredTurn(reference.dispatchId);
261
+ context = db.structuredProcessingContext(reference.dispatchId);
262
+ const tuple = original.selectedDispatch;
263
+ const contract = persistedProviderWireContract(tuple).canonical;
264
+ response = db.replayStructuredDispatch(reference.dispatchId, contract);
238
265
  }
239
- const activeSession = this.adapter.sessionRef?.() ?? this.adapter.sessionId();
240
- if (activeSession) {
241
- const leaseDb = new WorkflowDb(this.options.projectDir);
266
+ finally {
267
+ db.close();
268
+ }
269
+ const delta = validateContinuityDelta(response.continuity);
270
+ if (this.recoveryLeasePending && (response.result.kind !== "completed" || !response.result.payload.ready))
271
+ throw new Error("Fresh recovery requires a completed initialization checkpoint");
272
+ if (!context.policy.handback) {
273
+ this.publish(delta, "turn_completed", original);
274
+ }
275
+ if (response.result.kind === "handoff-request") {
276
+ if (context.policy.handback || this.options.durableSingleTurn || this.validatingFreshRecovery)
277
+ throw new Error("This operation cannot transfer its session");
278
+ // Publication is complete before the explicitly linked acceptance stage.
279
+ const completed = new WorkflowDb(this.options.projectDir);
242
280
  try {
243
- leaseDb.claimInitialRoleLease(this.options.runId, this.options.role, activeSession);
281
+ completed.beginStructuredHandoff(reference.dispatchId);
282
+ completed.completeStructuredPostprocessing(reference.dispatchId);
244
283
  }
245
284
  finally {
246
- leaseDb.close();
285
+ completed.close();
247
286
  }
248
- }
249
- if (original.failure?.category === "session-unavailable") {
250
- this.recordSessionUnavailable(original.failure, instruction);
251
- throw new SessionUnavailableContinuityError(this.options.runId, this.options.role, original.failure);
252
- }
253
- if (original.isError || original.failure)
254
- return original;
255
- const parsed = parseContinuityDelta(original.text);
256
- if (parsed.delta && !original.isError && !original.failure) {
257
- this.publish(parsed.delta, "turn_completed", original);
258
- this.moveRecoveryLeaseAfterCheckpoint();
259
- if (this.options.durableSingleTurn || this.validatingFreshRecovery) {
260
- return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
261
- rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
287
+ const successor = await this.options.handleHandoffRequest?.({ version: 1, reason: response.result.reason, roleState: response.result.roleState, delta, predecessorDispatchId: reference.dispatchId }, this.adapter, context.instruction);
288
+ if (!successor || successor === this.adapter)
289
+ throw new Error("Handoff request did not establish a validated successor");
290
+ const continuation = new WorkflowDb(this.options.projectDir);
291
+ let continuationId;
292
+ try {
293
+ const session = successor.sessionRef?.();
294
+ if (!session)
295
+ throw new Error("Handoff successor has no scoped session");
296
+ continuationId = continuation.planStructuredContinuation(reference.dispatchId, session);
262
297
  }
263
- const successor = await this.options.handleHandoffRequest?.(original.text, this.adapter, instruction);
264
- if (successor && successor !== this.adapter) {
265
- await this.adoptValidatedSuccessor(successor);
266
- return this.sendTurn([
267
- "Continue the frozen action after the accepted Rafi handoff. Do not repeat completed side effects.",
268
- instruction,
269
- ].join("\n\n"), policy);
298
+ finally {
299
+ continuation.close();
270
300
  }
271
- return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
272
- rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
301
+ await this.adoptValidatedSuccessor(successor);
302
+ const tuple = original.selectedDispatch;
303
+ const output = persistedProviderWireContract(tuple).canonical;
304
+ return this.sendTurn("Continue the frozen action after the accepted handoff. Reconcile receipts; do not repeat completed side effects.\n" + context.instruction, nativePolicy(output, { ...continuationPolicy(context.policy, reference.dispatchId), dispatchId: continuationId }));
273
305
  }
274
- if (this.options.durableSingleTurn || (this.validatingFreshRecovery && this.recoveryLeasePending && policy?.responseOnly)) {
275
- const invalidDb = new WorkflowDb(this.options.projectDir);
306
+ return { ...original, hostInstruction: context.instruction, continuityErrors: [], continuity: delta };
307
+ }
308
+ observeEvents(listener) {
309
+ if (!this.adapter.observeEvents)
310
+ throw new Error("wrapped provider does not expose turn observation");
311
+ return this.adapter.observeEvents(listener);
312
+ }
313
+ acceptHandbackTurn(turn) {
314
+ if (turn.structured) {
315
+ if (turn.isError || turn.failure || turn.structured.state !== "candidate")
316
+ throw new Error("cannot publish an invalid handback checkpoint");
317
+ const db = new WorkflowDb(this.options.projectDir);
318
+ let retained;
319
+ try {
320
+ if (!turn.selectedDispatch || db.operation(turn.selectedDispatch.dispatchId)?.runId !== this.options.runId)
321
+ throw new Error("Foreign handback dispatch");
322
+ retained = db.retainedStructuredTurn(turn.selectedDispatch.dispatchId);
323
+ }
324
+ finally {
325
+ db.close();
326
+ }
327
+ const tuple = retained.selectedDispatch;
328
+ const owner = new WorkflowDb(this.options.projectDir);
276
329
  try {
277
- invalidDb.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: "continuity_invalid", payload: { problems: parsed.error?.problems }, authoritativeStateRevision: this.revision() });
330
+ const response = verifyPersistedDispatch(persistedProviderWireContract(tuple).canonical, owner.structuredOwnerDispatch(tuple.dispatchId), retained.structured, tuple);
331
+ this.publish(validateContinuityDelta(response.continuity), "handback_turn_completed", retained);
278
332
  }
279
333
  finally {
280
- invalidDb.close();
334
+ owner.close();
281
335
  }
282
- return { ...original, isError: true, continuityErrors: parsed.error?.problems ?? ["unknown continuity error"], text: `QA continuity record was invalid: ${parsed.error?.problems.join("; ") ?? "unknown error"}`,
283
- hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
284
- rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
336
+ return;
285
337
  }
286
- const budget = new WorkflowDb(this.options.projectDir);
338
+ throw new Error("Handback acceptance requires a native receipt and validated continuity");
339
+ }
340
+ /** Explicit restart recovery of the already accepted action; never repeats acceptance. */
341
+ async resumePlannedContinuation() {
342
+ const db = new WorkflowDb(this.options.projectDir);
343
+ let continuation;
344
+ let context;
345
+ let output;
287
346
  try {
288
- const maximum = budget.autonomyPolicy(this.options.runId)?.limits.protocolCorrections ?? 1;
289
- if (!budget.reserveRecoveryAllowance(this.options.runId, policy?.logicalActionId ?? sha(instruction), "protocol-correction", maximum)) {
290
- throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "shared protocol correction budget exhausted");
347
+ continuation = db.structuredContinuation(this.options.runId, this.options.role);
348
+ if (!continuation)
349
+ return;
350
+ const operation = db.operation(continuation.continuationId);
351
+ if (operation?.status === "confirmed") {
352
+ if (db.pendingStructuredPostprocessing(this.options.runId).includes(continuation.continuationId)) {
353
+ await this.postConfirmedDispatch({ dispatchId: continuation.continuationId, runId: this.options.runId,
354
+ tupleDigest: db.structuredOwnerDispatch(continuation.continuationId).tupleDigest });
355
+ db.completeStructuredPostprocessing(continuation.continuationId);
356
+ db.finishStructuredContinuation(continuation.continuationId);
357
+ }
358
+ const turn = db.retainedStructuredTurn(continuation.continuationId);
359
+ const descriptor = persistedProviderWireContract(turn.selectedDispatch);
360
+ const response = db.replayStructuredDispatch(continuation.continuationId, descriptor.canonical);
361
+ return { ...turn, continuity: validateContinuityDelta(response.continuity) };
291
362
  }
363
+ if (continuation.state !== "continuation-planned" || operation && operation.status !== "planned")
364
+ throw new Error("Continuation may already have been submitted; explicit reconciliation is required");
365
+ const current = this.adapter.sessionRef?.();
366
+ if (!current || providerSessionKey(current) !== providerSessionKey(continuation.successor))
367
+ throw new Error("Reattach the durably accepted successor before continuation recovery");
368
+ context = db.structuredProcessingContext(continuation.predecessorId);
369
+ const predecessor = db.retainedStructuredTurn(continuation.predecessorId);
370
+ output = persistedProviderWireContract(predecessor.selectedDispatch).canonical;
292
371
  }
293
372
  finally {
294
- budget.close();
295
- }
296
- const repair = await dispatchWithGraphAccess(this.options.projectDir, this.adapter, [
297
- "Continuity protocol repair only. Do not run tools, repeat work, or change files.",
298
- `Your prior turn's continuity record was invalid: ${parsed.error?.problems.join("; ")}.`,
299
- continuityInstruction(),
300
- "Return only the continuity record.",
301
- ].join("\n"), { ...policy, purpose: "response-repair", responseOnly: true }, (text, p) => this.adapter.sendTurn(text, p));
302
- const repaired = parseContinuityDelta(repair.text);
303
- if (repaired.delta && !repair.isError && !repair.failure) {
304
- this.publish(repaired.delta, "turn_completed_after_repair", original);
305
- this.moveRecoveryLeaseAfterCheckpoint();
306
- const successor = await this.options.handleHandoffRequest?.(original.text, this.adapter, instruction);
307
- if (successor && successor !== this.adapter) {
308
- await this.adoptValidatedSuccessor(successor);
309
- return this.sendTurn(["Continue the frozen action after the accepted Rafi handoff. Do not repeat completed side effects.", instruction].join("\n\n"), policy);
310
- }
311
- return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
312
- rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
373
+ db.close();
313
374
  }
314
- const invalidDb = new WorkflowDb(this.options.projectDir);
375
+ return this.sendTurn("Continue the frozen action after the accepted handoff. Reconcile receipts; do not repeat completed side effects.\n" + context.instruction, nativePolicy(output, { ...continuationPolicy(context.policy, continuation.predecessorId), dispatchId: continuation.continuationId }));
376
+ }
377
+ async recoverConfirmedPublications() {
378
+ const db = new WorkflowDb(this.options.projectDir);
379
+ let pending;
315
380
  try {
316
- invalidDb.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: "continuity_invalid_twice", payload: { original: parsed.error?.problems, repair: repaired.error?.problems }, authoritativeStateRevision: this.revision() });
317
- invalidDb.setContinuityHeadState(this.options.runId, this.options.role, "invalid");
381
+ pending = db.pendingStructuredPostprocessing(this.options.runId);
318
382
  }
319
383
  finally {
320
- invalidDb.close();
384
+ db.close();
321
385
  }
322
- const handoff = this.reconstructionHandoff();
323
- if (this.options.recoverWithHandoff) {
324
- let successor;
386
+ for (const dispatchId of pending) {
387
+ const current = new WorkflowDb(this.options.projectDir);
388
+ let reference;
325
389
  try {
326
- successor = await this.options.recoverWithHandoff({
327
- reason: "continuity delta and same-session repair were both invalid",
328
- reconstruction: handoff,
329
- predecessor: this.adapter,
330
- });
390
+ reference = { dispatchId, runId: this.options.runId, tupleDigest: current.structuredOwnerDispatch(dispatchId).tupleDigest };
331
391
  }
332
- catch (error) {
333
- throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, `fresh validated successor also failed after the same-session repair: ${error instanceof Error ? error.message : String(error)}`);
392
+ finally {
393
+ current.close();
334
394
  }
335
- if (!successor.sessionId() || successor.sessionId() === this.adapter.sessionId()) {
336
- await successor.close().catch(() => { });
337
- throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "validated recovery callback did not return a genuinely fresh successor");
395
+ await this.postConfirmedDispatch(reference);
396
+ const completed = new WorkflowDb(this.options.projectDir);
397
+ try {
398
+ completed.completeStructuredPostprocessing(dispatchId);
399
+ completed.finishStructuredContinuation(dispatchId);
400
+ }
401
+ finally {
402
+ completed.close();
338
403
  }
339
- await this.adoptValidatedSuccessor(successor);
340
- return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction,
341
- rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
342
- }
343
- if (!this.options.createSuccessor)
344
- throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "continuity repair failed in the original session and no validated successor is configured");
345
- const successor = await this.options.createSuccessor(handoff);
346
- const accepted = await dispatchWithGraphAccess(this.options.projectDir, successor, `${handoff}\n\nReply with HANDOFF_ACCEPTED on the first line, then ${continuityInstruction()}`, { purpose: "contract-acceptance", responseOnly: true }, (text, p) => successor.sendTurn(text, p));
347
- const successorDelta = parseContinuityDelta(accepted.text);
348
- const firstAcceptanceLine = accepted.text.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
349
- if (firstAcceptanceLine !== "HANDOFF_ACCEPTED" || !successorDelta.delta || !successor.sessionId()) {
350
- await successor.close().catch(() => { });
351
- throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "fresh successor did not validate and accept the cumulative checkpoint");
352
- }
353
- await this.adoptValidatedSuccessor(successor);
354
- this.publish(successorDelta.delta, "fresh_successor_accepted", accepted);
355
- const leaseDb = new WorkflowDb(this.options.projectDir);
356
- try {
357
- leaseDb.moveRoleLeaseAfterValidatedRecovery(this.options.runId, this.options.role, successor.sessionRef?.() ?? successor.sessionId(), "double-invalid continuity recovery");
358
- }
359
- finally {
360
- leaseDb.close();
361
404
  }
362
- return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction,
363
- rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
364
- }
365
- observeEvents(listener) {
366
- if (!this.adapter.observeEvents)
367
- throw new Error("wrapped provider does not expose turn observation");
368
- return this.adapter.observeEvents(listener);
369
- }
370
- acceptHandbackTurn(turn) {
371
- const parsed = parseContinuityDelta(turn.rawResponse ?? turn.text);
372
- if (turn.isError || turn.failure || !parsed.delta)
373
- throw new Error("cannot publish an invalid handback checkpoint");
374
- this.publish(parsed.delta, "handback_turn_completed", turn);
375
- this.moveRecoveryLeaseAfterCheckpoint();
376
405
  }
377
406
  sessionId() { return this.adapter.sessionId(); }
378
407
  sessionRef() { return this.adapter.sessionRef?.(); }
@@ -422,30 +451,32 @@ export class ContinuityAdapter {
422
451
  const db = new WorkflowDb(this.options.projectDir);
423
452
  try {
424
453
  const sessionRef = this.adapter.sessionRef?.();
425
- return db.atomic(() => {
426
- db.appendContinuityEvent({ runId: this.options.runId, role: this.options.role, kind, payload: { isError: result.isError, numTurns: result.numTurns, sessionId: this.adapter.sessionId(), sessionRef, delta }, authoritativeStateRevision: this.revision(), sessionRef });
427
- return db.publishContinuityCheckpoint({ runId: this.options.runId, role: this.options.role, delta, authoritativeStateRevision: this.revision(), sessionRef });
454
+ let moved = false;
455
+ const checkpoint = db.atomic(() => {
456
+ if (this.recoveryLeasePending && sessionRef)
457
+ db.assertFreshRecoveryValidation(result.selectedDispatch.dispatchId, sessionRef);
458
+ const prior = db.providerCheckpoint(result.selectedDispatch.dispatchId, kind);
459
+ if (prior) {
460
+ if (JSON.stringify(prior.delta) !== JSON.stringify(delta))
461
+ throw new Error("Conflicting repeated continuity publication");
462
+ return prior;
463
+ }
464
+ db.appendContinuityEvent({ runId: this.options.runId, role: this.options.role, kind, payload: { dispatchId: result.selectedDispatch.dispatchId, isError: result.isError, numTurns: result.numTurns, sessionId: this.adapter.sessionId(), sessionRef, delta }, authoritativeStateRevision: this.revision(), sessionRef });
465
+ const published = db.publishContinuityCheckpoint({ runId: this.options.runId, role: this.options.role, delta, authoritativeStateRevision: this.revision(), sessionRef, origin: { kind: "provider", dispatchId: result.selectedDispatch.dispatchId, publicationKind: kind } });
466
+ if (this.recoveryLeasePending && sessionRef) {
467
+ db.moveRoleLeaseAfterValidatedRecovery(this.options.runId, this.options.role, sessionRef, "explicit fresh-recovery-only continuation");
468
+ moved = true;
469
+ }
470
+ return published;
428
471
  });
472
+ if (moved)
473
+ this.recoveryLeasePending = false;
474
+ return checkpoint;
429
475
  }
430
476
  finally {
431
477
  db.close();
432
478
  }
433
479
  }
434
- moveRecoveryLeaseAfterCheckpoint() {
435
- if (!this.recoveryLeasePending)
436
- return;
437
- const session = this.adapter.sessionRef?.() ?? this.adapter.sessionId();
438
- if (!session)
439
- return;
440
- const db = new WorkflowDb(this.options.projectDir);
441
- try {
442
- db.moveRoleLeaseAfterValidatedRecovery(this.options.runId, this.options.role, session, "explicit fresh-recovery-only continuation");
443
- }
444
- finally {
445
- db.close();
446
- }
447
- this.recoveryLeasePending = false;
448
- }
449
480
  recordSessionUnavailable(failure, instruction) {
450
481
  const sessionRef = this.adapter.sessionRef?.();
451
482
  const db = new WorkflowDb(this.options.projectDir);
@@ -473,22 +504,6 @@ export class ContinuityAdapter {
473
504
  db.close();
474
505
  }
475
506
  }
476
- reconstructionHandoff() {
477
- const db = new WorkflowDb(this.options.projectDir);
478
- try {
479
- const checkpoints = db.continuityCheckpoints(this.options.runId, this.options.role);
480
- const events = db.continuityEvents(this.options.runId, checkpoints.at(-1)?.sequence ?? 0);
481
- return [
482
- `Validated cumulative recovery for run ${this.options.runId}, role ${this.options.role}.`,
483
- JSON.stringify(mergeContinuityDeltas(checkpoints.map((checkpoint) => checkpoint.delta))),
484
- `Later host facts: ${JSON.stringify(events.map((event) => ({ kind: event.kind, payload: event.payload, digest: event.digest })))}`,
485
- "The prior in-flight operation is unknown unless a host fact proves completion. Reconcile receipts before retrying side effects.",
486
- ].join("\n\n");
487
- }
488
- finally {
489
- db.close();
490
- }
491
- }
492
507
  revision() { return this.options.authoritativeStateRevision?.() ?? this.options.settings.settings_revision; }
493
508
  pumpEvents() {
494
509
  const source = this.adapter;