@smithers-orchestrator/db 0.16.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 (130) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +43 -0
  3. package/src/JsonBounds.ts +6 -0
  4. package/src/SchemaRegistryEntry.ts +6 -0
  5. package/src/SqlMessageStorage.js +818 -0
  6. package/src/SqlMessageStorageEventHistoryQuery.ts +7 -0
  7. package/src/SqliteWriteRetryOptions.ts +7 -0
  8. package/src/adapter/AlertRow.ts +29 -0
  9. package/src/adapter/AlertSeverity.ts +2 -0
  10. package/src/adapter/AlertStatus.ts +2 -0
  11. package/src/adapter/ApprovalRow.ts +13 -0
  12. package/src/adapter/AttemptRow.ts +17 -0
  13. package/src/adapter/CacheRow.ts +12 -0
  14. package/src/adapter/DB_ALERT_ALLOWED_SEVERITIES.js +5 -0
  15. package/src/adapter/DB_ALERT_ALLOWED_STATUSES.js +6 -0
  16. package/src/adapter/DB_ALERT_ID_MAX_LENGTH.js +1 -0
  17. package/src/adapter/DB_ALERT_MESSAGE_MAX_LENGTH.js +1 -0
  18. package/src/adapter/DB_ALERT_POLICY_NAME_MAX_LENGTH.js +1 -0
  19. package/src/adapter/DB_RUN_ALLOWED_STATUSES.js +10 -0
  20. package/src/adapter/DB_RUN_ID_MAX_LENGTH.js +1 -0
  21. package/src/adapter/DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js +1 -0
  22. package/src/adapter/EventHistoryQuery.ts +7 -0
  23. package/src/adapter/HumanRequestRow.ts +19 -0
  24. package/src/adapter/NodeDiffCacheRow.ts +9 -0
  25. package/src/adapter/NodeRow.ts +10 -0
  26. package/src/adapter/PendingHumanRequestRow.ts +7 -0
  27. package/src/adapter/RunAncestryRow.ts +5 -0
  28. package/src/adapter/RunRow.ts +21 -0
  29. package/src/adapter/SignalQuery.ts +6 -0
  30. package/src/adapter/SignalRow.ts +9 -0
  31. package/src/adapter/SmithersDb.js +2236 -0
  32. package/src/adapter/StaleRunRecord.ts +7 -0
  33. package/src/adapter/index.js +27 -0
  34. package/src/adapter.js +2359 -0
  35. package/src/assertJsonPayloadWithinBounds.js +94 -0
  36. package/src/assertMaxBytes.js +23 -0
  37. package/src/assertMaxJsonDepth.js +40 -0
  38. package/src/assertMaxStringLength.js +16 -0
  39. package/src/assertOptionalArrayMaxLength.js +16 -0
  40. package/src/assertOptionalStringMaxLength.js +11 -0
  41. package/src/assertPositiveFiniteInteger.js +14 -0
  42. package/src/assertPositiveFiniteNumber.js +12 -0
  43. package/src/buildHumanRequestId.js +9 -0
  44. package/src/cache/nodeDiffCache.js +124 -0
  45. package/src/ensure.js +18 -0
  46. package/src/ensureSqlMessageStorage.js +11 -0
  47. package/src/ensureSqlMessageStorageEffect.js +12 -0
  48. package/src/frame-codec/FRAME_KEYFRAME_INTERVAL.js +1 -0
  49. package/src/frame-codec/FrameDelta.ts +6 -0
  50. package/src/frame-codec/FrameDeltaOp.ts +20 -0
  51. package/src/frame-codec/FrameEncoding.ts +1 -0
  52. package/src/frame-codec/JsonPath.ts +3 -0
  53. package/src/frame-codec/JsonPathSegment.ts +1 -0
  54. package/src/frame-codec/applyFrameDelta.js +143 -0
  55. package/src/frame-codec/applyFrameDeltaJson.js +10 -0
  56. package/src/frame-codec/encodeFrameDelta.js +247 -0
  57. package/src/frame-codec/index.js +15 -0
  58. package/src/frame-codec/normalizeFrameEncoding.js +13 -0
  59. package/src/frame-codec/parseFrameDelta.js +27 -0
  60. package/src/frame-codec/serializeFrameDelta.js +9 -0
  61. package/src/frame-codec.js +409 -0
  62. package/src/getSqlMessageStorage.js +11 -0
  63. package/src/index.d.ts +5203 -0
  64. package/src/index.js +20 -0
  65. package/src/input-bounds.js +12 -0
  66. package/src/input.js +17 -0
  67. package/src/internal-schema/index.js +19 -0
  68. package/src/internal-schema/smithersAlerts.js +27 -0
  69. package/src/internal-schema/smithersApprovals.js +18 -0
  70. package/src/internal-schema/smithersAttempts.js +20 -0
  71. package/src/internal-schema/smithersCache.js +13 -0
  72. package/src/internal-schema/smithersCron.js +11 -0
  73. package/src/internal-schema/smithersEvents.js +10 -0
  74. package/src/internal-schema/smithersFrames.js +14 -0
  75. package/src/internal-schema/smithersHumanRequests.js +17 -0
  76. package/src/internal-schema/smithersNodeDiffs.js +12 -0
  77. package/src/internal-schema/smithersNodes.js +13 -0
  78. package/src/internal-schema/smithersRalph.js +10 -0
  79. package/src/internal-schema/smithersRuns.js +22 -0
  80. package/src/internal-schema/smithersSandboxes.js +16 -0
  81. package/src/internal-schema/smithersSignals.js +12 -0
  82. package/src/internal-schema/smithersTimeTravelAudit.js +12 -0
  83. package/src/internal-schema/smithersToolCalls.js +19 -0
  84. package/src/internal-schema/smithersVectors.js +12 -0
  85. package/src/internal-schema.js +245 -0
  86. package/src/isRetryableSqliteWriteError.js +53 -0
  87. package/src/loadInputEffect.js +28 -0
  88. package/src/loadOutputsEffect.js +87 -0
  89. package/src/output/OutputKey.ts +1 -0
  90. package/src/output/buildKeyWhere.js +17 -0
  91. package/src/output/buildOutputRow.js +34 -0
  92. package/src/output/describeSchemaShape.js +70 -0
  93. package/src/output/getAgentOutputSchema.js +13 -0
  94. package/src/output/getKeyColumns.js +19 -0
  95. package/src/output/index.js +14 -0
  96. package/src/output/selectOutputRowEffect.js +30 -0
  97. package/src/output/stripAutoColumns.js +10 -0
  98. package/src/output/upsertOutputRowEffect.js +38 -0
  99. package/src/output/validateExistingOutput.js +17 -0
  100. package/src/output/validateOutput.js +17 -0
  101. package/src/output-schema-descriptor.js +163 -0
  102. package/src/output.js +240 -0
  103. package/src/react-output.js +10 -0
  104. package/src/runState/ComputeRunStateOptions.ts +4 -0
  105. package/src/runState/DeriveRunStateInput.ts +10 -0
  106. package/src/runState/RUN_STATE_HEARTBEAT_STALE_MS.js +1 -0
  107. package/src/runState/ReasonBlocked.ts +10 -0
  108. package/src/runState/ReasonUnhealthy.ts +6 -0
  109. package/src/runState/RunState.ts +12 -0
  110. package/src/runState/RunStateView.ts +11 -0
  111. package/src/runState/computeRunState.js +22 -0
  112. package/src/runState/computeRunStateFromRow.js +102 -0
  113. package/src/runState/deriveRunState.js +109 -0
  114. package/src/runState/parseEventMeta.js +18 -0
  115. package/src/runState/parseTimerMeta.js +16 -0
  116. package/src/runState-types.ts +23 -0
  117. package/src/runState.js +7 -0
  118. package/src/schema-signature.js +22 -0
  119. package/src/snapshot.js +125 -0
  120. package/src/sql-message-storage.js +839 -0
  121. package/src/storage/InMemoryStorage.js +484 -0
  122. package/src/storage/StorageService.js +7 -0
  123. package/src/storage/StorageServiceShape.ts +122 -0
  124. package/src/storage/StorageServiceTypes.ts +150 -0
  125. package/src/unwrapZodType.js +17 -0
  126. package/src/utils/camelToSnake.js +6 -0
  127. package/src/withSqliteWriteRetryEffect.js +110 -0
  128. package/src/write-retry.js +49 -0
  129. package/src/zodToCreateTableSQL.js +41 -0
  130. package/src/zodToTable.js +60 -0
