@sixb/sqlite 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/action-run-storage.d.ts +29 -0
  4. package/dist/action-run-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts +25 -0
  6. package/dist/agents/agent-storage.d.ts.map +1 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/messages.d.ts +14 -0
  10. package/dist/agents/messages.d.ts.map +1 -0
  11. package/dist/agents/rows.d.ts +77 -0
  12. package/dist/agents/rows.d.ts.map +1 -0
  13. package/dist/agents/runs.d.ts +26 -0
  14. package/dist/agents/runs.d.ts.map +1 -0
  15. package/dist/agents/threads.d.ts +14 -0
  16. package/dist/agents/threads.d.ts.map +1 -0
  17. package/dist/auth-storage/access-tokens.d.ts +33 -0
  18. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  19. package/dist/auth-storage/auth-storage.d.ts +46 -0
  20. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  21. package/dist/auth-storage/group-memberships.d.ts +21 -0
  22. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  23. package/dist/auth-storage/identities.d.ts +17 -0
  24. package/dist/auth-storage/identities.d.ts.map +1 -0
  25. package/dist/auth-storage/index.d.ts +3 -0
  26. package/dist/auth-storage/index.d.ts.map +1 -0
  27. package/dist/auth-storage/invitations.d.ts +28 -0
  28. package/dist/auth-storage/invitations.d.ts.map +1 -0
  29. package/dist/auth-storage/magic-links.d.ts +28 -0
  30. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  31. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  32. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  33. package/dist/auth-storage/rows.d.ts +134 -0
  34. package/dist/auth-storage/rows.d.ts.map +1 -0
  35. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  36. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  37. package/dist/auth-storage/service-accounts.d.ts +14 -0
  38. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  39. package/dist/auth-storage/sessions.d.ts +55 -0
  40. package/dist/auth-storage/sessions.d.ts.map +1 -0
  41. package/dist/auth-storage/shared.d.ts +131 -0
  42. package/dist/auth-storage/shared.d.ts.map +1 -0
  43. package/dist/auth-storage/users.d.ts +19 -0
  44. package/dist/auth-storage/users.d.ts.map +1 -0
  45. package/dist/index.d.ts +71 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +10204 -0
  48. package/dist/index.js.map +54 -0
  49. package/dist/latest-run-query.d.ts +22 -0
  50. package/dist/latest-run-query.d.ts.map +1 -0
  51. package/dist/migrations.d.ts +18 -0
  52. package/dist/migrations.d.ts.map +1 -0
  53. package/dist/object-query-compiler.d.ts +44 -0
  54. package/dist/object-query-compiler.d.ts.map +1 -0
  55. package/dist/object-storage.d.ts +93 -0
  56. package/dist/object-storage.d.ts.map +1 -0
  57. package/dist/ontology-storage/commits.d.ts +14 -0
  58. package/dist/ontology-storage/commits.d.ts.map +1 -0
  59. package/dist/ontology-storage/index.d.ts +22 -0
  60. package/dist/ontology-storage/index.d.ts.map +1 -0
  61. package/dist/ontology-storage/materialization-session.d.ts +49 -0
  62. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  63. package/dist/ontology-storage/materialization-state.d.ts +63 -0
  64. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  65. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  66. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  67. package/dist/ontology-storage/materializations.d.ts +32 -0
  68. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  69. package/dist/ontology-storage/outbox.d.ts +14 -0
  70. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  71. package/dist/ontology-storage/shared.d.ts +91 -0
  72. package/dist/ontology-storage/shared.d.ts.map +1 -0
  73. package/dist/ontology-storage/sources.d.ts +26 -0
  74. package/dist/ontology-storage/sources.d.ts.map +1 -0
  75. package/dist/pipeline-run-storage.d.ts +26 -0
  76. package/dist/pipeline-run-storage.d.ts.map +1 -0
  77. package/dist/projection-run-storage.d.ts +32 -0
  78. package/dist/projection-run-storage.d.ts.map +1 -0
  79. package/dist/rules-storage.d.ts +33 -0
  80. package/dist/rules-storage.d.ts.map +1 -0
  81. package/dist/run-list-query.d.ts +27 -0
  82. package/dist/run-list-query.d.ts.map +1 -0
  83. package/dist/storage-errors.d.ts +2 -0
  84. package/dist/storage-errors.d.ts.map +1 -0
  85. package/dist/sync-run-storage.d.ts +23 -0
  86. package/dist/sync-run-storage.d.ts.map +1 -0
  87. package/dist/timeseries-storage.d.ts +41 -0
  88. package/dist/timeseries-storage.d.ts.map +1 -0
  89. package/dist/transactions.d.ts +15 -0
  90. package/dist/transactions.d.ts.map +1 -0
  91. package/dist/tsconfig.json +5 -0
  92. package/dist/validation.d.ts +5 -0
  93. package/dist/validation.d.ts.map +1 -0
  94. package/dist/webhook-delivery-storage.d.ts +27 -0
  95. package/dist/webhook-delivery-storage.d.ts.map +1 -0
  96. package/dist/webhook-run-storage.d.ts +22 -0
  97. package/dist/webhook-run-storage.d.ts.map +1 -0
  98. package/dist/workflow-intervention-storage.d.ts +26 -0
  99. package/dist/workflow-intervention-storage.d.ts.map +1 -0
  100. package/dist/workflow-run-storage.d.ts +63 -0
  101. package/dist/workflow-run-storage.d.ts.map +1 -0
  102. package/package.json +58 -0
  103. package/src/action-run-storage.ts +761 -0
  104. package/src/agents/agent-storage.ts +46 -0
  105. package/src/agents/index.ts +5 -0
  106. package/src/agents/messages.ts +162 -0
  107. package/src/agents/rows.ts +205 -0
  108. package/src/agents/runs.ts +389 -0
  109. package/src/agents/threads.ts +133 -0
  110. package/src/auth-storage/access-tokens.ts +289 -0
  111. package/src/auth-storage/auth-storage.ts +784 -0
  112. package/src/auth-storage/group-memberships.ts +52 -0
  113. package/src/auth-storage/identities.ts +98 -0
  114. package/src/auth-storage/index.ts +2 -0
  115. package/src/auth-storage/invitations.ts +258 -0
  116. package/src/auth-storage/magic-links.ts +149 -0
  117. package/src/auth-storage/oidc-attempts.ts +109 -0
  118. package/src/auth-storage/rows.ts +338 -0
  119. package/src/auth-storage/service-account-group-memberships.ts +161 -0
  120. package/src/auth-storage/service-accounts.ts +168 -0
  121. package/src/auth-storage/sessions.ts +230 -0
  122. package/src/auth-storage/shared.ts +707 -0
  123. package/src/auth-storage/users.ts +226 -0
  124. package/src/index.ts +292 -0
  125. package/src/latest-run-query.ts +49 -0
  126. package/src/migrations/001-initial-schema.sql +1068 -0
  127. package/src/migrations.ts +245 -0
  128. package/src/object-query-compiler.ts +1198 -0
  129. package/src/object-storage.ts +605 -0
  130. package/src/ontology-storage/commits.ts +127 -0
  131. package/src/ontology-storage/index.ts +35 -0
  132. package/src/ontology-storage/materialization-session.ts +393 -0
  133. package/src/ontology-storage/materialization-state.ts +901 -0
  134. package/src/ontology-storage/materialization-writer.ts +394 -0
  135. package/src/ontology-storage/materializations.ts +838 -0
  136. package/src/ontology-storage/outbox.ts +237 -0
  137. package/src/ontology-storage/shared.ts +269 -0
  138. package/src/ontology-storage/sources.ts +653 -0
  139. package/src/pipeline-run-storage.ts +506 -0
  140. package/src/projection-run-storage.ts +592 -0
  141. package/src/rules-storage.ts +375 -0
  142. package/src/run-list-query.ts +98 -0
  143. package/src/sql.d.ts +4 -0
  144. package/src/storage-errors.ts +3 -0
  145. package/src/sync-run-storage.ts +299 -0
  146. package/src/timeseries-storage.ts +212 -0
  147. package/src/transactions.ts +96 -0
  148. package/src/validation.ts +30 -0
  149. package/src/webhook-delivery-storage.ts +272 -0
  150. package/src/webhook-run-storage.ts +240 -0
  151. package/src/workflow-intervention-storage.ts +398 -0
  152. package/src/workflow-run-storage.ts +1148 -0
