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.
- package/README.md +1 -1
- package/dist/adapters/claude.d.ts +23 -12
- package/dist/adapters/claude.js +231 -26
- package/dist/adapters/codex.d.ts +19 -7
- package/dist/adapters/codex.js +202 -19
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +5 -1
- package/dist/adapters/structured.d.ts +71 -0
- package/dist/adapters/structured.js +213 -0
- package/dist/adapters/types.d.ts +85 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/git.js +3 -8
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +15 -0
- package/dist/buildApproval.js +28 -9
- package/dist/buildAssignment.d.ts +1 -0
- package/dist/buildAssignment.js +82 -23
- package/dist/buildInterventions.d.ts +2 -2
- package/dist/buildInterventions.js +11 -4
- package/dist/buildLaunchGate.js +3 -2
- package/dist/buildRuns.d.ts +1 -0
- package/dist/buildRuns.js +6 -1
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +85 -39
- package/dist/cli/tickets.d.ts +0 -8
- package/dist/cli/tickets.js +50 -60
- package/dist/continuity.d.ts +10 -16
- package/dist/continuity.js +223 -208
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/dispatchNotSent.d.ts +4 -0
- package/dist/dispatchNotSent.js +10 -0
- package/dist/foreman.d.ts +15 -16
- package/dist/foreman.js +70 -278
- package/dist/gitExclude.d.ts +16 -0
- package/dist/gitExclude.js +46 -0
- package/dist/graph/boundary.d.ts +1 -1
- package/dist/graph/boundary.js +1 -1
- package/dist/graph/host.js +8 -19
- package/dist/graph/manager.d.ts +1 -1
- package/dist/graph/manager.js +1 -1
- package/dist/graph/preparation.d.ts +38 -0
- package/dist/graph/preparation.js +7 -6
- package/dist/graph/semantic.d.ts +1 -0
- package/dist/graph/semantic.js +4 -1
- package/dist/graph/session.js +7 -2
- package/dist/graph/turn.d.ts +3 -4
- package/dist/graph/turn.js +29 -41
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +5 -3
- package/dist/handoffs.js +92 -88
- package/dist/hostGraphApplicability.d.ts +2 -1
- package/dist/hostGraphApplicability.js +17 -19
- package/dist/hostQaAugmentation.js +21 -4
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -29
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/humanDecision.d.ts +7 -0
- package/dist/humanDecision.js +11 -0
- package/dist/interviews.d.ts +7 -0
- package/dist/interviews.js +18 -1
- package/dist/legacyCommunicationDisplay.d.ts +2 -0
- package/dist/legacyCommunicationDisplay.js +2 -0
- package/dist/managerPacket.js +3 -3
- package/dist/markers.d.ts +4 -3
- package/dist/markers.js +4 -20
- package/dist/observability.js +3 -8
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +60 -13
- package/dist/qaBuilderCoverage.js +22 -13
- package/dist/qaBuilderEvidence.d.ts +16 -1
- package/dist/qaBuilderEvidence.js +48 -8
- package/dist/qaContractCoverage.d.ts +2 -2
- package/dist/qaContractCoverage.js +4 -9
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +153 -254
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.d.ts +11 -0
- package/dist/qaPreparationStore.js +44 -4
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.d.ts +0 -3
- package/dist/qaRecovery.js +21 -27
- package/dist/qaReview.js +241 -406
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +76 -21
- package/dist/qaStructuredResponse.d.ts +18 -0
- package/dist/qaStructuredResponse.js +81 -0
- package/dist/qaTimeline.js +2 -2
- package/dist/qaVerificationContract.d.ts +1 -2
- package/dist/qaVerificationContract.js +24 -35
- package/dist/readinessGate.js +5 -4
- package/dist/recoveryOwners.d.ts +32 -0
- package/dist/recoveryOwners.js +87 -0
- package/dist/runtimePreflight.d.ts +11 -0
- package/dist/runtimePreflight.js +77 -0
- package/dist/runtimeReadiness.js +23 -15
- package/dist/sources/sourceRegistry.d.ts +0 -3
- package/dist/sources/sourceRegistry.js +0 -42
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +2 -0
- package/dist/structuredCutover.d.ts +31 -0
- package/dist/structuredCutover.js +510 -0
- package/dist/structuredCutoverFiles.d.ts +10 -0
- package/dist/structuredCutoverFiles.js +181 -0
- package/dist/structuredDispatchAuthority.d.ts +22 -0
- package/dist/structuredDispatchAuthority.js +110 -0
- package/dist/structuredDispatchRecord.d.ts +29 -0
- package/dist/structuredDispatchRecord.js +36 -0
- package/dist/structuredLiveBudget.d.ts +2 -0
- package/dist/structuredLiveBudget.js +23 -0
- package/dist/structuredOperation.d.ts +18 -0
- package/dist/structuredOperation.js +279 -0
- package/dist/supervisedStart.js +35 -21
- package/dist/ticketPlanning.d.ts +1 -376
- package/dist/ticketPlanning.js +2 -49
- package/dist/ticketPopulation.d.ts +0 -3
- package/dist/ticketPopulation.js +4 -21
- package/dist/ticketPopulationPageAuthority.d.ts +13 -0
- package/dist/ticketPopulationPageAuthority.js +30 -0
- package/dist/ticketPopulationPaging.d.ts +27 -0
- package/dist/ticketPopulationPaging.js +127 -0
- package/dist/tickets/renderMarkdown.d.ts +14 -0
- package/dist/tickets/renderMarkdown.js +39 -5
- package/dist/tickets/ticketLoader.js +10 -0
- package/dist/tickets/ticketSchema.d.ts +107 -0
- package/dist/tickets/ticketSchema.js +4 -1
- package/dist/workflowDb.d.ts +116 -3
- package/dist/workflowDb.js +852 -61
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +23 -7
package/dist/continuity.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
}
|
package/dist/continuity.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
228
|
+
if (!policy?.selectedDispatch)
|
|
229
|
+
journal.updateOperation(dispatchId, "uncertain", { error: String(error) });
|
|
218
230
|
throw error;
|
|
219
231
|
}
|
|
220
|
-
|
|
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.
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
281
|
+
completed.beginStructuredHandoff(reference.dispatchId);
|
|
282
|
+
completed.completeStructuredPostprocessing(reference.dispatchId);
|
|
244
283
|
}
|
|
245
284
|
finally {
|
|
246
|
-
|
|
285
|
+
completed.close();
|
|
247
286
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
334
|
+
owner.close();
|
|
281
335
|
}
|
|
282
|
-
return
|
|
283
|
-
hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
|
|
284
|
-
rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
|
|
336
|
+
return;
|
|
285
337
|
}
|
|
286
|
-
|
|
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
|
-
|
|
289
|
-
if (!
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
invalidDb.setContinuityHeadState(this.options.runId, this.options.role, "invalid");
|
|
381
|
+
pending = db.pendingStructuredPostprocessing(this.options.runId);
|
|
318
382
|
}
|
|
319
383
|
finally {
|
|
320
|
-
|
|
384
|
+
db.close();
|
|
321
385
|
}
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
let
|
|
386
|
+
for (const dispatchId of pending) {
|
|
387
|
+
const current = new WorkflowDb(this.options.projectDir);
|
|
388
|
+
let reference;
|
|
325
389
|
try {
|
|
326
|
-
|
|
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
|
-
|
|
333
|
-
|
|
392
|
+
finally {
|
|
393
|
+
current.close();
|
|
334
394
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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;
|