ai-foreman 1.7.28 → 2.0.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 (144) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +228 -24
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  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 +86 -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/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +30 -0
  22. package/dist/buildApproval.js +68 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +32 -24
  31. package/dist/cli/tickets.js +40 -24
  32. package/dist/continuity.d.ts +10 -14
  33. package/dist/continuity.js +198 -179
  34. package/dist/continuityOrigin.d.ts +13 -0
  35. package/dist/continuityOrigin.js +91 -0
  36. package/dist/foreman.d.ts +20 -5
  37. package/dist/foreman.js +57 -187
  38. package/dist/graph/boundary.d.ts +1 -1
  39. package/dist/graph/boundary.js +1 -1
  40. package/dist/graph/host.js +8 -19
  41. package/dist/graph/manager.d.ts +1 -1
  42. package/dist/graph/manager.js +1 -1
  43. package/dist/graph/preparation.d.ts +38 -0
  44. package/dist/graph/preparation.js +7 -6
  45. package/dist/graph/semantic.d.ts +1 -0
  46. package/dist/graph/semantic.js +4 -1
  47. package/dist/graph/session.js +7 -2
  48. package/dist/graph/turn.d.ts +2 -3
  49. package/dist/graph/turn.js +28 -40
  50. package/dist/handoffProtocol.js +2 -2
  51. package/dist/handoffs.d.ts +3 -1
  52. package/dist/handoffs.js +90 -59
  53. package/dist/hostGraphApplicability.d.ts +46 -0
  54. package/dist/hostGraphApplicability.js +97 -0
  55. package/dist/hostQaAugmentation.d.ts +18 -0
  56. package/dist/hostQaAugmentation.js +81 -0
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -28
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +48 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +11 -7
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaEffectiveConfig.d.ts +1 -0
  83. package/dist/qaEffectiveConfig.js +6 -1
  84. package/dist/qaFailureDelivery.js +97 -85
  85. package/dist/qaHostTransformation.d.ts +26 -0
  86. package/dist/qaHostTransformation.js +100 -0
  87. package/dist/qaPreparation.d.ts +8 -20
  88. package/dist/qaPreparation.js +133 -229
  89. package/dist/qaPreparationInputs.js +19 -0
  90. package/dist/qaPreparationStore.js +34 -6
  91. package/dist/qaPrerequisites.d.ts +2 -1
  92. package/dist/qaPrerequisites.js +21 -1
  93. package/dist/qaProtocolV2.d.ts +15 -0
  94. package/dist/qaRecovery.js +21 -4
  95. package/dist/qaReview.js +232 -394
  96. package/dist/qaReviewEvidence.d.ts +7 -3
  97. package/dist/qaReviewEvidence.js +15 -4
  98. package/dist/qaRuntime.js +1 -1
  99. package/dist/qaSnapshot.js +71 -21
  100. package/dist/qaStructuredResponse.d.ts +10 -0
  101. package/dist/qaStructuredResponse.js +57 -0
  102. package/dist/qaTimeline.js +5 -3
  103. package/dist/qaToolRecovery.d.ts +12 -0
  104. package/dist/qaToolRecovery.js +44 -0
  105. package/dist/qaVerificationContract.d.ts +3 -2
  106. package/dist/qaVerificationContract.js +58 -34
  107. package/dist/readinessGate.js +5 -4
  108. package/dist/runtimeReadiness.js +23 -15
  109. package/dist/statusReporter.d.ts +2 -0
  110. package/dist/statusReporter.js +2 -0
  111. package/dist/structuredCutover.d.ts +31 -0
  112. package/dist/structuredCutover.js +510 -0
  113. package/dist/structuredCutoverFiles.d.ts +10 -0
  114. package/dist/structuredCutoverFiles.js +181 -0
  115. package/dist/structuredDispatchAuthority.d.ts +22 -0
  116. package/dist/structuredDispatchAuthority.js +110 -0
  117. package/dist/structuredDispatchRecord.d.ts +29 -0
  118. package/dist/structuredDispatchRecord.js +36 -0
  119. package/dist/structuredLiveBudget.d.ts +2 -0
  120. package/dist/structuredLiveBudget.js +23 -0
  121. package/dist/structuredOperation.d.ts +18 -0
  122. package/dist/structuredOperation.js +257 -0
  123. package/dist/supervisedStart.d.ts +1 -1
  124. package/dist/supervisedStart.js +78 -31
  125. package/dist/supervisor.d.ts +18 -2
  126. package/dist/supervisor.js +74 -17
  127. package/dist/ticketPlanning.d.ts +1 -373
  128. package/dist/ticketPlanning.js +2 -26
  129. package/dist/ticketPopulation.d.ts +0 -3
  130. package/dist/ticketPopulation.js +4 -21
  131. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  132. package/dist/ticketPopulationPageAuthority.js +30 -0
  133. package/dist/ticketPopulationPaging.d.ts +27 -0
  134. package/dist/ticketPopulationPaging.js +105 -0
  135. package/dist/tickets/renderMarkdown.d.ts +14 -0
  136. package/dist/tickets/renderMarkdown.js +39 -5
  137. package/dist/tickets/ticketLoader.js +10 -0
  138. package/dist/tickets/ticketSchema.d.ts +107 -0
  139. package/dist/tickets/ticketSchema.js +4 -1
  140. package/dist/workflowDb.d.ts +100 -2
  141. package/dist/workflowDb.js +781 -35
  142. package/dist/workflowReader.d.ts +1 -0
  143. package/dist/workflowReader.js +9 -0
  144. package/package.json +15 -7