@@ -0,0 +1,839 @@
1
+ import * as Reactivity from "@effect/experimental/Reactivity";
2
+ import * as SqlClient from "@effect/sql/SqlClient";
3
+ import { SqlError } from "@effect/sql/SqlError";
4
+ import * as Statement from "@effect/sql/Statement";
5
+ import { Database } from "bun:sqlite";
6
+ import { Context, Effect, Layer, ManagedRuntime, Scope } from "effect";
7
+ import { camelToSnake } from "./utils/camelToSnake.js";
8
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
9
+ /** @typedef {import("./SqlMessageStorageEventHistoryQuery.ts").SqlMessageStorageEventHistoryQuery} SqlMessageStorageEventHistoryQuery */
10
+ /**
11
+ * @typedef {string | number | bigint | boolean | Uint8Array | null | undefined} SqliteParam
12
+ */
13
+
14
+ const ATTR_DB_SYSTEM_NAME = "db.system.name";
15
+ const CREATE_TABLE_STATEMENTS = [
16
+ `CREATE TABLE IF NOT EXISTS _smithers_runs (
17
+ run_id TEXT PRIMARY KEY,
18
+ parent_run_id TEXT,
19
+ workflow_name TEXT NOT NULL,
20
+ workflow_path TEXT,
21
+ workflow_hash TEXT,
22
+ status TEXT NOT NULL,
23
+ created_at_ms INTEGER NOT NULL,
24
+ started_at_ms INTEGER,
25
+ finished_at_ms INTEGER,
26
+ heartbeat_at_ms INTEGER,
27
+ runtime_owner_id TEXT,
28
+ cancel_requested_at_ms INTEGER,
29
+ hijack_requested_at_ms INTEGER,
30
+ hijack_target TEXT,
31
+ vcs_type TEXT,
32
+ vcs_root TEXT,
33
+ vcs_revision TEXT,
34
+ error_json TEXT,
35
+ config_json TEXT
36
+ )`,
37
+ `CREATE INDEX IF NOT EXISTS _smithers_runs_status_heartbeat_idx
38
+ ON _smithers_runs (status, heartbeat_at_ms)`,
39
+ `CREATE TABLE IF NOT EXISTS _smithers_nodes (
40
+ run_id TEXT NOT NULL,
41
+ node_id TEXT NOT NULL,
42
+ iteration INTEGER NOT NULL DEFAULT 0,
43
+ state TEXT NOT NULL,
44
+ last_attempt INTEGER,
45
+ updated_at_ms INTEGER NOT NULL,
46
+ output_table TEXT NOT NULL,
47
+ label TEXT,
48
+ PRIMARY KEY (run_id, node_id, iteration)
49
+ )`,
50
+ `CREATE TABLE IF NOT EXISTS _smithers_attempts (
51
+ run_id TEXT NOT NULL,
52
+ node_id TEXT NOT NULL,
53
+ iteration INTEGER NOT NULL DEFAULT 0,
54
+ attempt INTEGER NOT NULL,
55
+ state TEXT NOT NULL,
56
+ started_at_ms INTEGER NOT NULL,
57
+ finished_at_ms INTEGER,
58
+ heartbeat_at_ms INTEGER,
59
+ heartbeat_data_json TEXT,
60
+ error_json TEXT,
61
+ jj_pointer TEXT,
62
+ response_text TEXT,
63
+ jj_cwd TEXT,
64
+ cached INTEGER DEFAULT 0,
65
+ meta_json TEXT,
66
+ PRIMARY KEY (run_id, node_id, iteration, attempt)
67
+ )`,
68
+ `CREATE TABLE IF NOT EXISTS _smithers_frames (
69
+ run_id TEXT NOT NULL,
70
+ frame_no INTEGER NOT NULL,
71
+ created_at_ms INTEGER NOT NULL,
72
+ xml_json TEXT NOT NULL,
73
+ xml_hash TEXT NOT NULL,
74
+ encoding TEXT NOT NULL DEFAULT 'full',
75
+ mounted_task_ids_json TEXT,
76
+ task_index_json TEXT,
77
+ note TEXT,
78
+ PRIMARY KEY (run_id, frame_no)
79
+ )`,
80
+ `CREATE TABLE IF NOT EXISTS _smithers_approvals (
81
+ run_id TEXT NOT NULL,
82
+ node_id TEXT NOT NULL,
83
+ iteration INTEGER NOT NULL DEFAULT 0,
84
+ status TEXT NOT NULL,
85
+ requested_at_ms INTEGER,
86
+ decided_at_ms INTEGER,
87
+ note TEXT,
88
+ decided_by TEXT,
89
+ request_json TEXT,
90
+ decision_json TEXT,
91
+ auto_approved INTEGER NOT NULL DEFAULT 0,
92
+ PRIMARY KEY (run_id, node_id, iteration)
93
+ )`,
94
+ `CREATE TABLE IF NOT EXISTS _smithers_human_requests (
95
+ request_id TEXT PRIMARY KEY,
96
+ run_id TEXT NOT NULL,
97
+ node_id TEXT NOT NULL,
98
+ iteration INTEGER NOT NULL DEFAULT 0,
99
+ kind TEXT NOT NULL,
100
+ status TEXT NOT NULL,
101
+ prompt TEXT NOT NULL,
102
+ schema_json TEXT,
103
+ options_json TEXT,
104
+ response_json TEXT,
105
+ requested_at_ms INTEGER NOT NULL,
106
+ answered_at_ms INTEGER,
107
+ answered_by TEXT,
108
+ timeout_at_ms INTEGER
109
+ )`,
110
+ `CREATE TABLE IF NOT EXISTS _smithers_alerts (
111
+ alert_id TEXT PRIMARY KEY,
112
+ run_id TEXT,
113
+ policy_name TEXT NOT NULL,
114
+ severity TEXT NOT NULL,
115
+ status TEXT NOT NULL,
116
+ fired_at_ms INTEGER NOT NULL,
117
+ resolved_at_ms INTEGER,
118
+ acknowledged_at_ms INTEGER,
119
+ message TEXT NOT NULL,
120
+ details_json TEXT,
121
+ fingerprint TEXT,
122
+ node_id TEXT,
123
+ iteration INTEGER,
124
+ owner TEXT,
125
+ runbook TEXT,
126
+ labels_json TEXT,
127
+ reaction_json TEXT,
128
+ source_event_type TEXT,
129
+ first_fired_at_ms INTEGER,
130
+ last_fired_at_ms INTEGER,
131
+ occurrence_count INTEGER DEFAULT 1,
132
+ silenced_until_ms INTEGER,
133
+ acknowledged_by TEXT,
134
+ resolved_by TEXT
135
+ )`,
136
+ `CREATE TABLE IF NOT EXISTS _smithers_signals (
137
+ run_id TEXT NOT NULL,
138
+ seq INTEGER NOT NULL,
139
+ signal_name TEXT NOT NULL,
140
+ correlation_id TEXT,
141
+ payload_json TEXT NOT NULL,
142
+ received_at_ms INTEGER NOT NULL,
143
+ received_by TEXT,
144
+ PRIMARY KEY (run_id, seq)
145
+ )`,
146
+ `CREATE INDEX IF NOT EXISTS _smithers_signals_lookup_idx
147
+ ON _smithers_signals (run_id, signal_name, correlation_id, received_at_ms)`,
148
+ `CREATE TABLE IF NOT EXISTS _smithers_cache (
149
+ cache_key TEXT PRIMARY KEY,
150
+ created_at_ms INTEGER NOT NULL,
151
+ workflow_name TEXT NOT NULL,
152
+ node_id TEXT NOT NULL,
153
+ output_table TEXT NOT NULL,
154
+ schema_sig TEXT NOT NULL,
155
+ agent_sig TEXT,
156
+ tools_sig TEXT,
157
+ jj_pointer TEXT,
158
+ payload_json TEXT NOT NULL
159
+ )`,
160
+ `CREATE TABLE IF NOT EXISTS _smithers_node_diffs (
161
+ run_id TEXT NOT NULL,
162
+ node_id TEXT NOT NULL,
163
+ iteration INTEGER NOT NULL,
164
+ base_ref TEXT NOT NULL,
165
+ diff_json TEXT NOT NULL,
166
+ computed_at_ms INTEGER NOT NULL,
167
+ size_bytes INTEGER NOT NULL,
168
+ PRIMARY KEY (run_id, node_id, iteration, base_ref)
169
+ )`,
170
+ `CREATE TABLE IF NOT EXISTS _smithers_time_travel_audit (
171
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
172
+ run_id TEXT NOT NULL,
173
+ from_frame_no INTEGER NOT NULL,
174
+ to_frame_no INTEGER NOT NULL,
175
+ caller TEXT NOT NULL,
176
+ timestamp_ms INTEGER NOT NULL,
177
+ result TEXT NOT NULL,
178
+ duration_ms INTEGER
179
+ )`,
180
+ `CREATE INDEX IF NOT EXISTS _smithers_time_travel_audit_lookup_idx
181
+ ON _smithers_time_travel_audit (run_id, caller, timestamp_ms)`,
182
+ `CREATE TABLE IF NOT EXISTS _smithers_sandboxes (
183
+ run_id TEXT NOT NULL,
184
+ sandbox_id TEXT NOT NULL,
185
+ runtime TEXT NOT NULL DEFAULT 'bubblewrap',
186
+ remote_run_id TEXT,
187
+ workspace_id TEXT,
188
+ container_id TEXT,
189
+ config_json TEXT NOT NULL,
190
+ status TEXT NOT NULL DEFAULT 'pending',
191
+ shipped_at_ms INTEGER,
192
+ completed_at_ms INTEGER,
193
+ bundle_path TEXT,
194
+ PRIMARY KEY (run_id, sandbox_id)
195
+ )`,
196
+ `CREATE TABLE IF NOT EXISTS _smithers_tool_calls (
197
+ run_id TEXT NOT NULL,
198
+ node_id TEXT NOT NULL,
199
+ iteration INTEGER NOT NULL DEFAULT 0,
200
+ attempt INTEGER NOT NULL,
201
+ seq INTEGER NOT NULL,
202
+ tool_name TEXT NOT NULL,
203
+ input_json TEXT,
204
+ output_json TEXT,
205
+ started_at_ms INTEGER NOT NULL,
206
+ finished_at_ms INTEGER,
207
+ status TEXT NOT NULL,
208
+ error_json TEXT,
209
+ PRIMARY KEY (run_id, node_id, iteration, attempt, seq)
210
+ )`,
211
+ `CREATE TABLE IF NOT EXISTS _smithers_events (
212
+ run_id TEXT NOT NULL,
213
+ seq INTEGER NOT NULL,
214
+ timestamp_ms INTEGER NOT NULL,
215
+ type TEXT NOT NULL,
216
+ payload_json TEXT NOT NULL,
217
+ PRIMARY KEY (run_id, seq)
218
+ )`,
219
+ `CREATE TABLE IF NOT EXISTS _smithers_ralph (
220
+ run_id TEXT NOT NULL,
221
+ ralph_id TEXT NOT NULL,
222
+ iteration INTEGER NOT NULL DEFAULT 0,
223
+ done INTEGER NOT NULL DEFAULT 0,
224
+ updated_at_ms INTEGER NOT NULL,
225
+ PRIMARY KEY (run_id, ralph_id)
226
+ )`,
227
+ `CREATE TABLE IF NOT EXISTS _smithers_cron (
228
+ cron_id TEXT PRIMARY KEY,
229
+ pattern TEXT NOT NULL,
230
+ workflow_path TEXT NOT NULL,
231
+ enabled INTEGER DEFAULT 1,
232
+ created_at_ms INTEGER NOT NULL,
233
+ last_run_at_ms INTEGER,
234
+ next_run_at_ms INTEGER,
235
+ error_json TEXT
236
+ )`,
237
+ `CREATE TABLE IF NOT EXISTS _smithers_snapshots (
238
+ run_id TEXT NOT NULL,
239
+ frame_no INTEGER NOT NULL,
240
+ nodes_json TEXT NOT NULL,
241
+ outputs_json TEXT NOT NULL,
242
+ ralph_json TEXT NOT NULL,
243
+ input_json TEXT NOT NULL,
244
+ vcs_pointer TEXT,
245
+ workflow_hash TEXT,
246
+ content_hash TEXT NOT NULL,
247
+ created_at_ms INTEGER NOT NULL,
248
+ PRIMARY KEY (run_id, frame_no)
249
+ )`,
250
+ `CREATE TABLE IF NOT EXISTS _smithers_branches (
251
+ run_id TEXT PRIMARY KEY,
252
+ parent_run_id TEXT NOT NULL,
253
+ parent_frame_no INTEGER NOT NULL,
254
+ branch_label TEXT,
255
+ fork_description TEXT,
256
+ created_at_ms INTEGER NOT NULL
257
+ )`,
258
+ `CREATE TABLE IF NOT EXISTS _smithers_vcs_tags (
259
+ run_id TEXT NOT NULL,
260
+ frame_no INTEGER NOT NULL,
261
+ vcs_type TEXT NOT NULL,
262
+ vcs_pointer TEXT NOT NULL,
263
+ vcs_root TEXT,
264
+ jj_operation_id TEXT,
265
+ created_at_ms INTEGER NOT NULL,
266
+ PRIMARY KEY (run_id, frame_no)
267
+ )`,
268
+ `CREATE TABLE IF NOT EXISTS _smithers_vectors (
269
+ id TEXT PRIMARY KEY,
270
+ namespace TEXT NOT NULL,
271
+ content TEXT NOT NULL,
272
+ embedding BLOB NOT NULL,
273
+ dimensions INTEGER NOT NULL,
274
+ metadata_json TEXT,
275
+ document_id TEXT,
276
+ chunk_index INTEGER,
277
+ created_at_ms INTEGER NOT NULL
278
+ )`,
279
+ `CREATE TABLE IF NOT EXISTS _smithers_scorers (
280
+ id TEXT PRIMARY KEY,
281
+ run_id TEXT NOT NULL,
282
+ node_id TEXT NOT NULL,
283
+ iteration INTEGER NOT NULL DEFAULT 0,
284
+ attempt INTEGER NOT NULL DEFAULT 0,
285
+ scorer_id TEXT NOT NULL,
286
+ scorer_name TEXT NOT NULL,
287
+ source TEXT NOT NULL,
288
+ score REAL NOT NULL,
289
+ reason TEXT,
290
+ meta_json TEXT,
291
+ input_json TEXT,
292
+ output_json TEXT,
293
+ latency_ms REAL,
294
+ scored_at_ms INTEGER NOT NULL,
295
+ duration_ms REAL
296
+ )`,
297
+ `CREATE TABLE IF NOT EXISTS _smithers_memory_facts (
298
+ namespace TEXT NOT NULL,
299
+ key TEXT NOT NULL,
300
+ value_json TEXT NOT NULL,
301
+ schema_sig TEXT,
302
+ created_at_ms INTEGER NOT NULL,
303
+ updated_at_ms INTEGER NOT NULL,
304
+ ttl_ms INTEGER,
305
+ PRIMARY KEY (namespace, key)
306
+ )`,
307
+ `CREATE TABLE IF NOT EXISTS _smithers_memory_threads (
308
+ thread_id TEXT PRIMARY KEY,
309
+ namespace TEXT NOT NULL,
310
+ title TEXT,
311
+ metadata_json TEXT,
312
+ created_at_ms INTEGER NOT NULL,
313
+ updated_at_ms INTEGER NOT NULL
314
+ )`,
315
+ `CREATE TABLE IF NOT EXISTS _smithers_memory_messages (
316
+ id TEXT PRIMARY KEY,
317
+ thread_id TEXT NOT NULL,
318
+ role TEXT NOT NULL,
319
+ content_json TEXT NOT NULL,
320
+ run_id TEXT,
321
+ node_id TEXT,
322
+ created_at_ms INTEGER NOT NULL
323
+ )`,
324
+ ];
325
+ const MIGRATION_STATEMENTS = [
326
+ `ALTER TABLE _smithers_attempts ADD COLUMN response_text TEXT`,
327
+ `ALTER TABLE _smithers_attempts ADD COLUMN jj_cwd TEXT`,
328
+ `ALTER TABLE _smithers_attempts ADD COLUMN heartbeat_at_ms INTEGER`,
329
+ `ALTER TABLE _smithers_attempts ADD COLUMN heartbeat_data_json TEXT`,
330
+ `ALTER TABLE _smithers_attempts ADD COLUMN cached INTEGER DEFAULT 0`,
331
+ `ALTER TABLE _smithers_attempts ADD COLUMN meta_json TEXT`,
332
+ `ALTER TABLE _smithers_runs ADD COLUMN workflow_hash TEXT`,
333
+ `ALTER TABLE _smithers_runs ADD COLUMN heartbeat_at_ms INTEGER`,
334
+ `ALTER TABLE _smithers_runs ADD COLUMN runtime_owner_id TEXT`,
335
+ `ALTER TABLE _smithers_runs ADD COLUMN cancel_requested_at_ms INTEGER`,
336
+ `ALTER TABLE _smithers_runs ADD COLUMN hijack_requested_at_ms INTEGER`,
337
+ `ALTER TABLE _smithers_runs ADD COLUMN hijack_target TEXT`,
338
+ `ALTER TABLE _smithers_runs ADD COLUMN vcs_type TEXT`,
339
+ `ALTER TABLE _smithers_runs ADD COLUMN vcs_root TEXT`,
340
+ `ALTER TABLE _smithers_runs ADD COLUMN vcs_revision TEXT`,
341
+ `ALTER TABLE _smithers_runs ADD COLUMN parent_run_id TEXT`,
342
+ `ALTER TABLE _smithers_runs ADD COLUMN error_json TEXT`,
343
+ `ALTER TABLE _smithers_runs ADD COLUMN config_json TEXT`,
344
+ `ALTER TABLE _smithers_approvals ADD COLUMN request_json TEXT`,
345
+ `ALTER TABLE _smithers_approvals ADD COLUMN decision_json TEXT`,
346
+ `ALTER TABLE _smithers_approvals ADD COLUMN auto_approved INTEGER NOT NULL DEFAULT 0`,
347
+ `CREATE INDEX IF NOT EXISTS _smithers_runs_parent_idx ON _smithers_runs (parent_run_id)`,
348
+ // Ticket 0001: Alert model extensions
349
+ `ALTER TABLE _smithers_alerts ADD COLUMN fingerprint TEXT`,
350
+ `ALTER TABLE _smithers_alerts ADD COLUMN node_id TEXT`,
351
+ `ALTER TABLE _smithers_alerts ADD COLUMN iteration INTEGER`,
352
+ `ALTER TABLE _smithers_alerts ADD COLUMN owner TEXT`,
353
+ `ALTER TABLE _smithers_alerts ADD COLUMN runbook TEXT`,
354
+ `ALTER TABLE _smithers_alerts ADD COLUMN labels_json TEXT`,
355
+ `ALTER TABLE _smithers_alerts ADD COLUMN reaction_json TEXT`,
356
+ `ALTER TABLE _smithers_alerts ADD COLUMN source_event_type TEXT`,
357
+ `ALTER TABLE _smithers_alerts ADD COLUMN first_fired_at_ms INTEGER`,
358
+ `ALTER TABLE _smithers_alerts ADD COLUMN last_fired_at_ms INTEGER`,
359
+ `ALTER TABLE _smithers_alerts ADD COLUMN occurrence_count INTEGER DEFAULT 1`,
360
+ `ALTER TABLE _smithers_alerts ADD COLUMN silenced_until_ms INTEGER`,
361
+ `ALTER TABLE _smithers_alerts ADD COLUMN acknowledged_by TEXT`,
362
+ `ALTER TABLE _smithers_alerts ADD COLUMN resolved_by TEXT`,
363
+ `CREATE INDEX IF NOT EXISTS _smithers_alerts_fingerprint_idx ON _smithers_alerts (fingerprint)`,
364
+ `CREATE INDEX IF NOT EXISTS _smithers_alerts_run_status_idx ON _smithers_alerts (run_id, status)`,
365
+ ];
366
+ /**
367
+ * @param {string} identifier
368
+ * @returns {string}
369
+ */
370
+ function quoteIdentifier(identifier) {
371
+ return `"${identifier.replace(/"/g, "\"\"")}"`;
372
+ }
373
+ /**
374
+ * @param {string} value
375
+ * @returns {string}
376
+ */
377
+ function snakeToCamel(value) {
378
+ return value.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
379
+ }
380
+ /**
381
+ * @param {SqliteParam} value
382
+ * @returns {Exclude<SqliteParam, undefined>}
383
+ */
384
+ function encodeParam(value) {
385
+ if (typeof value === "boolean") {
386
+ return value ? 1 : 0;
387
+ }
388
+ return value ?? null;
389
+ }
390
+ /**
391
+ * @template T
392
+ * @param {ReadonlyArray<T>} rows
393
+ * @returns {ReadonlyArray<T>}
394
+ */
395
+ function transformRowKeys(rows) {
396
+ return rows.map((row) => {
397
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
398
+ return row;
399
+ }
400
+ const next = {};
401
+ for (const [key, value] of Object.entries(row)) {
402
+ next[snakeToCamel(key)] = value;
403
+ }
404
+ return next;
405
+ });
406
+ }
407
+ /**
408
+ * @template T
409
+ * @param {T} row
410
+ * @param {readonly string[]} [booleanColumns]
411
+ * @returns {T}
412
+ */
413
+ function applyBooleanColumns(row, booleanColumns) {
414
+ if (!booleanColumns || booleanColumns.length === 0) {
415
+ return row;
416
+ }
417
+ const next = { ...row };
418
+ for (const column of booleanColumns) {
419
+ const current = next[column];
420
+ if (current !== null && current !== undefined) {
421
+ next[column] = Boolean(current);
422
+ }
423
+ }
424
+ return next;
425
+ }
426
+ /**
427
+ * @param {string} table
428
+ * @param {Record<string, unknown>} row
429
+ * @param {{ orIgnore?: boolean; conflictColumns?: readonly string[]; updateColumns?: readonly string[]; }} [options]
430
+ */
431
+ function buildInsertSql(table, row, options) {
432
+ const entries = Object.entries(row).filter(([, value]) => value !== undefined);
433
+ const columns = entries.map(([key]) => camelToSnake(key));
434
+ const params = entries.map(([, value]) => encodeParam(value));
435
+ const tableSql = quoteIdentifier(table);
436
+ const columnSql = columns.map(quoteIdentifier).join(", ");
437
+ const placeholderSql = columns.map(() => "?").join(", ");
438
+ let statement = `INSERT${options?.orIgnore ? " OR IGNORE" : ""} INTO ${tableSql} (${columnSql}) ` +
439
+ `VALUES (${placeholderSql})`;
440
+ if (options?.conflictColumns && options.conflictColumns.length > 0) {
441
+ const conflictSql = options.conflictColumns.map(camelToSnake).map(quoteIdentifier).join(", ");
442
+ const updateColumns = (options.updateColumns ?? Object.keys(row))
443
+ .map(camelToSnake)
444
+ .filter((column) => !options.conflictColumns.includes(snakeToCamel(column)));
445
+ if (updateColumns.length === 0) {
446
+ statement += ` ON CONFLICT (${conflictSql}) DO NOTHING`;
447
+ }
448
+ else {
449
+ const updateSql = updateColumns
450
+ .map((column) => `${quoteIdentifier(column)} = excluded.${quoteIdentifier(column)}`)
451
+ .join(", ");
452
+ statement += ` ON CONFLICT (${conflictSql}) DO UPDATE SET ${updateSql}`;
453
+ }
454
+ }
455
+ return { statement, params };
456
+ }
457
+ /**
458
+ * @param {string} table
459
+ * @param {Record<string, unknown>} patch
460
+ * @param {string} whereSql
461
+ * @param {ReadonlyArray<SqliteParam>} [params]
462
+ */
463
+ function buildUpdateSql(table, patch, whereSql, params = []) {
464
+ const entries = Object.entries(patch).filter(([, value]) => value !== undefined);
465
+ if (entries.length === 0) {
466
+ return null;
467
+ }
468
+ const setSql = entries
469
+ .map(([key]) => `${quoteIdentifier(camelToSnake(key))} = ?`)
470
+ .join(", ");
471
+ return {
472
+ statement: `UPDATE ${quoteIdentifier(table)} SET ${setSql} WHERE ${whereSql}`,
473
+ params: [
474
+ ...entries.map(([, value]) => encodeParam(value)),
475
+ ...params.map(encodeParam),
476
+ ],
477
+ };
478
+ }
479
+ /**
480
+ * @param {BunSQLiteDatabase<any> | Database} db
481
+ * @returns {Database}
482
+ */
483
+ function resolveSqliteDatabase(db) {
484
+ if (db instanceof Database) {
485
+ return db;
486
+ }
487
+ const candidate = db.session?.client ?? db.$client;
488
+ if (!candidate || typeof candidate.query !== "function" || typeof candidate.run !== "function") {
489
+ throw new TypeError("SqlMessageStorage requires a Bun SQLite client.");
490
+ }
491
+ return candidate;
492
+ }
493
+ /**
494
+ * @param {Database} sqlite
495
+ * @returns {Connection}
496
+ */
497
+ function createConnection(sqlite) {
498
+ /**
499
+ * @param {string} statement
500
+ * @param {ReadonlyArray<unknown>} params
501
+ * @param {(<A extends object>(rows: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined} [transformRows]
502
+ */
503
+ const execute = (statement, params, transformRows) => Effect.withFiberRuntime((fiber) => {
504
+ const useSafeIntegers = Context.get(fiber.currentContext, SqlClient.SafeIntegers);
505
+ try {
506
+ const query = sqlite.query(statement);
507
+ // @ts-ignore bun-types missing safeIntegers()
508
+ query.safeIntegers(useSafeIntegers);
509
+ const rows = (query.all(...params) ?? []);
510
+ return Effect.succeed(transformRows ? transformRows(rows) : rows);
511
+ }
512
+ catch (cause) {
513
+ return Effect.fail(new SqlError({ cause, message: "Failed to execute SQLite statement" }));
514
+ }
515
+ });
516
+ return {
517
+ execute: (statement, params, transformRows) => execute(statement, params, transformRows),
518
+ executeRaw: (statement, params) => execute(statement, params, undefined),
519
+ executeValues: (statement, params) => Effect.withFiberRuntime((fiber) => {
520
+ const useSafeIntegers = Context.get(fiber.currentContext, SqlClient.SafeIntegers);
521
+ try {
522
+ const query = sqlite.query(statement);
523
+ // @ts-ignore bun-types missing safeIntegers()
524
+ query.safeIntegers(useSafeIntegers);
525
+ return Effect.succeed((query.values(...params) ?? []));
526
+ }
527
+ catch (cause) {
528
+ return Effect.fail(new SqlError({ cause, message: "Failed to execute SQLite values statement" }));
529
+ }
530
+ }),
531
+ executeUnprepared: (statement, params, transformRows) => execute(statement, params, transformRows),
532
+ executeStream: () => Effect.dieMessage("executeStream not implemented"),
533
+ };
534
+ }
535
+ /**
536
+ * @param {Database} sqlite
537
+ * @returns {Effect.Effect<SqlClient.SqlClient, never>}
538
+ */
539
+ function makeSqlClientEffect(sqlite) {
540
+ const compiler = Statement.makeCompilerSqlite(camelToSnake);
541
+ const connection = createConnection(sqlite);
542
+ return Effect.gen(function* () {
543
+ const semaphore = yield* Effect.makeSemaphore(1);
544
+ const acquirer = semaphore.withPermits(1)(Effect.succeed(connection));
545
+ const transactionAcquirer = Effect.uninterruptibleMask((restore) => Effect.as(Effect.zipRight(restore(semaphore.take(1)), Effect.tap(Effect.scope, (scope) => Scope.addFinalizer(scope, semaphore.release(1)))), connection));
546
+ const reactivity = yield* Reactivity.make;
547
+ return yield* SqlClient.make({
548
+ acquirer,
549
+ compiler,
550
+ transactionAcquirer,
551
+ spanAttributes: [[ATTR_DB_SYSTEM_NAME, "sqlite"]],
552
+ transformRows: transformRowKeys,
553
+ }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity));
554
+ });
555
+ }
556
+ /**
557
+ * @param {Database} sqlite
558
+ */
559
+ function makeSqlClientLayer(sqlite) {
560
+ return Layer.scoped(SqlClient.SqlClient, makeSqlClientEffect(sqlite));
561
+ }
562
+ export class SqlMessageStorage {
563
+ sqlite;
564
+ // TODO(Phase 8): Keep this per-DB runtime until the unified runtime can
565
+ // inject a scoped SqlClient without rebuilding the per-connection semaphore.
566
+ runtime;
567
+ tableColumnsCache = new Map();
568
+ /**
569
+ * @param {BunSQLiteDatabase<any> | Database} db
570
+ */
571
+ constructor(db) {
572
+ this.sqlite = resolveSqliteDatabase(db);
573
+ this.runtime = ManagedRuntime.make(makeSqlClientLayer(this.sqlite));
574
+ }
575
+ /**
576
+ * @param {string} table
577
+ * @returns {Set<string>}
578
+ */
579
+ getTableColumns(table) {
580
+ const cached = this.tableColumnsCache.get(table);
581
+ if (cached) {
582
+ return cached;
583
+ }
584
+ const rows = this.sqlite
585
+ .query(`PRAGMA table_info(${quoteIdentifier(table)})`)
586
+ .all();
587
+ const columns = new Set(rows
588
+ .map((row) => (typeof row.name === "string" ? snakeToCamel(row.name) : ""))
589
+ .filter((value) => value.length > 0));
590
+ this.tableColumnsCache.set(table, columns);
591
+ return columns;
592
+ }
593
+ /**
594
+ * @param {string} table
595
+ * @param {Record<string, unknown>} row
596
+ * @returns {Record<string, unknown>}
597
+ */
598
+ filterKnownColumns(table, row) {
599
+ const knownColumns = this.getTableColumns(table);
600
+ return Object.fromEntries(Object.entries(row).filter(([key, value]) => value !== undefined && knownColumns.has(key)));
601
+ }
602
+ /**
603
+ * @template A, E
604
+ * @param {Effect.Effect<A, E, SqlClient.SqlClient>} effect
605
+ * @returns {Promise<A>}
606
+ */
607
+ runEffect(effect) {
608
+ return this.runtime.runPromise(effect);
609
+ }
610
+ /**
611
+ * @template A
612
+ * @param {(connection: Connection) => Effect.Effect<A, SqlError>} f
613
+ * @returns {Promise<A>}
614
+ */
615
+ withConnection(f) {
616
+ return this.runEffect(Effect.flatMap(SqlClient.SqlClient, (client) => Effect.scoped(Effect.flatMap(client.reserve, f))));
617
+ }
618
+ /**
619
+ * @returns {Effect.Effect<void, never>}
620
+ */
621
+ ensureSchemaEffect() {
622
+ const sqlite = this.sqlite;
623
+ return Effect.sync(() => {
624
+ for (const statement of CREATE_TABLE_STATEMENTS) {
625
+ sqlite.run(statement);
626
+ }
627
+ for (const statement of MIGRATION_STATEMENTS) {
628
+ try {
629
+ sqlite.run(statement);
630
+ }
631
+ catch {
632
+ // Ignore legacy migration failures for already-applied changes.
633
+ }
634
+ }
635
+ const frameColumns = sqlite
636
+ .query(`PRAGMA table_info("_smithers_frames")`)
637
+ .all();
638
+ if (!frameColumns.some((column) => column.name === "encoding")) {
639
+ try {
640
+ sqlite.run(`ALTER TABLE _smithers_frames ADD COLUMN encoding TEXT NOT NULL DEFAULT 'full'`);
641
+ }
642
+ catch {
643
+ // Ignore if another caller added it first.
644
+ }
645
+ }
646
+ });
647
+ }
648
+ /**
649
+ * @returns {Promise<void>}
650
+ */
651
+ ensureSchema() {
652
+ return this.runtime.runPromise(this.ensureSchemaEffect());
653
+ }
654
+ /**
655
+ * @template T
656
+ * @param {string} statement
657
+ * @param {ReadonlyArray<SqliteParam>} [params]
658
+ * @param {{ booleanColumns?: readonly string[] }} [options]
659
+ * @returns {Promise<Array<T>>}
660
+ */
661
+ queryAll(statement, params = [], options) {
662
+ return this.withConnection((connection) => connection
663
+ .execute(statement, params.map(encodeParam), transformRowKeys)
664
+ .pipe(Effect.map((rows) => rows.map((row) => applyBooleanColumns(row, options?.booleanColumns)))));
665
+ }
666
+ /**
667
+ * @template T
668
+ * @param {string} statement
669
+ * @param {ReadonlyArray<SqliteParam>} [params]
670
+ * @param {{ booleanColumns?: readonly string[] }} [options]
671
+ * @returns {Promise<T | undefined>}
672
+ */
673
+ async queryOne(statement, params = [], options) {
674
+ const rows = await this.queryAll(statement, params, options);
675
+ return rows[0];
676
+ }
677
+ /**
678
+ * @param {string} statement
679
+ * @param {ReadonlyArray<SqliteParam>} [params]
680
+ * @returns {Promise<void>}
681
+ */
682
+ execute(statement, params = []) {
683
+ return this.withConnection((connection) => connection.executeRaw(statement, params.map(encodeParam)).pipe(Effect.asVoid));
684
+ }
685
+ /**
686
+ * @param {string} table
687
+ * @param {Record<string, unknown>} row
688
+ * @returns {Promise<void>}
689
+ */
690
+ insertIgnore(table, row) {
691
+ const filteredRow = this.filterKnownColumns(table, row);
692
+ const { statement, params } = buildInsertSql(table, filteredRow, { orIgnore: true });
693
+ return this.execute(statement, params);
694
+ }
695
+ /**
696
+ * @param {string} table
697
+ * @param {Record<string, unknown>} row
698
+ * @param {readonly string[]} conflictColumns
699
+ * @param {readonly string[]} [updateColumns]
700
+ * @returns {Promise<void>}
701
+ */
702
+ upsert(table, row, conflictColumns, updateColumns) {
703
+ const filteredRow = this.filterKnownColumns(table, row);
704
+ const { statement, params } = buildInsertSql(table, filteredRow, {
705
+ conflictColumns,
706
+ updateColumns,
707
+ });
708
+ return this.execute(statement, params);
709
+ }
710
+ /**
711
+ * @param {string} table
712
+ * @param {Record<string, unknown>} patch
713
+ * @param {string} whereSql
714
+ * @param {ReadonlyArray<SqliteParam>} [params]
715
+ * @returns {Promise<void>}
716
+ */
717
+ updateWhere(table, patch, whereSql, params = []) {
718
+ const built = buildUpdateSql(table, this.filterKnownColumns(table, patch), whereSql, params);
719
+ if (!built) {
720
+ return Promise.resolve();
721
+ }
722
+ return this.execute(built.statement, built.params);
723
+ }
724
+ /**
725
+ * @param {string} table
726
+ * @param {string} whereSql
727
+ * @param {ReadonlyArray<SqliteParam>} [params]
728
+ * @returns {Promise<void>}
729
+ */
730
+ deleteWhere(table, whereSql, params = []) {
731
+ return this.execute(`DELETE FROM ${quoteIdentifier(table)} WHERE ${whereSql}`, params);
732
+ }
733
+ /**
734
+ * @param {string} runId
735
+ * @param {SqlMessageStorageEventHistoryQuery} [query]
736
+ * @returns {{ whereSql: string; params: Array<SqliteParam> }}
737
+ */
738
+ buildEventHistoryWhere(runId, query = {}) {
739
+ const clauses = ["run_id = ?", "seq > ?"];
740
+ const params = [runId, query.afterSeq ?? -1];
741
+ if (typeof query.sinceTimestampMs === "number") {
742
+ clauses.push("timestamp_ms >= ?");
743
+ params.push(query.sinceTimestampMs);
744
+ }
745
+ if (query.types && query.types.length > 0) {
746
+ clauses.push(`type IN (${query.types.map(() => "?").join(", ")})`);
747
+ params.push(...query.types);
748
+ }
749
+ if (query.nodeId) {
750
+ clauses.push("json_extract(payload_json, '$.nodeId') = ?");
751
+ params.push(query.nodeId);
752
+ }
753
+ return {
754
+ whereSql: clauses.join(" AND "),
755
+ params,
756
+ };
757
+ }
758
+ /**
759
+ * @param {string} runId
760
+ * @param {SqlMessageStorageEventHistoryQuery} [query]
761
+ * @returns {Promise<Array<Record<string, unknown>>>}
762
+ */
763
+ listEventHistory(runId, query = {}) {
764
+ const limit = Math.max(1, Math.floor(query.limit ?? 200));
765
+ const { whereSql, params } = this.buildEventHistoryWhere(runId, query);
766
+ return this.queryAll(`SELECT * FROM _smithers_events
767
+ WHERE ${whereSql}
768
+ ORDER BY seq ASC
769
+ LIMIT ?`, [...params, limit]);
770
+ }
771
+ /**
772
+ * @param {string} runId
773
+ * @param {SqlMessageStorageEventHistoryQuery} [query]
774
+ * @returns {Promise<number>}
775
+ */
776
+ async countEventHistory(runId, query = {}) {
777
+ const { whereSql, params } = this.buildEventHistoryWhere(runId, query);
778
+ const row = await this.queryOne(`SELECT COUNT(*) AS count
779
+ FROM _smithers_events
780
+ WHERE ${whereSql}`, params);
781
+ return Number(row?.count ?? 0);
782
+ }
783
+ /**
784
+ * @param {string} runId
785
+ * @returns {Promise<number | undefined>}
786
+ */
787
+ async getLastEventSeq(runId) {
788
+ const row = await this.queryOne(`SELECT seq
789
+ FROM _smithers_events
790
+ WHERE run_id = ?
791
+ ORDER BY seq DESC
792
+ LIMIT 1`, [runId]);
793
+ return row?.seq;
794
+ }
795
+ /**
796
+ * @param {string} runId
797
+ * @param {string} type
798
+ * @returns {Promise<Array<Record<string, unknown>>>}
799
+ */
800
+ listEventsByType(runId, type) {
801
+ return this.queryAll(`SELECT *
802
+ FROM _smithers_events
803
+ WHERE run_id = ? AND type = ?
804
+ ORDER BY seq ASC`, [runId, type]);
805
+ }
806
+ /**
807
+ * @param {string} runId
808
+ * @returns {Promise<number | undefined>}
809
+ */
810
+ async getLastSignalSeq(runId) {
811
+ const row = await this.queryOne(`SELECT seq
812
+ FROM _smithers_signals
813
+ WHERE run_id = ?
814
+ ORDER BY seq DESC
815
+ LIMIT 1`, [runId]);
816
+ return row?.seq;
817
+ }
818
+ }
819
+ /**
820
+ * @param {BunSQLiteDatabase<any> | Database} db
821
+ * @returns {SqlMessageStorage}
822
+ */
823
+ export function getSqlMessageStorage(db) {
824
+ return new SqlMessageStorage(db);
825
+ }
826
+ /**
827
+ * @param {BunSQLiteDatabase<any> | Database} db
828
+ * @returns {Effect.Effect<void, never>}
829
+ */
830
+ export function ensureSqlMessageStorageEffect(db) {
831
+ return getSqlMessageStorage(db).ensureSchemaEffect();
832
+ }
833
+ /**
834
+ * @param {BunSQLiteDatabase<any> | Database} db
835
+ * @returns {Promise<void>}
836
+ */
837
+ export function ensureSqlMessageStorage(db) {
838
+ return getSqlMessageStorage(db).ensureSchema();
839
+ }