@abelxiaoxing/cadence 1.8.3 → 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 (208) hide show
  1. package/README.md +324 -111
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -26
  4. package/agents/diagnosis-worker.md +14 -25
  5. package/agents/implementation-worker.md +16 -35
  6. package/config/.env.example +2 -5
  7. package/config/workflow-task.example.json +7 -0
  8. package/docs/remote-access.md +60 -0
  9. package/docs/reuse-and-distribution.md +44 -0
  10. package/docs/service-management.md +108 -0
  11. package/licenses/bundled-web.txt +6245 -0
  12. package/licenses/pi-web-ui-MIT.txt +21 -0
  13. package/licenses/pi-web-ui-reuse.md +22 -0
  14. package/package.json +94 -21
  15. package/src/index.ts +22 -1807
  16. package/src/native-build.ts +6 -0
  17. package/src/native-client.ts +103 -0
  18. package/src/native-export.ts +58 -0
  19. package/src/native-history.ts +359 -0
  20. package/src/operator-cli.mjs +453 -1937
  21. package/src/plan-proposal.ts +87 -0
  22. package/src/sdk-worker.mjs +549 -0
  23. package/src/service/contracts/active.ts +82 -0
  24. package/src/service/contracts/budget.ts +148 -0
  25. package/src/service/contracts/build.ts +67 -0
  26. package/src/service/contracts/capture.ts +48 -0
  27. package/src/service/contracts/core.ts +33 -0
  28. package/src/service/contracts/current.ts +261 -0
  29. package/src/service/contracts/domain.ts +68 -0
  30. package/src/service/contracts/history.ts +2245 -0
  31. package/src/service/contracts/models.ts +139 -0
  32. package/src/service/coordination.ts +862 -0
  33. package/src/service/private-files.ts +204 -0
  34. package/src/service-cli.mjs +5592 -0
  35. package/src/service-client.ts +747 -0
  36. package/src/service-contract.ts +245 -0
  37. package/src/service-host.mjs +22382 -0
  38. package/src/service-http-client.ts +425 -0
  39. package/src/service-wire.ts +372 -0
  40. package/src/task-shell.ts +1045 -0
  41. package/src/task-store.ts +479 -0
  42. package/src/task-types.ts +184 -0
  43. package/src/verification-diagnostics.ts +29 -72
  44. package/src/worker-capture.mjs +1197 -0
  45. package/src/worker-public.mjs +1753 -0
  46. package/src/worker-redaction.mjs +608 -0
  47. package/src/worker-tools.mjs +202 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  50. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  56. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  62. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  65. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  68. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  71. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  74. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  77. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  80. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  89. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  92. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  95. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  98. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  101. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  104. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  107. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  108. package/web/dist/assets/cadence.css +2 -0
  109. package/web/dist/assets/cadence.js +7 -0
  110. package/web/dist/assets/markdown.js +296 -0
  111. package/web/dist/assets/react.js +9 -0
  112. package/web/dist/assets/rolldown-runtime.js +1 -0
  113. package/web/dist/assets/timeline-client.js +41 -0
  114. package/web/dist/assets/vis-timeline.css +1274 -0
  115. package/web/dist/assets/vis-timeline.js +32 -0
  116. package/web/dist/index.html +18 -0
  117. package/web/dist/manifest.json +505 -0
  118. package/config/plan-draft.example.json +0 -125
  119. package/config/plan-draft.multiple-tasks.example.json +0 -375
  120. package/config/plan-draft.quick.example.json +0 -78
  121. package/config/routes.example.json +0 -39
  122. package/prompts/abel-design.md +0 -382
  123. package/prompts/abel-diagnose.md +0 -71
  124. package/prompts/abel-implement.md +0 -284
  125. package/prompts/abel-init.md +0 -52
  126. package/src/activation.ts +0 -44
  127. package/src/activity-contracts.ts +0 -82
  128. package/src/agent-registry.ts +0 -53
  129. package/src/apply-transaction.ts +0 -1437
  130. package/src/artifact-store.ts +0 -213
  131. package/src/build-windows-job.mjs +0 -95
  132. package/src/candidate-artifact.ts +0 -143
  133. package/src/candidate-patch.ts +0 -271
  134. package/src/candidate-workspace.ts +0 -80
  135. package/src/canonical.ts +0 -16
  136. package/src/change-contract-schema.ts +0 -191
  137. package/src/change-contract.ts +0 -324
  138. package/src/change-verification.ts +0 -1617
  139. package/src/child-budget.ts +0 -58
  140. package/src/child-model.ts +0 -142
  141. package/src/child-session.ts +0 -640
  142. package/src/contracts.ts +0 -2476
  143. package/src/control-contracts.ts +0 -382
  144. package/src/delivery-compiler.ts +0 -1805
  145. package/src/delivery-revision.ts +0 -42
  146. package/src/design-control.ts +0 -1299
  147. package/src/design-diagnostics.ts +0 -181
  148. package/src/design-journal.ts +0 -1773
  149. package/src/directory-sync.ts +0 -36
  150. package/src/durable-contracts.ts +0 -231
  151. package/src/durable-workflow.ts +0 -978
  152. package/src/evidence-draft.ts +0 -122
  153. package/src/execution-profile.ts +0 -64
  154. package/src/execution-retention.ts +0 -77
  155. package/src/file-snapshot.ts +0 -163
  156. package/src/implement-continuation.ts +0 -559
  157. package/src/implement-graph.ts +0 -690
  158. package/src/implement-plan.ts +0 -72
  159. package/src/install-config.d.mts +0 -6
  160. package/src/isolation-backend.ts +0 -353
  161. package/src/model-source.ts +0 -22
  162. package/src/openspec-cli.ts +0 -494
  163. package/src/operator-cli-entry.mjs +0 -24
  164. package/src/operator-tools.ts +0 -309
  165. package/src/package-candidate.ts +0 -227
  166. package/src/package-delivery.ts +0 -486
  167. package/src/package-state.ts +0 -320
  168. package/src/package-verification.ts +0 -635
  169. package/src/package-workflow.ts +0 -342
  170. package/src/packet-runtime.ts +0 -570
  171. package/src/parent-provider.ts +0 -375
  172. package/src/phase-execution.ts +0 -2259
  173. package/src/pi-adapter.ts +0 -19
  174. package/src/plan-draft-summary.ts +0 -527
  175. package/src/plan-draft.ts +0 -502
  176. package/src/remove-path.ts +0 -32
  177. package/src/route-policy.ts +0 -545
  178. package/src/run-state.ts +0 -420
  179. package/src/run-store.ts +0 -1179
  180. package/src/safe-path.ts +0 -77
  181. package/src/scoped-grep-worker.mjs +0 -23
  182. package/src/scoped-tools.ts +0 -775
  183. package/src/single-task-draft.ts +0 -142
  184. package/src/sqlite-schema.ts +0 -171
  185. package/src/state-root.ts +0 -215
  186. package/src/storage-schema.ts +0 -360
  187. package/src/subagent-activity.ts +0 -1417
  188. package/src/submit-tool.ts +0 -950
  189. package/src/task-ledger.ts +0 -1338
  190. package/src/transport-budget.ts +0 -52
  191. package/src/verification-capability.ts +0 -1191
  192. package/src/verification-environment-io.ts +0 -389
  193. package/src/verification-environment.ts +0 -162
  194. package/src/verification-identity.ts +0 -77
  195. package/src/verification-scan.ts +0 -64
  196. package/src/windows-job-backend.ts +0 -419
  197. package/src/windows-job.c +0 -149
  198. package/src/worker-broker.ts +0 -806
  199. package/src/workflow-engine.ts +0 -28
  200. package/src/workflow-policy.ts +0 -1679
  201. package/src/workflow-recovery-policy.ts +0 -97
  202. package/src/workflow-scheduling.ts +0 -105
  203. package/src/workflow-state-machine.ts +0 -4355
  204. package/src/workflow-status.ts +0 -576
  205. package/src/workspace-io-entry.mjs +0 -76
  206. package/src/workspace-io-worker.mjs +0 -1312
  207. package/src/workspace-io.ts +0 -89
  208. 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
- }