@@ -1,10 +1,14 @@
1
+ import { persistedProviderWireContract } from "rafi-spec";
1
2
  import { dispatchWithGraphAccess, withGraphSessionAccess } from "./graph/session.js";
2
3
  import { localBuildAuthority } from "./buildAdmission.js";
3
4
  import { randomUUID, createHash } from "node:crypto";
4
- import { continuityDeltaV1Schema, zodIssues } from "rafi-spec";
5
+ import { continuityDeltaV1Schema, initializationOutput, zodIssues } from "rafi-spec";
5
6
  import { BuilderEventQueue } from "./activity.js";
6
7
  import { providerSessionKey } from "./sessionIdentity.js";
8
+ import { nativePolicy, operationInstruction, journalStructuredDispatch, continuationPolicy } from "./structuredOperation.js";
7
9
  import { WorkflowDb } from "./workflowDb.js";
10
+ import { assertSealedDispatch, withFreshRecoveryValidation } from "./structuredDispatchAuthority.js";
11
+ import { verifyPersistedDispatch } from "./structuredDispatchRecord.js";
8
12
  export const CONTINUITY_MARKER = "RAFI_CONTINUITY_DELTA:";
9
13
  export class ContinuityValidationError extends Error {
10
14
  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) {
@@ -136,7 +127,7 @@ export class ContinuityAdapter {
136
127
  db.ensureRun(options.runId);
137
128
  if (!db.continuityHead(options.runId, options.role)) {
138
129
  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() });
130
+ db.publishContinuityCheckpoint({ runId: options.runId, role: options.role, delta: baselineContinuityDelta(), hostEventKind: "role-baseline", inputCheckpointDigests: [], authoritativeStateRevision: this.revision() });
140
131
  }
141
132
  const initialSession = options.adapter.sessionRef?.() ?? options.adapter.sessionId();
142
133
  if (initialSession)
@@ -166,7 +157,7 @@ export class ContinuityAdapter {
166
157
  }
167
158
  this.validatingFreshRecovery = true;
168
159
  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 });
160
+ 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
161
  if (result.isError || result.failure || this.recoveryLeasePending)
171
162
  throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "fresh recovery did not establish a valid continuity checkpoint");
172
163
  }
@@ -179,13 +170,35 @@ export class ContinuityAdapter {
179
170
  }
180
171
  }
181
172
  get agent() { return this.adapter.agent; }
