@abelxiaoxing/cadence 1.0.14 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +39 -13
  2. package/agents/contract-reviewer.md +2 -2
  3. package/agents/design-explorer.md +1 -1
  4. package/agents/diagnosis-worker.md +6 -3
  5. package/agents/implementation-worker.md +14 -9
  6. package/config/.env.example +5 -17
  7. package/config/routes.example.json +42 -0
  8. package/package.json +3 -3
  9. package/prompts/abel-design.md +206 -228
  10. package/prompts/abel-diagnose.md +39 -18
  11. package/prompts/abel-implement.md +122 -178
  12. package/prompts/abel-init.md +36 -33
  13. package/skills/abel-workflow/SKILL.md +190 -168
  14. package/src/apply-transaction.ts +1427 -0
  15. package/src/artifact-store.ts +204 -0
  16. package/src/child-session.ts +136 -17
  17. package/src/contracts.ts +121 -696
  18. package/src/control-contracts.ts +246 -0
  19. package/src/delivery-compiler.ts +1118 -0
  20. package/src/design-control.ts +846 -0
  21. package/src/design-journal.ts +1569 -0
  22. package/src/file-snapshot.ts +5 -0
  23. package/src/implement-graph.ts +11 -4
  24. package/src/index.ts +1833 -118
  25. package/src/isolation-backend.ts +269 -0
  26. package/src/packet-runtime.ts +533 -0
  27. package/src/parent-provider.ts +23 -4
  28. package/src/route-policy.ts +483 -0
  29. package/src/run-state.ts +430 -0
  30. package/src/run-store.ts +990 -0
  31. package/src/safe-path.ts +11 -0
  32. package/src/scoped-tools.ts +36 -0
  33. package/src/state-root.ts +215 -0
  34. package/src/subagent-activity.ts +526 -27
  35. package/src/submit-tool.ts +274 -0
  36. package/src/task-ledger.ts +1380 -0
  37. package/src/verification-capability.ts +560 -45
  38. package/src/worker-broker.ts +703 -0
  39. package/src/workflow-engine.ts +7706 -0
  40. package/src/workspace-store.ts +544 -0
  41. package/src/candidate-preflight.ts +0 -1618
  42. package/src/drain.ts +0 -20
  43. package/src/patch.ts +0 -505
  44. package/src/result-store.ts +0 -312
  45. package/src/runtime.ts +0 -2436
  46. package/src/scheduler.ts +0 -384
  47. package/src/subagent-endpoint.ts +0 -158
  48. package/src/worker.ts +0 -204