@@ -0,0 +1,1148 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { Principal, WorkflowRunSource } from "@sixb/core"
3
+ import type { WorkflowIOSnapshot } from "@sixb/core/internal/workflows"
4
+ import type {
5
+ CancelWorkflowAgentNodeRunInput,
6
+ ConfirmWorkflowAgentNodeRunExecutionOwnershipInput,
7
+ ConfirmWorkflowRunExecutionOwnershipInput,
8
+ CreateWorkflowAgentNodeRunInput,
9
+ FinishWorkflowAgentNodeRunInput,
10
+ FinishWorkflowNodeRunInput,
11
+ FinishWorkflowRunInput,
12
+ ListLatestWorkflowRunsInput,
13
+ ListLatestWorkflowRunsResult,
14
+ ListWorkflowAgentNodeRunsInput,
15
+ ListWorkflowAgentNodeRunsResult,
16
+ ListWorkflowNodeRunsInput,
17
+ ListWorkflowNodeRunsResult,
18
+ ListWorkflowRunsInput,
19
+ ListWorkflowRunsResult,
20
+ QueueWorkflowRunInput,
21
+ ReclaimWorkflowAgentNodeRunInput,
22
+ ReclaimWorkflowRunInput,
23
+ ResumeWorkflowRunInput,
24
+ StartWorkflowAgentNodeRunInput,
25
+ StartWorkflowNodeRunInput,
26
+ StartWorkflowRunInput,
27
+ WaitWorkflowNodeRunInput,
28
+ WaitWorkflowRunInput,
29
+ WorkflowAgentNodeRunRecord,
30
+ WorkflowAgentNodeRunStorage,
31
+ WorkflowNodeRunRecord,
32
+ WorkflowNodeRunStorage,
33
+ WorkflowRunRecord,
34
+ WorkflowRunStorage,
35
+ } from "@sixb/core/storage"
36
+ import { WorkflowRunError } from "@sixb/core/storage"
37
+ import { queryLatestRunsByOwnerId } from "./latest-run-query"
38
+ import { installFreshSqliteSchema } from "./migrations"
39
+ import {
40
+ appendRunListFilters,
41
+ hasEmptyStatuses,
42
+ queryRunList,
43
+ type SqliteValue,
44
+ } from "./run-list-query"
45
+ import { isUniqueConstraintError } from "./storage-errors"
46
+ import {
47
+ closeSqliteStoreConnection,
48
+ openSqliteStoreConnection,
49
+ type SqliteStoreConnection,
50
+ } from "./transactions"
51
+
52
+ export interface SqliteWorkflowRunStorageOptions {
53
+ /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
54
+ path?: string
55
+ /** Internal shared connection used by bundled SqliteStorage. */
56
+ connection?: SqliteStoreConnection
57
+ }
58
+
59
+ export class SqliteWorkflowRunStorage implements WorkflowRunStorage {
60
+ readonly nodes: SqliteWorkflowNodeRunStorage
61
+ readonly agentNodes: SqliteWorkflowAgentNodeRunStorage
62
+
63
+ private readonly connection: SqliteStoreConnection
64
+ private readonly db: Database
65
+
66
+ constructor(options: SqliteWorkflowRunStorageOptions = {}) {
67
+ this.connection = openSqliteStoreConnection(options)
68
+ this.db = this.connection.db
69
+
70
+ if (this.connection.installFreshSchema) {
71
+ installFreshSqliteSchema(this.db)
72
+ }
73
+
74
+ this.nodes = new SqliteWorkflowNodeRunStorage(this.db)
75
+ this.agentNodes = new SqliteWorkflowAgentNodeRunStorage(this.db)
76
+ }
77
+
78
+ async queue(input: QueueWorkflowRunInput): Promise<WorkflowRunRecord> {
79
+ const queuedAt = input.queuedAt ?? new Date()
80
+
81
+ try {
82
+ this.db
83
+ .query(
84
+ `
85
+ INSERT INTO workflow_runs (
86
+ project_id,
87
+ id,
88
+ workflow_id,
89
+ status,
90
+ input,
91
+ queued_at,
92
+ started_at,
93
+ source,
94
+ requested_by_principal_type,
95
+ requested_by_principal_id,
96
+ attempt
97
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
98
+ `
99
+ )
100
+ .run(
101
+ input.projectId,
102
+ input.id,
103
+ input.workflowId,
104
+ "queued",
105
+ serializeRecord(input.input),
106
+ queuedAt.toISOString(),
107
+ queuedAt.toISOString(),
108
+ input.source ? JSON.stringify(input.source) : null,
109
+ input.requestedByPrincipal?.type ?? "system",
110
+ input.requestedByPrincipal?.id ?? "system",
111
+ 0
112
+ )
113
+ } catch (error) {
114
+ if (isUniqueConstraintError(error)) {
115
+ throw new WorkflowRunError(
116
+ `[SixbSqlite] Workflow run '${input.id}' already exists for project '${input.projectId}'.`
117
+ )
118
+ }
119
+
120
+ throw error
121
+ }
122
+
123
+ return this.requireWorkflowRun(input.projectId, input.id)
124
+ }
125
+
126
+ async start(input: StartWorkflowRunInput): Promise<WorkflowRunRecord> {
127
+ const startedAt = input.startedAt ?? new Date()
128
+
129
+ return this.db.transaction(() => {
130
+ const existing = this.db
131
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
132
+ .get(input.projectId, input.id) as WorkflowRunDatabaseRow | null
133
+
134
+ if (existing) {
135
+ if (existing.status !== "queued") {
136
+ throw new WorkflowRunError(
137
+ `[SixbSqlite] Workflow run '${input.id}' already exists for project '${input.projectId}'.`
138
+ )
139
+ }
140
+
141
+ if (existing.workflow_id !== input.workflowId) {
142
+ throw new WorkflowRunError(
143
+ `[SixbSqlite] Workflow run '${input.id}' workflow '${input.workflowId}' does not match existing workflow '${existing.workflow_id}'.`
144
+ )
145
+ }
146
+
147
+ this.db
148
+ .query(
149
+ `
150
+ UPDATE workflow_runs
151
+ SET
152
+ status = ?,
153
+ input = ?,
154
+ started_at = ?,
155
+ finished_at = NULL,
156
+ error = NULL,
157
+ source = COALESCE(source, ?)
158
+ , attempt = attempt + 1,
159
+ execution_token = ?, execution_queue_lease_expires_at = ?
160
+ WHERE project_id = ? AND id = ?
161
+ `
162
+ )
163
+ .run(
164
+ "running",
165
+ serializeRecord(input.input),
166
+ startedAt.toISOString(),
167
+ input.source ? JSON.stringify(input.source) : null,
168
+ input.execution?.token ?? null,
169
+ input.execution?.queueLeaseExpiresAt.toISOString() ?? null,
170
+ input.projectId,
171
+ input.id
172
+ )
173
+
174
+ return this.requireWorkflowRun(input.projectId, input.id)
175
+ }
176
+
177
+ try {
178
+ this.db
179
+ .query(
180
+ `
181
+ INSERT INTO workflow_runs (
182
+ project_id,
183
+ id,
184
+ workflow_id,
185
+ status,
186
+ input,
187
+ started_at,
188
+ source,
189
+ requested_by_principal_type,
190
+ requested_by_principal_id,
191
+ attempt,
192
+ execution_token,
193
+ execution_queue_lease_expires_at
194
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
195
+ `
196
+ )
197
+ .run(
198
+ input.projectId,
199
+ input.id,
200
+ input.workflowId,
201
+ "running",
202
+ serializeRecord(input.input),
203
+ startedAt.toISOString(),
204
+ input.source ? JSON.stringify(input.source) : null,
205
+ input.requestedByPrincipal?.type ?? "system",
206
+ input.requestedByPrincipal?.id ?? "system",
207
+ 1,
208
+ input.execution?.token ?? null,
209
+ input.execution?.queueLeaseExpiresAt.toISOString() ?? null
210
+ )
211
+ } catch (error) {
212
+ if (isUniqueConstraintError(error)) {
213
+ throw new WorkflowRunError(
214
+ `[SixbSqlite] Workflow run '${input.id}' already exists for project '${input.projectId}'.`
215
+ )
216
+ }
217
+
218
+ throw error
219
+ }
220
+
221
+ return this.requireWorkflowRun(input.projectId, input.id)
222
+ })()
223
+ }
224
+
225
+ async reclaim(input: ReclaimWorkflowRunInput): Promise<WorkflowRunRecord> {
226
+ return this.db.transaction(() => {
227
+ const row = this.requireStatus(input.projectId, input.id, "running")
228
+ this.db
229
+ .query(`
230
+ UPDATE workflow_runs SET attempt = ?, execution_token = ?,
231
+ execution_queue_lease_expires_at = ? WHERE project_id = ? AND id = ?
232
+ `)
233
+ .run(
234
+ row.attempt + 1,
235
+ input.execution.token,
236
+ input.execution.queueLeaseExpiresAt.toISOString(),
237
+ input.projectId,
238
+ input.id
239
+ )
240
+ return this.requireWorkflowRun(input.projectId, input.id)
241
+ })()
242
+ }
243
+
244
+ async confirmExecutionOwnership(
245
+ input: ConfirmWorkflowRunExecutionOwnershipInput
246
+ ): Promise<WorkflowRunRecord> {
247
+ return this.db.transaction(() => {
248
+ const row = this.requireStatus(input.projectId, input.id, "running")
249
+ assertSqliteWorkflowRunOwnership(row, input.executionToken)
250
+ const current = row.execution_queue_lease_expires_at
251
+ ? new Date(row.execution_queue_lease_expires_at)
252
+ : input.queueLeaseExpiresAt
253
+ this.db
254
+ .query(`
255
+ UPDATE workflow_runs SET execution_queue_lease_expires_at = ?
256
+ WHERE project_id = ? AND id = ?
257
+ `)
258
+ .run(
259
+ new Date(Math.max(current.getTime(), input.queueLeaseExpiresAt.getTime())).toISOString(),
260
+ input.projectId,
261
+ input.id
262
+ )
263
+ return this.requireWorkflowRun(input.projectId, input.id)
264
+ })()
265
+ }
266
+
267
+ async finish(input: FinishWorkflowRunInput): Promise<WorkflowRunRecord> {
268
+ return this.db.transaction(() => {
269
+ const existing = this.db
270
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
271
+ .get(input.projectId, input.id) as WorkflowRunDatabaseRow | null
272
+
273
+ if (!existing) {
274
+ throw new WorkflowRunError(
275
+ `[SixbSqlite] Workflow run '${input.id}' not found for project '${input.projectId}'.`
276
+ )
277
+ }
278
+
279
+ if (!canFinishWorkflowRun(existing.status, input.status)) {
280
+ throw new WorkflowRunError(
281
+ `[SixbSqlite] Workflow run '${input.id}' for project '${input.projectId}' cannot be finished from status '${existing.status}'.`
282
+ )
283
+ }
284
+ assertSqliteWorkflowRunOwnership(existing, input.executionToken)
285
+
286
+ this.db
287
+ .query(
288
+ `
289
+ UPDATE workflow_runs
290
+ SET
291
+ status = ?,
292
+ finished_at = ?,
293
+ error = ?
294
+ , execution_token = NULL, execution_queue_lease_expires_at = NULL
295
+ WHERE project_id = ? AND id = ?
296
+ `
297
+ )
298
+ .run(
299
+ input.status,
300
+ (input.finishedAt ?? new Date()).toISOString(),
301
+ input.status === "succeeded" ? null : (input.error ?? null),
302
+ input.projectId,
303
+ input.id
304
+ )
305
+
306
+ const updated = this.db
307
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
308
+ .get(input.projectId, input.id) as WorkflowRunDatabaseRow
309
+
310
+ return rowToWorkflowRunRecord(updated)
311
+ })()
312
+ }
313
+
314
+ async wait(input: WaitWorkflowRunInput): Promise<WorkflowRunRecord> {
315
+ return this.db.transaction(() => {
316
+ const existing = this.db
317
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
318
+ .get(input.projectId, input.id) as WorkflowRunDatabaseRow | null
319
+
320
+ if (!existing) {
321
+ throw new WorkflowRunError(
322
+ `[SixbSqlite] Workflow run '${input.id}' not found for project '${input.projectId}'.`
323
+ )
324
+ }
325
+
326
+ if (existing.status !== "running") {
327
+ throw new WorkflowRunError(
328
+ `[SixbSqlite] Workflow run '${input.id}' for project '${input.projectId}' must be running.`
329
+ )
330
+ }
331
+ assertSqliteWorkflowRunOwnership(existing, input.executionToken)
332
+
333
+ this.db
334
+ .query(
335
+ `
336
+ UPDATE workflow_runs
337
+ SET
338
+ status = ?,
339
+ finished_at = NULL,
340
+ error = NULL
341
+ , execution_token = NULL, execution_queue_lease_expires_at = NULL
342
+ WHERE project_id = ? AND id = ?
343
+ `
344
+ )
345
+ .run("waiting", input.projectId, input.id)
346
+
347
+ return this.requireWorkflowRun(input.projectId, input.id)
348
+ })()
349
+ }
350
+
351
+ async resume(input: ResumeWorkflowRunInput): Promise<WorkflowRunRecord> {
352
+ return this.db.transaction(() => {
353
+ const existing = this.db
354
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
355
+ .get(input.projectId, input.id) as WorkflowRunDatabaseRow | null
356
+
357
+ if (!existing) {
358
+ throw new WorkflowRunError(
359
+ `[SixbSqlite] Workflow run '${input.id}' not found for project '${input.projectId}'.`
360
+ )
361
+ }
362
+
363
+ if (existing.status !== "waiting") {
364
+ throw new WorkflowRunError(
365
+ `[SixbSqlite] Workflow run '${input.id}' for project '${input.projectId}' must be waiting.`
366
+ )
367
+ }
368
+
369
+ this.db
370
+ .query(
371
+ `
372
+ UPDATE workflow_runs
373
+ SET
374
+ status = ?,
375
+ finished_at = NULL,
376
+ error = NULL
377
+ , attempt = attempt + 1, execution_token = ?, execution_queue_lease_expires_at = ?
378
+ WHERE project_id = ? AND id = ?
379
+ `
380
+ )
381
+ .run(
382
+ "running",
383
+ input.execution?.token ?? null,
384
+ input.execution?.queueLeaseExpiresAt.toISOString() ?? null,
385
+ input.projectId,
386
+ input.id
387
+ )
388
+
389
+ return this.requireWorkflowRun(input.projectId, input.id)
390
+ })()
391
+ }
392
+
393
+ async getById(params: { projectId: string; id: string }): Promise<WorkflowRunRecord | null> {
394
+ const row = this.db
395
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
396
+ .get(params.projectId, params.id) as WorkflowRunDatabaseRow | null
397
+
398
+ return row ? rowToWorkflowRunRecord(row) : null
399
+ }
400
+
401
+ async list(input: ListWorkflowRunsInput): Promise<ListWorkflowRunsResult> {
402
+ if (hasEmptyStatuses(input) || input.workflowIds?.length === 0) {
403
+ return {
404
+ runs: [],
405
+ hasMore: false,
406
+ total: 0,
407
+ }
408
+ }
409
+
410
+ const whereClauses = ["project_id = ?"]
411
+ const args: SqliteValue[] = [input.projectId]
412
+
413
+ if (input.workflowId) {
414
+ whereClauses.push("workflow_id = ?")
415
+ args.push(input.workflowId)
416
+ }
417
+
418
+ if (input.workflowIds) {
419
+ whereClauses.push(`workflow_id IN (${input.workflowIds.map(() => "?").join(", ")})`)
420
+ args.push(...input.workflowIds)
421
+ }
422
+
423
+ appendRunListFilters(whereClauses, args, input)
424
+
425
+ const { rows, total, hasMore } = queryRunList<WorkflowRunDatabaseRow>({
426
+ db: this.db,
427
+ tableName: "workflow_runs",
428
+ whereClauses,
429
+ args,
430
+ order: input.order,
431
+ limit: input.limit,
432
+ offset: input.offset,
433
+ })
434
+
435
+ return {
436
+ runs: rows.map(rowToWorkflowRunRecord),
437
+ hasMore,
438
+ total,
439
+ }
440
+ }
441
+
442
+ async listLatestByWorkflowIds(
443
+ input: ListLatestWorkflowRunsInput
444
+ ): Promise<ListLatestWorkflowRunsResult> {
445
+ const rows = queryLatestRunsByOwnerId<WorkflowRunDatabaseRow>({
446
+ db: this.db,
447
+ tableName: "workflow_runs",
448
+ ownerColumn: "workflow_id",
449
+ ownerIds: input.workflowIds,
450
+ projectId: input.projectId,
451
+ ownerIdFor: (row) => row.workflow_id,
452
+ })
453
+
454
+ return {
455
+ runs: rows.map(rowToWorkflowRunRecord),
456
+ }
457
+ }
458
+
459
+ close(): void {
460
+ closeSqliteStoreConnection(this.connection)
461
+ }
462
+
463
+ private requireWorkflowRun(projectId: string, id: string): WorkflowRunRecord {
464
+ const record = this.db
465
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
466
+ .get(projectId, id) as WorkflowRunDatabaseRow | null
467
+
468
+ if (!record) {
469
+ throw new WorkflowRunError(
470
+ `[SixbSqlite] Failed to load workflow run '${id}' for project '${projectId}'.`
471
+ )
472
+ }
473
+
474
+ return rowToWorkflowRunRecord(record)
475
+ }
476
+
477
+ private requireStatus(
478
+ projectId: string,
479
+ id: string,
480
+ status: WorkflowRunRecord["status"]
481
+ ): WorkflowRunDatabaseRow {
482
+ const row = this.db
483
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
484
+ .get(projectId, id) as WorkflowRunDatabaseRow | null
485
+ if (!row) throw new WorkflowRunError(`[SixbSqlite] Workflow run '${id}' not found.`)
486
+ if (row.status !== status) {
487
+ throw new WorkflowRunError(
488
+ `[SixbSqlite] Workflow run '${id}' must be ${status} (status '${row.status}').`
489
+ )
490
+ }
491
+ return row
492
+ }
493
+ }
494
+
495
+ export class SqliteWorkflowAgentNodeRunStorage implements WorkflowAgentNodeRunStorage {
496
+ constructor(private readonly db: Database) {}
497
+
498
+ async create(input: CreateWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
499
+ try {
500
+ this.db
501
+ .query(`
502
+ INSERT INTO workflow_agent_node_runs (
503
+ project_id, node_run_id, agent_id, status, prompt, attempt, created_at
504
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
505
+ `)
506
+ .run(
507
+ input.projectId,
508
+ input.nodeRunId,
509
+ input.agentId,
510
+ "queued",
511
+ input.prompt,
512
+ 0,
513
+ (input.createdAt ?? new Date()).toISOString()
514
+ )
515
+ } catch (error) {
516
+ if (isUniqueConstraintError(error)) {
517
+ throw new WorkflowRunError(
518
+ `[SixbSqlite] Agent execution already exists for workflow node run '${input.nodeRunId}'.`
519
+ )
520
+ }
521
+ throw error
522
+ }
523
+ return this.require(input.projectId, input.nodeRunId)
524
+ }
525
+
526
+ async start(input: StartWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
527
+ return this.db.transaction(() => {
528
+ this.requireStatus(input.projectId, input.nodeRunId, "queued")
529
+ this.db
530
+ .query(`
531
+ UPDATE workflow_agent_node_runs SET
532
+ status = ?, execution_principal_type = ?, execution_principal_id = ?, model_id = ?,
533
+ attempt = 1, execution_token = ?, execution_queue_lease_expires_at = ?, started_at = ?
534
+ WHERE project_id = ? AND node_run_id = ?
535
+ `)
536
+ .run(
537
+ "running",
538
+ input.executionPrincipal?.type ?? null,
539
+ input.executionPrincipal?.id ?? null,
540
+ input.modelId ?? null,
541
+ input.execution.token,
542
+ input.execution.queueLeaseExpiresAt.toISOString(),
543
+ (input.startedAt ?? new Date()).toISOString(),
544
+ input.projectId,
545
+ input.nodeRunId
546
+ )
547
+ return this.require(input.projectId, input.nodeRunId)
548
+ })()
549
+ }
550
+
551
+ async reclaim(input: ReclaimWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
552
+ return this.db.transaction(() => {
553
+ const row = this.requireStatus(input.projectId, input.nodeRunId, "running")
554
+ this.db
555
+ .query(`
556
+ UPDATE workflow_agent_node_runs SET
557
+ attempt = ?, execution_token = ?, execution_queue_lease_expires_at = ?
558
+ WHERE project_id = ? AND node_run_id = ?
559
+ `)
560
+ .run(
561
+ Number(row.attempt) + 1,
562
+ input.execution.token,
563
+ input.execution.queueLeaseExpiresAt.toISOString(),
564
+ input.projectId,
565
+ input.nodeRunId
566
+ )
567
+ return this.require(input.projectId, input.nodeRunId)
568
+ })()
569
+ }
570
+
571
+ async confirmExecutionOwnership(
572
+ input: ConfirmWorkflowAgentNodeRunExecutionOwnershipInput
573
+ ): Promise<WorkflowAgentNodeRunRecord> {
574
+ return this.db.transaction(() => {
575
+ const row = this.requireStatus(input.projectId, input.nodeRunId, "running")
576
+ assertSqliteWorkflowAgentNodeOwnership(row, input.executionToken)
577
+ const current = row.execution_queue_lease_expires_at
578
+ ? new Date(row.execution_queue_lease_expires_at)
579
+ : input.queueLeaseExpiresAt
580
+ this.db
581
+ .query(`
582
+ UPDATE workflow_agent_node_runs SET execution_queue_lease_expires_at = ?
583
+ WHERE project_id = ? AND node_run_id = ?
584
+ `)
585
+ .run(
586
+ new Date(Math.max(current.getTime(), input.queueLeaseExpiresAt.getTime())).toISOString(),
587
+ input.projectId,
588
+ input.nodeRunId
589
+ )
590
+ return this.require(input.projectId, input.nodeRunId)
591
+ })()
592
+ }
593
+
594
+ async finish(input: FinishWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
595
+ return this.db.transaction(() => {
596
+ const row = this.requireStatus(input.projectId, input.nodeRunId, "running")
597
+ assertSqliteWorkflowAgentNodeOwnership(row, input.executionToken)
598
+ this.db
599
+ .query(`
600
+ UPDATE workflow_agent_node_runs SET
601
+ status = ?, model_id = COALESCE(?, model_id), finish_reason = ?, usage = ?, trace = ?,
602
+ diagnostics = ?, error = ?, execution_token = NULL,
603
+ execution_queue_lease_expires_at = NULL, completed_at = ?
604
+ WHERE project_id = ? AND node_run_id = ?
605
+ `)
606
+ .run(
607
+ input.status,
608
+ input.modelId ?? null,
609
+ input.finishReason ?? null,
610
+ input.usage ? JSON.stringify(input.usage) : null,
611
+ input.trace ? JSON.stringify(input.trace) : null,
612
+ input.diagnostics ? JSON.stringify(input.diagnostics) : null,
613
+ input.status === "succeeded" ? null : (input.error ?? null),
614
+ (input.completedAt ?? new Date()).toISOString(),
615
+ input.projectId,
616
+ input.nodeRunId
617
+ )
618
+ return this.require(input.projectId, input.nodeRunId)
619
+ })()
620
+ }
621
+
622
+ async cancel(input: CancelWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
623
+ return this.db.transaction(() => {
624
+ const row = this.require(input.projectId, input.nodeRunId)
625
+ if (row.status !== "queued" && row.status !== "running") {
626
+ throw new WorkflowRunError(
627
+ `[SixbSqlite] Agent workflow node run '${input.nodeRunId}' cannot be cancelled from status '${row.status}'.`
628
+ )
629
+ }
630
+ this.db
631
+ .query(
632
+ `UPDATE workflow_agent_node_runs SET status = ?, error = ?, execution_token = NULL,
633
+ execution_queue_lease_expires_at = NULL, completed_at = ?
634
+ WHERE project_id = ? AND node_run_id = ?`
635
+ )
636
+ .run(
637
+ "cancelled",
638
+ input.error ?? null,
639
+ (input.completedAt ?? new Date()).toISOString(),
640
+ input.projectId,
641
+ input.nodeRunId
642
+ )
643
+ return this.require(input.projectId, input.nodeRunId)
644
+ })()
645
+ }
646
+
647
+ async getByNodeRunId(params: {
648
+ projectId: string
649
+ nodeRunId: string
650
+ }): Promise<WorkflowAgentNodeRunRecord | null> {
651
+ const row = this.db
652
+ .query("SELECT * FROM workflow_agent_node_runs WHERE project_id = ? AND node_run_id = ?")
653
+ .get(params.projectId, params.nodeRunId) as WorkflowAgentNodeRunDatabaseRow | null
654
+ return row ? rowToWorkflowAgentNodeRunRecord(row) : null
655
+ }
656
+
657
+ async list(input: ListWorkflowAgentNodeRunsInput): Promise<ListWorkflowAgentNodeRunsResult> {
658
+ if (hasEmptyStatuses(input)) return { runs: [], total: 0, hasMore: false }
659
+ const whereClauses = ["project_id = ?"]
660
+ const args: SqliteValue[] = [input.projectId]
661
+ if (input.agentId) {
662
+ whereClauses.push("agent_id = ?")
663
+ args.push(input.agentId)
664
+ }
665
+ appendRunListFilters(whereClauses, args, input)
666
+ const result = queryRunList<WorkflowAgentNodeRunDatabaseRow>({
667
+ db: this.db,
668
+ tableName: "workflow_agent_node_runs",
669
+ whereClauses,
670
+ args,
671
+ order: input.order,
672
+ limit: input.limit,
673
+ offset: input.offset,
674
+ })
675
+ return { ...result, runs: result.rows.map(rowToWorkflowAgentNodeRunRecord) }
676
+ }
677
+
678
+ private require(projectId: string, nodeRunId: string): WorkflowAgentNodeRunRecord {
679
+ const row = this.db
680
+ .query("SELECT * FROM workflow_agent_node_runs WHERE project_id = ? AND node_run_id = ?")
681
+ .get(projectId, nodeRunId) as WorkflowAgentNodeRunDatabaseRow | null
682
+ if (!row) {
683
+ throw new WorkflowRunError(
684
+ `[SixbSqlite] Agent workflow node run '${nodeRunId}' not found for project '${projectId}'.`
685
+ )
686
+ }
687
+ return rowToWorkflowAgentNodeRunRecord(row)
688
+ }
689
+
690
+ private requireStatus(
691
+ projectId: string,
692
+ nodeRunId: string,
693
+ status: WorkflowAgentNodeRunRecord["status"]
694
+ ): WorkflowAgentNodeRunDatabaseRow {
695
+ const row = this.db
696
+ .query("SELECT * FROM workflow_agent_node_runs WHERE project_id = ? AND node_run_id = ?")
697
+ .get(projectId, nodeRunId) as WorkflowAgentNodeRunDatabaseRow | null
698
+ if (!row) {
699
+ throw new WorkflowRunError(
700
+ `[SixbSqlite] Agent workflow node run '${nodeRunId}' not found for project '${projectId}'.`
701
+ )
702
+ }
703
+ if (row.status !== status) {
704
+ throw new WorkflowRunError(
705
+ `[SixbSqlite] Agent workflow node run '${nodeRunId}' must be ${status} (status '${row.status}').`
706
+ )
707
+ }
708
+ return row
709
+ }
710
+ }
711
+
712
+ export class SqliteWorkflowNodeRunStorage implements WorkflowNodeRunStorage {
713
+ constructor(private readonly db: Database) {}
714
+
715
+ async start(input: StartWorkflowNodeRunInput): Promise<WorkflowNodeRunRecord> {
716
+ assertNonNegativeInteger(input.nodeIndex, "nodeIndex")
717
+
718
+ return this.db.transaction(() => {
719
+ const workflowRun = this.db
720
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
721
+ .get(input.projectId, input.workflowRunId) as WorkflowRunDatabaseRow | null
722
+
723
+ if (!workflowRun) {
724
+ throw new WorkflowRunError(
725
+ `[SixbSqlite] Workflow run '${input.workflowRunId}' not found for project '${input.projectId}'.`
726
+ )
727
+ }
728
+
729
+ if (workflowRun.status !== "running") {
730
+ throw new WorkflowRunError(
731
+ `[SixbSqlite] Workflow run '${input.workflowRunId}' for project '${input.projectId}' must be running.`
732
+ )
733
+ }
734
+ assertSqliteWorkflowRunOwnership(workflowRun, input.executionToken)
735
+
736
+ if (workflowRun.workflow_id !== input.workflowId) {
737
+ throw new WorkflowRunError(
738
+ `[SixbSqlite] Workflow node run '${input.id}' workflow '${input.workflowId}' does not match workflow run '${input.workflowRunId}' workflow '${workflowRun.workflow_id}'.`
739
+ )
740
+ }
741
+
742
+ const startedAt = input.startedAt ?? new Date()
743
+
744
+ try {
745
+ this.db
746
+ .query(
747
+ `
748
+ INSERT INTO workflow_node_runs (
749
+ project_id,
750
+ id,
751
+ workflow_run_id,
752
+ workflow_id,
753
+ node_index,
754
+ node_type,
755
+ node_id,
756
+ node_key,
757
+ status,
758
+ input,
759
+ started_at
760
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
761
+ `
762
+ )
763
+ .run(
764
+ input.projectId,
765
+ input.id,
766
+ input.workflowRunId,
767
+ input.workflowId,
768
+ input.nodeIndex,
769
+ input.nodeType,
770
+ input.nodeId,
771
+ input.nodeKey,
772
+ "running",
773
+ serializeRecord(input.input),
774
+ startedAt.toISOString()
775
+ )
776
+ } catch (error) {
777
+ if (isUniqueConstraintError(error)) {
778
+ throw new WorkflowRunError(
779
+ `[SixbSqlite] Workflow node run '${input.id}' already exists for project '${input.projectId}'.`
780
+ )
781
+ }
782
+
783
+ throw error
784
+ }
785
+
786
+ const row = this.db
787
+ .query("SELECT * FROM workflow_node_runs WHERE project_id = ? AND id = ?")
788
+ .get(input.projectId, input.id) as WorkflowNodeRunDatabaseRow | null
789
+
790
+ if (!row) {
791
+ throw new WorkflowRunError(
792
+ `[SixbSqlite] Failed to load workflow node run '${input.id}' for project '${input.projectId}'.`
793
+ )
794
+ }
795
+
796
+ return rowToWorkflowNodeRunRecord(row)
797
+ })()
798
+ }
799
+
800
+ async finish(input: FinishWorkflowNodeRunInput): Promise<WorkflowNodeRunRecord> {
801
+ return this.db.transaction(() => {
802
+ const existing = this.db
803
+ .query("SELECT * FROM workflow_node_runs WHERE project_id = ? AND id = ?")
804
+ .get(input.projectId, input.id) as WorkflowNodeRunDatabaseRow | null
805
+
806
+ if (!existing) {
807
+ throw new WorkflowRunError(
808
+ `[SixbSqlite] Workflow node run '${input.id}' not found for project '${input.projectId}'.`
809
+ )
810
+ }
811
+ this.assertNodeParentOwnership(existing, input.executionToken)
812
+
813
+ if (existing.status !== "running" && existing.status !== "waiting") {
814
+ throw new WorkflowRunError(
815
+ `[SixbSqlite] Workflow node run '${input.id}' for project '${input.projectId}' is already terminal.`
816
+ )
817
+ }
818
+
819
+ this.db
820
+ .query(
821
+ `
822
+ UPDATE workflow_node_runs
823
+ SET
824
+ status = ?,
825
+ finished_at = ?,
826
+ output = ?,
827
+ error = ?
828
+ WHERE project_id = ? AND id = ?
829
+ `
830
+ )
831
+ .run(
832
+ input.status,
833
+ (input.finishedAt ?? new Date()).toISOString(),
834
+ input.status === "succeeded" && input.output ? serializeRecord(input.output) : null,
835
+ input.status === "succeeded" ? null : (input.error ?? null),
836
+ input.projectId,
837
+ input.id
838
+ )
839
+
840
+ const updated = this.db
841
+ .query("SELECT * FROM workflow_node_runs WHERE project_id = ? AND id = ?")
842
+ .get(input.projectId, input.id) as WorkflowNodeRunDatabaseRow
843
+
844
+ return rowToWorkflowNodeRunRecord(updated)
845
+ })()
846
+ }
847
+
848
+ async wait(input: WaitWorkflowNodeRunInput): Promise<WorkflowNodeRunRecord> {
849
+ return this.db.transaction(() => {
850
+ const existing = this.db
851
+ .query("SELECT * FROM workflow_node_runs WHERE project_id = ? AND id = ?")
852
+ .get(input.projectId, input.id) as WorkflowNodeRunDatabaseRow | null
853
+
854
+ if (!existing) {
855
+ throw new WorkflowRunError(
856
+ `[SixbSqlite] Workflow node run '${input.id}' not found for project '${input.projectId}'.`
857
+ )
858
+ }
859
+ this.assertNodeParentOwnership(existing, input.executionToken)
860
+
861
+ if (existing.status !== "running") {
862
+ throw new WorkflowRunError(
863
+ `[SixbSqlite] Workflow node run '${input.id}' for project '${input.projectId}' must be running.`
864
+ )
865
+ }
866
+
867
+ this.db
868
+ .query(
869
+ `
870
+ UPDATE workflow_node_runs
871
+ SET
872
+ status = ?,
873
+ finished_at = NULL,
874
+ output = NULL,
875
+ error = NULL
876
+ WHERE project_id = ? AND id = ?
877
+ `
878
+ )
879
+ .run("waiting", input.projectId, input.id)
880
+
881
+ const updated = this.db
882
+ .query("SELECT * FROM workflow_node_runs WHERE project_id = ? AND id = ?")
883
+ .get(input.projectId, input.id) as WorkflowNodeRunDatabaseRow
884
+
885
+ return rowToWorkflowNodeRunRecord(updated)
886
+ })()
887
+ }
888
+
889
+ async getById(params: { projectId: string; id: string }): Promise<WorkflowNodeRunRecord | null> {
890
+ const row = this.db
891
+ .query("SELECT * FROM workflow_node_runs WHERE project_id = ? AND id = ?")
892
+ .get(params.projectId, params.id) as WorkflowNodeRunDatabaseRow | null
893
+
894
+ return row ? rowToWorkflowNodeRunRecord(row) : null
895
+ }
896
+
897
+ async list(input: ListWorkflowNodeRunsInput): Promise<ListWorkflowNodeRunsResult> {
898
+ if (hasEmptyStatuses(input)) {
899
+ return {
900
+ nodes: [],
901
+ hasMore: false,
902
+ total: 0,
903
+ }
904
+ }
905
+
906
+ const whereClauses = ["project_id = ?"]
907
+ const args: SqliteValue[] = [input.projectId]
908
+
909
+ if (input.workflowRunId) {
910
+ whereClauses.push("workflow_run_id = ?")
911
+ args.push(input.workflowRunId)
912
+ }
913
+
914
+ if (input.workflowId) {
915
+ whereClauses.push("workflow_id = ?")
916
+ args.push(input.workflowId)
917
+ }
918
+
919
+ if (input.nodeType) {
920
+ whereClauses.push("node_type = ?")
921
+ args.push(input.nodeType)
922
+ }
923
+
924
+ if (input.nodeId) {
925
+ whereClauses.push("node_id = ?")
926
+ args.push(input.nodeId)
927
+ }
928
+
929
+ if (input.nodeKey) {
930
+ whereClauses.push("node_key = ?")
931
+ args.push(input.nodeKey)
932
+ }
933
+
934
+ appendRunListFilters(whereClauses, args, input)
935
+
936
+ const { rows, total, hasMore } = queryRunList<WorkflowNodeRunDatabaseRow>({
937
+ db: this.db,
938
+ tableName: "workflow_node_runs",
939
+ whereClauses,
940
+ args,
941
+ order: input.order,
942
+ limit: input.limit,
943
+ offset: input.offset,
944
+ })
945
+
946
+ return {
947
+ nodes: rows.map(rowToWorkflowNodeRunRecord),
948
+ hasMore,
949
+ total,
950
+ }
951
+ }
952
+
953
+ private assertNodeParentOwnership(node: WorkflowNodeRunDatabaseRow, token?: string): void {
954
+ const run = this.db
955
+ .query("SELECT * FROM workflow_runs WHERE project_id = ? AND id = ?")
956
+ .get(node.project_id, node.workflow_run_id) as WorkflowRunDatabaseRow | null
957
+ if (!run) throw new WorkflowRunError("[SixbSqlite] Parent workflow run was not found.")
958
+ assertSqliteWorkflowRunOwnership(run, token)
959
+ }
960
+ }
961
+
962
+ function serializeRecord(value: WorkflowIOSnapshot): string {
963
+ return JSON.stringify(value)
964
+ }
965
+
966
+ function parseRecord(value: string): WorkflowIOSnapshot {
967
+ return JSON.parse(value) as WorkflowIOSnapshot
968
+ }
969
+
970
+ function rowToWorkflowRunRecord(row: WorkflowRunDatabaseRow): WorkflowRunRecord {
971
+ return {
972
+ id: row.id,
973
+ projectId: row.project_id,
974
+ workflowId: row.workflow_id,
975
+ status: row.status,
976
+ input: parseRecord(row.input),
977
+ queuedAt: row.queued_at ? new Date(row.queued_at) : undefined,
978
+ startedAt: new Date(row.started_at),
979
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
980
+ error: row.error ?? undefined,
981
+ source: row.source ? (JSON.parse(row.source) as WorkflowRunSource) : undefined,
982
+ requestedByPrincipal: {
983
+ type: row.requested_by_principal_type,
984
+ id: row.requested_by_principal_id,
985
+ },
986
+ attempt: row.attempt,
987
+ ...(row.execution_token && row.execution_queue_lease_expires_at
988
+ ? {
989
+ execution: {
990
+ token: row.execution_token,
991
+ queueLeaseExpiresAt: new Date(row.execution_queue_lease_expires_at),
992
+ },
993
+ }
994
+ : {}),
995
+ }
996
+ }
997
+
998
+ function rowToWorkflowNodeRunRecord(row: WorkflowNodeRunDatabaseRow): WorkflowNodeRunRecord {
999
+ return {
1000
+ id: row.id,
1001
+ projectId: row.project_id,
1002
+ workflowRunId: row.workflow_run_id,
1003
+ workflowId: row.workflow_id,
1004
+ nodeIndex: row.node_index,
1005
+ nodeType: row.node_type,
1006
+ nodeId: row.node_id,
1007
+ nodeKey: row.node_key,
1008
+ status: row.status,
1009
+ input: parseRecord(row.input),
1010
+ startedAt: new Date(row.started_at),
1011
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
1012
+ output: row.output ? parseRecord(row.output) : undefined,
1013
+ error: row.error ?? undefined,
1014
+ }
1015
+ }
1016
+
1017
+ function assertNonNegativeInteger(value: number, fieldName: string): void {
1018
+ if (!Number.isInteger(value) || value < 0) {
1019
+ throw new WorkflowRunError(
1020
+ `[SixbSqlite] Workflow run ${fieldName} must be a non-negative integer.`
1021
+ )
1022
+ }
1023
+ }
1024
+
1025
+ function canFinishWorkflowRun(
1026
+ current: WorkflowRunRecord["status"],
1027
+ next: FinishWorkflowRunInput["status"]
1028
+ ): boolean {
1029
+ return (
1030
+ current === "running" ||
1031
+ (current === "waiting" && next !== "succeeded") ||
1032
+ (current === "queued" && next !== "succeeded")
1033
+ )
1034
+ }
1035
+
1036
+ interface WorkflowRunDatabaseRow {
1037
+ project_id: string
1038
+ id: string
1039
+ workflow_id: string
1040
+ status: WorkflowRunRecord["status"]
1041
+ input: string
1042
+ queued_at: string | null
1043
+ started_at: string
1044
+ finished_at: string | null
1045
+ error: string | null
1046
+ source: string | null
1047
+ requested_by_principal_type: Principal["type"]
1048
+ requested_by_principal_id: string
1049
+ attempt: number
1050
+ execution_token: string | null
1051
+ execution_queue_lease_expires_at: string | null
1052
+ }
1053
+
1054
+ interface WorkflowNodeRunDatabaseRow {
1055
+ project_id: string
1056
+ id: string
1057
+ workflow_run_id: string
1058
+ workflow_id: string
1059
+ node_index: number
1060
+ node_type: WorkflowNodeRunRecord["nodeType"]
1061
+ node_id: string
1062
+ node_key: string
1063
+ status: WorkflowNodeRunRecord["status"]
1064
+ input: string
1065
+ started_at: string
1066
+ finished_at: string | null
1067
+ output: string | null
1068
+ error: string | null
1069
+ }
1070
+
1071
+ interface WorkflowAgentNodeRunDatabaseRow {
1072
+ project_id: string
1073
+ node_run_id: string
1074
+ agent_id: string
1075
+ status: WorkflowAgentNodeRunRecord["status"]
1076
+ prompt: string
1077
+ execution_principal_type: "serviceAccount" | null
1078
+ execution_principal_id: string | null
1079
+ model_id: string | null
1080
+ finish_reason: WorkflowAgentNodeRunRecord["finishReason"] | null
1081
+ usage: string | null
1082
+ trace: string | null
1083
+ diagnostics: string | null
1084
+ error: string | null
1085
+ attempt: number
1086
+ execution_token: string | null
1087
+ execution_queue_lease_expires_at: string | null
1088
+ created_at: string
1089
+ started_at: string | null
1090
+ completed_at: string | null
1091
+ }
1092
+
1093
+ function assertSqliteWorkflowAgentNodeOwnership(
1094
+ row: WorkflowAgentNodeRunDatabaseRow,
1095
+ token: string
1096
+ ): void {
1097
+ if (row.execution_token !== token) {
1098
+ throw new WorkflowRunError(
1099
+ `[SixbSqlite] Execution token is no longer current on agent workflow node run '${row.node_run_id}'.`
1100
+ )
1101
+ }
1102
+ }
1103
+
1104
+ function rowToWorkflowAgentNodeRunRecord(
1105
+ row: WorkflowAgentNodeRunDatabaseRow
1106
+ ): WorkflowAgentNodeRunRecord {
1107
+ return {
1108
+ projectId: row.project_id,
1109
+ nodeRunId: row.node_run_id,
1110
+ agentId: row.agent_id,
1111
+ status: row.status,
1112
+ prompt: row.prompt,
1113
+ ...(row.execution_principal_type && row.execution_principal_id
1114
+ ? {
1115
+ executionPrincipal: {
1116
+ type: row.execution_principal_type,
1117
+ id: row.execution_principal_id,
1118
+ },
1119
+ }
1120
+ : {}),
1121
+ ...(row.model_id ? { modelId: row.model_id } : {}),
1122
+ ...(row.finish_reason ? { finishReason: row.finish_reason } : {}),
1123
+ ...(row.usage ? { usage: JSON.parse(row.usage) } : {}),
1124
+ ...(row.trace ? { trace: JSON.parse(row.trace) } : {}),
1125
+ ...(row.diagnostics ? { diagnostics: JSON.parse(row.diagnostics) } : {}),
1126
+ ...(row.error ? { error: row.error } : {}),
1127
+ attempt: row.attempt,
1128
+ ...(row.execution_token && row.execution_queue_lease_expires_at
1129
+ ? {
1130
+ execution: {
1131
+ token: row.execution_token,
1132
+ queueLeaseExpiresAt: new Date(row.execution_queue_lease_expires_at),
1133
+ },
1134
+ }
1135
+ : {}),
1136
+ createdAt: new Date(row.created_at),
1137
+ ...(row.started_at ? { startedAt: new Date(row.started_at) } : {}),
1138
+ ...(row.completed_at ? { completedAt: new Date(row.completed_at) } : {}),
1139
+ }
1140
+ }
1141
+
1142
+ function assertSqliteWorkflowRunOwnership(row: WorkflowRunDatabaseRow, token?: string): void {
1143
+ if (row.execution_token !== (token ?? null)) {
1144
+ throw new WorkflowRunError(
1145
+ `[SixbSqlite] Execution token is no longer current on workflow run '${row.id}'.`
1146
+ )
1147
+ }
1148
+ }