173
+ /** Every confirmed dispatch publishes a checkpoint, so continuity is mandatory. */
174
+ structuredDispatchRequirement() {
175
+ return { instruction: CONTINUITY_REQUIRED_INSTRUCTION, validate: response => { validateContinuityDelta(response.continuity); } };
176
+ }
182
177
  async sendTurn(instruction, policy) {
183
- return withGraphSessionAccess(this.options.projectDir, this.adapter, () => this.sendTurnOwned(instruction, policy));
178
+ if (!policy?.output)
179
+ throw new Error("Continuity dispatch requires a native operation contract");
180
+ if (policy.dispatchCapability) {
181
+ assertSealedDispatch(this.options.projectDir, policy.dispatchCapability, policy.selectedDispatch, instruction);
182
+ return this.sendTurnOwned(instruction, policy);
183
+ }
184
+ // The continuity requirement is applied by journalStructuredDispatch through
185
+ // structuredDispatchRequirement(), before sealing, on every dispatch path.
186
+ const hostInstruction = instruction;
187
+ return withGraphSessionAccess(this.options.projectDir, this.adapter, () => policy?.output
188
+ ? journalStructuredDispatch(this.options.projectDir, this, instruction, { ...policy, journalRunId: policy.journalRunId ?? this.options.runId }, (text, selected) => this.sendTurnOwned(text, selected))
189
+ : this.sendTurnOwned(instruction, policy)).then(result => {
190
+ if (result.failure?.category === "session-unavailable") {
191
+ this.recordSessionUnavailable(result.failure, hostInstruction);
192
+ throw new SessionUnavailableContinuityError(this.options.runId, this.options.role, result.failure);
193
+ }
194
+ return { ...result, hostInstruction };
195
+ });
184
196
  }
185
197
  async sendTurnOwned(instruction, policy) {
186
- const providerInstruction = `${instruction}\n\n${continuityInstruction(this.options.role)}`;
198
+ const providerInstruction = instruction;
187
199
  const journal = new WorkflowDb(this.options.projectDir);
188
- const dispatchId = `dispatch:${this.options.runId}:${this.options.role}:${randomUUID()}`;
200
+ const dispatchId = policy?.dispatchId ?? `dispatch:${this.options.runId}:${this.options.role}:${randomUUID()}`;
201
+ const beforeSession = this.adapter.sessionRef?.();
189
202
  let original;
190
203
  try {
191
204
  journal.atomic(() => {
@@ -204,22 +217,32 @@ export class ContinuityAdapter {
204
217
  if (!(this.validatingFreshRecovery && this.recoveryLeasePending && policy?.responseOnly) && owner && session && (owner.providerSessionId !== session || (owner.sessionKey && ref && owner.sessionKey !== providerSessionKey(ref)))) {
205
218
  throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "provider session no longer owns role dispatch authority");
206
219
  }
207
- if (journal.unresolvedRoleDispatches(this.options.runId, this.options.role).length)
220
+ if (journal.unresolvedRoleDispatches(this.options.runId, this.options.role).filter(item => item.idempotencyKey !== dispatchId).length)
208
221
  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");
222
+ if (!policy?.selectedDispatch)
223
+ 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) } });
224
+ if (!policy?.selectedDispatch)
225
+ journal.updateOperation(dispatchId, "in_progress");
211
226
  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
227
  });
213
228
  try {
214
229
  original = await dispatchWithGraphAccess(this.options.projectDir, this.adapter, providerInstruction, policy, (text, p) => this.adapter.sendTurn(text, p));
215
230
  }
216
231
  catch (error) {
217
- journal.updateOperation(dispatchId, "uncertain", { error: String(error) });
232
+ if (!policy?.selectedDispatch)
233
+ journal.updateOperation(dispatchId, "uncertain", { error: String(error) });
218
234
  throw error;
219
235
  }
220
- const responseDigest = journal.putEvidence("handoff", original.rawResponse ?? original.text);
236
+ // Preserve the native transport result before interpreting it or publishing
237
+ // a checkpoint. The outer journal remains the sole completion owner.
238
+ // journalStructuredDispatch owns the one retained raw receipt for native
239
+ // turns. Continuity records retain only its durable reference/fact.
240
+ const responseDigest = policy?.selectedDispatch ? undefined : journal.putEvidence("handoff", Buffer.from(JSON.stringify(original)));
221
241
  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?.() } });