@@ -0,0 +1,990 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmodSync, closeSync, lstatSync, openSync } from "node:fs";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import type { ControlStage } from "./control-contracts.ts";
5
+ import {
6
+ createRunEvent,
7
+ type DeliveryGate,
8
+ type RunEvent,
9
+ type RunProjection,
10
+ type RunState,
11
+ reduceRunEvents,
12
+ ZERO_EVENT_HASH,
13
+ } from "./run-state.ts";
14
+ import {
15
+ prepareStateRoot,
16
+ type ResolvedStateRoot,
17
+ StateRootError,
18
+ } from "./state-root.ts";
19
+
20
+ export const RUN_STORE_SCHEMA_VERSION = 4 as const;
21
+
22
+ const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
23
+ const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
24
+ const SHA256 = /^[a-f0-9]{64}$/u;
25
+
26
+ interface RunRow {
27
+ run_id: string;
28
+ projection_json: string;
29
+ }
30
+
31
+ interface EventRow {
32
+ sequence: number;
33
+ type: string;
34
+ payload_json: string;
35
+ prior_hash: string;
36
+ event_hash: string;
37
+ }
38
+
39
+ interface OperationRow {
40
+ state: string;
41
+ outcome_json: string | null;
42
+ lease_token: string | null;
43
+ lease_expires_at: number | null;
44
+ }
45
+
46
+ export interface RunStoreOptions {
47
+ now?: () => number;
48
+ busyTimeoutMs?: number;
49
+ }
50
+
51
+ export interface StartRunInput {
52
+ stage: ControlStage;
53
+ change?: string;
54
+ provisionalKey?: string;
55
+ operationId: string;
56
+ }
57
+
58
+ export interface BindDeliveryInput {
59
+ runId: string;
60
+ gate: DeliveryGate;
61
+ revision: number;
62
+ receiptHash: string;
63
+ approvalProof?: {
64
+ revision: number;
65
+ contractHash: string;
66
+ recordHash: string;
67
+ };
68
+ operationId: string;
69
+ lease?: OperationLease;
70
+ }
71
+
72
+ export interface OperationLease {
73
+ runId: string;
74
+ operationId: string;
75
+ token: string;
76
+ expiresAt: number;
77
+ }
78
+
79
+ export interface OperationLeaseStatus {
80
+ state: "running" | "committed" | "interrupted";
81
+ outcome?: Record<string, unknown>;
82
+ expiresAt?: number;
83
+ }
84
+
85
+ const SCHEMA = `
86
+ CREATE TABLE IF NOT EXISTS schema_meta (
87
+ version INTEGER PRIMARY KEY CHECK (version = 4)
88
+ ) STRICT;
89
+
90
+ CREATE TABLE IF NOT EXISTS runs (
91
+ run_id TEXT PRIMARY KEY,
92
+ root_hash TEXT NOT NULL,
93
+ stage TEXT NOT NULL,
94
+ lookup_key TEXT NOT NULL,
95
+ change_name TEXT,
96
+ provisional_key TEXT,
97
+ state TEXT NOT NULL,
98
+ sequence INTEGER NOT NULL,
99
+ event_hash TEXT NOT NULL,
100
+ delivery_revision INTEGER,
101
+ projection_json TEXT NOT NULL,
102
+ terminal_tombstone TEXT,
103
+ UNIQUE (root_hash, stage, lookup_key)
104
+ ) STRICT;
105
+
106
+ CREATE TABLE IF NOT EXISTS delivery_bindings (
107
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
108
+ gate TEXT NOT NULL,
109
+ revision INTEGER NOT NULL,
110
+ receipt_hash TEXT NOT NULL,
111
+ approval_revision INTEGER,
112
+ contract_hash TEXT,
113
+ record_hash TEXT,
114
+ operation_id TEXT NOT NULL,
115
+ PRIMARY KEY (run_id, gate, revision),
116
+ UNIQUE (run_id, operation_id)
117
+ ) STRICT;
118
+
119
+ CREATE TABLE IF NOT EXISTS events (
120
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
121
+ sequence INTEGER NOT NULL,
122
+ type TEXT NOT NULL,
123
+ payload_json TEXT NOT NULL,
124
+ prior_hash TEXT NOT NULL,
125
+ event_hash TEXT NOT NULL,
126
+ PRIMARY KEY (run_id, sequence),
127
+ UNIQUE (run_id, event_hash)
128
+ ) STRICT;
129
+
130
+ CREATE TABLE IF NOT EXISTS tasks (
131
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
132
+ task_id TEXT NOT NULL,
133
+ state TEXT NOT NULL,
134
+ phase TEXT,
135
+ projection_json TEXT NOT NULL,
136
+ PRIMARY KEY (run_id, task_id)
137
+ ) STRICT;
138
+
139
+ CREATE TABLE IF NOT EXISTS operations (
140
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
141
+ operation_id TEXT NOT NULL,
142
+ kind TEXT NOT NULL,
143
+ state TEXT NOT NULL,
144
+ outcome_json TEXT,
145
+ lease_token TEXT,
146
+ lease_expires_at INTEGER,
147
+ PRIMARY KEY (run_id, operation_id)
148
+ ) STRICT;
149
+
150
+ CREATE TABLE IF NOT EXISTS route_health (
151
+ route_fingerprint TEXT PRIMARY KEY,
152
+ state TEXT NOT NULL,
153
+ next_half_open_at INTEGER,
154
+ projection_json TEXT NOT NULL
155
+ ) STRICT;
156
+
157
+ CREATE TABLE IF NOT EXISTS artifacts (
158
+ content_hash TEXT PRIMARY KEY,
159
+ size_bytes INTEGER NOT NULL,
160
+ seal_state TEXT NOT NULL,
161
+ owner_run_id TEXT REFERENCES runs(run_id) ON DELETE CASCADE,
162
+ reference_count INTEGER NOT NULL
163
+ ) STRICT;
164
+
165
+ CREATE TABLE IF NOT EXISTS workspace_revisions (
166
+ revision_id TEXT PRIMARY KEY,
167
+ owner_run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
168
+ parent_revision_id TEXT,
169
+ manifest_hash TEXT NOT NULL
170
+ ) STRICT;
171
+
172
+ CREATE TABLE IF NOT EXISTS apply_transactions (
173
+ transaction_id TEXT PRIMARY KEY,
174
+ owner_run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
175
+ state TEXT NOT NULL,
176
+ intent_json TEXT NOT NULL,
177
+ recovery_json TEXT
178
+ ) STRICT;
179
+
180
+ CREATE TABLE IF NOT EXISTS bootstrap_handoffs (
181
+ handoff_id TEXT PRIMARY KEY,
182
+ owner_run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
183
+ receipt_hash TEXT NOT NULL UNIQUE,
184
+ state TEXT NOT NULL,
185
+ facts_json TEXT NOT NULL
186
+ ) STRICT;
187
+ `;
188
+
189
+ function requireIdentifier(value: string, label: string): void {
190
+ if (!IDENTIFIER.test(value)) throw new Error(`invalid-${label}`);
191
+ }
192
+
193
+ function parseProjection(value: string): RunProjection {
194
+ const parsed = JSON.parse(value) as RunProjection;
195
+ if (
196
+ !parsed ||
197
+ typeof parsed !== "object" ||
198
+ typeof parsed.runId !== "string"
199
+ ) {
200
+ throw new Error("run-projection-invalid");
201
+ }
202
+ return parsed;
203
+ }
204
+
205
+ function lookupKey(input: StartRunInput): string {
206
+ const hasChange = typeof input.change === "string";
207
+ const hasProvisional = typeof input.provisionalKey === "string";
208
+ if (hasChange === hasProvisional) throw new Error("invalid-run-lookup-key");
209
+ if (hasChange) {
210
+ if (!CHANGE_NAME.test(input.change as string))
211
+ throw new Error("invalid-change");
212
+ return `change:${input.change}`;
213
+ }
214
+ if (!SHA256.test(input.provisionalKey as string)) {
215
+ throw new Error("invalid-provisional-key");
216
+ }
217
+ if (input.stage !== "abel-design") {
218
+ throw new Error("provisional-run-requires-design");
219
+ }
220
+ return `provisional:${input.provisionalKey}`;
221
+ }
222
+
223
+ export class RunStore {
224
+ readonly stateRoot: ResolvedStateRoot;
225
+ readonly #database: DatabaseSync;
226
+ readonly #now: () => number;
227
+ #closed = false;
228
+
229
+ private constructor(
230
+ stateRoot: ResolvedStateRoot,
231
+ database: DatabaseSync,
232
+ options: RunStoreOptions,
233
+ ) {
234
+ this.stateRoot = stateRoot;
235
+ this.#database = database;
236
+ this.#now = options.now ?? Date.now;
237
+ }
238
+
239
+ static open(
240
+ unresolved: ResolvedStateRoot,
241
+ options: RunStoreOptions = {},
242
+ ): RunStore {
243
+ const stateRoot = prepareStateRoot(unresolved);
244
+ try {
245
+ const existing = lstatSync(stateRoot.databasePath);
246
+ if (existing.isSymbolicLink()) {
247
+ throw new StateRootError("state-component-symlink");
248
+ }
249
+ if (!existing.isFile()) {
250
+ throw new StateRootError("state-component-type");
251
+ }
252
+ } catch (error) {
253
+ if (
254
+ error instanceof StateRootError ||
255
+ (error as NodeJS.ErrnoException).code !== "ENOENT"
256
+ ) {
257
+ throw error;
258
+ }
259
+ const descriptor = openSync(stateRoot.databasePath, "ax", 0o600);
260
+ closeSync(descriptor);
261
+ }
262
+ chmodSync(stateRoot.databasePath, 0o600);
263
+
264
+ const database = new DatabaseSync(stateRoot.databasePath);
265
+ database.exec("PRAGMA foreign_keys = ON");
266
+ database.exec("PRAGMA journal_mode = WAL");
267
+ database.exec("PRAGMA synchronous = FULL");
268
+ database.exec(
269
+ `PRAGMA busy_timeout = ${Math.max(1, Math.min(30_000, options.busyTimeoutMs ?? 5_000))}`,
270
+ );
271
+ database.exec(SCHEMA);
272
+ const schemaRow = database
273
+ .prepare("SELECT version FROM schema_meta LIMIT 1")
274
+ .get() as { version: number } | undefined;
275
+ if (!schemaRow) {
276
+ database
277
+ .prepare("INSERT INTO schema_meta(version) VALUES (?)")
278
+ .run(RUN_STORE_SCHEMA_VERSION);
279
+ } else if (schemaRow.version !== RUN_STORE_SCHEMA_VERSION) {
280
+ database.close();
281
+ throw new Error("run-store-schema-version-rejected");
282
+ }
283
+ const store = new RunStore(stateRoot, database, options);
284
+ store.#secureDatabaseFiles();
285
+ store.recoverExpiredLeases();
286
+ return store;
287
+ }
288
+
289
+ #assertOpen(): void {
290
+ if (this.#closed) throw new Error("run-store-closed");
291
+ }
292
+
293
+ #transaction<T>(work: () => T): T {
294
+ this.#assertOpen();
295
+ this.#database.exec("BEGIN IMMEDIATE");
296
+ let value: T;
297
+ try {
298
+ value = work();
299
+ this.#database.exec("COMMIT");
300
+ } catch (error) {
301
+ this.#database.exec("ROLLBACK");
302
+ throw error;
303
+ }
304
+ this.#secureDatabaseFiles();
305
+ return value;
306
+ }
307
+
308
+ #secureDatabaseFiles(): void {
309
+ for (const file of [
310
+ this.stateRoot.databasePath,
311
+ `${this.stateRoot.databasePath}-wal`,
312
+ `${this.stateRoot.databasePath}-shm`,
313
+ ]) {
314
+ try {
315
+ const stat = lstatSync(file);
316
+ if (stat.isSymbolicLink() || !stat.isFile()) {
317
+ throw new StateRootError(
318
+ stat.isSymbolicLink()
319
+ ? "state-component-symlink"
320
+ : "state-component-type",
321
+ );
322
+ }
323
+ chmodSync(file, 0o600);
324
+ } catch (error) {
325
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
326
+ }
327
+ }
328
+ }
329
+
330
+ #runRow(runId: string): RunRow {
331
+ const row = this.#database
332
+ .prepare("SELECT run_id, projection_json FROM runs WHERE run_id = ?")
333
+ .get(runId) as RunRow | undefined;
334
+ if (!row) throw new Error("run-not-found");
335
+ return row;
336
+ }
337
+
338
+ #operation(runId: string, operationId: string): OperationRow | undefined {
339
+ return this.#database
340
+ .prepare(
341
+ `SELECT state, outcome_json, lease_token, lease_expires_at
342
+ FROM operations WHERE run_id = ? AND operation_id = ?`,
343
+ )
344
+ .get(runId, operationId) as OperationRow | undefined;
345
+ }
346
+
347
+ #operationOutcome(
348
+ runId: string,
349
+ operationId: string,
350
+ ): RunProjection | undefined {
351
+ const row = this.#operation(runId, operationId);
352
+ if (row?.state !== "committed" || row.outcome_json === null)
353
+ return undefined;
354
+ return parseProjection(row.outcome_json);
355
+ }
356
+
357
+ #recordOperation(
358
+ runId: string,
359
+ operationId: string,
360
+ kind: string,
361
+ outcome: RunProjection,
362
+ ): void {
363
+ this.#database
364
+ .prepare(
365
+ `INSERT INTO operations(
366
+ run_id, operation_id, kind, state, outcome_json
367
+ ) VALUES (?, ?, ?, 'committed', ?)
368
+ ON CONFLICT(run_id, operation_id) DO UPDATE SET
369
+ state = 'committed', outcome_json = excluded.outcome_json,
370
+ lease_token = NULL, lease_expires_at = NULL`,
371
+ )
372
+ .run(runId, operationId, kind, JSON.stringify(outcome));
373
+ }
374
+
375
+ #events(runId: string): RunEvent[] {
376
+ const rows = this.#database
377
+ .prepare(
378
+ `SELECT sequence, type, payload_json, prior_hash, event_hash
379
+ FROM events WHERE run_id = ? ORDER BY sequence`,
380
+ )
381
+ .all(runId) as unknown as EventRow[];
382
+ return rows.map((row) => ({
383
+ sequence: Number(row.sequence),
384
+ type: row.type as RunEvent["type"],
385
+ payload: JSON.parse(row.payload_json) as Record<string, unknown>,
386
+ priorHash: row.prior_hash,
387
+ hash: row.event_hash,
388
+ }));
389
+ }
390
+
391
+ #appendEvent(
392
+ runId: string,
393
+ type: RunEvent["type"],
394
+ payload: Record<string, unknown>,
395
+ ): RunProjection {
396
+ const row = this.#runRow(runId);
397
+ const current = parseProjection(row.projection_json);
398
+ const event = createRunEvent(
399
+ current.sequence + 1,
400
+ type,
401
+ payload,
402
+ current.eventHash,
403
+ );
404
+ const projection = reduceRunEvents([...this.#events(runId), event]);
405
+ this.#database
406
+ .prepare(
407
+ `INSERT INTO events(
408
+ run_id, sequence, type, payload_json, prior_hash, event_hash
409
+ ) VALUES (?, ?, ?, ?, ?, ?)`,
410
+ )
411
+ .run(
412
+ runId,
413
+ event.sequence,
414
+ event.type,
415
+ JSON.stringify(event.payload),
416
+ event.priorHash,
417
+ event.hash,
418
+ );
419
+ this.#database
420
+ .prepare(
421
+ `UPDATE runs SET state = ?, sequence = ?, event_hash = ?,
422
+ delivery_revision = ?, projection_json = ?, terminal_tombstone = ?
423
+ WHERE run_id = ?`,
424
+ )
425
+ .run(
426
+ projection.state,
427
+ projection.sequence,
428
+ projection.eventHash,
429
+ projection.deliveryRevision ?? null,
430
+ JSON.stringify(projection),
431
+ projection.terminal ?? null,
432
+ runId,
433
+ );
434
+ return projection;
435
+ }
436
+
437
+ startRun(input: StartRunInput): RunProjection {
438
+ requireIdentifier(input.operationId, "operation-id");
439
+ if (input.stage !== "abel-design" && input.stage !== "abel-implement") {
440
+ throw new Error("invalid-run-stage");
441
+ }
442
+ const key = lookupKey(input);
443
+ return this.#transaction(() => {
444
+ let existing = this.#database
445
+ .prepare(
446
+ `SELECT run_id, projection_json FROM runs
447
+ WHERE root_hash = ? AND stage = ? AND lookup_key = ?`,
448
+ )
449
+ .get(this.stateRoot.consumerRootHash, input.stage, key) as
450
+ | RunRow
451
+ | undefined;
452
+ if (existing) {
453
+ const projection = parseProjection(existing.projection_json);
454
+ if (
455
+ input.stage === "abel-design" &&
456
+ ["completed", "discarded", "rejected"].includes(projection.state)
457
+ ) {
458
+ this.#database
459
+ .prepare("UPDATE runs SET lookup_key = ? WHERE run_id = ?")
460
+ .run(`terminal:${existing.run_id}`, existing.run_id);
461
+ existing = undefined;
462
+ }
463
+ }
464
+ if (existing) {
465
+ const replay = this.#operationOutcome(
466
+ existing.run_id,
467
+ input.operationId,
468
+ );
469
+ if (replay) return replay;
470
+ const projection = parseProjection(existing.projection_json);
471
+ this.#recordOperation(
472
+ existing.run_id,
473
+ input.operationId,
474
+ "start",
475
+ projection,
476
+ );
477
+ return projection;
478
+ }
479
+
480
+ const runId = randomUUID();
481
+ const event = createRunEvent(
482
+ 1,
483
+ "run-created",
484
+ {
485
+ runId,
486
+ rootHash: this.stateRoot.consumerRootHash,
487
+ stage: input.stage,
488
+ ...(input.change ? { change: input.change } : {}),
489
+ ...(input.provisionalKey
490
+ ? { provisionalKey: input.provisionalKey }
491
+ : {}),
492
+ },
493
+ ZERO_EVENT_HASH,
494
+ );
495
+ const projection = reduceRunEvents([event]);
496
+ this.#database
497
+ .prepare(
498
+ `INSERT INTO runs(
499
+ run_id, root_hash, stage, lookup_key, change_name,
500
+ provisional_key, state, sequence, event_hash, projection_json
501
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
502
+ )
503
+ .run(
504
+ runId,
505
+ this.stateRoot.consumerRootHash,
506
+ input.stage,
507
+ key,
508
+ input.change ?? null,
509
+ input.provisionalKey ?? null,
510
+ projection.state,
511
+ projection.sequence,
512
+ projection.eventHash,
513
+ JSON.stringify(projection),
514
+ );
515
+ this.#database
516
+ .prepare(
517
+ `INSERT INTO events(
518
+ run_id, sequence, type, payload_json, prior_hash, event_hash
519
+ ) VALUES (?, ?, ?, ?, ?, ?)`,
520
+ )
521
+ .run(
522
+ runId,
523
+ event.sequence,
524
+ event.type,
525
+ JSON.stringify(event.payload),
526
+ event.priorHash,
527
+ event.hash,
528
+ );
529
+ this.#recordOperation(runId, input.operationId, "start", projection);
530
+ return projection;
531
+ });
532
+ }
533
+
534
+ bindChange(input: {
535
+ runId: string;
536
+ change: string;
537
+ operationId: string;
538
+ }): RunProjection {
539
+ requireIdentifier(input.operationId, "operation-id");
540
+ if (!CHANGE_NAME.test(input.change)) throw new Error("invalid-change");
541
+ return this.#transaction(() => {
542
+ const replay = this.#operationOutcome(input.runId, input.operationId);
543
+ if (replay) return replay;
544
+ const current = this.status(input.runId);
545
+ if (current.stage !== "abel-design" || !current.provisionalKey) {
546
+ throw new Error("change-binding-not-allowed");
547
+ }
548
+ const conflict = this.#database
549
+ .prepare(
550
+ `SELECT run_id FROM runs
551
+ WHERE root_hash = ? AND stage = ? AND lookup_key = ?`,
552
+ )
553
+ .get(current.rootHash, current.stage, `change:${input.change}`) as
554
+ | { run_id: string }
555
+ | undefined;
556
+ if (conflict && conflict.run_id !== input.runId) {
557
+ throw new Error("change-run-already-exists");
558
+ }
559
+ this.#database
560
+ .prepare(
561
+ `UPDATE runs SET lookup_key = ?, change_name = ?, provisional_key = NULL
562
+ WHERE run_id = ?`,
563
+ )
564
+ .run(`change:${input.change}`, input.change, input.runId);
565
+ const projection = this.#appendEvent(input.runId, "change-bound", {
566
+ change: input.change,
567
+ });
568
+ this.#recordOperation(
569
+ input.runId,
570
+ input.operationId,
571
+ "change-bound",
572
+ projection,
573
+ );
574
+ return projection;
575
+ });
576
+ }
577
+
578
+ #requireDeliveryBinding(input: BindDeliveryInput): void {
579
+ requireIdentifier(input.operationId, "operation-id");
580
+ if (
581
+ (input.gate !== "gate-a" && input.gate !== "gate-b") ||
582
+ !Number.isSafeInteger(input.revision) ||
583
+ input.revision < 1 ||
584
+ !SHA256.test(input.receiptHash) ||
585
+ (input.approvalProof !== undefined &&
586
+ (!Number.isSafeInteger(input.approvalProof.revision) ||
587
+ input.approvalProof.revision < 1 ||
588
+ !SHA256.test(input.approvalProof.contractHash) ||
589
+ !SHA256.test(input.approvalProof.recordHash)))
590
+ ) {
591
+ throw new Error("invalid-delivery-binding");
592
+ }
593
+ }
594
+
595
+ #bindDelivery(input: BindDeliveryInput): RunProjection {
596
+ if (input.lease) {
597
+ if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
598
+ this.#assertLease(input.lease);
599
+ }
600
+ const replay = this.#operationOutcome(input.runId, input.operationId);
601
+ if (replay) return replay;
602
+ const existing = this.#database
603
+ .prepare(
604
+ `SELECT receipt_hash, approval_revision, contract_hash, record_hash
605
+ FROM delivery_bindings
606
+ WHERE run_id = ? AND gate = ? AND revision = ?`,
607
+ )
608
+ .get(input.runId, input.gate, input.revision) as
609
+ | {
610
+ receipt_hash: string;
611
+ approval_revision: number | null;
612
+ contract_hash: string | null;
613
+ record_hash: string | null;
614
+ }
615
+ | undefined;
616
+ if (existing) {
617
+ if (
618
+ existing.receipt_hash !== input.receiptHash ||
619
+ existing.approval_revision !==
620
+ (input.approvalProof?.revision ?? null) ||
621
+ existing.contract_hash !==
622
+ (input.approvalProof?.contractHash ?? null) ||
623
+ existing.record_hash !== (input.approvalProof?.recordHash ?? null)
624
+ ) {
625
+ throw new Error("delivery-revision-conflict");
626
+ }
627
+ const projection = this.status(input.runId);
628
+ this.#recordOperation(
629
+ input.runId,
630
+ input.operationId,
631
+ "delivery-bound",
632
+ projection,
633
+ );
634
+ return projection;
635
+ }
636
+ this.#runRow(input.runId);
637
+ this.#database
638
+ .prepare(
639
+ `INSERT INTO delivery_bindings(
640
+ run_id, gate, revision, receipt_hash, approval_revision,
641
+ contract_hash, record_hash, operation_id
642
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
643
+ )
644
+ .run(
645
+ input.runId,
646
+ input.gate,
647
+ input.revision,
648
+ input.receiptHash,
649
+ input.approvalProof?.revision ?? null,
650
+ input.approvalProof?.contractHash ?? null,
651
+ input.approvalProof?.recordHash ?? null,
652
+ input.operationId,
653
+ );
654
+ const projection = this.#appendEvent(input.runId, "delivery-bound", {
655
+ gate: input.gate,
656
+ revision: input.revision,
657
+ receiptHash: input.receiptHash,
658
+ ...(input.approvalProof
659
+ ? { approvalProof: structuredClone(input.approvalProof) }
660
+ : {}),
661
+ });
662
+ this.#recordOperation(
663
+ input.runId,
664
+ input.operationId,
665
+ "delivery-bound",
666
+ projection,
667
+ );
668
+ return projection;
669
+ }
670
+
671
+ bindDelivery(input: BindDeliveryInput): RunProjection {
672
+ this.#requireDeliveryBinding(input);
673
+ return this.#transaction(() => this.#bindDelivery(input));
674
+ }
675
+
676
+ bindDeliveryAtomically(
677
+ input: BindDeliveryInput,
678
+ write: (database: DatabaseSync) => void,
679
+ ): RunProjection {
680
+ this.#requireDeliveryBinding(input);
681
+ if (typeof write !== "function") {
682
+ throw new Error("delivery-binding-write-invalid");
683
+ }
684
+ return this.#transaction(() => {
685
+ const projection = this.#bindDelivery(input);
686
+ write(this.#database);
687
+ return projection;
688
+ });
689
+ }
690
+
691
+ bindDeliveriesAtomically(
692
+ inputs: readonly BindDeliveryInput[],
693
+ write: (database: DatabaseSync) => void,
694
+ ): RunProjection {
695
+ if (inputs.length === 0) throw new Error("delivery-binding-empty");
696
+ for (const input of inputs) this.#requireDeliveryBinding(input);
697
+ const runId = inputs[0]?.runId;
698
+ if (
699
+ !runId ||
700
+ inputs.some(
701
+ (input) =>
702
+ input.runId !== runId ||
703
+ (input.lease?.token ?? null) !== (inputs[0]?.lease?.token ?? null),
704
+ ) ||
705
+ typeof write !== "function"
706
+ ) {
707
+ throw new Error("delivery-binding-write-invalid");
708
+ }
709
+ return this.#transaction(() => {
710
+ let projection: RunProjection | undefined;
711
+ for (const input of inputs) projection = this.#bindDelivery(input);
712
+ write(this.#database);
713
+ if (!projection) throw new Error("delivery-binding-empty");
714
+ return projection;
715
+ });
716
+ }
717
+
718
+ acquireLease(input: {
719
+ runId: string;
720
+ operationId: string;
721
+ ttlMs: number;
722
+ preempt?: boolean;
723
+ exclusive?: boolean;
724
+ }): OperationLease {
725
+ requireIdentifier(input.operationId, "operation-id");
726
+ if (!Number.isSafeInteger(input.ttlMs) || input.ttlMs < 1) {
727
+ throw new Error("invalid-lease-ttl");
728
+ }
729
+ return this.#transaction(() => {
730
+ this.#runRow(input.runId);
731
+ const now = this.#now();
732
+ const existing = this.#operation(input.runId, input.operationId);
733
+ if (
734
+ existing?.state === "running" &&
735
+ existing.lease_token &&
736
+ existing.lease_expires_at !== null &&
737
+ existing.lease_expires_at >= now
738
+ ) {
739
+ if (!input.preempt) {
740
+ if (input.exclusive) throw new Error("operation-already-running");
741
+ return {
742
+ runId: input.runId,
743
+ operationId: input.operationId,
744
+ token: existing.lease_token,
745
+ expiresAt: existing.lease_expires_at,
746
+ };
747
+ }
748
+ }
749
+ const token = randomUUID();
750
+ const expiresAt = now + input.ttlMs;
751
+ this.#database
752
+ .prepare(
753
+ `INSERT INTO operations(
754
+ run_id, operation_id, kind, state, lease_token, lease_expires_at
755
+ ) VALUES (?, ?, 'lease', 'running', ?, ?)
756
+ ON CONFLICT(run_id, operation_id) DO UPDATE SET
757
+ state = 'running', outcome_json = NULL,
758
+ lease_token = excluded.lease_token,
759
+ lease_expires_at = excluded.lease_expires_at`,
760
+ )
761
+ .run(input.runId, input.operationId, token, expiresAt);
762
+ return {
763
+ runId: input.runId,
764
+ operationId: input.operationId,
765
+ token,
766
+ expiresAt,
767
+ };
768
+ });
769
+ }
770
+
771
+ renewLease(lease: OperationLease, ttlMs: number): OperationLease {
772
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 1)
773
+ throw new Error("invalid-lease-ttl");
774
+ return this.#transaction(() => {
775
+ const existing = this.#operation(lease.runId, lease.operationId);
776
+ if (
777
+ existing?.state !== "running" ||
778
+ existing.lease_token !== lease.token ||
779
+ existing.lease_expires_at === null ||
780
+ existing.lease_expires_at < this.#now()
781
+ ) {
782
+ throw new Error("lease-fenced");
783
+ }
784
+ const expiresAt = this.#now() + ttlMs;
785
+ this.#database
786
+ .prepare(
787
+ `UPDATE operations SET lease_expires_at = ?
788
+ WHERE run_id = ? AND operation_id = ? AND lease_token = ?`,
789
+ )
790
+ .run(expiresAt, lease.runId, lease.operationId, lease.token);
791
+ return { ...lease, expiresAt };
792
+ });
793
+ }
794
+
795
+ #assertLease(lease: OperationLease): void {
796
+ const existing = this.#operation(lease.runId, lease.operationId);
797
+ if (
798
+ existing?.state !== "running" ||
799
+ existing.lease_token !== lease.token ||
800
+ existing.lease_expires_at === null ||
801
+ existing.lease_expires_at < this.#now()
802
+ ) {
803
+ throw new Error("lease-fenced");
804
+ }
805
+ }
806
+
807
+ assertLease(lease: OperationLease): void {
808
+ this.#transaction(() => this.#assertLease(lease));
809
+ }
810
+
811
+ inspectOperation(
812
+ runId: string,
813
+ operationId: string,
814
+ ): OperationLeaseStatus | undefined {
815
+ this.#assertOpen();
816
+ requireIdentifier(operationId, "operation-id");
817
+ this.#runRow(runId);
818
+ let operation = this.#operation(runId, operationId);
819
+ if (!operation) return undefined;
820
+ if (
821
+ operation.state === "running" &&
822
+ operation.lease_expires_at !== null &&
823
+ operation.lease_expires_at < this.#now()
824
+ ) {
825
+ this.#transaction(() => {
826
+ this.#database
827
+ .prepare(
828
+ `UPDATE operations
829
+ SET state = 'interrupted', lease_token = NULL,
830
+ lease_expires_at = NULL
831
+ WHERE run_id = ? AND operation_id = ? AND state = 'running'
832
+ AND lease_expires_at < ?`,
833
+ )
834
+ .run(runId, operationId, this.#now());
835
+ });
836
+ operation = this.#operation(runId, operationId);
837
+ if (!operation) throw new Error("operation-recovery-invalid");
838
+ }
839
+ if (
840
+ operation.state !== "running" &&
841
+ operation.state !== "committed" &&
842
+ operation.state !== "interrupted"
843
+ ) {
844
+ throw new Error("operation-state-invalid");
845
+ }
846
+ let outcome: Record<string, unknown> | undefined;
847
+ if (operation.outcome_json !== null) {
848
+ const parsed = JSON.parse(operation.outcome_json) as unknown;
849
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
850
+ throw new Error("operation-outcome-invalid");
851
+ }
852
+ outcome = parsed as Record<string, unknown>;
853
+ }
854
+ return {
855
+ state: operation.state,
856
+ ...(outcome ? { outcome: structuredClone(outcome) } : {}),
857
+ ...(operation.lease_expires_at === null
858
+ ? {}
859
+ : { expiresAt: operation.lease_expires_at }),
860
+ };
861
+ }
862
+
863
+ commitLease(
864
+ lease: OperationLease,
865
+ outcome: Record<string, unknown>,
866
+ ): Record<string, unknown> {
867
+ const serialized = JSON.stringify(outcome);
868
+ if (!serialized) throw new Error("operation-outcome-invalid");
869
+ return this.#transaction(() => {
870
+ this.#assertLease(lease);
871
+ const result = this.#database
872
+ .prepare(
873
+ `UPDATE operations
874
+ SET state = 'committed', outcome_json = ?,
875
+ lease_token = NULL, lease_expires_at = NULL
876
+ WHERE run_id = ? AND operation_id = ? AND state = 'running'
877
+ AND lease_token = ?`,
878
+ )
879
+ .run(serialized, lease.runId, lease.operationId, lease.token);
880
+ if (Number(result.changes) !== 1) throw new Error("lease-fenced");
881
+ return structuredClone(outcome);
882
+ });
883
+ }
884
+
885
+ interruptLease(lease: OperationLease): void {
886
+ this.#transaction(() => {
887
+ this.#assertLease(lease);
888
+ const result = this.#database
889
+ .prepare(
890
+ `UPDATE operations
891
+ SET state = 'interrupted', outcome_json = NULL,
892
+ lease_token = NULL, lease_expires_at = NULL
893
+ WHERE run_id = ? AND operation_id = ? AND state = 'running'
894
+ AND lease_token = ?`,
895
+ )
896
+ .run(lease.runId, lease.operationId, lease.token);
897
+ if (Number(result.changes) !== 1) throw new Error("lease-fenced");
898
+ });
899
+ }
900
+
901
+ transition(input: {
902
+ runId: string;
903
+ to: RunState;
904
+ operationId: string;
905
+ code?: string;
906
+ lease?: OperationLease;
907
+ }): RunProjection {
908
+ requireIdentifier(input.operationId, "operation-id");
909
+ return this.#transaction(() => {
910
+ if (input.lease) {
911
+ if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
912
+ this.#assertLease(input.lease);
913
+ }
914
+ const replay = this.#operationOutcome(input.runId, input.operationId);
915
+ if (replay) return replay;
916
+ const current = this.status(input.runId);
917
+ const projection = this.#appendEvent(input.runId, "state-transitioned", {
918
+ from: current.state,
919
+ to: input.to,
920
+ ...(input.code ? { code: input.code } : {}),
921
+ });
922
+ this.#recordOperation(
923
+ input.runId,
924
+ input.operationId,
925
+ "state-transitioned",
926
+ projection,
927
+ );
928
+ return projection;
929
+ });
930
+ }
931
+
932
+ status(runId: string): RunProjection {
933
+ this.#assertOpen();
934
+ return parseProjection(this.#runRow(runId).projection_json);
935
+ }
936
+
937
+ listEvents(runId: string): RunEvent[] {
938
+ this.#assertOpen();
939
+ this.#runRow(runId);
940
+ return structuredClone(this.#events(runId));
941
+ }
942
+
943
+ rebuildProjection(runId: string): RunProjection {
944
+ return reduceRunEvents(this.listEvents(runId));
945
+ }
946
+
947
+ verifyIntegrity(runId: string): RunProjection {
948
+ const rebuilt = this.rebuildProjection(runId);
949
+ const stored = this.status(runId);
950
+ if (JSON.stringify(rebuilt) !== JSON.stringify(stored)) {
951
+ throw new Error("run-projection-integrity-mismatch");
952
+ }
953
+ return rebuilt;
954
+ }
955
+
956
+ recoverExpiredLeases(): number {
957
+ this.#assertOpen();
958
+ const expired = this.#database
959
+ .prepare(
960
+ `SELECT run_id, operation_id FROM operations
961
+ WHERE state = 'running' AND lease_expires_at < ?`,
962
+ )
963
+ .all(this.#now()) as unknown as Array<{
964
+ run_id: string;
965
+ operation_id: string;
966
+ }>;
967
+ if (expired.length === 0) return 0;
968
+ return this.#transaction(() => {
969
+ for (const row of expired) {
970
+ this.#database
971
+ .prepare(
972
+ `UPDATE operations
973
+ SET state = 'interrupted', lease_token = NULL,
974
+ lease_expires_at = NULL
975
+ WHERE run_id = ? AND operation_id = ? AND state = 'running'`,
976
+ )
977
+ .run(row.run_id, row.operation_id);
978
+ }
979
+ return expired.length;
980
+ });
981
+ }
982
+
983
+ close(): void {
984
+ if (this.#closed) return;
985
+ this.#database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
986
+ this.#secureDatabaseFiles();
987
+ this.#database.close();
988
+ this.#closed = true;
989
+ }
990
+ }