@abelxiaoxing/cadence 1.0.14 → 1.1.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.
- package/README.md +29 -13
- package/agents/contract-reviewer.md +2 -2
- package/agents/design-explorer.md +1 -1
- package/agents/diagnosis-worker.md +6 -3
- package/agents/implementation-worker.md +14 -9
- package/config/.env.example +5 -17
- package/config/routes.example.json +42 -0
- package/package.json +3 -3
- package/prompts/abel-design.md +150 -228
- package/prompts/abel-diagnose.md +37 -18
- package/prompts/abel-implement.md +111 -178
- package/prompts/abel-init.md +36 -33
- package/skills/abel-workflow/SKILL.md +156 -168
- package/src/apply-transaction.ts +1427 -0
- package/src/artifact-store.ts +204 -0
- package/src/child-session.ts +136 -17
- package/src/contracts.ts +105 -696
- package/src/control-contracts.ts +259 -0
- package/src/delivery-compiler.ts +1088 -0
- package/src/file-snapshot.ts +5 -0
- package/src/implement-graph.ts +11 -4
- package/src/index.ts +1465 -133
- package/src/isolation-backend.ts +269 -0
- package/src/packet-runtime.ts +533 -0
- package/src/parent-provider.ts +23 -4
- package/src/route-policy.ts +483 -0
- package/src/run-state.ts +393 -0
- package/src/run-store.ts +918 -0
- package/src/safe-path.ts +11 -0
- package/src/scoped-tools.ts +36 -0
- package/src/state-root.ts +215 -0
- package/src/subagent-activity.ts +526 -27
- package/src/submit-tool.ts +274 -0
- package/src/task-ledger.ts +1380 -0
- package/src/verification-capability.ts +529 -42
- package/src/worker-broker.ts +703 -0
- package/src/workflow-engine.ts +7391 -0
- package/src/workspace-store.ts +544 -0
- package/src/candidate-preflight.ts +0 -1618
- package/src/drain.ts +0 -20
- package/src/patch.ts +0 -505
- package/src/result-store.ts +0 -312
- package/src/runtime.ts +0 -2436
- package/src/scheduler.ts +0 -384
- package/src/subagent-endpoint.ts +0 -158
- package/src/worker.ts +0 -204
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, lstatSync, mkdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
|
|
6
|
+
import type { ArtifactStore } from "./artifact-store.ts";
|
|
7
|
+
import {
|
|
8
|
+
diffWritePaths,
|
|
9
|
+
IMPLEMENTATION_PHASES,
|
|
10
|
+
type ImplementationPhase,
|
|
11
|
+
isValidRelativePath,
|
|
12
|
+
} from "./contracts.ts";
|
|
13
|
+
|
|
14
|
+
export const TASK_LEDGER_LIMITS = {
|
|
15
|
+
maxSegmentBytes: 128 * 1024,
|
|
16
|
+
maxCandidateBytes: 8 * 1024 * 1024,
|
|
17
|
+
maxProjectionBytes: 64 * 1024,
|
|
18
|
+
maxObjectiveBytes: 4 * 1024,
|
|
19
|
+
maxEventsPerTask: 512,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
23
|
+
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
24
|
+
const CLASSIFICATIONS = new Set([
|
|
25
|
+
"expected-red",
|
|
26
|
+
"expected-green",
|
|
27
|
+
"expected-refactor",
|
|
28
|
+
]);
|
|
29
|
+
const ZERO_HASH = "0".repeat(64);
|
|
30
|
+
|
|
31
|
+
export interface CandidateIdentity {
|
|
32
|
+
candidateId: string;
|
|
33
|
+
runId: string;
|
|
34
|
+
deliveryRevision: number;
|
|
35
|
+
taskId: string;
|
|
36
|
+
phase: ImplementationPhase;
|
|
37
|
+
attemptId: string;
|
|
38
|
+
approvedPaths: string[];
|
|
39
|
+
isolatedRevisionId: string;
|
|
40
|
+
verificationId: string;
|
|
41
|
+
routeId: string;
|
|
42
|
+
routeFingerprint: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface CandidateRow {
|
|
46
|
+
identity_json: string;
|
|
47
|
+
state: string;
|
|
48
|
+
next_sequence: number;
|
|
49
|
+
total_bytes: number;
|
|
50
|
+
artifact_hash: string | null;
|
|
51
|
+
candidate_hash: string | null;
|
|
52
|
+
segment_count: number | null;
|
|
53
|
+
paths_json: string | null;
|
|
54
|
+
pause_code: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface SegmentRow {
|
|
58
|
+
sequence: number;
|
|
59
|
+
artifact_hash: string;
|
|
60
|
+
size_bytes: number;
|
|
61
|
+
segment_hash: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface TaskRow {
|
|
65
|
+
delivery_revision: number;
|
|
66
|
+
boundary_hash: string;
|
|
67
|
+
objective: string;
|
|
68
|
+
context_refs_json: string;
|
|
69
|
+
initial_phase: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface EventRow {
|
|
73
|
+
ordinal: number;
|
|
74
|
+
event_id: string;
|
|
75
|
+
event_json: string;
|
|
76
|
+
prior_hash: string;
|
|
77
|
+
event_hash: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface TaskLedgerOptions {
|
|
81
|
+
root: string;
|
|
82
|
+
artifacts: ArtifactStore;
|
|
83
|
+
databasePath?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface BeginCandidateInput extends CandidateIdentity {}
|
|
87
|
+
|
|
88
|
+
export interface AppendCandidateSegmentInput extends CandidateIdentity {
|
|
89
|
+
sequence: number;
|
|
90
|
+
bytes: Uint8Array;
|
|
91
|
+
segmentHash: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SealCandidateInput extends CandidateIdentity {
|
|
95
|
+
segmentCount: number;
|
|
96
|
+
totalBytes: number;
|
|
97
|
+
candidateHash: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type AppendCandidateSegmentResult =
|
|
101
|
+
| {
|
|
102
|
+
ok: true;
|
|
103
|
+
state: "unsealed";
|
|
104
|
+
nextSequence: number;
|
|
105
|
+
totalBytes: number;
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
ok: false;
|
|
109
|
+
state: "paused";
|
|
110
|
+
code: string;
|
|
111
|
+
limitBytes: number;
|
|
112
|
+
replayed?: true;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type SealCandidateResult =
|
|
116
|
+
| {
|
|
117
|
+
ok: true;
|
|
118
|
+
state: "sealed";
|
|
119
|
+
artifactHash: string;
|
|
120
|
+
bytes: number;
|
|
121
|
+
paths: string[];
|
|
122
|
+
replayed?: true;
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
ok: false;
|
|
126
|
+
state: "paused";
|
|
127
|
+
code: string;
|
|
128
|
+
limitBytes: number;
|
|
129
|
+
}
|
|
130
|
+
| {
|
|
131
|
+
ok: false;
|
|
132
|
+
state: "unsealed";
|
|
133
|
+
code: "candidate-incomplete";
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
interface TaskContextReference {
|
|
137
|
+
kind: "path";
|
|
138
|
+
path: string;
|
|
139
|
+
hash: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface OpenTaskInput {
|
|
143
|
+
runId: string;
|
|
144
|
+
deliveryRevision: number;
|
|
145
|
+
taskId: string;
|
|
146
|
+
boundaryHash: string;
|
|
147
|
+
objective: string;
|
|
148
|
+
contextRefs: TaskContextReference[];
|
|
149
|
+
initialPhase: ImplementationPhase;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface PhaseVerifiedEvent {
|
|
153
|
+
runId: string;
|
|
154
|
+
taskId: string;
|
|
155
|
+
eventId: string;
|
|
156
|
+
kind: "phase-verified";
|
|
157
|
+
phase: ImplementationPhase;
|
|
158
|
+
commandId: string;
|
|
159
|
+
exitCode: number;
|
|
160
|
+
expectedClassification: string;
|
|
161
|
+
actualClassification: string;
|
|
162
|
+
diagnostic: { kind: "assertion" | "compiler"; id: string };
|
|
163
|
+
artifactHash: string;
|
|
164
|
+
isolatedRevisionId: string;
|
|
165
|
+
outputFacts: Array<
|
|
166
|
+
| { path: string; kind: "absent" }
|
|
167
|
+
| { path: string; kind: "file"; hash: string; bytes: number }
|
|
168
|
+
>;
|
|
169
|
+
routeId: string;
|
|
170
|
+
routeFingerprint: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface CorrectionEvent {
|
|
174
|
+
runId: string;
|
|
175
|
+
taskId: string;
|
|
176
|
+
eventId: string;
|
|
177
|
+
kind: "artifact-correction";
|
|
178
|
+
phase: ImplementationPhase;
|
|
179
|
+
correctionCategory: "artifact" | "stale" | "verification" | "checkpoint";
|
|
180
|
+
safeFailure: { code: string; stage: string };
|
|
181
|
+
candidateId: string;
|
|
182
|
+
attemptId: string;
|
|
183
|
+
routeId: string;
|
|
184
|
+
routeFingerprint: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface RepairVerifiedEvent {
|
|
188
|
+
runId: string;
|
|
189
|
+
taskId: string;
|
|
190
|
+
eventId: string;
|
|
191
|
+
kind: "repair-verified";
|
|
192
|
+
phase: ImplementationPhase;
|
|
193
|
+
attempt: number;
|
|
194
|
+
commandId: string;
|
|
195
|
+
exitCode: 0;
|
|
196
|
+
diagnostic: { kind: "assertion" | "compiler"; id: string };
|
|
197
|
+
artifactHash: string;
|
|
198
|
+
isolatedRevisionId: string;
|
|
199
|
+
outputFacts: PhaseVerifiedEvent["outputFacts"];
|
|
200
|
+
attribution: "introduced";
|
|
201
|
+
failureIdentities: string[];
|
|
202
|
+
routeId: string;
|
|
203
|
+
routeFingerprint: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface RouteReboundEvent {
|
|
207
|
+
runId: string;
|
|
208
|
+
taskId: string;
|
|
209
|
+
eventId: string;
|
|
210
|
+
kind: "route-rebound";
|
|
211
|
+
phase: ImplementationPhase;
|
|
212
|
+
routeFingerprint: string;
|
|
213
|
+
routeId: string;
|
|
214
|
+
attemptId: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export type VerifiedTaskEvent =
|
|
218
|
+
| PhaseVerifiedEvent
|
|
219
|
+
| RepairVerifiedEvent
|
|
220
|
+
| CorrectionEvent
|
|
221
|
+
| RouteReboundEvent;
|
|
222
|
+
|
|
223
|
+
function sha256(bytes: Uint8Array | string): string {
|
|
224
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function hashEvent(
|
|
228
|
+
priorHash: string,
|
|
229
|
+
ordinal: number,
|
|
230
|
+
eventJson: string,
|
|
231
|
+
): string {
|
|
232
|
+
return createHash("sha256")
|
|
233
|
+
.update("cadence-task-ledger-event-v2\0")
|
|
234
|
+
.update(priorHash)
|
|
235
|
+
.update("\0")
|
|
236
|
+
.update(String(ordinal))
|
|
237
|
+
.update("\0")
|
|
238
|
+
.update(eventJson)
|
|
239
|
+
.digest("hex");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function ensurePrivateDirectory(directory: string): void {
|
|
243
|
+
if (!path.isAbsolute(directory)) throw new Error("task-ledger-root-invalid");
|
|
244
|
+
const absolute = path.resolve(directory);
|
|
245
|
+
let cursor = absolute;
|
|
246
|
+
const missing: string[] = [];
|
|
247
|
+
for (;;) {
|
|
248
|
+
const stat = lstatSync(cursor, { throwIfNoEntry: false });
|
|
249
|
+
if (stat) {
|
|
250
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
251
|
+
throw new Error("task-ledger-root-invalid");
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
missing.push(cursor);
|
|
256
|
+
const parent = path.dirname(cursor);
|
|
257
|
+
if (parent === cursor) throw new Error("task-ledger-root-invalid");
|
|
258
|
+
cursor = parent;
|
|
259
|
+
}
|
|
260
|
+
for (const target of missing.reverse()) {
|
|
261
|
+
mkdirSync(target, { mode: 0o700 });
|
|
262
|
+
chmodSync(target, 0o700);
|
|
263
|
+
}
|
|
264
|
+
chmodSync(absolute, 0o700);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function hasExactKeys(value: object, expected: readonly string[]): boolean {
|
|
268
|
+
const actual = Object.keys(value).sort();
|
|
269
|
+
return (
|
|
270
|
+
actual.length === expected.length &&
|
|
271
|
+
expected
|
|
272
|
+
.slice()
|
|
273
|
+
.sort()
|
|
274
|
+
.every((key, index) => actual[index] === key)
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function requireIdentifier(
|
|
279
|
+
value: unknown,
|
|
280
|
+
code: string,
|
|
281
|
+
): asserts value is string {
|
|
282
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value)) {
|
|
283
|
+
throw new Error(code);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function requireHash(value: unknown, code: string): asserts value is string {
|
|
288
|
+
if (typeof value !== "string" || !SHA256.test(value)) {
|
|
289
|
+
throw new Error(code);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function requirePhase(value: unknown): asserts value is ImplementationPhase {
|
|
294
|
+
if (!(IMPLEMENTATION_PHASES as readonly unknown[]).includes(value)) {
|
|
295
|
+
throw new Error("ledger-phase-invalid");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizePaths(value: unknown): string[] {
|
|
300
|
+
if (
|
|
301
|
+
!Array.isArray(value) ||
|
|
302
|
+
value.length === 0 ||
|
|
303
|
+
value.some(
|
|
304
|
+
(entry) =>
|
|
305
|
+
!isValidRelativePath(entry) ||
|
|
306
|
+
entry === "." ||
|
|
307
|
+
typeof entry !== "string",
|
|
308
|
+
) ||
|
|
309
|
+
new Set(value).size !== value.length
|
|
310
|
+
) {
|
|
311
|
+
throw new Error("candidate-approved-paths-invalid");
|
|
312
|
+
}
|
|
313
|
+
return [...value].sort();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function candidateIdentity(input: CandidateIdentity): CandidateIdentity {
|
|
317
|
+
if (
|
|
318
|
+
!hasExactKeys(input, [
|
|
319
|
+
"candidateId",
|
|
320
|
+
"runId",
|
|
321
|
+
"deliveryRevision",
|
|
322
|
+
"taskId",
|
|
323
|
+
"phase",
|
|
324
|
+
"attemptId",
|
|
325
|
+
"approvedPaths",
|
|
326
|
+
"isolatedRevisionId",
|
|
327
|
+
"verificationId",
|
|
328
|
+
"routeId",
|
|
329
|
+
"routeFingerprint",
|
|
330
|
+
]) ||
|
|
331
|
+
!Number.isSafeInteger(input.deliveryRevision) ||
|
|
332
|
+
input.deliveryRevision < 1
|
|
333
|
+
) {
|
|
334
|
+
throw new Error("candidate-identity-invalid");
|
|
335
|
+
}
|
|
336
|
+
requireIdentifier(input.candidateId, "candidate-identity-invalid");
|
|
337
|
+
requireIdentifier(input.runId, "candidate-identity-invalid");
|
|
338
|
+
requireIdentifier(input.taskId, "candidate-identity-invalid");
|
|
339
|
+
requireIdentifier(input.attemptId, "candidate-identity-invalid");
|
|
340
|
+
requireIdentifier(input.verificationId, "candidate-identity-invalid");
|
|
341
|
+
requireIdentifier(input.routeId, "candidate-identity-invalid");
|
|
342
|
+
requireHash(input.routeFingerprint, "candidate-identity-invalid");
|
|
343
|
+
requirePhase(input.phase);
|
|
344
|
+
requireHash(input.isolatedRevisionId, "candidate-identity-invalid");
|
|
345
|
+
return {
|
|
346
|
+
candidateId: input.candidateId,
|
|
347
|
+
runId: input.runId,
|
|
348
|
+
deliveryRevision: input.deliveryRevision,
|
|
349
|
+
taskId: input.taskId,
|
|
350
|
+
phase: input.phase,
|
|
351
|
+
attemptId: input.attemptId,
|
|
352
|
+
approvedPaths: normalizePaths(input.approvedPaths),
|
|
353
|
+
isolatedRevisionId: input.isolatedRevisionId,
|
|
354
|
+
verificationId: input.verificationId,
|
|
355
|
+
routeId: input.routeId,
|
|
356
|
+
routeFingerprint: input.routeFingerprint,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function identityFromSegment(
|
|
361
|
+
input: AppendCandidateSegmentInput,
|
|
362
|
+
): CandidateIdentity {
|
|
363
|
+
const {
|
|
364
|
+
sequence: _sequence,
|
|
365
|
+
bytes: _bytes,
|
|
366
|
+
segmentHash: _segmentHash,
|
|
367
|
+
...identity
|
|
368
|
+
} = input;
|
|
369
|
+
return candidateIdentity(identity);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function identityFromSeal(input: SealCandidateInput): CandidateIdentity {
|
|
373
|
+
const {
|
|
374
|
+
segmentCount: _segmentCount,
|
|
375
|
+
totalBytes: _totalBytes,
|
|
376
|
+
candidateHash: _candidateHash,
|
|
377
|
+
...identity
|
|
378
|
+
} = input;
|
|
379
|
+
return candidateIdentity(identity);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function sameIdentity(rowJson: string, identity: CandidateIdentity): boolean {
|
|
383
|
+
return rowJson === JSON.stringify(identity);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function requireTaskKey(runId: unknown, taskId: unknown): void {
|
|
387
|
+
requireIdentifier(runId, "ledger-task-identity-invalid");
|
|
388
|
+
requireIdentifier(taskId, "ledger-task-identity-invalid");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function normalizeContextRefs(value: unknown): TaskContextReference[] {
|
|
392
|
+
if (!Array.isArray(value) || value.length > 128) {
|
|
393
|
+
throw new Error("ledger-context-refs-invalid");
|
|
394
|
+
}
|
|
395
|
+
const refs = value.map((entry) => {
|
|
396
|
+
if (
|
|
397
|
+
!entry ||
|
|
398
|
+
typeof entry !== "object" ||
|
|
399
|
+
Array.isArray(entry) ||
|
|
400
|
+
!hasExactKeys(entry, ["kind", "path", "hash"]) ||
|
|
401
|
+
(entry as TaskContextReference).kind !== "path" ||
|
|
402
|
+
!isValidRelativePath((entry as TaskContextReference).path)
|
|
403
|
+
) {
|
|
404
|
+
throw new Error("ledger-context-refs-invalid");
|
|
405
|
+
}
|
|
406
|
+
requireHash(
|
|
407
|
+
(entry as TaskContextReference).hash,
|
|
408
|
+
"ledger-context-refs-invalid",
|
|
409
|
+
);
|
|
410
|
+
return { ...(entry as TaskContextReference) };
|
|
411
|
+
});
|
|
412
|
+
const identities = refs.map((entry) => `${entry.kind}:${entry.path}`);
|
|
413
|
+
if (new Set(identities).size !== identities.length) {
|
|
414
|
+
throw new Error("ledger-context-refs-invalid");
|
|
415
|
+
}
|
|
416
|
+
return refs.sort((left, right) => left.path.localeCompare(right.path));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function safeText(value: unknown, code: string, maximum = 256): string {
|
|
420
|
+
if (
|
|
421
|
+
typeof value !== "string" ||
|
|
422
|
+
value.length === 0 ||
|
|
423
|
+
value.length > maximum ||
|
|
424
|
+
value.includes("\0") ||
|
|
425
|
+
/[\r\n]/u.test(value)
|
|
426
|
+
) {
|
|
427
|
+
throw new Error(code);
|
|
428
|
+
}
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function normalizePhaseEvent(input: PhaseVerifiedEvent): PhaseVerifiedEvent {
|
|
433
|
+
if (
|
|
434
|
+
!hasExactKeys(input, [
|
|
435
|
+
"runId",
|
|
436
|
+
"taskId",
|
|
437
|
+
"eventId",
|
|
438
|
+
"kind",
|
|
439
|
+
"phase",
|
|
440
|
+
"commandId",
|
|
441
|
+
"exitCode",
|
|
442
|
+
"expectedClassification",
|
|
443
|
+
"actualClassification",
|
|
444
|
+
"diagnostic",
|
|
445
|
+
"artifactHash",
|
|
446
|
+
"isolatedRevisionId",
|
|
447
|
+
"outputFacts",
|
|
448
|
+
"routeId",
|
|
449
|
+
"routeFingerprint",
|
|
450
|
+
]) ||
|
|
451
|
+
!Number.isSafeInteger(input.exitCode) ||
|
|
452
|
+
input.exitCode < 0 ||
|
|
453
|
+
!CLASSIFICATIONS.has(input.expectedClassification) ||
|
|
454
|
+
!CLASSIFICATIONS.has(input.actualClassification) ||
|
|
455
|
+
!input.diagnostic ||
|
|
456
|
+
typeof input.diagnostic !== "object" ||
|
|
457
|
+
Array.isArray(input.diagnostic) ||
|
|
458
|
+
!hasExactKeys(input.diagnostic, ["kind", "id"]) ||
|
|
459
|
+
!["assertion", "compiler"].includes(input.diagnostic.kind) ||
|
|
460
|
+
!Array.isArray(input.outputFacts) ||
|
|
461
|
+
input.outputFacts.length > 128
|
|
462
|
+
) {
|
|
463
|
+
throw new Error("ledger-event-field-invalid");
|
|
464
|
+
}
|
|
465
|
+
requirePhase(input.phase);
|
|
466
|
+
safeText(input.commandId, "ledger-event-field-invalid", 128);
|
|
467
|
+
safeText(input.diagnostic.id, "ledger-event-field-invalid", 512);
|
|
468
|
+
requireHash(input.artifactHash, "ledger-event-field-invalid");
|
|
469
|
+
requireHash(input.isolatedRevisionId, "ledger-event-field-invalid");
|
|
470
|
+
requireIdentifier(input.routeId, "ledger-event-field-invalid");
|
|
471
|
+
requireHash(input.routeFingerprint, "ledger-event-field-invalid");
|
|
472
|
+
const outputFacts = input.outputFacts.map((fact) => {
|
|
473
|
+
if (
|
|
474
|
+
!fact ||
|
|
475
|
+
typeof fact !== "object" ||
|
|
476
|
+
Array.isArray(fact) ||
|
|
477
|
+
!isValidRelativePath(fact.path) ||
|
|
478
|
+
(fact.kind !== "absent" && fact.kind !== "file")
|
|
479
|
+
) {
|
|
480
|
+
throw new Error("ledger-event-field-invalid");
|
|
481
|
+
}
|
|
482
|
+
if (fact.kind === "absent") {
|
|
483
|
+
if (!hasExactKeys(fact, ["path", "kind"])) {
|
|
484
|
+
throw new Error("ledger-event-field-invalid");
|
|
485
|
+
}
|
|
486
|
+
return { path: fact.path, kind: "absent" as const };
|
|
487
|
+
}
|
|
488
|
+
if (
|
|
489
|
+
!hasExactKeys(fact, ["path", "kind", "hash", "bytes"]) ||
|
|
490
|
+
!Number.isSafeInteger(fact.bytes) ||
|
|
491
|
+
fact.bytes < 0
|
|
492
|
+
) {
|
|
493
|
+
throw new Error("ledger-event-field-invalid");
|
|
494
|
+
}
|
|
495
|
+
requireHash(fact.hash, "ledger-event-field-invalid");
|
|
496
|
+
return { ...fact };
|
|
497
|
+
});
|
|
498
|
+
return {
|
|
499
|
+
...input,
|
|
500
|
+
diagnostic: { ...input.diagnostic },
|
|
501
|
+
outputFacts,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function normalizeVerifiedEvent(input: VerifiedTaskEvent): VerifiedTaskEvent {
|
|
506
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
507
|
+
throw new Error("ledger-event-field-invalid");
|
|
508
|
+
}
|
|
509
|
+
requireTaskKey(input.runId, input.taskId);
|
|
510
|
+
requireIdentifier(input.eventId, "ledger-event-field-invalid");
|
|
511
|
+
if (input.kind === "phase-verified") return normalizePhaseEvent(input);
|
|
512
|
+
if (input.kind === "repair-verified") {
|
|
513
|
+
if (
|
|
514
|
+
!hasExactKeys(input, [
|
|
515
|
+
"runId",
|
|
516
|
+
"taskId",
|
|
517
|
+
"eventId",
|
|
518
|
+
"kind",
|
|
519
|
+
"phase",
|
|
520
|
+
"attempt",
|
|
521
|
+
"commandId",
|
|
522
|
+
"exitCode",
|
|
523
|
+
"diagnostic",
|
|
524
|
+
"artifactHash",
|
|
525
|
+
"isolatedRevisionId",
|
|
526
|
+
"outputFacts",
|
|
527
|
+
"attribution",
|
|
528
|
+
"failureIdentities",
|
|
529
|
+
"routeId",
|
|
530
|
+
"routeFingerprint",
|
|
531
|
+
]) ||
|
|
532
|
+
!Number.isSafeInteger(input.attempt) ||
|
|
533
|
+
input.attempt < 1 ||
|
|
534
|
+
input.attempt > 3 ||
|
|
535
|
+
input.exitCode !== 0 ||
|
|
536
|
+
input.attribution !== "introduced" ||
|
|
537
|
+
!Array.isArray(input.failureIdentities) ||
|
|
538
|
+
input.failureIdentities.length === 0 ||
|
|
539
|
+
input.failureIdentities.length > 256 ||
|
|
540
|
+
input.failureIdentities.some(
|
|
541
|
+
(identity) => typeof identity !== "string" || !SHA256.test(identity),
|
|
542
|
+
)
|
|
543
|
+
) {
|
|
544
|
+
throw new Error("ledger-event-field-invalid");
|
|
545
|
+
}
|
|
546
|
+
const normalizedPhase = normalizePhaseEvent({
|
|
547
|
+
runId: input.runId,
|
|
548
|
+
taskId: input.taskId,
|
|
549
|
+
eventId: input.eventId,
|
|
550
|
+
kind: "phase-verified",
|
|
551
|
+
phase: input.phase,
|
|
552
|
+
commandId: input.commandId,
|
|
553
|
+
exitCode: input.exitCode,
|
|
554
|
+
expectedClassification: "expected-green",
|
|
555
|
+
actualClassification: "expected-green",
|
|
556
|
+
diagnostic: input.diagnostic,
|
|
557
|
+
artifactHash: input.artifactHash,
|
|
558
|
+
isolatedRevisionId: input.isolatedRevisionId,
|
|
559
|
+
outputFacts: input.outputFacts,
|
|
560
|
+
routeId: input.routeId,
|
|
561
|
+
routeFingerprint: input.routeFingerprint,
|
|
562
|
+
});
|
|
563
|
+
return {
|
|
564
|
+
...input,
|
|
565
|
+
diagnostic: normalizedPhase.diagnostic,
|
|
566
|
+
outputFacts: normalizedPhase.outputFacts,
|
|
567
|
+
failureIdentities: [...new Set(input.failureIdentities)].sort(),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (input.kind === "artifact-correction") {
|
|
571
|
+
if (
|
|
572
|
+
!hasExactKeys(input, [
|
|
573
|
+
"runId",
|
|
574
|
+
"taskId",
|
|
575
|
+
"eventId",
|
|
576
|
+
"kind",
|
|
577
|
+
"phase",
|
|
578
|
+
"correctionCategory",
|
|
579
|
+
"safeFailure",
|
|
580
|
+
"candidateId",
|
|
581
|
+
"attemptId",
|
|
582
|
+
"routeId",
|
|
583
|
+
"routeFingerprint",
|
|
584
|
+
]) ||
|
|
585
|
+
!["artifact", "stale", "verification", "checkpoint"].includes(
|
|
586
|
+
input.correctionCategory,
|
|
587
|
+
) ||
|
|
588
|
+
!input.safeFailure ||
|
|
589
|
+
typeof input.safeFailure !== "object" ||
|
|
590
|
+
!hasExactKeys(input.safeFailure, ["code", "stage"])
|
|
591
|
+
) {
|
|
592
|
+
throw new Error("ledger-event-field-invalid");
|
|
593
|
+
}
|
|
594
|
+
requirePhase(input.phase);
|
|
595
|
+
safeText(input.safeFailure.code, "ledger-event-field-invalid");
|
|
596
|
+
safeText(input.safeFailure.stage, "ledger-event-field-invalid");
|
|
597
|
+
requireIdentifier(input.candidateId, "ledger-event-field-invalid");
|
|
598
|
+
requireIdentifier(input.attemptId, "ledger-event-field-invalid");
|
|
599
|
+
requireIdentifier(input.routeId, "ledger-event-field-invalid");
|
|
600
|
+
requireHash(input.routeFingerprint, "ledger-event-field-invalid");
|
|
601
|
+
return structuredClone(input);
|
|
602
|
+
}
|
|
603
|
+
if (input.kind === "route-rebound") {
|
|
604
|
+
if (
|
|
605
|
+
!hasExactKeys(input, [
|
|
606
|
+
"runId",
|
|
607
|
+
"taskId",
|
|
608
|
+
"eventId",
|
|
609
|
+
"kind",
|
|
610
|
+
"phase",
|
|
611
|
+
"routeFingerprint",
|
|
612
|
+
"routeId",
|
|
613
|
+
"attemptId",
|
|
614
|
+
])
|
|
615
|
+
) {
|
|
616
|
+
throw new Error("ledger-event-field-invalid");
|
|
617
|
+
}
|
|
618
|
+
requirePhase(input.phase);
|
|
619
|
+
requireHash(input.routeFingerprint, "ledger-event-field-invalid");
|
|
620
|
+
requireIdentifier(input.routeId, "ledger-event-field-invalid");
|
|
621
|
+
requireIdentifier(input.attemptId, "ledger-event-field-invalid");
|
|
622
|
+
return { ...input };
|
|
623
|
+
}
|
|
624
|
+
throw new Error("ledger-event-field-invalid");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const FORBIDDEN_PROJECTION_KEY =
|
|
628
|
+
/(?:raw|prompt|transcript|reasoning|model.?output|credential|secret|tool.?history|command.?log)/iu;
|
|
629
|
+
|
|
630
|
+
function validateProjectionValue(value: unknown, depth = 0): void {
|
|
631
|
+
if (depth > 12) throw new Error("ledger-projection-invalid");
|
|
632
|
+
if (
|
|
633
|
+
value === null ||
|
|
634
|
+
typeof value === "string" ||
|
|
635
|
+
typeof value === "number" ||
|
|
636
|
+
typeof value === "boolean"
|
|
637
|
+
) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (Array.isArray(value)) {
|
|
641
|
+
if (value.length > 512) throw new Error("ledger-projection-invalid");
|
|
642
|
+
for (const entry of value) validateProjectionValue(entry, depth + 1);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (!value || typeof value !== "object") {
|
|
646
|
+
throw new Error("ledger-projection-invalid");
|
|
647
|
+
}
|
|
648
|
+
const entries = Object.entries(value);
|
|
649
|
+
if (entries.length > 128) throw new Error("ledger-projection-invalid");
|
|
650
|
+
for (const [key, entry] of entries) {
|
|
651
|
+
if (FORBIDDEN_PROJECTION_KEY.test(key)) {
|
|
652
|
+
throw new Error("ledger-projection-private-field");
|
|
653
|
+
}
|
|
654
|
+
validateProjectionValue(entry, depth + 1);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function serializeTaskLedgerProjection(value: unknown): string {
|
|
659
|
+
validateProjectionValue(value);
|
|
660
|
+
const serialized = JSON.stringify(value);
|
|
661
|
+
if (Buffer.byteLength(serialized) > TASK_LEDGER_LIMITS.maxProjectionBytes) {
|
|
662
|
+
throw new Error("ledger-projection-capacity-exceeded");
|
|
663
|
+
}
|
|
664
|
+
return serialized;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Durable task facts and bounded sealed candidate segments. */
|
|
668
|
+
export class TaskLedger {
|
|
669
|
+
readonly root: string;
|
|
670
|
+
readonly #artifacts: ArtifactStore;
|
|
671
|
+
readonly #database: DatabaseSync;
|
|
672
|
+
#closed = false;
|
|
673
|
+
|
|
674
|
+
constructor(options: TaskLedgerOptions) {
|
|
675
|
+
if (!options || typeof options !== "object") {
|
|
676
|
+
throw new Error("task-ledger-options-invalid");
|
|
677
|
+
}
|
|
678
|
+
this.root = path.resolve(options.root);
|
|
679
|
+
ensurePrivateDirectory(this.root);
|
|
680
|
+
this.#artifacts = options.artifacts;
|
|
681
|
+
const databasePath = path.resolve(
|
|
682
|
+
options.databasePath ?? path.join(this.root, "task-ledger.sqlite3"),
|
|
683
|
+
);
|
|
684
|
+
const relative = path.relative(this.root, databasePath);
|
|
685
|
+
if (
|
|
686
|
+
relative.startsWith("..") ||
|
|
687
|
+
path.isAbsolute(relative) ||
|
|
688
|
+
databasePath === this.root
|
|
689
|
+
) {
|
|
690
|
+
throw new Error("task-ledger-database-invalid");
|
|
691
|
+
}
|
|
692
|
+
const existing = lstatSync(databasePath, { throwIfNoEntry: false });
|
|
693
|
+
if (existing && (!existing.isFile() || existing.isSymbolicLink())) {
|
|
694
|
+
throw new Error("task-ledger-database-invalid");
|
|
695
|
+
}
|
|
696
|
+
this.#database = new DatabaseSync(databasePath);
|
|
697
|
+
this.#database.exec(
|
|
698
|
+
"PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; PRAGMA foreign_keys = ON;",
|
|
699
|
+
);
|
|
700
|
+
this.#database.exec(`
|
|
701
|
+
CREATE TABLE IF NOT EXISTS candidates (
|
|
702
|
+
candidate_id TEXT PRIMARY KEY,
|
|
703
|
+
identity_json TEXT NOT NULL,
|
|
704
|
+
state TEXT NOT NULL,
|
|
705
|
+
next_sequence INTEGER NOT NULL,
|
|
706
|
+
total_bytes INTEGER NOT NULL,
|
|
707
|
+
artifact_hash TEXT,
|
|
708
|
+
candidate_hash TEXT,
|
|
709
|
+
segment_count INTEGER,
|
|
710
|
+
paths_json TEXT,
|
|
711
|
+
pause_code TEXT
|
|
712
|
+
) STRICT;
|
|
713
|
+
CREATE TABLE IF NOT EXISTS candidate_segments (
|
|
714
|
+
candidate_id TEXT NOT NULL REFERENCES candidates(candidate_id) ON DELETE CASCADE,
|
|
715
|
+
sequence INTEGER NOT NULL,
|
|
716
|
+
artifact_hash TEXT NOT NULL,
|
|
717
|
+
size_bytes INTEGER NOT NULL,
|
|
718
|
+
segment_hash TEXT NOT NULL,
|
|
719
|
+
PRIMARY KEY (candidate_id, sequence)
|
|
720
|
+
) STRICT;
|
|
721
|
+
CREATE TABLE IF NOT EXISTS task_ledger (
|
|
722
|
+
run_id TEXT NOT NULL,
|
|
723
|
+
task_id TEXT NOT NULL,
|
|
724
|
+
delivery_revision INTEGER NOT NULL,
|
|
725
|
+
boundary_hash TEXT NOT NULL,
|
|
726
|
+
objective TEXT NOT NULL,
|
|
727
|
+
context_refs_json TEXT NOT NULL,
|
|
728
|
+
initial_phase TEXT NOT NULL,
|
|
729
|
+
PRIMARY KEY (run_id, task_id)
|
|
730
|
+
) STRICT;
|
|
731
|
+
CREATE TABLE IF NOT EXISTS task_events (
|
|
732
|
+
run_id TEXT NOT NULL,
|
|
733
|
+
task_id TEXT NOT NULL,
|
|
734
|
+
ordinal INTEGER NOT NULL,
|
|
735
|
+
event_id TEXT NOT NULL,
|
|
736
|
+
event_json TEXT NOT NULL,
|
|
737
|
+
prior_hash TEXT NOT NULL,
|
|
738
|
+
event_hash TEXT NOT NULL,
|
|
739
|
+
PRIMARY KEY (run_id, task_id, ordinal),
|
|
740
|
+
UNIQUE (run_id, task_id, event_id),
|
|
741
|
+
FOREIGN KEY (run_id, task_id) REFERENCES task_ledger(run_id, task_id) ON DELETE CASCADE
|
|
742
|
+
) STRICT;
|
|
743
|
+
CREATE TABLE IF NOT EXISTS durable_facts (
|
|
744
|
+
fact_key TEXT PRIMARY KEY,
|
|
745
|
+
fact_json TEXT NOT NULL
|
|
746
|
+
) STRICT;
|
|
747
|
+
`);
|
|
748
|
+
chmodSync(databasePath, 0o600);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
#assertOpen(): void {
|
|
752
|
+
if (this.#closed) throw new Error("task-ledger-closed");
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
#candidate(candidateId: string): CandidateRow {
|
|
756
|
+
this.#assertOpen();
|
|
757
|
+
requireIdentifier(candidateId, "candidate-identity-invalid");
|
|
758
|
+
const row = this.#database
|
|
759
|
+
.prepare(
|
|
760
|
+
`SELECT identity_json, state, next_sequence, total_bytes,
|
|
761
|
+
artifact_hash, candidate_hash, segment_count, paths_json,
|
|
762
|
+
pause_code
|
|
763
|
+
FROM candidates WHERE candidate_id = ?`,
|
|
764
|
+
)
|
|
765
|
+
.get(candidateId) as CandidateRow | undefined;
|
|
766
|
+
if (!row) throw new Error("candidate-unavailable");
|
|
767
|
+
return row;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
beginCandidate(input: BeginCandidateInput) {
|
|
771
|
+
this.#assertOpen();
|
|
772
|
+
const identity = candidateIdentity(input);
|
|
773
|
+
const json = JSON.stringify(identity);
|
|
774
|
+
const existing = this.#database
|
|
775
|
+
.prepare(
|
|
776
|
+
"SELECT identity_json, state FROM candidates WHERE candidate_id = ?",
|
|
777
|
+
)
|
|
778
|
+
.get(identity.candidateId) as
|
|
779
|
+
| { identity_json: string; state: string }
|
|
780
|
+
| undefined;
|
|
781
|
+
if (existing) {
|
|
782
|
+
if (existing.identity_json !== json) {
|
|
783
|
+
throw new Error("candidate-identity-mismatch");
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
ok: true,
|
|
787
|
+
candidateId: identity.candidateId,
|
|
788
|
+
state: existing.state,
|
|
789
|
+
replayed: true,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
this.#database
|
|
793
|
+
.prepare(
|
|
794
|
+
`INSERT INTO candidates (
|
|
795
|
+
candidate_id, identity_json, state, next_sequence, total_bytes
|
|
796
|
+
) VALUES (?, ?, 'unsealed', 0, 0)`,
|
|
797
|
+
)
|
|
798
|
+
.run(identity.candidateId, json);
|
|
799
|
+
return { ok: true, candidateId: identity.candidateId, state: "unsealed" };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
appendCandidateSegment(
|
|
803
|
+
input: AppendCandidateSegmentInput,
|
|
804
|
+
): AppendCandidateSegmentResult {
|
|
805
|
+
this.#assertOpen();
|
|
806
|
+
if (
|
|
807
|
+
!hasExactKeys(input, [
|
|
808
|
+
"candidateId",
|
|
809
|
+
"runId",
|
|
810
|
+
"deliveryRevision",
|
|
811
|
+
"taskId",
|
|
812
|
+
"phase",
|
|
813
|
+
"attemptId",
|
|
814
|
+
"approvedPaths",
|
|
815
|
+
"isolatedRevisionId",
|
|
816
|
+
"verificationId",
|
|
817
|
+
"routeId",
|
|
818
|
+
"routeFingerprint",
|
|
819
|
+
"sequence",
|
|
820
|
+
"bytes",
|
|
821
|
+
"segmentHash",
|
|
822
|
+
])
|
|
823
|
+
) {
|
|
824
|
+
throw new Error("candidate-segment-invalid");
|
|
825
|
+
}
|
|
826
|
+
const identity = identityFromSegment(input);
|
|
827
|
+
const row = this.#candidate(identity.candidateId);
|
|
828
|
+
if (!sameIdentity(row.identity_json, identity)) {
|
|
829
|
+
throw new Error("candidate-identity-mismatch");
|
|
830
|
+
}
|
|
831
|
+
if (row.state === "paused") {
|
|
832
|
+
return {
|
|
833
|
+
ok: false,
|
|
834
|
+
state: "paused",
|
|
835
|
+
code: row.pause_code ?? "needs-task-split",
|
|
836
|
+
limitBytes: TASK_LEDGER_LIMITS.maxCandidateBytes,
|
|
837
|
+
replayed: true,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
if (row.state !== "unsealed") throw new Error("candidate-already-sealed");
|
|
841
|
+
if (
|
|
842
|
+
!Number.isSafeInteger(input.sequence) ||
|
|
843
|
+
input.sequence < 0 ||
|
|
844
|
+
input.sequence !== row.next_sequence
|
|
845
|
+
) {
|
|
846
|
+
throw new Error("candidate-segment-order-invalid");
|
|
847
|
+
}
|
|
848
|
+
if (
|
|
849
|
+
!(input.bytes instanceof Uint8Array) ||
|
|
850
|
+
input.bytes.byteLength === 0 ||
|
|
851
|
+
input.bytes.byteLength > TASK_LEDGER_LIMITS.maxSegmentBytes
|
|
852
|
+
) {
|
|
853
|
+
throw new Error("candidate-segment-size-invalid");
|
|
854
|
+
}
|
|
855
|
+
requireHash(input.segmentHash, "candidate-segment-hash-invalid");
|
|
856
|
+
const bytes = Buffer.from(input.bytes);
|
|
857
|
+
if (sha256(bytes) !== input.segmentHash) {
|
|
858
|
+
throw new Error("candidate-segment-hash-invalid");
|
|
859
|
+
}
|
|
860
|
+
if (
|
|
861
|
+
row.total_bytes + bytes.byteLength >
|
|
862
|
+
TASK_LEDGER_LIMITS.maxCandidateBytes
|
|
863
|
+
) {
|
|
864
|
+
this.#database
|
|
865
|
+
.prepare(
|
|
866
|
+
"UPDATE candidates SET state = 'paused', pause_code = 'needs-task-split' WHERE candidate_id = ?",
|
|
867
|
+
)
|
|
868
|
+
.run(identity.candidateId);
|
|
869
|
+
return {
|
|
870
|
+
ok: false,
|
|
871
|
+
state: "paused",
|
|
872
|
+
code: "needs-task-split",
|
|
873
|
+
limitBytes: TASK_LEDGER_LIMITS.maxCandidateBytes,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
const artifact = this.#artifacts.put(bytes);
|
|
877
|
+
this.#artifacts.retain(artifact.hash);
|
|
878
|
+
try {
|
|
879
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
880
|
+
this.#database
|
|
881
|
+
.prepare(
|
|
882
|
+
`INSERT INTO candidate_segments (
|
|
883
|
+
candidate_id, sequence, artifact_hash, size_bytes, segment_hash
|
|
884
|
+
) VALUES (?, ?, ?, ?, ?)`,
|
|
885
|
+
)
|
|
886
|
+
.run(
|
|
887
|
+
identity.candidateId,
|
|
888
|
+
input.sequence,
|
|
889
|
+
artifact.hash,
|
|
890
|
+
artifact.bytes,
|
|
891
|
+
input.segmentHash,
|
|
892
|
+
);
|
|
893
|
+
this.#database
|
|
894
|
+
.prepare(
|
|
895
|
+
`UPDATE candidates
|
|
896
|
+
SET next_sequence = next_sequence + 1,
|
|
897
|
+
total_bytes = total_bytes + ?
|
|
898
|
+
WHERE candidate_id = ?`,
|
|
899
|
+
)
|
|
900
|
+
.run(artifact.bytes, identity.candidateId);
|
|
901
|
+
this.#database.exec("COMMIT");
|
|
902
|
+
} catch (error) {
|
|
903
|
+
try {
|
|
904
|
+
this.#database.exec("ROLLBACK");
|
|
905
|
+
} catch {}
|
|
906
|
+
this.#artifacts.release(artifact.hash);
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
ok: true,
|
|
911
|
+
state: "unsealed",
|
|
912
|
+
nextSequence: input.sequence + 1,
|
|
913
|
+
totalBytes: row.total_bytes + artifact.bytes,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
#segments(candidateId: string): SegmentRow[] {
|
|
918
|
+
return this.#database
|
|
919
|
+
.prepare(
|
|
920
|
+
`SELECT sequence, artifact_hash, size_bytes, segment_hash
|
|
921
|
+
FROM candidate_segments WHERE candidate_id = ? ORDER BY sequence`,
|
|
922
|
+
)
|
|
923
|
+
.all(candidateId) as unknown as SegmentRow[];
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
sealCandidate(input: SealCandidateInput): SealCandidateResult {
|
|
927
|
+
this.#assertOpen();
|
|
928
|
+
if (
|
|
929
|
+
!hasExactKeys(input, [
|
|
930
|
+
"candidateId",
|
|
931
|
+
"runId",
|
|
932
|
+
"deliveryRevision",
|
|
933
|
+
"taskId",
|
|
934
|
+
"phase",
|
|
935
|
+
"attemptId",
|
|
936
|
+
"approvedPaths",
|
|
937
|
+
"isolatedRevisionId",
|
|
938
|
+
"verificationId",
|
|
939
|
+
"routeId",
|
|
940
|
+
"routeFingerprint",
|
|
941
|
+
"segmentCount",
|
|
942
|
+
"totalBytes",
|
|
943
|
+
"candidateHash",
|
|
944
|
+
])
|
|
945
|
+
) {
|
|
946
|
+
throw new Error("candidate-seal-invalid");
|
|
947
|
+
}
|
|
948
|
+
const identity = identityFromSeal(input);
|
|
949
|
+
const row = this.#candidate(identity.candidateId);
|
|
950
|
+
if (!sameIdentity(row.identity_json, identity)) {
|
|
951
|
+
throw new Error("candidate-identity-mismatch");
|
|
952
|
+
}
|
|
953
|
+
if (
|
|
954
|
+
!Number.isSafeInteger(input.segmentCount) ||
|
|
955
|
+
input.segmentCount < 1 ||
|
|
956
|
+
!Number.isSafeInteger(input.totalBytes) ||
|
|
957
|
+
input.totalBytes < 1
|
|
958
|
+
) {
|
|
959
|
+
throw new Error("candidate-seal-invalid");
|
|
960
|
+
}
|
|
961
|
+
requireHash(input.candidateHash, "candidate-seal-hash-invalid");
|
|
962
|
+
if (row.state === "sealed") {
|
|
963
|
+
if (
|
|
964
|
+
row.segment_count !== input.segmentCount ||
|
|
965
|
+
row.total_bytes !== input.totalBytes ||
|
|
966
|
+
row.candidate_hash !== input.candidateHash
|
|
967
|
+
) {
|
|
968
|
+
throw new Error("candidate-seal-conflict");
|
|
969
|
+
}
|
|
970
|
+
if (!row.artifact_hash) {
|
|
971
|
+
throw new Error("candidate-seal-integrity-invalid");
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
ok: true,
|
|
975
|
+
state: "sealed",
|
|
976
|
+
artifactHash: row.artifact_hash,
|
|
977
|
+
bytes: row.total_bytes,
|
|
978
|
+
paths: JSON.parse(row.paths_json ?? "[]") as string[],
|
|
979
|
+
replayed: true,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
if (row.state === "paused") {
|
|
983
|
+
return {
|
|
984
|
+
ok: false,
|
|
985
|
+
state: "paused",
|
|
986
|
+
code: row.pause_code ?? "needs-task-split",
|
|
987
|
+
limitBytes: TASK_LEDGER_LIMITS.maxCandidateBytes,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const segments = this.#segments(identity.candidateId);
|
|
991
|
+
if (
|
|
992
|
+
segments.length !== input.segmentCount ||
|
|
993
|
+
row.next_sequence !== input.segmentCount
|
|
994
|
+
) {
|
|
995
|
+
return { ok: false, state: "unsealed", code: "candidate-incomplete" };
|
|
996
|
+
}
|
|
997
|
+
const buffers = segments.map((segment, index) => {
|
|
998
|
+
if (segment.sequence !== index) {
|
|
999
|
+
throw new Error("candidate-segment-order-invalid");
|
|
1000
|
+
}
|
|
1001
|
+
const bytes = Buffer.from(this.#artifacts.read(segment.artifact_hash));
|
|
1002
|
+
if (
|
|
1003
|
+
bytes.byteLength !== segment.size_bytes ||
|
|
1004
|
+
sha256(bytes) !== segment.segment_hash
|
|
1005
|
+
) {
|
|
1006
|
+
throw new Error("candidate-segment-integrity-invalid");
|
|
1007
|
+
}
|
|
1008
|
+
return bytes;
|
|
1009
|
+
});
|
|
1010
|
+
const candidate = Buffer.concat(buffers);
|
|
1011
|
+
if (
|
|
1012
|
+
candidate.byteLength !== input.totalBytes ||
|
|
1013
|
+
candidate.byteLength !== row.total_bytes ||
|
|
1014
|
+
sha256(candidate) !== input.candidateHash
|
|
1015
|
+
) {
|
|
1016
|
+
throw new Error("candidate-seal-hash-invalid");
|
|
1017
|
+
}
|
|
1018
|
+
let diff: string;
|
|
1019
|
+
try {
|
|
1020
|
+
diff = new TextDecoder("utf-8", { fatal: true }).decode(candidate);
|
|
1021
|
+
} catch {
|
|
1022
|
+
throw new Error("candidate-diff-invalid");
|
|
1023
|
+
}
|
|
1024
|
+
let paths: string[];
|
|
1025
|
+
try {
|
|
1026
|
+
paths = diffWritePaths(diff).paths;
|
|
1027
|
+
} catch {
|
|
1028
|
+
throw new Error("candidate-diff-invalid");
|
|
1029
|
+
}
|
|
1030
|
+
if (paths.some((relative) => !identity.approvedPaths.includes(relative))) {
|
|
1031
|
+
throw new Error("candidate-write-set-mismatch");
|
|
1032
|
+
}
|
|
1033
|
+
const artifact = this.#artifacts.put(candidate);
|
|
1034
|
+
this.#artifacts.retain(artifact.hash);
|
|
1035
|
+
this.#database
|
|
1036
|
+
.prepare(
|
|
1037
|
+
`UPDATE candidates
|
|
1038
|
+
SET state = 'sealed', artifact_hash = ?, candidate_hash = ?,
|
|
1039
|
+
segment_count = ?, paths_json = ?, pause_code = NULL
|
|
1040
|
+
WHERE candidate_id = ? AND state = 'unsealed'`,
|
|
1041
|
+
)
|
|
1042
|
+
.run(
|
|
1043
|
+
artifact.hash,
|
|
1044
|
+
input.candidateHash,
|
|
1045
|
+
input.segmentCount,
|
|
1046
|
+
JSON.stringify(paths),
|
|
1047
|
+
identity.candidateId,
|
|
1048
|
+
);
|
|
1049
|
+
return {
|
|
1050
|
+
ok: true,
|
|
1051
|
+
state: "sealed",
|
|
1052
|
+
artifactHash: artifact.hash,
|
|
1053
|
+
bytes: artifact.bytes,
|
|
1054
|
+
paths,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
readSealedCandidate(candidateId: string): Uint8Array {
|
|
1059
|
+
const row = this.#candidate(candidateId);
|
|
1060
|
+
if (row.state !== "sealed" || !row.artifact_hash) {
|
|
1061
|
+
throw new Error("candidate-not-sealed");
|
|
1062
|
+
}
|
|
1063
|
+
const bytes = this.#artifacts.read(row.artifact_hash);
|
|
1064
|
+
if (
|
|
1065
|
+
bytes.byteLength !== row.total_bytes ||
|
|
1066
|
+
sha256(bytes) !== row.candidate_hash
|
|
1067
|
+
) {
|
|
1068
|
+
throw new Error("candidate-seal-integrity-invalid");
|
|
1069
|
+
}
|
|
1070
|
+
return Buffer.from(bytes);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
openTask(input: OpenTaskInput) {
|
|
1074
|
+
this.#assertOpen();
|
|
1075
|
+
if (
|
|
1076
|
+
!input ||
|
|
1077
|
+
typeof input !== "object" ||
|
|
1078
|
+
Array.isArray(input) ||
|
|
1079
|
+
!hasExactKeys(input, [
|
|
1080
|
+
"runId",
|
|
1081
|
+
"deliveryRevision",
|
|
1082
|
+
"taskId",
|
|
1083
|
+
"boundaryHash",
|
|
1084
|
+
"objective",
|
|
1085
|
+
"contextRefs",
|
|
1086
|
+
"initialPhase",
|
|
1087
|
+
]) ||
|
|
1088
|
+
!Number.isSafeInteger(input.deliveryRevision) ||
|
|
1089
|
+
input.deliveryRevision < 1 ||
|
|
1090
|
+
typeof input.objective !== "string" ||
|
|
1091
|
+
input.objective.length === 0 ||
|
|
1092
|
+
Buffer.byteLength(input.objective) > TASK_LEDGER_LIMITS.maxObjectiveBytes
|
|
1093
|
+
) {
|
|
1094
|
+
throw new Error("ledger-task-invalid");
|
|
1095
|
+
}
|
|
1096
|
+
requireTaskKey(input.runId, input.taskId);
|
|
1097
|
+
requireHash(input.boundaryHash, "ledger-task-invalid");
|
|
1098
|
+
requirePhase(input.initialPhase);
|
|
1099
|
+
const contextRefs = normalizeContextRefs(input.contextRefs);
|
|
1100
|
+
const existing = this.#database
|
|
1101
|
+
.prepare(
|
|
1102
|
+
`SELECT delivery_revision, boundary_hash, objective,
|
|
1103
|
+
context_refs_json, initial_phase
|
|
1104
|
+
FROM task_ledger WHERE run_id = ? AND task_id = ?`,
|
|
1105
|
+
)
|
|
1106
|
+
.get(input.runId, input.taskId) as TaskRow | undefined;
|
|
1107
|
+
const expected = {
|
|
1108
|
+
delivery_revision: input.deliveryRevision,
|
|
1109
|
+
boundary_hash: input.boundaryHash,
|
|
1110
|
+
objective: input.objective,
|
|
1111
|
+
context_refs_json: JSON.stringify(contextRefs),
|
|
1112
|
+
initial_phase: input.initialPhase,
|
|
1113
|
+
};
|
|
1114
|
+
if (existing) {
|
|
1115
|
+
if (JSON.stringify(existing) !== JSON.stringify(expected)) {
|
|
1116
|
+
throw new Error("ledger-task-conflict");
|
|
1117
|
+
}
|
|
1118
|
+
return { accepted: true, replayed: true };
|
|
1119
|
+
}
|
|
1120
|
+
this.#database
|
|
1121
|
+
.prepare(
|
|
1122
|
+
`INSERT INTO task_ledger (
|
|
1123
|
+
run_id, task_id, delivery_revision, boundary_hash, objective,
|
|
1124
|
+
context_refs_json, initial_phase
|
|
1125
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1126
|
+
)
|
|
1127
|
+
.run(
|
|
1128
|
+
input.runId,
|
|
1129
|
+
input.taskId,
|
|
1130
|
+
input.deliveryRevision,
|
|
1131
|
+
input.boundaryHash,
|
|
1132
|
+
input.objective,
|
|
1133
|
+
JSON.stringify(contextRefs),
|
|
1134
|
+
input.initialPhase,
|
|
1135
|
+
);
|
|
1136
|
+
return { accepted: true };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
#task(runId: string, taskId: string): TaskRow {
|
|
1140
|
+
requireTaskKey(runId, taskId);
|
|
1141
|
+
const row = this.#database
|
|
1142
|
+
.prepare(
|
|
1143
|
+
`SELECT delivery_revision, boundary_hash, objective,
|
|
1144
|
+
context_refs_json, initial_phase
|
|
1145
|
+
FROM task_ledger WHERE run_id = ? AND task_id = ?`,
|
|
1146
|
+
)
|
|
1147
|
+
.get(runId, taskId) as TaskRow | undefined;
|
|
1148
|
+
if (!row) throw new Error("ledger-task-unavailable");
|
|
1149
|
+
return row;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
#events(runId: string, taskId: string): EventRow[] {
|
|
1153
|
+
const rows = this.#database
|
|
1154
|
+
.prepare(
|
|
1155
|
+
`SELECT ordinal, event_id, event_json, prior_hash, event_hash
|
|
1156
|
+
FROM task_events WHERE run_id = ? AND task_id = ? ORDER BY ordinal`,
|
|
1157
|
+
)
|
|
1158
|
+
.all(runId, taskId) as unknown as EventRow[];
|
|
1159
|
+
let prior = ZERO_HASH;
|
|
1160
|
+
for (const [index, row] of rows.entries()) {
|
|
1161
|
+
if (
|
|
1162
|
+
row.ordinal !== index + 1 ||
|
|
1163
|
+
row.prior_hash !== prior ||
|
|
1164
|
+
row.event_hash !== hashEvent(prior, row.ordinal, row.event_json)
|
|
1165
|
+
) {
|
|
1166
|
+
throw new Error("ledger-event-integrity-invalid");
|
|
1167
|
+
}
|
|
1168
|
+
prior = row.event_hash;
|
|
1169
|
+
}
|
|
1170
|
+
return rows;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
commitVerifiedEvent(input: VerifiedTaskEvent) {
|
|
1174
|
+
this.#assertOpen();
|
|
1175
|
+
const normalized = normalizeVerifiedEvent(input);
|
|
1176
|
+
this.#task(normalized.runId, normalized.taskId);
|
|
1177
|
+
const rows = this.#events(normalized.runId, normalized.taskId);
|
|
1178
|
+
const existing = rows.find((row) => row.event_id === normalized.eventId);
|
|
1179
|
+
const json = JSON.stringify(normalized);
|
|
1180
|
+
if (existing) {
|
|
1181
|
+
if (existing.event_json !== json)
|
|
1182
|
+
throw new Error("ledger-event-conflict");
|
|
1183
|
+
return { accepted: true, ordinal: existing.ordinal, replayed: true };
|
|
1184
|
+
}
|
|
1185
|
+
if (rows.length >= TASK_LEDGER_LIMITS.maxEventsPerTask) {
|
|
1186
|
+
throw new Error("ledger-event-capacity-exceeded");
|
|
1187
|
+
}
|
|
1188
|
+
if (normalized.kind === "phase-verified") {
|
|
1189
|
+
const completed = rows
|
|
1190
|
+
.map((row) => JSON.parse(row.event_json) as VerifiedTaskEvent)
|
|
1191
|
+
.filter(
|
|
1192
|
+
(event): event is PhaseVerifiedEvent =>
|
|
1193
|
+
event.kind === "phase-verified",
|
|
1194
|
+
)
|
|
1195
|
+
.map((event) => event.phase);
|
|
1196
|
+
const required =
|
|
1197
|
+
normalized.phase === "green"
|
|
1198
|
+
? "red"
|
|
1199
|
+
: normalized.phase === "refactor"
|
|
1200
|
+
? "green"
|
|
1201
|
+
: undefined;
|
|
1202
|
+
if (required && !completed.includes(required)) {
|
|
1203
|
+
throw new Error("ledger-phase-order-invalid");
|
|
1204
|
+
}
|
|
1205
|
+
if (completed.includes(normalized.phase)) {
|
|
1206
|
+
throw new Error("ledger-phase-order-invalid");
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const ordinal = rows.length + 1;
|
|
1210
|
+
const priorHash = rows.at(-1)?.event_hash ?? ZERO_HASH;
|
|
1211
|
+
const digest = hashEvent(priorHash, ordinal, json);
|
|
1212
|
+
this.#database
|
|
1213
|
+
.prepare(
|
|
1214
|
+
`INSERT INTO task_events (
|
|
1215
|
+
run_id, task_id, ordinal, event_id, event_json, prior_hash,
|
|
1216
|
+
event_hash
|
|
1217
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1218
|
+
)
|
|
1219
|
+
.run(
|
|
1220
|
+
normalized.runId,
|
|
1221
|
+
normalized.taskId,
|
|
1222
|
+
ordinal,
|
|
1223
|
+
normalized.eventId,
|
|
1224
|
+
json,
|
|
1225
|
+
priorHash,
|
|
1226
|
+
digest,
|
|
1227
|
+
);
|
|
1228
|
+
return { accepted: true, ordinal };
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
putDurableFact(factKey: string, fact: unknown): unknown {
|
|
1232
|
+
this.#assertOpen();
|
|
1233
|
+
requireIdentifier(factKey, "ledger-fact-key-invalid");
|
|
1234
|
+
const factJson = serializeTaskLedgerProjection(fact);
|
|
1235
|
+
const existing = this.#database
|
|
1236
|
+
.prepare("SELECT fact_json FROM durable_facts WHERE fact_key = ?")
|
|
1237
|
+
.get(factKey) as { fact_json: string } | undefined;
|
|
1238
|
+
if (existing) {
|
|
1239
|
+
if (existing.fact_json !== factJson) {
|
|
1240
|
+
throw new Error("ledger-fact-conflict");
|
|
1241
|
+
}
|
|
1242
|
+
return JSON.parse(existing.fact_json) as unknown;
|
|
1243
|
+
}
|
|
1244
|
+
this.#database
|
|
1245
|
+
.prepare("INSERT INTO durable_facts(fact_key, fact_json) VALUES (?, ?)")
|
|
1246
|
+
.run(factKey, factJson);
|
|
1247
|
+
return JSON.parse(factJson) as unknown;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
durableFact(factKey: string): unknown | undefined {
|
|
1251
|
+
this.#assertOpen();
|
|
1252
|
+
requireIdentifier(factKey, "ledger-fact-key-invalid");
|
|
1253
|
+
const row = this.#database
|
|
1254
|
+
.prepare("SELECT fact_json FROM durable_facts WHERE fact_key = ?")
|
|
1255
|
+
.get(factKey) as { fact_json: string } | undefined;
|
|
1256
|
+
return row ? (JSON.parse(row.fact_json) as unknown) : undefined;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
replaceDurableFact(
|
|
1260
|
+
factKey: string,
|
|
1261
|
+
expectedFact: unknown,
|
|
1262
|
+
fact: unknown,
|
|
1263
|
+
): unknown {
|
|
1264
|
+
this.#assertOpen();
|
|
1265
|
+
requireIdentifier(factKey, "ledger-fact-key-invalid");
|
|
1266
|
+
const expectedJson = serializeTaskLedgerProjection(expectedFact);
|
|
1267
|
+
const factJson = serializeTaskLedgerProjection(fact);
|
|
1268
|
+
const result = this.#database
|
|
1269
|
+
.prepare(
|
|
1270
|
+
"UPDATE durable_facts SET fact_json = ? WHERE fact_key = ? AND fact_json = ?",
|
|
1271
|
+
)
|
|
1272
|
+
.run(factJson, factKey, expectedJson);
|
|
1273
|
+
if (Number(result.changes) !== 1) {
|
|
1274
|
+
throw new Error("ledger-fact-conflict");
|
|
1275
|
+
}
|
|
1276
|
+
return JSON.parse(factJson) as unknown;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
commitVerifiedEvents(events: VerifiedTaskEvent[]): void {
|
|
1280
|
+
this.#assertOpen();
|
|
1281
|
+
if (!Array.isArray(events) || events.length === 0 || events.length > 4) {
|
|
1282
|
+
throw new Error("ledger-event-batch-invalid");
|
|
1283
|
+
}
|
|
1284
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
1285
|
+
try {
|
|
1286
|
+
for (const event of events) this.commitVerifiedEvent(event);
|
|
1287
|
+
this.#database.exec("COMMIT");
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
this.#database.exec("ROLLBACK");
|
|
1290
|
+
throw error;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
commitTaskCompletion(input: {
|
|
1295
|
+
events: VerifiedTaskEvent[];
|
|
1296
|
+
factKey: string;
|
|
1297
|
+
fact: unknown;
|
|
1298
|
+
}): unknown {
|
|
1299
|
+
this.#assertOpen();
|
|
1300
|
+
if (
|
|
1301
|
+
!input ||
|
|
1302
|
+
typeof input !== "object" ||
|
|
1303
|
+
Array.isArray(input) ||
|
|
1304
|
+
!hasExactKeys(input, ["events", "factKey", "fact"]) ||
|
|
1305
|
+
!Array.isArray(input.events) ||
|
|
1306
|
+
input.events.length === 0 ||
|
|
1307
|
+
input.events.length > 4
|
|
1308
|
+
) {
|
|
1309
|
+
throw new Error("ledger-task-completion-invalid");
|
|
1310
|
+
}
|
|
1311
|
+
requireIdentifier(input.factKey, "ledger-fact-key-invalid");
|
|
1312
|
+
serializeTaskLedgerProjection(input.fact);
|
|
1313
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
1314
|
+
try {
|
|
1315
|
+
for (const event of input.events) this.commitVerifiedEvent(event);
|
|
1316
|
+
const fact = this.putDurableFact(input.factKey, input.fact);
|
|
1317
|
+
this.#database.exec("COMMIT");
|
|
1318
|
+
return fact;
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
this.#database.exec("ROLLBACK");
|
|
1321
|
+
throw error;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
recordWorkerClaim(_input: Record<string, unknown>) {
|
|
1326
|
+
this.#assertOpen();
|
|
1327
|
+
return { accepted: false, reason: "worker-claim-untrusted" } as const;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
projection(input: {
|
|
1331
|
+
runId: string;
|
|
1332
|
+
taskId: string;
|
|
1333
|
+
nextPhase: ImplementationPhase;
|
|
1334
|
+
}) {
|
|
1335
|
+
this.#assertOpen();
|
|
1336
|
+
if (
|
|
1337
|
+
!input ||
|
|
1338
|
+
typeof input !== "object" ||
|
|
1339
|
+
Array.isArray(input) ||
|
|
1340
|
+
!hasExactKeys(input, ["runId", "taskId", "nextPhase"])
|
|
1341
|
+
) {
|
|
1342
|
+
throw new Error("ledger-projection-request-invalid");
|
|
1343
|
+
}
|
|
1344
|
+
requirePhase(input.nextPhase);
|
|
1345
|
+
const task = this.#task(input.runId, input.taskId);
|
|
1346
|
+
const history = this.#events(input.runId, input.taskId).map((row) => {
|
|
1347
|
+
const value = JSON.parse(row.event_json) as VerifiedTaskEvent;
|
|
1348
|
+
const {
|
|
1349
|
+
runId: _runId,
|
|
1350
|
+
taskId: _taskId,
|
|
1351
|
+
eventId: _eventId,
|
|
1352
|
+
...fact
|
|
1353
|
+
} = value;
|
|
1354
|
+
return { ordinal: row.ordinal, ...fact };
|
|
1355
|
+
});
|
|
1356
|
+
const projection = {
|
|
1357
|
+
version: 2,
|
|
1358
|
+
runId: input.runId,
|
|
1359
|
+
taskId: input.taskId,
|
|
1360
|
+
deliveryRevision: task.delivery_revision,
|
|
1361
|
+
boundaryHash: task.boundary_hash,
|
|
1362
|
+
objective: task.objective,
|
|
1363
|
+
contextRefs: JSON.parse(task.context_refs_json) as TaskContextReference[],
|
|
1364
|
+
currentPhase: input.nextPhase,
|
|
1365
|
+
history,
|
|
1366
|
+
};
|
|
1367
|
+
serializeTaskLedgerProjection(projection);
|
|
1368
|
+
return structuredClone(projection);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
close(): void {
|
|
1372
|
+
if (this.#closed) return;
|
|
1373
|
+
this.#closed = true;
|
|
1374
|
+
this.#database.close();
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
[Symbol.dispose](): void {
|
|
1378
|
+
this.close();
|
|
1379
|
+
}
|
|
1380
|
+
}
|