242
+ journal.appendContinuityEvent({ runId: this.options.runId, role: this.options.role, kind: "provider_response_received",
243
+ payload: { dispatchId, ...(responseDigest ? { responseDigest } : { journalOwnedEvidence: true }) }, authoritativeStateRevision: this.revision() });
244
+ if (!policy?.selectedDispatch)
245
+ 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
246
  if (original.failure?.dispatchState === "not-sent")
224
247
  journal.appendContinuityEvent({ runId: this.options.runId, role: this.options.role, kind: "turn_not_sent", payload: { dispatchId, role: this.options.role }, authoritativeStateRevision: this.revision() });
225
248
  });
@@ -227,152 +250,162 @@ export class ContinuityAdapter {
227
250
  finally {
228
251
  journal.close();
229
252
  }
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 ?? [] };
253
+ return original;
254
+ }
255
+ async postConfirmedDispatch(reference) {
256
+ const db = new WorkflowDb(this.options.projectDir);
257
+ let original;
258
+ let context;
259
+ let response;
260
+ try {
261
+ const operation = db.operation(reference.dispatchId);
262
+ if (operation?.runId !== this.options.runId || reference.runId !== this.options.runId || db.structuredOwnerDispatch(reference.dispatchId).tupleDigest !== reference.tupleDigest)
263
+ throw new Error("Continuity postprocessing belongs to a foreign dispatch");
264
+ original = db.retainedStructuredTurn(reference.dispatchId);
265
+ context = db.structuredProcessingContext(reference.dispatchId);
266
+ const tuple = original.selectedDispatch;
267
+ const contract = persistedProviderWireContract(tuple).canonical;
268
+ response = db.replayStructuredDispatch(reference.dispatchId, contract);
238
269
  }
239
- const activeSession = this.adapter.sessionRef?.() ?? this.adapter.sessionId();
240
- if (activeSession) {
241
- const leaseDb = new WorkflowDb(this.options.projectDir);
270
+ finally {
271
+ db.close();
272
+ }
273
+ const delta = validateContinuityDelta(response.continuity);
274
+ if (this.recoveryLeasePending && (response.result.kind !== "completed" || !response.result.payload.ready))
275
+ throw new Error("Fresh recovery requires a completed initialization checkpoint");
276
+ if (!context.policy.handback) {
277
+ this.publish(delta, "turn_completed", original);
278
+ }
279
+ if (response.result.kind === "handoff-request") {
280
+ if (context.policy.handback || this.options.durableSingleTurn || this.validatingFreshRecovery)
281
+ throw new Error("This operation cannot transfer its session");
282
+ // Publication is complete before the explicitly linked acceptance stage.
283
+ const completed = new WorkflowDb(this.options.projectDir);
242
284
  try {
243
- leaseDb.claimInitialRoleLease(this.options.runId, this.options.role, activeSession);
285
+ completed.beginStructuredHandoff(reference.dispatchId);
286
+ completed.completeStructuredPostprocessing(reference.dispatchId);
244
287
  }
245
288
  finally {
246
- leaseDb.close();
289
+ completed.close();
247
290
  }
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 };
291
+ const successor = await this.options.handleHandoffRequest?.({ version: 1, reason: response.result.reason, roleState: response.result.roleState, delta, predecessorDispatchId: reference.dispatchId }, this.adapter, context.instruction);
292
+ if (!successor || successor === this.adapter)
293
+ throw new Error("Handoff request did not establish a validated successor");
294
+ const continuation = new WorkflowDb(this.options.projectDir);
295
+ let continuationId;
296
+ try {
297
+ const session = successor.sessionRef?.();
298
+ if (!session)
299
+ throw new Error("Handoff successor has no scoped session");
300
+ continuationId = continuation.planStructuredContinuation(reference.dispatchId, session);
262
301
  }
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);
302
+ finally {
303
+ continuation.close();
270
304
  }
271
- return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
272
- rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
305
+ await this.adoptValidatedSuccessor(successor);
306
+ const tuple = original.selectedDispatch;
307
+ const output = persistedProviderWireContract(tuple).canonical;
308
+ 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
309
  }
