@abelxiaoxing/cadence 1.8.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +324 -117
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -18
  4. package/agents/diagnosis-worker.md +14 -14
  5. package/agents/implementation-worker.md +16 -17
  6. package/config/workflow-task.example.json +7 -0
  7. package/docs/remote-access.md +60 -0
  8. package/docs/reuse-and-distribution.md +44 -0
  9. package/docs/service-management.md +108 -0
  10. package/licenses/bundled-web.txt +6245 -0
  11. package/licenses/pi-web-ui-MIT.txt +21 -0
  12. package/licenses/pi-web-ui-reuse.md +22 -0
  13. package/package.json +94 -21
  14. package/src/index.ts +21 -1802
  15. package/src/native-build.ts +6 -0
  16. package/src/native-client.ts +103 -0
  17. package/src/native-export.ts +58 -0
  18. package/src/native-history.ts +359 -0
  19. package/src/operator-cli.mjs +453 -1937
  20. package/src/plan-proposal.ts +87 -0
  21. package/src/sdk-worker.mjs +549 -0
  22. package/src/service/contracts/active.ts +82 -0
  23. package/src/service/contracts/budget.ts +148 -0
  24. package/src/service/contracts/build.ts +67 -0
  25. package/src/service/contracts/capture.ts +48 -0
  26. package/src/service/contracts/core.ts +33 -0
  27. package/src/service/contracts/current.ts +261 -0
  28. package/src/service/contracts/domain.ts +68 -0
  29. package/src/service/contracts/history.ts +2245 -0
  30. package/src/service/contracts/models.ts +139 -0
  31. package/src/service/coordination.ts +862 -0
  32. package/src/service/private-files.ts +204 -0
  33. package/src/service-cli.mjs +5592 -0
  34. package/src/service-client.ts +747 -0
  35. package/src/service-contract.ts +245 -0
  36. package/src/service-host.mjs +22382 -0
  37. package/src/service-http-client.ts +425 -0
  38. package/src/service-wire.ts +372 -0
  39. package/src/task-shell.ts +1045 -0
  40. package/src/task-store.ts +479 -0
  41. package/src/task-types.ts +184 -0
  42. package/src/verification-diagnostics.ts +29 -72
  43. package/src/worker-capture.mjs +1197 -0
  44. package/src/worker-public.mjs +1753 -0
  45. package/src/worker-redaction.mjs +608 -0
  46. package/src/worker-tools.mjs +202 -0
  47. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  50. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  56. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  62. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  65. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  68. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  71. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  74. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  77. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  80. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  89. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  92. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  95. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  98. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  101. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  104. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  107. package/web/dist/assets/cadence.css +2 -0
  108. package/web/dist/assets/cadence.js +7 -0
  109. package/web/dist/assets/markdown.js +296 -0
  110. package/web/dist/assets/react.js +9 -0
  111. package/web/dist/assets/rolldown-runtime.js +1 -0
  112. package/web/dist/assets/timeline-client.js +41 -0
  113. package/web/dist/assets/vis-timeline.css +1274 -0
  114. package/web/dist/assets/vis-timeline.js +32 -0
  115. package/web/dist/index.html +18 -0
  116. package/web/dist/manifest.json +505 -0
  117. package/config/plan-draft.example.json +0 -125
  118. package/config/plan-draft.multiple-tasks.example.json +0 -375
  119. package/config/plan-draft.quick.example.json +0 -78
  120. package/prompts/abel-design.md +0 -382
  121. package/prompts/abel-diagnose.md +0 -71
  122. package/prompts/abel-implement.md +0 -284
  123. package/prompts/abel-init.md +0 -52
  124. package/src/activation.ts +0 -44
  125. package/src/activity-contracts.ts +0 -82
  126. package/src/agent-registry.ts +0 -53
  127. package/src/apply-transaction.ts +0 -1437
  128. package/src/artifact-store.ts +0 -213
  129. package/src/build-windows-job.mjs +0 -95
  130. package/src/candidate-artifact.ts +0 -143
  131. package/src/candidate-context.ts +0 -289
  132. package/src/candidate-patch.ts +0 -271
  133. package/src/candidate-workspace.ts +0 -80
  134. package/src/canonical.ts +0 -16
  135. package/src/change-contract-schema.ts +0 -191
  136. package/src/change-contract.ts +0 -324
  137. package/src/change-verification.ts +0 -1617
  138. package/src/contracts.ts +0 -2466
  139. package/src/control-contracts.ts +0 -382
  140. package/src/delivery-compiler.ts +0 -1805
  141. package/src/delivery-revision.ts +0 -42
  142. package/src/design-control.ts +0 -1299
  143. package/src/design-diagnostics.ts +0 -181
  144. package/src/design-journal.ts +0 -1773
  145. package/src/directory-sync.ts +0 -36
  146. package/src/durable-contracts.ts +0 -227
  147. package/src/durable-workflow.ts +0 -822
  148. package/src/evidence-draft.ts +0 -122
  149. package/src/execution-profile.ts +0 -64
  150. package/src/execution-retention.ts +0 -77
  151. package/src/file-snapshot.ts +0 -163
  152. package/src/implement-continuation.ts +0 -559
  153. package/src/implement-graph.ts +0 -690
  154. package/src/implement-plan.ts +0 -72
  155. package/src/install-config.d.mts +0 -6
  156. package/src/isolation-backend.ts +0 -353
  157. package/src/model-source.ts +0 -22
  158. package/src/openspec-cli.ts +0 -494
  159. package/src/operator-cli-entry.mjs +0 -24
  160. package/src/operator-tools.ts +0 -309
  161. package/src/package-candidate-process.ts +0 -206
  162. package/src/package-candidate.ts +0 -24
  163. package/src/package-delivery.ts +0 -486
  164. package/src/package-state.ts +0 -320
  165. package/src/package-verification.ts +0 -635
  166. package/src/package-workflow.ts +0 -305
  167. package/src/packet-runtime.ts +0 -459
  168. package/src/phase-execution.ts +0 -2163
  169. package/src/pi-adapter.ts +0 -19
  170. package/src/plan-draft-summary.ts +0 -527
  171. package/src/plan-draft.ts +0 -502
  172. package/src/remove-path.ts +0 -32
  173. package/src/run-state.ts +0 -420
  174. package/src/run-store.ts +0 -1179
  175. package/src/safe-path.ts +0 -77
  176. package/src/scoped-grep-worker.mjs +0 -23
  177. package/src/scoped-tools.ts +0 -775
  178. package/src/single-task-draft.ts +0 -142
  179. package/src/sqlite-schema.ts +0 -171
  180. package/src/state-root.ts +0 -215
  181. package/src/storage-schema.ts +0 -360
  182. package/src/subagent-activity.ts +0 -1417
  183. package/src/subagent-process.ts +0 -476
  184. package/src/task-ledger.ts +0 -1338
  185. package/src/transport-budget.ts +0 -50
  186. package/src/verification-capability.ts +0 -1191
  187. package/src/verification-environment-io.ts +0 -389
  188. package/src/verification-environment.ts +0 -162
  189. package/src/verification-identity.ts +0 -77
  190. package/src/verification-scan.ts +0 -64
  191. package/src/windows-job-backend.ts +0 -419
  192. package/src/windows-job.c +0 -149
  193. package/src/workflow-engine.ts +0 -28
  194. package/src/workflow-policy.ts +0 -1670
  195. package/src/workflow-recovery-policy.ts +0 -97
  196. package/src/workflow-scheduling.ts +0 -105
  197. package/src/workflow-state-machine.ts +0 -4341
  198. package/src/workflow-status.ts +0 -576
  199. package/src/workspace-io-entry.mjs +0 -76
  200. package/src/workspace-io-worker.mjs +0 -1312
  201. package/src/workspace-io.ts +0 -89
  202. package/src/workspace-store.ts +0 -634