274
- if (this.options.durableSingleTurn || (this.validatingFreshRecovery && this.recoveryLeasePending && policy?.responseOnly)) {
275
- const invalidDb = new WorkflowDb(this.options.projectDir);
310
+ return { ...original, hostInstruction: context.instruction, continuityErrors: [], continuity: delta };
311
+ }
312
+ observeEvents(listener) {
313
+ if (!this.adapter.observeEvents)
314
+ throw new Error("wrapped provider does not expose turn observation");
315
+ return this.adapter.observeEvents(listener);
316
+ }
317
+ acceptHandbackTurn(turn) {
318
+ if (turn.structured) {
319
+ if (turn.isError || turn.failure || turn.structured.state !== "candidate")
320
+ throw new Error("cannot publish an invalid handback checkpoint");
321
+ const db = new WorkflowDb(this.options.projectDir);
322
+ let retained;
276
323
  try {
277
- invalidDb.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: "continuity_invalid", payload: { problems: parsed.error?.problems }, authoritativeStateRevision: this.revision() });
324
+ if (!turn.selectedDispatch || db.operation(turn.selectedDispatch.dispatchId)?.runId !== this.options.runId)
325
+ throw new Error("Foreign handback dispatch");
326
+ retained = db.retainedStructuredTurn(turn.selectedDispatch.dispatchId);
278
327
  }
279
328
  finally {
280
- invalidDb.close();
329
+ db.close();
281
330
  }
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 };
331
+ const tuple = retained.selectedDispatch;
332
+ const owner = new WorkflowDb(this.options.projectDir);
333
+ try {
334
+ const response = verifyPersistedDispatch(persistedProviderWireContract(tuple).canonical, owner.structuredOwnerDispatch(tuple.dispatchId), retained.structured, tuple);
335
+ this.publish(validateContinuityDelta(response.continuity), "handback_turn_completed", retained);
336
+ }
337
+ finally {
338
+ owner.close();
339
+ }
340
+ return;
285
341
  }
286
- const budget = new WorkflowDb(this.options.projectDir);
342
+ throw new Error("Handback acceptance requires a native receipt and validated continuity");
343
+ }
344
+ /** Explicit restart recovery of the already accepted action; never repeats acceptance. */
345
+ async resumePlannedContinuation() {
346
+ const db = new WorkflowDb(this.options.projectDir);
347
+ let continuation;
348
+ let context;
349
+ let output;
287
350
  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");
351
+ continuation = db.structuredContinuation(this.options.runId, this.options.role);
352
+ if (!continuation)
353
+ return;
354
+ const operation = db.operation(continuation.continuationId);
355
+ if (operation?.status === "confirmed") {
356
+ if (db.pendingStructuredPostprocessing(this.options.runId).includes(continuation.continuationId)) {
357
+ await this.postConfirmedDispatch({ dispatchId: continuation.continuationId, runId: this.options.runId,
358
+ tupleDigest: db.structuredOwnerDispatch(continuation.continuationId).tupleDigest });
359
+ db.completeStructuredPostprocessing(continuation.continuationId);
360
+ db.finishStructuredContinuation(continuation.continuationId);
361
+ }
362
+ const turn = db.retainedStructuredTurn(continuation.continuationId);
363
+ const descriptor = persistedProviderWireContract(turn.selectedDispatch);
364
+ const response = db.replayStructuredDispatch(continuation.continuationId, descriptor.canonical);
365
+ return { ...turn, continuity: validateContinuityDelta(response.continuity) };
291
366
  }
367
+ if (continuation.state !== "continuation-planned" || operation && operation.status !== "planned")
368
+ throw new Error("Continuation may already have been submitted; explicit reconciliation is required");
369
+ const current = this.adapter.sessionRef?.();
370
+ if (!current || providerSessionKey(current) !== providerSessionKey(continuation.successor))
371
+ throw new Error("Reattach the durably accepted successor before continuation recovery");
372
+ context = db.structuredProcessingContext(continuation.predecessorId);
373
+ const predecessor = db.retainedStructuredTurn(continuation.predecessorId);
374
+ output = persistedProviderWireContract(predecessor.selectedDispatch).canonical;
292
375
  }
293
376
  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 };
377
+ db.close();
313
378
  }
314
- const invalidDb = new WorkflowDb(this.options.projectDir);
379
+ 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 }));
380
+ }
381
+ async recoverConfirmedPublications() {
382
+ const db = new WorkflowDb(this.options.projectDir);
383
+ let pending;
315
384
  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");
385
+ pending = db.pendingStructuredPostprocessing(this.options.runId);
318
386
  }
319
387
  finally {
320
- invalidDb.close();
388
+ db.close();
321
389
  }
322
- const handoff = this.reconstructionHandoff();
323
- if (this.options.recoverWithHandoff) {
324
- let successor;
390
+ for (const dispatchId of pending) {
391
+ const current = new WorkflowDb(this.options.projectDir);
392
+ let reference;
325
393
  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
- });
394
+ reference = { dispatchId, runId: this.options.runId, tupleDigest: current.structuredOwnerDispatch(dispatchId).tupleDigest };
331
395
  }
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)}`);
396
+ finally {
397
+ current.close();
334
398
  }
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");
399
+ await this.postConfirmedDispatch(reference);
400
+ const completed = new WorkflowDb(this.options.projectDir);
401
+ try {
402
+ completed.completeStructuredPostprocessing(dispatchId);
403
+ completed.finishStructuredContinuation(dispatchId);
404
+ }
405
+ finally {
406
+ completed.close();
338
407
  }
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
408
  }
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
409
  }
377
410
  sessionId() { return this.adapter.sessionId(); }
378
411
  sessionRef() { return this.adapter.sessionRef?.(); }
@@ -422,30 +455,32 @@ export class ContinuityAdapter {
422
455
  const db = new WorkflowDb(this.options.projectDir);
423
456
  try {
424
457
  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 });
458
+ let moved = false;
459
+ const checkpoint = db.atomic(() => {
460
+ if (this.recoveryLeasePending && sessionRef)
461
+ db.assertFreshRecoveryValidation(result.selectedDispatch.dispatchId, sessionRef);
462
+ const prior = db.providerCheckpoint(result.selectedDispatch.dispatchId, kind);
463
+ if (prior) {
464
+ if (JSON.stringify(prior.delta) !== JSON.stringify(delta))
465
+ throw new Error("Conflicting repeated continuity publication");
466
+ return prior;
467
+ }
468
+ 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 });
469
+ 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 } });
470
+ if (this.recoveryLeasePending && sessionRef) {
471
+ db.moveRoleLeaseAfterValidatedRecovery(this.options.runId, this.options.role, sessionRef, "explicit fresh-recovery-only continuation");
472
+ moved = true;
473
+ }
474
+ return published;
428
475
  });
476
+ if (moved)
477
+ this.recoveryLeasePending = false;
478
+ return checkpoint;
429
479
  }
430
480
  finally {
431
481
  db.close();
432
482
  }
433
483
  }
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
484
  recordSessionUnavailable(failure, instruction) {
450
485
  const sessionRef = this.adapter.sessionRef?.();
451
486
  const db = new WorkflowDb(this.options.projectDir);
@@ -473,22 +508,6 @@ export class ContinuityAdapter {
473
508
  db.close();
474
509
  }
475
510
  }
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
511
  revision() { return this.options.authoritativeStateRevision?.() ?? this.options.settings.settings_revision; }
493
512
  pumpEvents() {
494
513
  const source = this.adapter;
@@ -0,0 +1,13 @@
1
+ import type Database from "better-sqlite3";
2
+ export type CheckpointOrigin = {
3
+ kind: "provider";
4
+ dispatchId: string;
5
+ publicationKind: string;
6
+ } | {
7
+ kind: "host";
8
+ eventSequence: number;
9
+ inputCheckpointDigests: string[];
10
+ };
11
+ /** Read-only replay: never opens a writer or migrates storage. */
12
+ export declare function verifyCheckpointOrigin(db: Database.Database, projectDir: string, checkpointId: number, visited?: Set<number>): void;
13
+ export declare function verifyContinuityHeads(db: Database.Database, projectDir: string, runId: string): void;
@@ -0,0 +1,91 @@
1
+ import { persistedProviderWireContract } from "rafi-spec";
2
+ import { createHash } from "node:crypto";
3
+ import { canonicalJson } from "rafi-spec";
4
+ import { graphDerivedAllowed } from "./graph/derived.js";
5
+ import { hydrateStructuredReceipt, persistedDispatchTuple, verifyPersistedDispatch } from "./structuredDispatchRecord.js";
6
+ /** Read-only replay: never opens a writer or migrates storage. */
7
+ export function verifyCheckpointOrigin(db, projectDir, checkpointId, visited = new Set()) {
8
+ if (visited.has(checkpointId))
9
+ throw new Error("Cyclic checkpoint provenance");
10
+ visited.add(checkpointId);
11
+ const checkpoint = db.prepare("SELECT * FROM continuity_checkpoints WHERE checkpoint_id=?").get(checkpointId);
12
+ const row = db.prepare("SELECT origin_json,delta_digest FROM continuity_checkpoint_origins WHERE checkpoint_id=?").get(checkpointId);
13
+ if (!checkpoint || !row || hash(checkpoint.delta_json) !== row.delta_digest)
14
+ throw new Error("Checkpoint has no verified provenance");
15
+ const origin = JSON.parse(row.origin_json);
16
+ if (origin.kind === "provider") {
17
+ const operation = db.prepare("SELECT * FROM operation_journal WHERE idempotency_key=?").get(origin.dispatchId);
18
+ if (!operation || operation.run_id !== checkpoint.run_id || operation.status !== "confirmed")
19
+ throw new Error("Checkpoint origin is not a confirmed dispatch in this run");
20
+ const intent = JSON.parse(operation.intent_json), result = JSON.parse(operation.result_json);
21
+ const tuple = persistedDispatchTuple(intent.sealedDispatch);
22
+ if (canonicalJson(intent.tuple) !== intent.sealedDispatch.tupleJson || tuple.dispatchId !== origin.dispatchId || intent.sessionRef?.role !== checkpoint.role)
23
+ throw new Error("Checkpoint origin tuple/role mismatch");
24
+ const frozen = intent.sessionRef;
25
+ if (!frozen || frozen.sessionId !== tuple.sessionId || frozen.provider !== tuple.provider || frozen.generation !== tuple.generation)
26
+ throw new Error("Checkpoint origin frozen session mismatch");
27
+ const review = db.prepare("SELECT intent_json FROM qa_turns WHERE operation_id=?").get(origin.dispatchId);
28
+ if (review && canonicalJson(JSON.parse(review.intent_json).sealedDispatch) !== canonicalJson(intent.sealedDispatch))
29
+ throw new Error("Checkpoint QA owner differs from transport intent");
30
+ const delivery = db.prepare("SELECT record_json FROM qa_delivery_turns WHERE turn_record_id=?").get(origin.dispatchId);
31
+ if (delivery && canonicalJson(JSON.parse(delivery.record_json).sealedDispatch) !== canonicalJson(intent.sealedDispatch))
32
+ throw new Error("Checkpoint remediation owner differs from transport intent");
33
+ const descriptor = persistedProviderWireContract(tuple);
34
+ if (!descriptor || result.validation !== "accepted")
35
+ throw new Error("Checkpoint origin descriptor/validation mismatch");
36
+ const read = (digest) => {
37
+ const bytes = db.prepare("SELECT content FROM content_refs WHERE digest=?").get(digest)?.content;
38
+ if (!bytes || hash(bytes) !== digest || !graphDerivedAllowed(db, projectDir, digest))
39
+ throw new Error("Checkpoint origin evidence missing, altered or revoked");
40
+ return bytes;
41
+ };
42
+ const retained = JSON.parse(read(result.responseDigest).toString());
43
+ if (retained.tupleDigest !== tuple.digest)
44
+ throw new Error("Checkpoint origin receipt tuple mismatch");
45
+ const response = verifyPersistedDispatch(descriptor.canonical, intent.sealedDispatch, hydrateStructuredReceipt(retained, read));
46
+ if (canonicalJson(response.continuity) !== canonicalJson(JSON.parse(checkpoint.delta_json)))
47
+ throw new Error("Checkpoint delta differs from its native origin");
48
+ if (checkpoint.session_ref_json) {
49
+ const session = JSON.parse(checkpoint.session_ref_json);
50
+ if (["provider", "role", "stream", "cwd", "configRoot", "workspaceIdentity", "ticketId", "deliveryUnitId"].some(field => session[field] !== frozen[field]))
51
+ throw new Error("Checkpoint scoped session differs from transport");
52
+ if (session.sessionId !== tuple.sessionId || session.generation !== tuple.generation) {
53
+ // Generation promotion is permitted only by the recorded accepted handoff.
54
+ const acceptance = db.prepare("SELECT acceptance_receipt_digest FROM handoffs WHERE run_id=? AND successor_session_id=? AND generation=? AND state='accepted'").get(checkpoint.run_id, session.sessionId, session.generation);
55
+ if (!acceptance)
56
+ throw new Error("Checkpoint session differs from its dispatch");
57
+ const receipt = JSON.parse(read(acceptance.acceptance_receipt_digest).toString());
58
+ if (receipt.runId !== checkpoint.run_id || receipt.role !== checkpoint.role || receipt.acceptanceCheckpointDigest !== checkpoint.digest
59
+ || canonicalJson(receipt.successorSessionRef) !== canonicalJson(session) || session.sessionId !== tuple.sessionId)
60
+ throw new Error("Checkpoint generation promotion lacks its exact accepted handoff lineage");
61
+ }
62
+ }
63
+ }
64
+ else if (origin.kind === "host") {
65
+ const event = db.prepare("SELECT run_id,role,payload_json FROM continuity_events WHERE sequence=?").get(origin.eventSequence);
66
+ if (!event || event.run_id !== checkpoint.run_id || event.role !== "host" || JSON.parse(event.payload_json).checkpointDeltaDigest !== row.delta_digest)
67
+ throw new Error("Host checkpoint event provenance mismatch");
68
+ for (const digest of origin.inputCheckpointDigests) {
69
+ const input = db.prepare("SELECT checkpoint_id FROM continuity_checkpoints WHERE run_id=? AND digest=?").get(checkpoint.run_id, digest);
70
+ if (!input || input.checkpoint_id >= checkpointId)
71
+ throw new Error("Host checkpoint input is unavailable or not a predecessor");
72
+ verifyCheckpointOrigin(db, projectDir, input.checkpoint_id, new Set(visited));
73
+ }
74
+ }
75
+ else
76
+ throw new Error("Unknown checkpoint provenance");
77
+ }
78
+ function hash(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
79
+ export function verifyContinuityHeads(db, projectDir, runId) {
80
+ const heads = db.prepare("SELECT * FROM continuity_heads WHERE run_id=? ORDER BY role").all(runId);
81
+ const roles = heads.filter(head => head.role !== "run");
82
+ for (const head of roles) {
83
+ const checkpoint = db.prepare("SELECT checkpoint_id,event_sequence FROM continuity_checkpoints WHERE run_id=? AND role=? AND digest=?").get(runId, head.role, head.digest);
84
+ if (!checkpoint || checkpoint.event_sequence !== head.event_sequence)
85
+ throw new Error("Continuity head has no matching checkpoint");
86
+ verifyCheckpointOrigin(db, projectDir, checkpoint.checkpoint_id);
87
+ }
88
+ const run = heads.find(head => head.role === "run");
89
+ if (run && (run.digest !== hash(canonicalJson(roles.map(row => ({ role: row.role, digest: row.digest })))) || run.event_sequence !== Math.max(...roles.map(row => row.event_sequence))))
90
+ throw new Error("Run continuity head differs from its verified role heads");
91
+ }