package/src/run-store.ts DELETED
@@ -1,1179 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { chmodSync, closeSync, lstatSync, openSync } from "node:fs";
3
- import { DatabaseSync } from "node:sqlite";
4
- import {
5
- createRunEvent,
6
- type DeliveryGate,
7
- type RunEvent,
8
- type RunProjection,
9
- type RunStage,
10
- type RunState,
11
- reduceRunEvents,
12
- ZERO_EVENT_HASH,
13
- } from "./run-state.ts";
14
- import { configureSqlite, matchesSqliteSchema } from "./sqlite-schema.ts";
15
- import {
16
- prepareStateRoot,
17
- type ResolvedStateRoot,
18
- StateRootError,
19
- } from "./state-root.ts";
20
- import {
21
- LEGACY_RUN_SCHEMA_V2,
22
- RUN_SCHEMA as SCHEMA,
23
- } from "./storage-schema.ts";
24
-
25
- export class RunStoreFormatError extends Error {
26
- readonly code = "run-store-reset-required" as const;
27
- readonly databasePath: string;
28
- readonly recovery = Object.freeze({
29
- action: "reset-private-run-store" as const,
30
- requiresBackup: true as const,
31
- retryCommand: "status-or-start" as const,
32
- });
33
-
34
- constructor(databasePath: string) {
35
- super("run-store-reset-required");
36
- this.name = "RunStoreFormatError";
37
- this.databasePath = databasePath;
38
- }
39
- }
40
-
41
- export class RunStoreMigrationError extends Error {
42
- readonly code = "run-store-migration-failed" as const;
43
- readonly schemaVersion: number;
44
-
45
- constructor(schemaVersion: number, cause: unknown) {
46
- super("run-store-migration-failed", { cause });
47
- this.name = "RunStoreMigrationError";
48
- this.schemaVersion = schemaVersion;
49
- }
50
- }
51
-
52
- const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
53
- const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
54
- const SHA256 = /^[a-f0-9]{64}$/u;
55
-
56
- interface RunRow {
57
- run_id: string;
58
- projection_json: string;
59
- }
60
-
61
- interface EventRow {
62
- sequence: number;
63
- type: string;
64
- payload_json: string;
65
- prior_hash: string;
66
- event_hash: string;
67
- }
68
-
69
- interface OperationRow {
70
- kind: string;
71
- state: string;
72
- outcome_json: string | null;
73
- lease_token: string | null;
74
- lease_expires_at: number | null;
75
- }
76
-
77
- export interface RunStoreOptions {
78
- now?: () => number;
79
- busyTimeoutMs?: number;
80
- }
81
-
82
- export interface StartRunInput {
83
- stage: RunStage;
84
- change?: string;
85
- provisionalKey?: string;
86
- provisionalAliases?: readonly string[];
87
- operationId: string;
88
- }
89
-
90
- export interface BindDeliveryInput {
91
- runId: string;
92
- gate: DeliveryGate;
93
- revision: number;
94
- receiptHash: string;
95
- approvalProof?: {
96
- revision: number;
97
- contractHash: string;
98
- recordHash: string;
99
- };
100
- operationId: string;
101
- lease?: OperationLease;
102
- }
103
-
104
- export interface OperationLease {
105
- runId: string;
106
- operationId: string;
107
- token: string;
108
- expiresAt: number;
109
- }
110
-
111
- export interface OperationLeaseStatus {
112
- state: "running" | "committed" | "interrupted";
113
- outcome?: Record<string, unknown>;
114
- expiresAt?: number;
115
- }
116
-
117
- function requireIdentifier(value: string, label: string): void {
118
- if (!IDENTIFIER.test(value)) throw new Error(`invalid-${label}`);
119
- }
120
-
121
- const LEGACY_SCHEMA_VERSIONS = new Map([
122
- [2, LEGACY_RUN_SCHEMA_V2],
123
- [4, SCHEMA],
124
- ]);
125
-
126
- function applicationTables(database: DatabaseSync): string[] {
127
- return (
128
- database
129
- .prepare(
130
- `SELECT name FROM sqlite_master
131
- WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
132
- ORDER BY name`,
133
- )
134
- .all() as unknown as Array<{ name: string }>
135
- ).map(({ name }) => name);
136
- }
137
-
138
- function tableColumns(database: DatabaseSync, table: string): string[] {
139
- return (
140
- database.prepare(`PRAGMA table_info(${table})`).all() as unknown as Array<{
141
- name: string;
142
- }>
143
- ).map(({ name }) => name);
144
- }
145
-
146
- function hasRequiredCoreSchema(
147
- database: DatabaseSync,
148
- _tables: readonly string[],
149
- ): boolean {
150
- return matchesSqliteSchema(database, SCHEMA);
151
- }
152
-
153
- function hasCurrentSchema(
154
- database: DatabaseSync,
155
- tables: readonly string[],
156
- ): boolean {
157
- return (
158
- !tables.includes("schema_meta") && hasRequiredCoreSchema(database, tables)
159
- );
160
- }
161
-
162
- function supportedLegacyVersion(
163
- database: DatabaseSync,
164
- tables: readonly string[],
165
- ): number | undefined {
166
- if (
167
- !tables.includes("schema_meta") ||
168
- tableColumns(database, "schema_meta").join("\0") !== "version"
169
- ) {
170
- return undefined;
171
- }
172
- const rows = database
173
- .prepare("SELECT version FROM schema_meta")
174
- .all() as unknown as Array<{
175
- version: number;
176
- }>;
177
- if (rows.length !== 1) return undefined;
178
- const version = rows[0]?.version;
179
- const schema = LEGACY_SCHEMA_VERSIONS.get(version);
180
- if (!schema || !matchesSqliteSchema(database, schema)) return undefined;
181
- // A v2 database cannot already contain any proof fields. Do not repair a
182
- // partial/ambiguous upgrade or reinterpret malformed fields as authority.
183
- if (
184
- version === 2 &&
185
- tableColumns(database, "delivery_bindings").some((column) =>
186
- ["approval_revision", "contract_hash", "record_hash"].includes(column),
187
- )
188
- )
189
- return undefined;
190
- return version;
191
- }
192
-
193
- function isSqliteLocked(error: unknown): boolean {
194
- return (
195
- typeof error === "object" &&
196
- error !== null &&
197
- "errcode" in error &&
198
- ((error as { errcode?: unknown }).errcode === 5 ||
199
- (error as { errcode?: unknown }).errcode === 6)
200
- );
201
- }
202
-
203
- function isStoreUnavailable(error: unknown): boolean {
204
- if (typeof error !== "object" || error === null) return false;
205
- const errcode = (error as { errcode?: unknown }).errcode;
206
- if ([8, 10, 13, 14, 23, 24].includes(Number(errcode))) return true;
207
- const code = (error as { code?: unknown }).code;
208
- return (
209
- typeof code === "string" &&
210
- [
211
- "EACCES",
212
- "EBUSY",
213
- "EMFILE",
214
- "ENFILE",
215
- "ENOSPC",
216
- "EPERM",
217
- "EROFS",
218
- ].includes(code)
219
- );
220
- }
221
-
222
- function ensureCurrentSchema(
223
- database: DatabaseSync,
224
- databasePath: string,
225
- ): void {
226
- const initialTables = applicationTables(database);
227
- if (hasCurrentSchema(database, initialTables)) return;
228
- let transactionStarted = false;
229
- let migrating: number | undefined;
230
- try {
231
- database.exec("BEGIN IMMEDIATE");
232
- transactionStarted = true;
233
- const tables = applicationTables(database);
234
- if (tables.length === 0) {
235
- database.exec(SCHEMA);
236
- } else if (hasCurrentSchema(database, tables)) {
237
- // Another opener may already have completed the same migration.
238
- } else {
239
- migrating = supportedLegacyVersion(database, tables);
240
- if (migrating === undefined) throw new RunStoreFormatError(databasePath);
241
- if (migrating === 2) {
242
- database.exec(`
243
- ALTER TABLE delivery_bindings ADD COLUMN approval_revision INTEGER;
244
- ALTER TABLE delivery_bindings ADD COLUMN contract_hash TEXT;
245
- ALTER TABLE delivery_bindings ADD COLUMN record_hash TEXT;
246
- `);
247
- }
248
- if (!hasRequiredCoreSchema(database, tables))
249
- throw new RunStoreFormatError(databasePath);
250
- database.exec("DROP TABLE schema_meta");
251
- }
252
- database.exec("COMMIT");
253
- } catch (error) {
254
- if (transactionStarted) {
255
- try {
256
- database.exec("ROLLBACK");
257
- } catch {
258
- // The original schema or commit failure remains authoritative.
259
- }
260
- }
261
- if (
262
- error instanceof RunStoreFormatError ||
263
- error instanceof RunStoreMigrationError ||
264
- isSqliteLocked(error)
265
- ) {
266
- throw error;
267
- }
268
- if (migrating) {
269
- throw new RunStoreMigrationError(migrating, error);
270
- }
271
- throw error;
272
- }
273
- }
274
-
275
- function parseProjection(value: string): RunProjection {
276
- const parsed = JSON.parse(value) as RunProjection;
277
- if (
278
- !parsed ||
279
- typeof parsed !== "object" ||
280
- typeof parsed.runId !== "string"
281
- ) {
282
- throw new Error("run-projection-invalid");
283
- }
284
- return parsed;
285
- }
286
-
287
- function lookupKeys(input: StartRunInput): string[] {
288
- const hasChange = typeof input.change === "string";
289
- const hasProvisional = typeof input.provisionalKey === "string";
290
- if (hasChange === hasProvisional) throw new Error("invalid-run-lookup-key");
291
- if (hasChange) {
292
- if (input.provisionalAliases !== undefined) {
293
- throw new Error("invalid-run-lookup-key");
294
- }
295
- if (!CHANGE_NAME.test(input.change as string))
296
- throw new Error("invalid-change");
297
- return [`change:${input.change}`];
298
- }
299
- if (!SHA256.test(input.provisionalKey as string)) {
300
- throw new Error("invalid-provisional-key");
301
- }
302
- if (
303
- input.provisionalAliases !== undefined &&
304
- (!Array.isArray(input.provisionalAliases) ||
305
- input.provisionalAliases.length > 4 ||
306
- input.provisionalAliases.some((alias) => !SHA256.test(alias)))
307
- ) {
308
- throw new Error("invalid-provisional-key");
309
- }
310
- if (input.stage !== "abel-design") {
311
- throw new Error("provisional-run-requires-design");
312
- }
313
- return [
314
- ...new Set([input.provisionalKey, ...(input.provisionalAliases ?? [])]),
315
- ].map((hash) => `provisional:${hash}`);
316
- }
317
-
318
- export class RunStore {
319
- readonly stateRoot: ResolvedStateRoot;
320
- readonly #database: DatabaseSync;
321
- readonly #now: () => number;
322
- #closed = false;
323
-
324
- private constructor(
325
- stateRoot: ResolvedStateRoot,
326
- database: DatabaseSync,
327
- options: RunStoreOptions,
328
- ) {
329
- this.stateRoot = stateRoot;
330
- this.#database = database;
331
- this.#now = options.now ?? Date.now;
332
- }
333
-
334
- static open(
335
- unresolved: ResolvedStateRoot,
336
- options: RunStoreOptions = {},
337
- ): RunStore {
338
- const stateRoot = prepareStateRoot(unresolved);
339
- try {
340
- const existing = lstatSync(stateRoot.databasePath);
341
- if (existing.isSymbolicLink()) {
342
- throw new StateRootError("state-component-symlink");
343
- }
344
- if (!existing.isFile()) {
345
- throw new StateRootError("state-component-type");
346
- }
347
- } catch (error) {
348
- if (
349
- error instanceof StateRootError ||
350
- (error as NodeJS.ErrnoException).code !== "ENOENT"
351
- ) {
352
- throw error;
353
- }
354
- const descriptor = openSync(stateRoot.databasePath, "ax", 0o600);
355
- closeSync(descriptor);
356
- }
357
- chmodSync(stateRoot.databasePath, 0o600);
358
-
359
- let database: DatabaseSync | undefined;
360
- try {
361
- database = new DatabaseSync(stateRoot.databasePath);
362
- configureSqlite(database, options.busyTimeoutMs);
363
- ensureCurrentSchema(database, stateRoot.databasePath);
364
- } catch (error) {
365
- try {
366
- database?.close();
367
- } catch {
368
- // The run-store initialization failure remains authoritative.
369
- }
370
- if (
371
- error instanceof RunStoreFormatError ||
372
- error instanceof RunStoreMigrationError ||
373
- isSqliteLocked(error) ||
374
- isStoreUnavailable(error)
375
- ) {
376
- throw error;
377
- }
378
- throw new RunStoreFormatError(stateRoot.databasePath);
379
- }
380
- if (!database) throw new RunStoreFormatError(stateRoot.databasePath);
381
- const store = new RunStore(stateRoot, database, options);
382
- try {
383
- store.#secureDatabaseFiles();
384
- store.recoverExpiredLeases();
385
- return store;
386
- } catch (error) {
387
- try {
388
- database.close();
389
- } catch {
390
- // The permission or lease-recovery failure remains authoritative.
391
- }
392
- throw error;
393
- }
394
- }
395
-
396
- #assertOpen(): void {
397
- if (this.#closed) throw new Error("run-store-closed");
398
- }
399
-
400
- #transaction<T>(work: () => T): T {
401
- this.#assertOpen();
402
- this.#database.exec("BEGIN IMMEDIATE");
403
- let value: T;
404
- try {
405
- value = work();
406
- this.#database.exec("COMMIT");
407
- } catch (error) {
408
- try {
409
- this.#database.exec("ROLLBACK");
410
- } catch {
411
- // The original operation or commit failure remains authoritative.
412
- }
413
- throw error;
414
- }
415
- this.#secureDatabaseFiles();
416
- return value;
417
- }
418
-
419
- #secureDatabaseFiles(): void {
420
- for (const file of [
421
- this.stateRoot.databasePath,
422
- `${this.stateRoot.databasePath}-wal`,
423
- `${this.stateRoot.databasePath}-shm`,
424
- ]) {
425
- try {
426
- const stat = lstatSync(file);
427
- if (stat.isSymbolicLink() || !stat.isFile()) {
428
- throw new StateRootError(
429
- stat.isSymbolicLink()
430
- ? "state-component-symlink"
431
- : "state-component-type",
432
- );
433
- }
434
- chmodSync(file, 0o600);
435
- } catch (error) {
436
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
437
- }
438
- }
439
- }
440
-
441
- #runRow(runId: string): RunRow {
442
- const row = this.#database
443
- .prepare("SELECT run_id, projection_json FROM runs WHERE run_id = ?")
444
- .get(runId) as RunRow | undefined;
445
- if (!row) throw new Error("run-not-found");
446
- return row;
447
- }
448
-
449
- #operation(runId: string, operationId: string): OperationRow | undefined {
450
- return this.#database
451
- .prepare(
452
- `SELECT kind, state, outcome_json, lease_token, lease_expires_at
453
- FROM operations WHERE run_id = ? AND operation_id = ?`,
454
- )
455
- .get(runId, operationId) as OperationRow | undefined;
456
- }
457
-
458
- #operationOutcome(
459
- runId: string,
460
- operationId: string,
461
- kind: string,
462
- ): RunProjection | undefined {
463
- const row = this.#operation(runId, operationId);
464
- if (row && row.kind !== kind) throw new Error("operation-id-conflict");
465
- if (row?.state !== "committed" || row.outcome_json === null)
466
- return undefined;
467
- return parseProjection(row.outcome_json);
468
- }
469
-
470
- #recordOperation(
471
- runId: string,
472
- operationId: string,
473
- kind: string,
474
- outcome: RunProjection,
475
- ): void {
476
- this.#database
477
- .prepare(
478
- `INSERT INTO operations(
479
- run_id, operation_id, kind, state, outcome_json
480
- ) VALUES (?, ?, ?, 'committed', ?)
481
- ON CONFLICT(run_id, operation_id) DO UPDATE SET
482
- state = 'committed', outcome_json = excluded.outcome_json,
483
- lease_token = NULL, lease_expires_at = NULL`,
484
- )
485
- .run(runId, operationId, kind, JSON.stringify(outcome));
486
- }
487
-
488
- #events(runId: string): RunEvent[] {
489
- const rows = this.#database
490
- .prepare(
491
- `SELECT sequence, type, payload_json, prior_hash, event_hash
492
- FROM events WHERE run_id = ? ORDER BY sequence`,
493
- )
494
- .all(runId) as unknown as EventRow[];
495
- return rows.map((row) => ({
496
- sequence: Number(row.sequence),
497
- type: row.type as RunEvent["type"],
498
- payload: JSON.parse(row.payload_json) as Record<string, unknown>,
499
- priorHash: row.prior_hash,
500
- hash: row.event_hash,
501
- }));
502
- }
503
-
504
- #appendEvent(
505
- runId: string,
506
- type: RunEvent["type"],
507
- payload: Record<string, unknown>,
508
- ): RunProjection {
509
- const row = this.#runRow(runId);
510
- const current = parseProjection(row.projection_json);
511
- const event = createRunEvent(
512
- current.sequence + 1,
513
- type,
514
- payload,
515
- current.eventHash,
516
- );
517
- const projection = reduceRunEvents([...this.#events(runId), event]);
518
- this.#database
519
- .prepare(
520
- `INSERT INTO events(
521
- run_id, sequence, type, payload_json, prior_hash, event_hash
522
- ) VALUES (?, ?, ?, ?, ?, ?)`,
523
- )
524
- .run(
525
- runId,
526
- event.sequence,
527
- event.type,
528
- JSON.stringify(event.payload),
529
- event.priorHash,
530
- event.hash,
531
- );
532
- this.#database
533
- .prepare(
534
- `UPDATE runs SET state = ?, sequence = ?, event_hash = ?,
535
- delivery_revision = ?, projection_json = ?, terminal_tombstone = ?
536
- WHERE run_id = ?`,
537
- )
538
- .run(
539
- projection.state,
540
- projection.sequence,
541
- projection.eventHash,
542
- projection.deliveryRevision ?? null,
543
- JSON.stringify(projection),
544
- projection.terminal ?? null,
545
- runId,
546
- );
547
- return projection;
548
- }
549
-
550
- startRun(input: StartRunInput): RunProjection {
551
- requireIdentifier(input.operationId, "operation-id");
552
- if (input.stage !== "abel-design" && input.stage !== "abel-implement") {
553
- throw new Error("invalid-run-stage");
554
- }
555
- const keys = lookupKeys(input);
556
- const key = keys[0];
557
- if (!key) throw new Error("invalid-run-lookup-key");
558
- return this.#transaction(() => {
559
- const operationRows = this.#database
560
- .prepare(
561
- `SELECT operations.run_id, operations.kind, operations.state,
562
- operations.outcome_json, runs.lookup_key
563
- FROM operations
564
- JOIN runs ON runs.run_id = operations.run_id
565
- WHERE runs.root_hash = ? AND runs.stage = ?
566
- AND operations.operation_id = ?`,
567
- )
568
- .all(
569
- this.stateRoot.consumerRootHash,
570
- input.stage,
571
- input.operationId,
572
- ) as unknown as Array<{
573
- run_id: string;
574
- kind: string;
575
- state: string;
576
- outcome_json: string | null;
577
- lookup_key: string;
578
- }>;
579
- if (operationRows.length > 0) {
580
- const replay = operationRows.find(
581
- (row) =>
582
- row.kind === "start" &&
583
- row.state === "committed" &&
584
- row.outcome_json !== null &&
585
- keys.includes(row.lookup_key),
586
- );
587
- if (operationRows.length !== 1 || !replay) {
588
- throw new Error("operation-id-conflict");
589
- }
590
- const outcome = parseProjection(replay.outcome_json as string);
591
- if (outcome.runId !== replay.run_id) {
592
- throw new Error("operation-outcome-invalid");
593
- }
594
- return outcome;
595
- }
596
- let existing: RunRow | undefined;
597
- for (const compatibleKey of keys) {
598
- const candidate = this.#database
599
- .prepare(
600
- `SELECT run_id, projection_json FROM runs
601
- WHERE root_hash = ? AND stage = ? AND lookup_key = ?`,
602
- )
603
- .get(this.stateRoot.consumerRootHash, input.stage, compatibleKey) as
604
- | RunRow
605
- | undefined;
606
- if (candidate && existing && candidate.run_id !== existing.run_id) {
607
- throw new Error("run-lookup-conflict");
608
- }
609
- existing ??= candidate;
610
- }
611
- if (existing) {
612
- const projection = parseProjection(existing.projection_json);
613
- if (
614
- input.stage === "abel-design" &&
615
- ["completed", "discarded", "rejected"].includes(projection.state)
616
- ) {
617
- this.#database
618
- .prepare("UPDATE runs SET lookup_key = ? WHERE run_id = ?")
619
- .run(`terminal:${existing.run_id}`, existing.run_id);
620
- existing = undefined;
621
- }
622
- }
623
- if (existing) {
624
- const replay = this.#operationOutcome(
625
- existing.run_id,
626
- input.operationId,
627
- "start",
628
- );
629
- if (replay) return replay;
630
- const projection = parseProjection(existing.projection_json);
631
- this.#recordOperation(
632
- existing.run_id,
633
- input.operationId,
634
- "start",
635
- projection,
636
- );
637
- return projection;
638
- }
639
-
640
- const runId = randomUUID();
641
- const event = createRunEvent(
642
- 1,
643
- "run-created",
644
- {
645
- runId,
646
- rootHash: this.stateRoot.consumerRootHash,
647
- stage: input.stage,
648
- ...(input.change ? { change: input.change } : {}),
649
- ...(input.provisionalKey
650
- ? { provisionalKey: input.provisionalKey }
651
- : {}),
652
- },
653
- ZERO_EVENT_HASH,
654
- );
655
- const projection = reduceRunEvents([event]);
656
- this.#database
657
- .prepare(
658
- `INSERT INTO runs(
659
- run_id, root_hash, stage, lookup_key, change_name,
660
- provisional_key, state, sequence, event_hash, projection_json
661
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
662
- )
663
- .run(
664
- runId,
665
- this.stateRoot.consumerRootHash,
666
- input.stage,
667
- key,
668
- input.change ?? null,
669
- input.provisionalKey ?? null,
670
- projection.state,
671
- projection.sequence,
672
- projection.eventHash,
673
- JSON.stringify(projection),
674
- );
675
- this.#database
676
- .prepare(
677
- `INSERT INTO events(
678
- run_id, sequence, type, payload_json, prior_hash, event_hash
679
- ) VALUES (?, ?, ?, ?, ?, ?)`,
680
- )
681
- .run(
682
- runId,
683
- event.sequence,
684
- event.type,
685
- JSON.stringify(event.payload),
686
- event.priorHash,
687
- event.hash,
688
- );
689
- this.#recordOperation(runId, input.operationId, "start", projection);
690
- return projection;
691
- });
692
- }
693
-
694
- bindChange(input: {
695
- runId: string;
696
- change: string;
697
- operationId: string;
698
- }): RunProjection {
699
- requireIdentifier(input.operationId, "operation-id");
700
- if (!CHANGE_NAME.test(input.change)) throw new Error("invalid-change");
701
- return this.#transaction(() => {
702
- const replay = this.#operationOutcome(
703
- input.runId,
704
- input.operationId,
705
- "change-bound",
706
- );
707
- if (replay) return replay;
708
- const current = this.status(input.runId);
709
- if (current.stage !== "abel-design" || !current.provisionalKey) {
710
- throw new Error("change-binding-not-allowed");
711
- }
712
- const conflict = this.#database
713
- .prepare(
714
- `SELECT run_id FROM runs
715
- WHERE root_hash = ? AND stage = ? AND lookup_key = ?`,
716
- )
717
- .get(current.rootHash, current.stage, `change:${input.change}`) as
718
- | { run_id: string }
719
- | undefined;
720
- if (conflict && conflict.run_id !== input.runId) {
721
- throw new Error("change-run-already-exists");
722
- }
723
- this.#database
724
- .prepare(
725
- `UPDATE runs SET lookup_key = ?, change_name = ?, provisional_key = NULL
726
- WHERE run_id = ?`,
727
- )
728
- .run(`change:${input.change}`, input.change, input.runId);
729
- const projection = this.#appendEvent(input.runId, "change-bound", {
730
- change: input.change,
731
- });
732
- this.#recordOperation(
733
- input.runId,
734
- input.operationId,
735
- "change-bound",
736
- projection,
737
- );
738
- return projection;
739
- });
740
- }
741
-
742
- #requireDeliveryBinding(input: BindDeliveryInput): void {
743
- requireIdentifier(input.operationId, "operation-id");
744
- if (
745
- (input.gate !== "gate-a" && input.gate !== "gate-b") ||
746
- !Number.isSafeInteger(input.revision) ||
747
- input.revision < 1 ||
748
- !SHA256.test(input.receiptHash) ||
749
- (input.approvalProof !== undefined &&
750
- (!Number.isSafeInteger(input.approvalProof.revision) ||
751
- input.approvalProof.revision < 1 ||
752
- !SHA256.test(input.approvalProof.contractHash) ||
753
- !SHA256.test(input.approvalProof.recordHash)))
754
- ) {
755
- throw new Error("invalid-delivery-binding");
756
- }
757
- }
758
-
759
- #bindDelivery(input: BindDeliveryInput): RunProjection {
760
- if (input.lease) {
761
- if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
762
- this.#assertLease(input.lease);
763
- }
764
- const replay = this.#operationOutcome(
765
- input.runId,
766
- input.operationId,
767
- "delivery-bound",
768
- );
769
- if (replay) return replay;
770
- const existing = this.#database
771
- .prepare(
772
- `SELECT receipt_hash, approval_revision, contract_hash, record_hash
773
- FROM delivery_bindings
774
- WHERE run_id = ? AND gate = ? AND revision = ?`,
775
- )
776
- .get(input.runId, input.gate, input.revision) as
777
- | {
778
- receipt_hash: string;
779
- approval_revision: number | null;
780
- contract_hash: string | null;
781
- record_hash: string | null;
782
- }
783
- | undefined;
784
- if (existing) {
785
- if (
786
- existing.receipt_hash !== input.receiptHash ||
787
- existing.approval_revision !==
788
- (input.approvalProof?.revision ?? null) ||
789
- existing.contract_hash !==
790
- (input.approvalProof?.contractHash ?? null) ||
791
- existing.record_hash !== (input.approvalProof?.recordHash ?? null)
792
- ) {
793
- throw new Error("delivery-revision-conflict");
794
- }
795
- const projection = this.status(input.runId);
796
- this.#recordOperation(
797
- input.runId,
798
- input.operationId,
799
- "delivery-bound",
800
- projection,
801
- );
802
- return projection;
803
- }
804
- this.#runRow(input.runId);
805
- this.#database
806
- .prepare(
807
- `INSERT INTO delivery_bindings(
808
- run_id, gate, revision, receipt_hash, approval_revision,
809
- contract_hash, record_hash, operation_id
810
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
811
- )
812
- .run(
813
- input.runId,
814
- input.gate,
815
- input.revision,
816
- input.receiptHash,
817
- input.approvalProof?.revision ?? null,
818
- input.approvalProof?.contractHash ?? null,
819
- input.approvalProof?.recordHash ?? null,
820
- input.operationId,
821
- );
822
- const projection = this.#appendEvent(input.runId, "delivery-bound", {
823
- gate: input.gate,
824
- revision: input.revision,
825
- receiptHash: input.receiptHash,
826
- ...(input.approvalProof
827
- ? { approvalProof: structuredClone(input.approvalProof) }
828
- : {}),
829
- });
830
- this.#recordOperation(
831
- input.runId,
832
- input.operationId,
833
- "delivery-bound",
834
- projection,
835
- );
836
- return projection;
837
- }
838
-
839
- bindDelivery(input: BindDeliveryInput): RunProjection {
840
- this.#requireDeliveryBinding(input);
841
- return this.#transaction(() => this.#bindDelivery(input));
842
- }
843
-
844
- bindDeliveryAtomically(
845
- input: BindDeliveryInput,
846
- write: (database: DatabaseSync) => void,
847
- ): RunProjection {
848
- this.#requireDeliveryBinding(input);
849
- if (typeof write !== "function") {
850
- throw new Error("delivery-binding-write-invalid");
851
- }
852
- return this.#transaction(() => {
853
- const projection = this.#bindDelivery(input);
854
- write(this.#database);
855
- return projection;
856
- });
857
- }
858
-
859
- bindDeliveriesAtomically(
860
- inputs: readonly BindDeliveryInput[],
861
- write: (database: DatabaseSync) => void,
862
- ): RunProjection {
863
- if (inputs.length === 0) throw new Error("delivery-binding-empty");
864
- for (const input of inputs) this.#requireDeliveryBinding(input);
865
- const runId = inputs[0]?.runId;
866
- if (
867
- !runId ||
868
- inputs.some(
869
- (input) =>
870
- input.runId !== runId ||
871
- (input.lease?.token ?? null) !== (inputs[0]?.lease?.token ?? null),
872
- ) ||
873
- typeof write !== "function"
874
- ) {
875
- throw new Error("delivery-binding-write-invalid");
876
- }
877
- return this.#transaction(() => {
878
- let projection: RunProjection | undefined;
879
- for (const input of inputs) projection = this.#bindDelivery(input);
880
- write(this.#database);
881
- if (!projection) throw new Error("delivery-binding-empty");
882
- return projection;
883
- });
884
- }
885
-
886
- acquireLease(input: {
887
- runId: string;
888
- operationId: string;
889
- ttlMs: number;
890
- preempt?: boolean;
891
- exclusive?: boolean;
892
- }): OperationLease {
893
- requireIdentifier(input.operationId, "operation-id");
894
- if (!Number.isSafeInteger(input.ttlMs) || input.ttlMs < 1) {
895
- throw new Error("invalid-lease-ttl");
896
- }
897
- return this.#transaction(() => {
898
- this.#runRow(input.runId);
899
- const now = this.#now();
900
- const existing = this.#operation(input.runId, input.operationId);
901
- if (
902
- existing?.state === "running" &&
903
- existing.lease_token &&
904
- existing.lease_expires_at !== null &&
905
- existing.lease_expires_at >= now
906
- ) {
907
- if (!input.preempt) {
908
- if (input.exclusive) throw new Error("operation-already-running");
909
- return {
910
- runId: input.runId,
911
- operationId: input.operationId,
912
- token: existing.lease_token,
913
- expiresAt: existing.lease_expires_at,
914
- };
915
- }
916
- }
917
- const token = randomUUID();
918
- const expiresAt = now + input.ttlMs;
919
- this.#database
920
- .prepare(
921
- `INSERT INTO operations(
922
- run_id, operation_id, kind, state, lease_token, lease_expires_at
923
- ) VALUES (?, ?, 'lease', 'running', ?, ?)
924
- ON CONFLICT(run_id, operation_id) DO UPDATE SET
925
- state = 'running', outcome_json = NULL,
926
- lease_token = excluded.lease_token,
927
- lease_expires_at = excluded.lease_expires_at`,
928
- )
929
- .run(input.runId, input.operationId, token, expiresAt);
930
- return {
931
- runId: input.runId,
932
- operationId: input.operationId,
933
- token,
934
- expiresAt,
935
- };
936
- });
937
- }
938
-
939
- renewLease(lease: OperationLease, ttlMs: number): OperationLease {
940
- if (!Number.isSafeInteger(ttlMs) || ttlMs < 1)
941
- throw new Error("invalid-lease-ttl");
942
- return this.#transaction(() => {
943
- const existing = this.#operation(lease.runId, lease.operationId);
944
- if (
945
- existing?.state !== "running" ||
946
- existing.lease_token !== lease.token ||
947
- existing.lease_expires_at === null ||
948
- existing.lease_expires_at < this.#now()
949
- ) {
950
- throw new Error("lease-fenced");
951
- }
952
- const expiresAt = this.#now() + ttlMs;
953
- this.#database
954
- .prepare(
955
- `UPDATE operations SET lease_expires_at = ?
956
- WHERE run_id = ? AND operation_id = ? AND lease_token = ?`,
957
- )
958
- .run(expiresAt, lease.runId, lease.operationId, lease.token);
959
- return { ...lease, expiresAt };
960
- });
961
- }
962
-
963
- #assertLease(lease: OperationLease): void {
964
- const existing = this.#operation(lease.runId, lease.operationId);
965
- if (
966
- existing?.state !== "running" ||
967
- existing.lease_token !== lease.token ||
968
- existing.lease_expires_at === null ||
969
- existing.lease_expires_at < this.#now()
970
- ) {
971
- throw new Error("lease-fenced");
972
- }
973
- }
974
-
975
- assertLease(lease: OperationLease): void {
976
- this.#transaction(() => this.#assertLease(lease));
977
- }
978
-
979
- inspectOperation(
980
- runId: string,
981
- operationId: string,
982
- ): OperationLeaseStatus | undefined {
983
- this.#assertOpen();
984
- requireIdentifier(operationId, "operation-id");
985
- this.#runRow(runId);
986
- let operation = this.#operation(runId, operationId);
987
- if (!operation) return undefined;
988
- if (
989
- operation.state === "running" &&
990
- operation.lease_expires_at !== null &&
991
- operation.lease_expires_at < this.#now()
992
- ) {
993
- this.#transaction(() => {
994
- this.#database
995
- .prepare(
996
- `UPDATE operations
997
- SET state = 'interrupted', lease_token = NULL,
998
- lease_expires_at = NULL
999
- WHERE run_id = ? AND operation_id = ? AND state = 'running'
1000
- AND lease_expires_at < ?`,
1001
- )
1002
- .run(runId, operationId, this.#now());
1003
- });
1004
- operation = this.#operation(runId, operationId);
1005
- if (!operation) throw new Error("operation-recovery-invalid");
1006
- }
1007
- if (
1008
- operation.state !== "running" &&
1009
- operation.state !== "committed" &&
1010
- operation.state !== "interrupted"
1011
- ) {
1012
- throw new Error("operation-state-invalid");
1013
- }
1014
- let outcome: Record<string, unknown> | undefined;
1015
- if (operation.outcome_json !== null) {
1016
- const parsed = JSON.parse(operation.outcome_json) as unknown;
1017
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1018
- throw new Error("operation-outcome-invalid");
1019
- }
1020
- outcome = parsed as Record<string, unknown>;
1021
- }
1022
- return {
1023
- state: operation.state,
1024
- ...(outcome ? { outcome: structuredClone(outcome) } : {}),
1025
- ...(operation.lease_expires_at === null
1026
- ? {}
1027
- : { expiresAt: operation.lease_expires_at }),
1028
- };
1029
- }
1030
-
1031
- commitLease(
1032
- lease: OperationLease,
1033
- outcome: Record<string, unknown>,
1034
- ): Record<string, unknown> {
1035
- const serialized = JSON.stringify(outcome);
1036
- if (!serialized) throw new Error("operation-outcome-invalid");
1037
- return this.#transaction(() => {
1038
- this.#assertLease(lease);
1039
- const result = this.#database
1040
- .prepare(
1041
- `UPDATE operations
1042
- SET state = 'committed', outcome_json = ?,
1043
- lease_token = NULL, lease_expires_at = NULL
1044
- WHERE run_id = ? AND operation_id = ? AND state = 'running'
1045
- AND lease_token = ?`,
1046
- )
1047
- .run(serialized, lease.runId, lease.operationId, lease.token);
1048
- if (Number(result.changes) !== 1) throw new Error("lease-fenced");
1049
- return structuredClone(outcome);
1050
- });
1051
- }
1052
-
1053
- interruptLease(lease: OperationLease): void {
1054
- this.#transaction(() => {
1055
- this.#assertLease(lease);
1056
- const result = this.#database
1057
- .prepare(
1058
- `UPDATE operations
1059
- SET state = 'interrupted', outcome_json = NULL,
1060
- lease_token = NULL, lease_expires_at = NULL
1061
- WHERE run_id = ? AND operation_id = ? AND state = 'running'
1062
- AND lease_token = ?`,
1063
- )
1064
- .run(lease.runId, lease.operationId, lease.token);
1065
- if (Number(result.changes) !== 1) throw new Error("lease-fenced");
1066
- });
1067
- }
1068
-
1069
- transition(input: {
1070
- runId: string;
1071
- to: RunState;
1072
- operationId: string;
1073
- code?: string;
1074
- lease?: OperationLease;
1075
- }): RunProjection {
1076
- return this.transitionAtomically(input, () => {});
1077
- }
1078
-
1079
- transitionAtomically(
1080
- input: {
1081
- runId: string;
1082
- to: RunState;
1083
- operationId: string;
1084
- code?: string;
1085
- lease?: OperationLease;
1086
- },
1087
- write: (database: DatabaseSync) => void,
1088
- ): RunProjection {
1089
- requireIdentifier(input.operationId, "operation-id");
1090
- if (typeof write !== "function") {
1091
- throw new Error("state-transition-write-invalid");
1092
- }
1093
- return this.#transaction(() => {
1094
- if (input.lease) {
1095
- if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
1096
- this.#assertLease(input.lease);
1097
- }
1098
- const replay = this.#operationOutcome(
1099
- input.runId,
1100
- input.operationId,
1101
- "state-transitioned",
1102
- );
1103
- if (replay) return replay;
1104
- const current = this.status(input.runId);
1105
- const projection = this.#appendEvent(input.runId, "state-transitioned", {
1106
- from: current.state,
1107
- to: input.to,
1108
- ...(input.code ? { code: input.code } : {}),
1109
- });
1110
- write(this.#database);
1111
- this.#recordOperation(
1112
- input.runId,
1113
- input.operationId,
1114
- "state-transitioned",
1115
- projection,
1116
- );
1117
- return projection;
1118
- });
1119
- }
1120
-
1121
- status(runId: string): RunProjection {
1122
- this.#assertOpen();
1123
- return parseProjection(this.#runRow(runId).projection_json);
1124
- }
1125
-
1126
- listEvents(runId: string): RunEvent[] {
1127
- this.#assertOpen();
1128
- this.#runRow(runId);
1129
- return structuredClone(this.#events(runId));
1130
- }
1131
-
1132
- rebuildProjection(runId: string): RunProjection {
1133
- return reduceRunEvents(this.listEvents(runId));
1134
- }
1135
-
1136
- verifyIntegrity(runId: string): RunProjection {
1137
- const rebuilt = this.rebuildProjection(runId);
1138
- const stored = this.status(runId);
1139
- if (JSON.stringify(rebuilt) !== JSON.stringify(stored)) {
1140
- throw new Error("run-projection-integrity-mismatch");
1141
- }
1142
- return rebuilt;
1143
- }
1144
-
1145
- recoverExpiredLeases(): number {
1146
- this.#assertOpen();
1147
- const expired = this.#database
1148
- .prepare(
1149
- `SELECT run_id, operation_id FROM operations
1150
- WHERE state = 'running' AND lease_expires_at < ?`,
1151
- )
1152
- .all(this.#now()) as unknown as Array<{
1153
- run_id: string;
1154
- operation_id: string;
1155
- }>;
1156
- if (expired.length === 0) return 0;
1157
- return this.#transaction(() => {
1158
- for (const row of expired) {
1159
- this.#database
1160
- .prepare(
1161
- `UPDATE operations
1162
- SET state = 'interrupted', lease_token = NULL,
1163
- lease_expires_at = NULL
1164
- WHERE run_id = ? AND operation_id = ? AND state = 'running'`,
1165
- )
1166
- .run(row.run_id, row.operation_id);
1167
- }
1168
- return expired.length;
1169
- });
1170
- }
1171
-
1172
- close(): void {
1173
- if (this.#closed) return;
1174
- this.#database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
1175
- this.#secureDatabaseFiles();
1176
- this.#database.close();
1177
- this.#closed = true;
1178
- }
1179
- }