@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,2236 @@
1
+ import { getTableName, sql } from "drizzle-orm";
2
+ import { Effect, Exit, FiberId, Metric } from "effect";
3
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
4
+ import { getSqlMessageStorage } from "../sql-message-storage.js";
5
+ import { alertsAcknowledgedTotal, alertsFiredTotal, dbQueryDuration, dbTransactionDuration, dbTransactionRetries, dbTransactionRollbacks, } from "@smithers-orchestrator/observability/metrics";
6
+ import { assertOptionalStringMaxLength, assertPositiveFiniteNumber, } from "../input-bounds.js";
7
+ import { FRAME_KEYFRAME_INTERVAL, applyFrameDeltaJson, encodeFrameDelta, normalizeFrameEncoding, serializeFrameDelta, } from "../frame-codec.js";
8
+ import { getKeyColumns } from "../output.js";
9
+ import { withSqliteWriteRetryEffect } from "../write-retry.js";
10
+ import { camelToSnake } from "../utils/camelToSnake.js";
11
+ import { DB_ALERT_ID_MAX_LENGTH } from "./DB_ALERT_ID_MAX_LENGTH.js";
12
+ import { DB_ALERT_POLICY_NAME_MAX_LENGTH } from "./DB_ALERT_POLICY_NAME_MAX_LENGTH.js";
13
+ import { DB_ALERT_MESSAGE_MAX_LENGTH } from "./DB_ALERT_MESSAGE_MAX_LENGTH.js";
14
+ import { DB_ALERT_ALLOWED_SEVERITIES } from "./DB_ALERT_ALLOWED_SEVERITIES.js";
15
+ import { DB_ALERT_ALLOWED_STATUSES } from "./DB_ALERT_ALLOWED_STATUSES.js";
16
+ import { DB_RUN_ID_MAX_LENGTH } from "./DB_RUN_ID_MAX_LENGTH.js";
17
+ import { DB_RUN_WORKFLOW_NAME_MAX_LENGTH } from "./DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js";
18
+ import { DB_RUN_ALLOWED_STATUSES } from "./DB_RUN_ALLOWED_STATUSES.js";
19
+ import { alertsActive } from "@smithers-orchestrator/observability/metrics";
20
+ /** @typedef {import("./AlertRow.ts").AlertRow} AlertRow */
21
+ /** @typedef {import("./AlertStatus.ts").AlertStatus} AlertStatus */
22
+ /** @typedef {import("./AttemptRow.ts").AttemptRow} AttemptRow */
23
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
24
+ /** @typedef {import("./EventHistoryQuery.ts").EventHistoryQuery} EventHistoryQuery */
25
+ /** @typedef {import("./HumanRequestRow.ts").HumanRequestRow} HumanRequestRow */
26
+ /** @typedef {import("../output/OutputKey.ts").OutputKey} OutputKey */
27
+ /**
28
+ * @template A, E
29
+ * @typedef {Effect.Effect<A, E> & PromiseLike<A>} RunnableEffect
30
+ */
31
+ /** @typedef {import("./SignalQuery.ts").SignalQuery} SignalQuery */
32
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
33
+ /**
34
+ * @typedef {{ runId: string; frameNo: number; createdAtMs: number; xmlJson: string; xmlHash: string; encoding: string; mountedTaskIdsJson: string | null; taskIndexJson: string | null; note: string | null; }} FrameRow
35
+ */
36
+ /**
37
+ * @typedef {{ runId: string; nodeId: string; iteration: number; baseRef: string; diffJson: string; computedAtMs: number; sizeBytes: number; }} NodeDiffCacheRow
38
+ */
39
+ /**
40
+ * @typedef {{ count: number }} CountRow
41
+ */
42
+ /**
43
+ * @typedef {{ ralphId: string; runId: string; done?: boolean }} RalphRow
44
+ */
45
+ /**
46
+ * @typedef {{ cacheKey: string; createdAtMs?: number; nodeId: string; outputTable: string }} CacheRowLike
47
+ */
48
+
49
+ /**
50
+ * @typedef {{ runId: string; frameNo: number; createdAtMs: number; xmlJson: string; xmlHash: string; encoding: string; mountedTaskIdsJson: string | null; taskIndexJson: string | null; note: string | null; }} FrameRow
51
+ */
52
+ /**
53
+ * @typedef {{ runId: string; nodeId: string; iteration: number; baseRef: string; diffJson: string; computedAtMs: number; sizeBytes: number; }} NodeDiffCacheRow
54
+ */
55
+ /**
56
+ * @typedef {{ count: number }} CountRow
57
+ */
58
+ /**
59
+ * @typedef {{ ralphId: string; runId: string; done?: boolean }} RalphRow
60
+ */
61
+ /**
62
+ * @typedef {{ cacheKey: string; createdAtMs?: number; nodeId: string; outputTable: string }} CacheRowLike
63
+ */
64
+
65
+
66
+ const FRAME_XML_CACHE_MAX = 512;
67
+ const RUN_HEARTBEAT_STALE_MS = 30_000;
68
+ const RAW_QUERY_ALLOWED_PREFIX = /^(?:select|with|explain|values)\b/i;
69
+ const RAW_QUERY_FORBIDDEN_KEYWORDS = /\b(?:drop|delete|insert|update|alter|create|attach|detach|pragma)\b/i;
70
+ const ACTIVE_ALERT_STATUSES = new Set([
71
+ "firing",
72
+ "acknowledged",
73
+ "silenced",
74
+ ]);
75
+ /**
76
+ * @param {string} queryString
77
+ * @returns {string}
78
+ */
79
+ function stripSqlCommentsAndLiterals(queryString) {
80
+ let sanitized = "";
81
+ let index = 0;
82
+ while (index < queryString.length) {
83
+ const char = queryString[index];
84
+ const nextChar = queryString[index + 1];
85
+ if (char === "-" && nextChar === "-") {
86
+ sanitized += " ";
87
+ index += 2;
88
+ while (index < queryString.length && queryString[index] !== "\n") {
89
+ index += 1;
90
+ }
91
+ continue;
92
+ }
93
+ if (char === "/" && nextChar === "*") {
94
+ sanitized += " ";
95
+ index += 2;
96
+ while (index < queryString.length) {
97
+ if (queryString[index] === "*" && queryString[index + 1] === "/") {
98
+ index += 2;
99
+ break;
100
+ }
101
+ index += 1;
102
+ }
103
+ continue;
104
+ }
105
+ if (char === "'" || char === "\"" || char === "`") {
106
+ const quote = char;
107
+ sanitized += " ";
108
+ index += 1;
109
+ while (index < queryString.length) {
110
+ if (queryString[index] === quote) {
111
+ if (queryString[index + 1] === quote) {
112
+ index += 2;
113
+ continue;
114
+ }
115
+ index += 1;
116
+ break;
117
+ }
118
+ index += 1;
119
+ }
120
+ continue;
121
+ }
122
+ if (char === "[") {
123
+ sanitized += " ";
124
+ index += 1;
125
+ while (index < queryString.length) {
126
+ if (queryString[index] === "]") {
127
+ index += 1;
128
+ break;
129
+ }
130
+ index += 1;
131
+ }
132
+ continue;
133
+ }
134
+ sanitized += char;
135
+ index += 1;
136
+ }
137
+ return sanitized;
138
+ }
139
+ /**
140
+ * @param {string} queryString
141
+ * @returns {string}
142
+ */
143
+ function validateReadOnlyRawQuery(queryString) {
144
+ const trimmedQuery = queryString.trim();
145
+ if (!trimmedQuery) {
146
+ throw toSmithersError(new Error("Raw query must not be empty"), undefined, {
147
+ code: "INVALID_INPUT",
148
+ details: { operation: "raw query validation" },
149
+ });
150
+ }
151
+ const sanitizedQuery = stripSqlCommentsAndLiterals(trimmedQuery).trim();
152
+ if (!sanitizedQuery) {
153
+ throw toSmithersError(new Error("Raw query must not be empty"), undefined, {
154
+ code: "INVALID_INPUT",
155
+ details: { operation: "raw query validation" },
156
+ });
157
+ }
158
+ const singleStatementQuery = sanitizedQuery.replace(/;+\s*$/, "").trim();
159
+ if (singleStatementQuery.includes(";")) {
160
+ throw toSmithersError(new Error("Raw query must contain a single read-only SQL statement"), undefined, {
161
+ code: "INVALID_INPUT",
162
+ details: { operation: "raw query validation" },
163
+ });
164
+ }
165
+ const forbiddenKeyword = singleStatementQuery.match(RAW_QUERY_FORBIDDEN_KEYWORDS)?.[0];
166
+ if (forbiddenKeyword) {
167
+ throw toSmithersError(new Error(`Raw query cannot use ${forbiddenKeyword.toUpperCase()} statements`), undefined, {
168
+ code: "INVALID_INPUT",
169
+ details: {
170
+ operation: "raw query validation",
171
+ keyword: forbiddenKeyword.toUpperCase(),
172
+ },
173
+ });
174
+ }
175
+ if (!RAW_QUERY_ALLOWED_PREFIX.test(singleStatementQuery)) {
176
+ throw toSmithersError(new Error("Raw query only supports read-only SELECT, WITH, EXPLAIN, or VALUES statements"), undefined, {
177
+ code: "INVALID_INPUT",
178
+ details: { operation: "raw query validation" },
179
+ });
180
+ }
181
+ return trimmedQuery;
182
+ }
183
+ /**
184
+ * @param {unknown} status
185
+ */
186
+ function validateRunStatus(status) {
187
+ if (typeof status !== "string" ||
188
+ !DB_RUN_ALLOWED_STATUSES.includes(status)) {
189
+ throw toSmithersError(new Error("Invalid run status"), `Run status must be one of: ${DB_RUN_ALLOWED_STATUSES.join(", ")}`, {
190
+ code: "INVALID_INPUT",
191
+ details: { status },
192
+ });
193
+ }
194
+ }
195
+ /**
196
+ * @param {unknown} severity
197
+ */
198
+ function validateAlertSeverity(severity) {
199
+ if (typeof severity !== "string" ||
200
+ !DB_ALERT_ALLOWED_SEVERITIES.includes(severity)) {
201
+ throw toSmithersError(new Error("Invalid alert severity"), `Alert severity must be one of: ${DB_ALERT_ALLOWED_SEVERITIES.join(", ")}`, {
202
+ code: "INVALID_INPUT",
203
+ details: { severity },
204
+ });
205
+ }
206
+ }
207
+ /**
208
+ * @param {unknown} status
209
+ */
210
+ function validateAlertStatus(status) {
211
+ if (typeof status !== "string" ||
212
+ !DB_ALERT_ALLOWED_STATUSES.includes(status)) {
213
+ throw toSmithersError(new Error("Invalid alert status"), `Alert status must be one of: ${DB_ALERT_ALLOWED_STATUSES.join(", ")}`, {
214
+ code: "INVALID_INPUT",
215
+ details: { status },
216
+ });
217
+ }
218
+ }
219
+ /**
220
+ * @param {Record<string, unknown>} row
221
+ * @param {string} field
222
+ */
223
+ function validateOptionalPositiveTimestamp(row, field) {
224
+ const value = row[field];
225
+ if (value === undefined || value === null)
226
+ return;
227
+ assertPositiveFiniteNumber(field, Number(value));
228
+ }
229
+ /**
230
+ * @param {unknown} row
231
+ * @returns {void}
232
+ */
233
+ function validateRunRow(row) {
234
+ if (!row || typeof row !== "object") {
235
+ throw toSmithersError(new Error("Invalid run row"), "Run row must be an object", {
236
+ code: "INVALID_INPUT",
237
+ });
238
+ }
239
+ const r = /** @type {Record<string, unknown>} */ (row);
240
+ assertOptionalStringMaxLength("runId", r.runId, DB_RUN_ID_MAX_LENGTH);
241
+ assertOptionalStringMaxLength("parentRunId", r.parentRunId, DB_RUN_ID_MAX_LENGTH);
242
+ assertOptionalStringMaxLength("workflowName", r.workflowName, DB_RUN_WORKFLOW_NAME_MAX_LENGTH);
243
+ validateRunStatus(r.status);
244
+ validateOptionalPositiveTimestamp(r, "createdAtMs");
245
+ validateOptionalPositiveTimestamp(r, "startedAtMs");
246
+ validateOptionalPositiveTimestamp(r, "finishedAtMs");
247
+ validateOptionalPositiveTimestamp(r, "heartbeatAtMs");
248
+ validateOptionalPositiveTimestamp(r, "cancelRequestedAtMs");
249
+ validateOptionalPositiveTimestamp(r, "hijackRequestedAtMs");
250
+ }
251
+ /**
252
+ * @param {unknown} patch
253
+ * @returns {void}
254
+ */
255
+ function validateRunPatch(patch) {
256
+ if (!patch || typeof patch !== "object")
257
+ return;
258
+ const p = /** @type {Record<string, unknown>} */ (patch);
259
+ if ("workflowName" in p) {
260
+ assertOptionalStringMaxLength("workflowName", p.workflowName, DB_RUN_WORKFLOW_NAME_MAX_LENGTH);
261
+ }
262
+ if ("status" in p) {
263
+ validateRunStatus(p.status);
264
+ }
265
+ validateOptionalPositiveTimestamp(p, "startedAtMs");
266
+ validateOptionalPositiveTimestamp(p, "finishedAtMs");
267
+ validateOptionalPositiveTimestamp(p, "heartbeatAtMs");
268
+ validateOptionalPositiveTimestamp(p, "cancelRequestedAtMs");
269
+ validateOptionalPositiveTimestamp(p, "hijackRequestedAtMs");
270
+ }
271
+ /**
272
+ * @param {AlertRow} row
273
+ */
274
+ function validateAlertRow(row) {
275
+ if (!row || typeof row !== "object") {
276
+ throw toSmithersError(new Error("Invalid alert row"), "Alert row must be an object", { code: "INVALID_INPUT" });
277
+ }
278
+ assertOptionalStringMaxLength("alertId", row.alertId, DB_ALERT_ID_MAX_LENGTH);
279
+ assertOptionalStringMaxLength("runId", row.runId, DB_RUN_ID_MAX_LENGTH);
280
+ assertOptionalStringMaxLength("policyName", row.policyName, DB_ALERT_POLICY_NAME_MAX_LENGTH);
281
+ assertOptionalStringMaxLength("message", row.message, DB_ALERT_MESSAGE_MAX_LENGTH);
282
+ if (typeof row.alertId !== "string" || row.alertId.length === 0) {
283
+ throw toSmithersError(new Error("Invalid alert ID"), "Alert ID must be a non-empty string", { code: "INVALID_INPUT", details: { alertId: row.alertId } });
284
+ }
285
+ if (row.runId !== null && row.runId !== undefined && typeof row.runId !== "string") {
286
+ throw toSmithersError(new Error("Invalid alert run ID"), "Alert run ID must be a string or null", { code: "INVALID_INPUT", details: { runId: row.runId } });
287
+ }
288
+ if (typeof row.policyName !== "string" || row.policyName.length === 0) {
289
+ throw toSmithersError(new Error("Invalid alert policy name"), "Alert policy name must be a non-empty string", { code: "INVALID_INPUT", details: { policyName: row.policyName } });
290
+ }
291
+ if (typeof row.message !== "string" || row.message.length === 0) {
292
+ throw toSmithersError(new Error("Invalid alert message"), "Alert message must be a non-empty string", { code: "INVALID_INPUT", details: { message: row.message } });
293
+ }
294
+ if (row.detailsJson !== null &&
295
+ row.detailsJson !== undefined &&
296
+ typeof row.detailsJson !== "string") {
297
+ throw toSmithersError(new Error("Invalid alert details JSON"), "Alert details JSON must be a string or null", { code: "INVALID_INPUT", details: { detailsJson: row.detailsJson } });
298
+ }
299
+ validateAlertSeverity(row.severity);
300
+ validateAlertStatus(row.status);
301
+ validateOptionalPositiveTimestamp(row, "firedAtMs");
302
+ validateOptionalPositiveTimestamp(row, "resolvedAtMs");
303
+ validateOptionalPositiveTimestamp(row, "acknowledgedAtMs");
304
+ }
305
+ /**
306
+ * @param {string | null | undefined} status
307
+ * @returns {status is AlertStatus}
308
+ */
309
+ function isAlertActiveStatus(status) {
310
+ return status !== undefined && status !== null && ACTIVE_ALERT_STATUSES.has(status);
311
+ }
312
+ /**
313
+ * Returns the row unchanged. Heartbeat-based classification now lives in
314
+ * `deriveRunState`, which correctly returns "stale" / "orphaned" rather than
315
+ * misusing "continued" (which means the run forked into a new run, and is
316
+ * treated as a success by `deriveRunState`).
317
+ *
318
+ * @template T
319
+ * @param {T} row
320
+ * @returns {T}
321
+ */
322
+ function classifyRunRowStatus(row) {
323
+ return row;
324
+ }
325
+ /**
326
+ * @template A, E
327
+ * @param {Effect.Effect<A, E>} effect
328
+ * @returns {RunnableEffect<A, E>}
329
+ */
330
+ function runnableEffect(effect) {
331
+ const runnable = effect;
332
+ if (typeof runnable.then !== "function") {
333
+ Object.defineProperty(runnable, "then", {
334
+ configurable: true,
335
+ value: (onfulfilled, onrejected) => Effect.runPromise(effect).then(onfulfilled, onrejected),
336
+ });
337
+ }
338
+ return runnable;
339
+ }
340
+ export class SmithersDb {
341
+ /** @type {BunSQLiteDatabase<Record<string, unknown>>} */
342
+ db;
343
+ /** @type {ReturnType<typeof getSqlMessageStorage>} */
344
+ internalStorage;
345
+ /** @type {Map<string, string>} */
346
+ reconstructedFrameXmlCache = new Map();
347
+ transactionDepth = 0;
348
+ /** @type {string | null} */
349
+ transactionOwnerThread = null;
350
+ /** @type {Promise<unknown>} */
351
+ transactionTail = Promise.resolve();
352
+ /**
353
+ * @param {BunSQLiteDatabase<Record<string, unknown>>} db
354
+ */
355
+ constructor(db) {
356
+ this.db = db;
357
+ this.internalStorage = getSqlMessageStorage(db);
358
+ }
359
+ /**
360
+ * @param {string} runId
361
+ * @param {number} frameNo
362
+ * @returns {string}
363
+ */
364
+ frameCacheKey(runId, frameNo) {
365
+ return `${runId}:${frameNo}`;
366
+ }
367
+ /**
368
+ * @param {string} runId
369
+ * @param {number} frameNo
370
+ * @returns {string | undefined}
371
+ */
372
+ getCachedFrameXml(runId, frameNo) {
373
+ const key = this.frameCacheKey(runId, frameNo);
374
+ const value = this.reconstructedFrameXmlCache.get(key);
375
+ if (value === undefined)
376
+ return undefined;
377
+ // Keep recently-used entries hot.
378
+ this.reconstructedFrameXmlCache.delete(key);
379
+ this.reconstructedFrameXmlCache.set(key, value);
380
+ return value;
381
+ }
382
+ /**
383
+ * @param {string} runId
384
+ * @param {number} frameNo
385
+ * @param {string} xmlJson
386
+ */
387
+ rememberFrameXml(runId, frameNo, xmlJson) {
388
+ const key = this.frameCacheKey(runId, frameNo);
389
+ if (this.reconstructedFrameXmlCache.has(key)) {
390
+ this.reconstructedFrameXmlCache.delete(key);
391
+ }
392
+ else if (this.reconstructedFrameXmlCache.size >= FRAME_XML_CACHE_MAX) {
393
+ const oldest = this.reconstructedFrameXmlCache.keys().next().value;
394
+ if (oldest !== undefined) {
395
+ this.reconstructedFrameXmlCache.delete(oldest);
396
+ }
397
+ }
398
+ this.reconstructedFrameXmlCache.set(key, xmlJson);
399
+ }
400
+ /**
401
+ * @param {string} runId
402
+ */
403
+ clearFrameCacheForRun(runId) {
404
+ for (const key of this.reconstructedFrameXmlCache.keys()) {
405
+ if (key.startsWith(`${runId}:`)) {
406
+ this.reconstructedFrameXmlCache.delete(key);
407
+ }
408
+ }
409
+ }
410
+ /**
411
+ * @param {string} queryString
412
+ * @returns {RunnableEffect<unknown[], SmithersError>}
413
+ */
414
+ rawQuery(queryString) {
415
+ const self = this;
416
+ return runnableEffect(Effect.gen(function* () {
417
+ const validatedQuery = yield* Effect.try({
418
+ try: () => validateReadOnlyRawQuery(queryString),
419
+ catch: (cause) => toSmithersError(cause, "validate raw query", {
420
+ code: "INVALID_INPUT",
421
+ details: { operation: "raw query validation" },
422
+ }),
423
+ });
424
+ return yield* self.read(`raw query ${validatedQuery.slice(0, 20)}`, () => {
425
+ const client = self.db.session.client;
426
+ const stmt = client.query(validatedQuery);
427
+ return Promise.resolve(stmt.all());
428
+ });
429
+ }));
430
+ }
431
+ /**
432
+ * @param {string} currentFiberThread
433
+ * @returns {boolean}
434
+ */
435
+ ownsActiveTransaction(currentFiberThread) {
436
+ return (this.transactionDepth > 0 &&
437
+ this.transactionOwnerThread === currentFiberThread);
438
+ }
439
+ /**
440
+ * @template A
441
+ * @param {string} label
442
+ * @param {() => PromiseLike<A>} operation
443
+ * @returns {RunnableEffect<A, SmithersError>}
444
+ */
445
+ read(label, operation) {
446
+ const self = this;
447
+ return runnableEffect(Effect.gen(function* () {
448
+ const start = performance.now();
449
+ const readOperation = Effect.tryPromise({
450
+ try: () => operation(),
451
+ catch: (cause) => toSmithersError(cause, label, {
452
+ code: "DB_QUERY_FAILED",
453
+ details: { operation: label },
454
+ }),
455
+ });
456
+ const currentFiberId = yield* Effect.fiberId;
457
+ const currentFiberThread = FiberId.threadName(currentFiberId);
458
+ let result;
459
+ if (self.ownsActiveTransaction(currentFiberThread)) {
460
+ result = yield* readOperation;
461
+ }
462
+ else {
463
+ const releaseTurn = yield* self.acquireTransactionTurn();
464
+ result = yield* readOperation.pipe(Effect.ensuring(Effect.sync(() => {
465
+ releaseTurn();
466
+ })));
467
+ }
468
+ yield* Metric.update(dbQueryDuration, performance.now() - start);
469
+ return result;
470
+ }).pipe(Effect.annotateLogs({ dbOperation: label }), Effect.withLogSpan(`db:${label}`)));
471
+ }
472
+ /**
473
+ * @template A
474
+ * @param {string} label
475
+ * @param {() => PromiseLike<A>} operation
476
+ * @returns {RunnableEffect<A, SmithersError>}
477
+ */
478
+ write(label, operation) {
479
+ const self = this;
480
+ return runnableEffect(Effect.gen(function* () {
481
+ const start = performance.now();
482
+ const writeOperation = Effect.tryPromise({
483
+ try: () => operation(),
484
+ catch: (cause) => toSmithersError(cause, label, {
485
+ code: "DB_WRITE_FAILED",
486
+ details: { operation: label },
487
+ }),
488
+ });
489
+ const currentFiberId = yield* Effect.fiberId;
490
+ const currentFiberThread = FiberId.threadName(currentFiberId);
491
+ let result;
492
+ if (self.ownsActiveTransaction(currentFiberThread)) {
493
+ result = yield* writeOperation;
494
+ }
495
+ else {
496
+ const releaseTurn = yield* self.acquireTransactionTurn();
497
+ result = yield* withSqliteWriteRetryEffect(() => writeOperation, { label }).pipe(Effect.ensuring(Effect.sync(() => {
498
+ releaseTurn();
499
+ })));
500
+ }
501
+ yield* Metric.update(dbQueryDuration, performance.now() - start);
502
+ return result;
503
+ }).pipe(Effect.annotateLogs({ dbOperation: label }), Effect.withLogSpan(`db:${label}`)));
504
+ }
505
+ /**
506
+ * @returns {Effect.Effect<{ run: (sql: string) => unknown; query: (sql: string) => { run: (...args: unknown[]) => unknown; get: (...args: unknown[]) => Record<string, unknown> | null | undefined; all: () => Array<Record<string, unknown>> }; exec: (sql: string) => unknown; $client?: unknown }, SmithersError, never>}
507
+ */
508
+ getSqliteTransactionClient() {
509
+ return Effect.try({
510
+ try: () => {
511
+ const candidate = this.db.session?.client ?? this.db.$client;
512
+ if (!candidate || typeof candidate.run !== "function") {
513
+ throw new Error("SmithersDb.withTransaction requires Bun SQLite client transaction primitives.");
514
+ }
515
+ return candidate;
516
+ },
517
+ catch: (cause) => toSmithersError(cause, "resolve sqlite transaction client", {
518
+ code: "DB_WRITE_FAILED",
519
+ details: { operation: "resolve sqlite transaction client" },
520
+ }),
521
+ });
522
+ }
523
+ /**
524
+ * @returns {Effect.Effect<() => void, SmithersError, never>}
525
+ */
526
+ acquireTransactionTurn() {
527
+ return Effect.tryPromise({
528
+ try: async () => {
529
+ let release;
530
+ const gate = new Promise((resolve) => {
531
+ release = resolve;
532
+ });
533
+ const previous = this.transactionTail.catch(() => undefined);
534
+ this.transactionTail = previous.then(() => gate);
535
+ await previous;
536
+ return release;
537
+ },
538
+ catch: (cause) => toSmithersError(cause, "acquire sqlite transaction turn", {
539
+ code: "DB_WRITE_FAILED",
540
+ details: { operation: "acquire sqlite transaction turn" },
541
+ }),
542
+ });
543
+ }
544
+ /**
545
+ * @template A
546
+ * @param {string} writeGroup
547
+ * @param {Effect.Effect<A, SmithersError>} operation
548
+ * @returns {RunnableEffect<A, SmithersError>}
549
+ */
550
+ withTransactionEffect(writeGroup, operation) {
551
+ const self = this;
552
+ const label = `sqlite transaction ${writeGroup}`;
553
+ return runnableEffect(withSqliteWriteRetryEffect(() => Effect.gen(function* () {
554
+ const currentFiberId = yield* Effect.fiberId;
555
+ const currentFiberThread = FiberId.threadName(currentFiberId);
556
+ if (self.ownsActiveTransaction(currentFiberThread)) {
557
+ return yield* Effect.fail(toSmithersError(new Error(`Nested sqlite transactions are not supported (writeGroup: ${writeGroup}).`), label, {
558
+ code: "DB_WRITE_FAILED",
559
+ details: { writeGroup, nestedTransaction: true },
560
+ }));
561
+ }
562
+ const releaseTurn = yield* self.acquireTransactionTurn();
563
+ const start = performance.now();
564
+ return yield* Effect.gen(function* () {
565
+ const client = yield* self.getSqliteTransactionClient();
566
+ /**
567
+ * @param {"operation" | "commit"} phase
568
+ * @param {unknown} error
569
+ */
570
+ const rollback = (phase, error) => Effect.gen(function* () {
571
+ yield* Metric.increment(dbTransactionRollbacks);
572
+ yield* Effect.logWarning("transaction rollback").pipe(Effect.annotateLogs({
573
+ writeGroup,
574
+ phase,
575
+ error: String(error),
576
+ }));
577
+ yield* Effect.sync(() => {
578
+ try {
579
+ client.run("ROLLBACK");
580
+ }
581
+ catch {
582
+ // ignore rollback failures
583
+ }
584
+ });
585
+ });
586
+ yield* Effect.try({
587
+ try: () => {
588
+ client.run("BEGIN IMMEDIATE");
589
+ self.transactionDepth += 1;
590
+ self.transactionOwnerThread = currentFiberThread;
591
+ },
592
+ catch: (cause) => toSmithersError(cause, "begin sqlite transaction", {
593
+ code: "DB_WRITE_FAILED",
594
+ details: { writeGroup, phase: "begin" },
595
+ }),
596
+ });
597
+ const operationExit = yield* Effect.exit(operation);
598
+ if (Exit.isFailure(operationExit)) {
599
+ yield* rollback("operation", operationExit.cause);
600
+ return yield* Effect.failCause(operationExit.cause);
601
+ }
602
+ const commitExit = yield* Effect.exit(Effect.try({
603
+ try: () => {
604
+ client.run("COMMIT");
605
+ },
606
+ catch: (cause) => toSmithersError(cause, "commit sqlite transaction", {
607
+ code: "DB_WRITE_FAILED",
608
+ details: { writeGroup, phase: "commit" },
609
+ }),
610
+ }));
611
+ if (Exit.isFailure(commitExit)) {
612
+ yield* rollback("commit", commitExit.cause);
613
+ return yield* Effect.failCause(commitExit.cause);
614
+ }
615
+ return operationExit.value;
616
+ }).pipe(Effect.ensuring(Effect.gen(function* () {
617
+ self.transactionDepth = Math.max(0, self.transactionDepth - 1);
618
+ if (self.transactionDepth === 0) {
619
+ self.transactionOwnerThread = null;
620
+ }
621
+ yield* Metric.update(dbTransactionDuration, performance.now() - start);
622
+ }))).pipe(Effect.ensuring(Effect.sync(() => {
623
+ releaseTurn();
624
+ })));
625
+ }), { label }).pipe(Effect.annotateLogs({ writeGroup }), Effect.withLogSpan("db:transaction")));
626
+ }
627
+ /**
628
+ * @template A
629
+ * @param {string} writeGroup
630
+ * @param {Effect.Effect<A, SmithersError>} operation
631
+ * @returns {Promise<A>}
632
+ */
633
+ withTransaction(writeGroup, operation) {
634
+ return Effect.runPromise(this.withTransactionEffect(writeGroup, operation));
635
+ }
636
+ /**
637
+ * @param {Record<string, unknown>} row
638
+ * @returns {RunnableEffect<void, SmithersError>}
639
+ */
640
+ insertRun(row) {
641
+ validateRunRow(row);
642
+ return this.write("insert run", () => this.internalStorage.insertIgnore("_smithers_runs", row));
643
+ }
644
+ /**
645
+ * @param {string} runId
646
+ * @param {Record<string, unknown>} patch
647
+ * @returns {RunnableEffect<void, SmithersError>}
648
+ */
649
+ updateRun(runId, patch) {
650
+ validateRunPatch(patch);
651
+ return this.write(`update run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", patch, "run_id = ?", [runId]));
652
+ }
653
+ /**
654
+ * @param {string} runId
655
+ * @param {Record<string, unknown>} patch
656
+ * @returns {RunnableEffect<void, SmithersError>}
657
+ */
658
+ updateRunEffect(runId, patch) {
659
+ return this.updateRun(runId, patch);
660
+ }
661
+ /**
662
+ * @param {string} runId
663
+ * @param {string} runtimeOwnerId
664
+ * @param {number} heartbeatAtMs
665
+ * @returns {RunnableEffect<void, SmithersError>}
666
+ */
667
+ heartbeatRun(runId, runtimeOwnerId, heartbeatAtMs) {
668
+ return this.write(`heartbeat run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", { heartbeatAtMs }, "run_id = ? AND runtime_owner_id = ?", [runId, runtimeOwnerId]));
669
+ }
670
+ /**
671
+ * @param {string} runId
672
+ * @param {number} cancelRequestedAtMs
673
+ * @returns {RunnableEffect<void, SmithersError>}
674
+ */
675
+ requestRunCancel(runId, cancelRequestedAtMs) {
676
+ return this.write(`cancel run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", { cancelRequestedAtMs }, "run_id = ?", [runId]));
677
+ }
678
+ /**
679
+ * @param {string} runId
680
+ * @param {number} hijackRequestedAtMs
681
+ * @param {string | null} [hijackTarget]
682
+ * @returns {RunnableEffect<void, SmithersError>}
683
+ */
684
+ requestRunHijack(runId, hijackRequestedAtMs, hijackTarget) {
685
+ return this.write(`hijack run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", {
686
+ hijackRequestedAtMs,
687
+ hijackTarget: hijackTarget ?? null,
688
+ }, "run_id = ?", [runId]));
689
+ }
690
+ /**
691
+ * @param {string} runId
692
+ * @returns {RunnableEffect<void, SmithersError>}
693
+ */
694
+ clearRunHijack(runId) {
695
+ return this.write(`clear hijack run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", {
696
+ hijackRequestedAtMs: null,
697
+ hijackTarget: null,
698
+ }, "run_id = ?", [runId]));
699
+ }
700
+ /**
701
+ * @param {string} runId
702
+ * @returns {RunnableEffect<RunRow | undefined, SmithersError>}
703
+ */
704
+ getRun(runId) {
705
+ return this.read(`get run ${runId}`, async () => {
706
+ const row = await this.internalStorage.queryOne(`SELECT *
707
+ FROM _smithers_runs
708
+ WHERE run_id = ?
709
+ LIMIT 1`, [runId]);
710
+ return row ? classifyRunRowStatus(row) : undefined;
711
+ });
712
+ }
713
+ /**
714
+ * @param {string} runId
715
+ * @returns {RunnableEffect<RunAncestryRow[], SmithersError>}
716
+ */
717
+ listRunAncestry(runId, limit = 1000) {
718
+ return this.read(`list run ancestry ${runId}`, () => this.internalStorage.queryAll(`WITH RECURSIVE ancestry(run_id, parent_run_id, depth) AS (
719
+ SELECT run_id, parent_run_id, 0
720
+ FROM _smithers_runs
721
+ WHERE run_id = ?
722
+ UNION ALL
723
+ SELECT child.run_id, child.parent_run_id, ancestry.depth + 1
724
+ FROM _smithers_runs child
725
+ JOIN ancestry ON child.run_id = ancestry.parent_run_id
726
+ WHERE ancestry.parent_run_id IS NOT NULL
727
+ )
728
+ SELECT
729
+ run_id,
730
+ parent_run_id,
731
+ depth
732
+ FROM ancestry
733
+ ORDER BY depth ASC
734
+ LIMIT ?`, [runId, limit]));
735
+ }
736
+ /**
737
+ * @param {string} parentRunId
738
+ * @returns {RunnableEffect<RunRow | undefined, SmithersError>}
739
+ */
740
+ getLatestChildRun(parentRunId) {
741
+ return this.read(`get latest child run ${parentRunId}`, () => this.internalStorage.queryOne(`SELECT *
742
+ FROM _smithers_runs
743
+ WHERE parent_run_id = ?
744
+ ORDER BY created_at_ms DESC
745
+ LIMIT 1`, [parentRunId]));
746
+ }
747
+ /**
748
+ * @param {string} [status]
749
+ * @returns {RunnableEffect<RunRow[], SmithersError>}
750
+ */
751
+ listRuns(limit = 50, status) {
752
+ return this.read(`list runs ${status ?? "all"}`, async () => {
753
+ const clauses = [];
754
+ const params = [];
755
+ if (status === "running") {
756
+ clauses.push("(status = ? OR status = ?)");
757
+ params.push("running", "continued");
758
+ }
759
+ else if (status) {
760
+ clauses.push("status = ?");
761
+ params.push(status);
762
+ }
763
+ const whereSql = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
764
+ const rows = await this.internalStorage.queryAll(`SELECT *
765
+ FROM _smithers_runs
766
+ ${whereSql}
767
+ ORDER BY created_at_ms DESC
768
+ LIMIT ?`, [...params, limit]);
769
+ return rows.map((row) => classifyRunRowStatus(row));
770
+ });
771
+ }
772
+ /**
773
+ * @param {number} staleBeforeMs
774
+ * @returns {RunnableEffect<StaleRunRecord[], SmithersError>}
775
+ */
776
+ listStaleRunningRuns(staleBeforeMs, limit = 1000) {
777
+ return this.read(`list stale running runs before ${staleBeforeMs}`, () => this.internalStorage.queryAll(`SELECT
778
+ run_id,
779
+ workflow_path,
780
+ heartbeat_at_ms,
781
+ runtime_owner_id,
782
+ status
783
+ FROM _smithers_runs
784
+ WHERE status = 'running'
785
+ AND (heartbeat_at_ms IS NULL OR heartbeat_at_ms < ?)
786
+ ORDER BY COALESCE(heartbeat_at_ms, 0) ASC
787
+ LIMIT ?`, [staleBeforeMs, limit]));
788
+ }
789
+ /**
790
+ * @param {{ runId: string; expectedStatus?: string; expectedRuntimeOwnerId: string | null; expectedHeartbeatAtMs: number | null; staleBeforeMs: number; claimOwnerId: string; claimHeartbeatAtMs: number; requireStale?: boolean; }} params
791
+ * @returns {RunnableEffect<boolean, SmithersError>}
792
+ */
793
+ claimRunForResume(params) {
794
+ return this.write(`claim stale run ${params.runId}`, () => {
795
+ const client = this.db.session.client;
796
+ const expectedStatus = params.expectedStatus ?? "running";
797
+ const requireStale = params.requireStale ?? expectedStatus === "running";
798
+ client
799
+ .query(`UPDATE _smithers_runs
800
+ SET runtime_owner_id = ?, heartbeat_at_ms = ?
801
+ WHERE run_id = ?
802
+ AND status = ?
803
+ AND COALESCE(runtime_owner_id, '') = COALESCE(?, '')
804
+ AND COALESCE(heartbeat_at_ms, -1) = COALESCE(?, -1)
805
+ AND (? = 0 OR heartbeat_at_ms IS NULL OR heartbeat_at_ms < ?)`)
806
+ .run(params.claimOwnerId, params.claimHeartbeatAtMs, params.runId, expectedStatus, params.expectedRuntimeOwnerId, params.expectedHeartbeatAtMs, requireStale ? 1 : 0, params.staleBeforeMs);
807
+ return this.internalStorage
808
+ .queryOne("SELECT changes() AS count")
809
+ .then((row) => Number(row?.count ?? 0) > 0);
810
+ });
811
+ }
812
+ /**
813
+ * @param {{ runId: string; claimOwnerId: string; restoreRuntimeOwnerId: string | null; restoreHeartbeatAtMs: number | null; }} params
814
+ * @returns {RunnableEffect<void, SmithersError>}
815
+ */
816
+ releaseRunResumeClaim(params) {
817
+ return this.write(`release stale run claim ${params.runId}`, () => {
818
+ return this.internalStorage.execute(`UPDATE _smithers_runs
819
+ SET runtime_owner_id = ?, heartbeat_at_ms = ?
820
+ WHERE run_id = ? AND runtime_owner_id = ?`, [
821
+ params.restoreRuntimeOwnerId,
822
+ params.restoreHeartbeatAtMs,
823
+ params.runId,
824
+ params.claimOwnerId,
825
+ ]);
826
+ });
827
+ }
828
+ /**
829
+ * @param {{ runId: string; expectedRuntimeOwnerId: string; expectedHeartbeatAtMs: number | null; patch: Record<string, unknown>; }} params
830
+ * @returns {RunnableEffect<boolean, SmithersError>}
831
+ */
832
+ updateClaimedRun(params) {
833
+ validateRunPatch(params.patch);
834
+ return this.write(`update claimed run ${params.runId}`, () => {
835
+ const client = this.db.session.client;
836
+ const patchEntries = Object.entries(params.patch);
837
+ if (patchEntries.length === 0) {
838
+ return Promise.resolve(true);
839
+ }
840
+ const assignments = patchEntries.map(([key]) => `${camelToSnake(key)} = ?`);
841
+ client
842
+ .query(`UPDATE _smithers_runs
843
+ SET ${assignments.join(", ")}
844
+ WHERE run_id = ?
845
+ AND runtime_owner_id = ?
846
+ AND COALESCE(heartbeat_at_ms, -1) = COALESCE(?, -1)`)
847
+ .run(...patchEntries.map(([, value]) => value), params.runId, params.expectedRuntimeOwnerId, params.expectedHeartbeatAtMs);
848
+ return this.internalStorage
849
+ .queryOne("SELECT changes() AS count")
850
+ .then((row) => Number(row?.count ?? 0) > 0);
851
+ });
852
+ }
853
+ /**
854
+ * @param {Record<string, unknown>} row
855
+ * @returns {RunnableEffect<void, SmithersError>}
856
+ */
857
+ insertNode(row) {
858
+ return this.insertNodeEffect(row);
859
+ }
860
+ /**
861
+ * @param {Record<string, unknown>} row
862
+ * @returns {RunnableEffect<void, SmithersError>}
863
+ */
864
+ insertNodeEffect(row) {
865
+ return this.write(`insert node ${row.nodeId}`, () => this.internalStorage.upsert("_smithers_nodes", row, ["runId", "nodeId", "iteration"]));
866
+ }
867
+ /**
868
+ * @param {string} runId
869
+ * @param {string} nodeId
870
+ * @param {number} iteration
871
+ * @returns {RunnableEffect<NodeRow | undefined, SmithersError>}
872
+ */
873
+ getNode(runId, nodeId, iteration) {
874
+ return this.read(`get node ${nodeId}`, () => this.internalStorage.queryOne(`SELECT *
875
+ FROM _smithers_nodes
876
+ WHERE run_id = ? AND node_id = ? AND iteration = ?
877
+ LIMIT 1`, [runId, nodeId, iteration]));
878
+ }
879
+ /**
880
+ * @param {string} runId
881
+ * @param {string} nodeId
882
+ * @returns {RunnableEffect<NodeRow[], SmithersError>}
883
+ */
884
+ listNodeIterations(runId, nodeId) {
885
+ return this.read(`list node iterations ${nodeId}`, () => this.internalStorage.queryAll(`SELECT *
886
+ FROM _smithers_nodes
887
+ WHERE run_id = ? AND node_id = ?
888
+ ORDER BY iteration DESC`, [runId, nodeId]));
889
+ }
890
+ /**
891
+ * @param {string} runId
892
+ * @returns {RunnableEffect<NodeRow[], SmithersError>}
893
+ */
894
+ listNodes(runId) {
895
+ return this.read(`list nodes ${runId}`, () => this.internalStorage.queryAll(`SELECT *
896
+ FROM _smithers_nodes
897
+ WHERE run_id = ?`, [runId]));
898
+ }
899
+ /**
900
+ * @param {Table} table
901
+ * @param {OutputKey} key
902
+ * @param {Record<string, unknown>} payload
903
+ * @returns {RunnableEffect<unknown, SmithersError>}
904
+ */
905
+ upsertOutputRow(table, key, payload) {
906
+ const cols = getKeyColumns(table);
907
+ const values = { ...payload };
908
+ values.runId = key.runId;
909
+ values.nodeId = key.nodeId;
910
+ if (cols.iteration) {
911
+ values.iteration = key.iteration ?? 0;
912
+ }
913
+ const target = cols.iteration
914
+ ? [cols.runId, cols.nodeId, cols.iteration]
915
+ : [cols.runId, cols.nodeId];
916
+ const tableName = table?.["_"]?.name ?? "output";
917
+ return this.write(`upsert output ${tableName}`, () => this.db
918
+ .insert(table)
919
+ .values(values)
920
+ .onConflictDoUpdate({
921
+ target: target,
922
+ set: values,
923
+ }));
924
+ }
925
+ /**
926
+ * @param {Table} table
927
+ * @param {OutputKey} key
928
+ * @param {Record<string, unknown>} payload
929
+ * @returns {RunnableEffect<unknown, SmithersError>}
930
+ */
931
+ upsertOutputRowEffect(table, key, payload) {
932
+ return this.upsertOutputRow(table, key, payload);
933
+ }
934
+ /**
935
+ * @param {string} tableName
936
+ * @param {OutputKey} key
937
+ * @returns {RunnableEffect<void, SmithersError>}
938
+ */
939
+ deleteOutputRow(tableName, key) {
940
+ return this.write(`delete output ${tableName}`, () => {
941
+ const client = this.db.session.client;
942
+ let resolvedTableName = tableName;
943
+ let escapedTableName = resolvedTableName.replaceAll(`"`, `""`);
944
+ let tableInfo = client
945
+ .query(`PRAGMA table_info("${escapedTableName}")`)
946
+ .all();
947
+ if (tableInfo.length === 0) {
948
+ const schemaCandidates = [
949
+ this.db?._?.fullSchema,
950
+ this.db?._?.schema,
951
+ this.db?.schema,
952
+ ];
953
+ for (const candidate of schemaCandidates) {
954
+ if (!candidate || typeof candidate !== "object")
955
+ continue;
956
+ const table = candidate[tableName];
957
+ if (!table)
958
+ continue;
959
+ try {
960
+ resolvedTableName = getTableName(table);
961
+ escapedTableName = resolvedTableName.replaceAll(`"`, `""`);
962
+ tableInfo = client
963
+ .query(`PRAGMA table_info("${escapedTableName}")`)
964
+ .all();
965
+ if (tableInfo.length > 0) {
966
+ break;
967
+ }
968
+ }
969
+ catch { }
970
+ }
971
+ }
972
+ const columnNames = new Set(tableInfo
973
+ .map((column) => column.name)
974
+ .filter((name) => typeof name === "string"));
975
+ const runIdColumn = columnNames.has("run_id")
976
+ ? "run_id"
977
+ : columnNames.has("runId")
978
+ ? "runId"
979
+ : null;
980
+ const nodeIdColumn = columnNames.has("node_id")
981
+ ? "node_id"
982
+ : columnNames.has("nodeId")
983
+ ? "nodeId"
984
+ : null;
985
+ const iterationColumn = columnNames.has("iteration")
986
+ ? "iteration"
987
+ : null;
988
+ if (!runIdColumn || !nodeIdColumn) {
989
+ throw new Error(`Output table ${tableName} is missing runId/nodeId columns`);
990
+ }
991
+ if (iterationColumn) {
992
+ client
993
+ .query(`DELETE FROM "${escapedTableName}"
994
+ WHERE "${runIdColumn}" = ? AND "${nodeIdColumn}" = ? AND "${iterationColumn}" = ?`)
995
+ .run(key.runId, key.nodeId, key.iteration ?? 0);
996
+ }
997
+ else {
998
+ client
999
+ .query(`DELETE FROM "${escapedTableName}"
1000
+ WHERE "${runIdColumn}" = ? AND "${nodeIdColumn}" = ?`)
1001
+ .run(key.runId, key.nodeId);
1002
+ }
1003
+ return Promise.resolve(undefined);
1004
+ });
1005
+ }
1006
+ /**
1007
+ * @param {string} tableName
1008
+ * @param {OutputKey} key
1009
+ * @returns {RunnableEffect<void, SmithersError>}
1010
+ */
1011
+ deleteOutputRowEffect(tableName, key) {
1012
+ return this.deleteOutputRow(tableName, key);
1013
+ }
1014
+ /**
1015
+ * @param {string} tableName
1016
+ * @param {string} runId
1017
+ * @param {string} nodeId
1018
+ * @returns {RunnableEffect<Record<string, unknown> | null, SmithersError>}
1019
+ */
1020
+ getRawNodeOutput(tableName, runId, nodeId) {
1021
+ return runnableEffect(this.read(`get raw node output ${tableName}`, () => {
1022
+ const query = sql.raw(`SELECT * FROM "${tableName}" WHERE run_id = '${runId}' AND node_id = '${nodeId}' ORDER BY iteration DESC LIMIT 1`);
1023
+ const res = this.db.get(query);
1024
+ return Promise.resolve(res ?? null);
1025
+ }).pipe(Effect.catchAll(() => Effect.succeed(null))));
1026
+ }
1027
+ /**
1028
+ * @param {string} tableName
1029
+ * @param {string} runId
1030
+ * @param {string} nodeId
1031
+ * @param {number} iteration
1032
+ * @returns {RunnableEffect<Record<string, unknown> | null, SmithersError>}
1033
+ */
1034
+ getRawNodeOutputForIteration(tableName, runId, nodeId, iteration) {
1035
+ return runnableEffect(this.read(`get raw node output ${tableName} iteration ${iteration}`, () => {
1036
+ const escaped = tableName.replaceAll(`"`, `""`);
1037
+ const client = this.db.session.client;
1038
+ const stmt = client.query(`SELECT * FROM "${escaped}" WHERE run_id = ? AND node_id = ? AND iteration = ? LIMIT 1`);
1039
+ const row = stmt.get(runId, nodeId, iteration);
1040
+ return Promise.resolve(row ?? null);
1041
+ }).pipe(Effect.catchAll(() => Effect.succeed(null))));
1042
+ }
1043
+ /**
1044
+ * @param {Record<string, unknown>} row
1045
+ * @returns {RunnableEffect<void, SmithersError>}
1046
+ */
1047
+ insertAttempt(row) {
1048
+ return this.write(`insert attempt ${row.nodeId}#${row.attempt}`, () => this.internalStorage.upsert("_smithers_attempts", row, ["runId", "nodeId", "iteration", "attempt"]));
1049
+ }
1050
+ /**
1051
+ * @param {Record<string, unknown>} row
1052
+ * @returns {RunnableEffect<void, SmithersError>}
1053
+ */
1054
+ insertAttemptEffect(row) {
1055
+ return this.insertAttempt(row);
1056
+ }
1057
+ /**
1058
+ * @param {string} runId
1059
+ * @param {string} nodeId
1060
+ * @param {number} iteration
1061
+ * @param {number} attempt
1062
+ * @param {Record<string, unknown>} patch
1063
+ * @returns {RunnableEffect<void, SmithersError>}
1064
+ */
1065
+ updateAttempt(runId, nodeId, iteration, attempt, patch) {
1066
+ return this.write(`update attempt ${nodeId}#${attempt}`, () => this.internalStorage.updateWhere("_smithers_attempts", patch, "run_id = ? AND node_id = ? AND iteration = ? AND attempt = ?", [runId, nodeId, iteration, attempt]));
1067
+ }
1068
+ /**
1069
+ * @param {string} runId
1070
+ * @param {string} nodeId
1071
+ * @param {number} iteration
1072
+ * @param {number} attempt
1073
+ * @param {Record<string, unknown>} patch
1074
+ * @returns {RunnableEffect<void, SmithersError>}
1075
+ */
1076
+ updateAttemptEffect(runId, nodeId, iteration, attempt, patch) {
1077
+ return this.updateAttempt(runId, nodeId, iteration, attempt, patch);
1078
+ }
1079
+ /**
1080
+ * @param {string} runId
1081
+ * @param {string} nodeId
1082
+ * @param {number} iteration
1083
+ * @param {number} attempt
1084
+ * @param {number} heartbeatAtMs
1085
+ * @param {string | null} heartbeatDataJson
1086
+ * @returns {RunnableEffect<void, SmithersError>}
1087
+ */
1088
+ heartbeatAttempt(runId, nodeId, iteration, attempt, heartbeatAtMs, heartbeatDataJson) {
1089
+ return this.write(`heartbeat attempt ${nodeId}#${attempt}`, () => this.internalStorage.updateWhere("_smithers_attempts", {
1090
+ heartbeatAtMs,
1091
+ heartbeatDataJson,
1092
+ }, "run_id = ? AND node_id = ? AND iteration = ? AND attempt = ? AND state = ?", [runId, nodeId, iteration, attempt, "in-progress"]));
1093
+ }
1094
+ /**
1095
+ * @param {string} runId
1096
+ * @param {string} nodeId
1097
+ * @param {number} iteration
1098
+ * @returns {RunnableEffect<AttemptRow[], SmithersError>}
1099
+ */
1100
+ listAttempts(runId, nodeId, iteration) {
1101
+ return this.read(`list attempts ${nodeId}`, () => this.internalStorage.queryAll(`SELECT *
1102
+ FROM _smithers_attempts
1103
+ WHERE run_id = ? AND node_id = ? AND iteration = ?
1104
+ ORDER BY attempt DESC`, [runId, nodeId, iteration], { booleanColumns: ["cached"] }));
1105
+ }
1106
+ /**
1107
+ * @param {string} runId
1108
+ * @returns {RunnableEffect<AttemptRow[], SmithersError>}
1109
+ */
1110
+ listAttemptsForRun(runId) {
1111
+ return this.read(`list attempts for run ${runId}`, () => this.internalStorage.queryAll(`SELECT *
1112
+ FROM _smithers_attempts
1113
+ WHERE run_id = ?
1114
+ ORDER BY started_at_ms ASC, node_id ASC, iteration ASC, attempt ASC`, [runId], { booleanColumns: ["cached"] }));
1115
+ }
1116
+ /**
1117
+ * @param {string} runId
1118
+ * @param {string} nodeId
1119
+ * @param {number} iteration
1120
+ * @param {number} attempt
1121
+ * @returns {RunnableEffect<AttemptRow | undefined, SmithersError>}
1122
+ */
1123
+ getAttempt(runId, nodeId, iteration, attempt) {
1124
+ return this.read(`get attempt ${nodeId}#${attempt}`, () => this.internalStorage.queryOne(`SELECT *
1125
+ FROM _smithers_attempts
1126
+ WHERE run_id = ? AND node_id = ? AND iteration = ? AND attempt = ?
1127
+ LIMIT 1`, [runId, nodeId, iteration, attempt], { booleanColumns: ["cached"] }));
1128
+ }
1129
+ /**
1130
+ * @param {string} runId
1131
+ * @returns {RunnableEffect<AttemptRow[], SmithersError>}
1132
+ */
1133
+ listInProgressAttempts(runId) {
1134
+ return this.read(`list in-progress attempts ${runId}`, () => this.internalStorage.queryAll(`SELECT *
1135
+ FROM _smithers_attempts
1136
+ WHERE run_id = ? AND state = ?`, [runId, "in-progress"], { booleanColumns: ["cached"] }));
1137
+ }
1138
+ /**
1139
+ * @returns {RunnableEffect<AttemptRow[], SmithersError>}
1140
+ */
1141
+ listAllInProgressAttempts() {
1142
+ return this.read("list all in-progress attempts", () => this.internalStorage.queryAll(`SELECT *
1143
+ FROM _smithers_attempts
1144
+ WHERE state = ?`, ["in-progress"], { booleanColumns: ["cached"] }));
1145
+ }
1146
+ /**
1147
+ * @param {string} runId
1148
+ * @param {number} frameNo
1149
+ * @param {number} [limit]
1150
+ * @returns {RunnableEffect<FrameRow[], SmithersError>}
1151
+ */
1152
+ listFrameChainDesc(runId, frameNo, limit) {
1153
+ return this.read(`list frame chain ${runId}:${frameNo}`, () => this.internalStorage.queryAll(`SELECT *
1154
+ FROM _smithers_frames
1155
+ WHERE run_id = ? AND frame_no <= ?
1156
+ ORDER BY frame_no DESC${typeof limit === "number" ? " LIMIT ?" : ""}`, typeof limit === "number" ? [runId, frameNo, limit] : [runId, frameNo]));
1157
+ }
1158
+ /**
1159
+ * @param {string} runId
1160
+ * @param {number} frameNo
1161
+ * @param {Map<number, string>} [localCache]
1162
+ * @returns {Effect.Effect<string | undefined, SmithersError>}
1163
+ */
1164
+ reconstructFrameXml(runId, frameNo, localCache = new Map()) {
1165
+ const self = this;
1166
+ return Effect.gen(function* () {
1167
+ const localHit = localCache.get(frameNo);
1168
+ if (localHit !== undefined)
1169
+ return localHit;
1170
+ const cacheHit = self.getCachedFrameXml(runId, frameNo);
1171
+ if (cacheHit !== undefined) {
1172
+ localCache.set(frameNo, cacheHit);
1173
+ return cacheHit;
1174
+ }
1175
+ let rows = (yield* self.listFrameChainDesc(runId, frameNo, FRAME_KEYFRAME_INTERVAL + 2));
1176
+ if (rows.length === 0)
1177
+ return undefined;
1178
+ let anchorIndex = rows.findIndex((row) => normalizeFrameEncoding(row.encoding) !== "delta");
1179
+ if (anchorIndex < 0) {
1180
+ rows = (yield* self.listFrameChainDesc(runId, frameNo));
1181
+ anchorIndex = rows.findIndex((row) => normalizeFrameEncoding(row.encoding) !== "delta");
1182
+ }
1183
+ if (anchorIndex < 0) {
1184
+ return rows.find((row) => row.frameNo === frameNo)?.xmlJson;
1185
+ }
1186
+ const chain = rows.slice(0, anchorIndex + 1).reverse();
1187
+ let currentXml = "";
1188
+ for (const frameRow of chain) {
1189
+ const rowEncoding = normalizeFrameEncoding(frameRow.encoding);
1190
+ if (rowEncoding === "delta") {
1191
+ if (!currentXml) {
1192
+ currentXml = String(frameRow.xmlJson ?? "null");
1193
+ }
1194
+ else {
1195
+ currentXml = yield* Effect.try({
1196
+ try: () => applyFrameDeltaJson(currentXml, String(frameRow.xmlJson ?? "")),
1197
+ catch: (cause) => toSmithersError(cause, `apply frame delta ${runId}:${frameRow.frameNo}`, {
1198
+ code: "DB_QUERY_FAILED",
1199
+ details: { runId, frameNo: frameRow.frameNo },
1200
+ }),
1201
+ });
1202
+ }
1203
+ }
1204
+ else {
1205
+ currentXml = String(frameRow.xmlJson ?? "null");
1206
+ }
1207
+ localCache.set(frameRow.frameNo, currentXml);
1208
+ self.rememberFrameXml(runId, frameRow.frameNo, currentXml);
1209
+ }
1210
+ return localCache.get(frameNo);
1211
+ });
1212
+ }
1213
+ /**
1214
+ * @param {FrameRow} row
1215
+ * @param {Map<number, string>} [localCache]
1216
+ * @returns {Effect.Effect<FrameRow, SmithersError>}
1217
+ */
1218
+ inflateFrameRow(row, localCache = new Map()) {
1219
+ const self = this;
1220
+ return Effect.gen(function* () {
1221
+ const encoding = normalizeFrameEncoding(row?.encoding);
1222
+ if (encoding !== "delta") {
1223
+ const xmlJson = String(row?.xmlJson ?? "null");
1224
+ localCache.set(row.frameNo, xmlJson);
1225
+ self.rememberFrameXml(row.runId, row.frameNo, xmlJson);
1226
+ return { ...row, encoding, xmlJson };
1227
+ }
1228
+ const xmlJson = yield* self.reconstructFrameXml(row.runId, row.frameNo, localCache);
1229
+ return {
1230
+ ...row,
1231
+ encoding,
1232
+ xmlJson: xmlJson ?? String(row?.xmlJson ?? "null"),
1233
+ };
1234
+ });
1235
+ }
1236
+ /**
1237
+ * @param {Record<string, unknown>} row
1238
+ * @returns {RunnableEffect<void, SmithersError>}
1239
+ */
1240
+ insertFrame(row) {
1241
+ const self = this;
1242
+ return runnableEffect(Effect.gen(function* () {
1243
+ const runId = String(row.runId);
1244
+ const frameNo = Number(row.frameNo);
1245
+ const fullXmlJson = String(row.xmlJson ?? "null");
1246
+ let encoding = "keyframe";
1247
+ let persistedXmlJson = fullXmlJson;
1248
+ if (frameNo > 0 && frameNo % FRAME_KEYFRAME_INTERVAL !== 0) {
1249
+ const previousXmlJson = yield* self.reconstructFrameXml(runId, frameNo - 1);
1250
+ if (typeof previousXmlJson === "string") {
1251
+ const delta = yield* Effect.try({
1252
+ try: () => encodeFrameDelta(previousXmlJson, fullXmlJson),
1253
+ catch: (cause) => toSmithersError(cause, `encode frame delta ${runId}:${frameNo}`, {
1254
+ code: "DB_WRITE_FAILED",
1255
+ details: { runId, frameNo },
1256
+ }),
1257
+ });
1258
+ const deltaJson = serializeFrameDelta(delta);
1259
+ if (deltaJson.length < fullXmlJson.length) {
1260
+ encoding = "delta";
1261
+ persistedXmlJson = deltaJson;
1262
+ }
1263
+ }
1264
+ }
1265
+ const persistedRow = {
1266
+ ...row,
1267
+ xmlJson: persistedXmlJson,
1268
+ encoding,
1269
+ };
1270
+ yield* self.write(`insert frame ${frameNo}`, () => self.internalStorage.upsert("_smithers_frames", persistedRow, ["runId", "frameNo"]));
1271
+ self.clearFrameCacheForRun(runId);
1272
+ self.rememberFrameXml(runId, frameNo, fullXmlJson);
1273
+ }));
1274
+ }
1275
+ /**
1276
+ * @param {Record<string, unknown>} row
1277
+ * @returns {RunnableEffect<void, SmithersError>}
1278
+ */
1279
+ insertFrameEffect(row) {
1280
+ return this.insertFrame(row);
1281
+ }
1282
+ /**
1283
+ * @param {string} runId
1284
+ * @returns {RunnableEffect<FrameRow | undefined, SmithersError>}
1285
+ */
1286
+ getLastFrame(runId) {
1287
+ const self = this;
1288
+ return runnableEffect(Effect.gen(function* () {
1289
+ const row = yield* self.read(`get last frame ${runId}`, () => self.internalStorage.queryOne(`SELECT *
1290
+ FROM _smithers_frames
1291
+ WHERE run_id = ?
1292
+ ORDER BY frame_no DESC
1293
+ LIMIT 1`, [runId]));
1294
+ if (!row)
1295
+ return undefined;
1296
+ return yield* self.inflateFrameRow(row);
1297
+ }));
1298
+ }
1299
+ /**
1300
+ * @param {Record<string, unknown>} row
1301
+ * @returns {RunnableEffect<void, SmithersError>}
1302
+ */
1303
+ insertOrUpdateApproval(row) {
1304
+ return this.write(`upsert approval ${row.nodeId}`, () => this.internalStorage.upsert("_smithers_approvals", row, ["runId", "nodeId", "iteration"]));
1305
+ }
1306
+ /**
1307
+ * @param {string} runId
1308
+ * @param {string} nodeId
1309
+ * @param {number} iteration
1310
+ * @returns {RunnableEffect<ApprovalRow | undefined, SmithersError>}
1311
+ */
1312
+ getApproval(runId, nodeId, iteration) {
1313
+ return this.read(`get approval ${nodeId}`, () => this.internalStorage.queryOne(`SELECT *
1314
+ FROM _smithers_approvals
1315
+ WHERE run_id = ? AND node_id = ? AND iteration = ?
1316
+ LIMIT 1`, [runId, nodeId, iteration], { booleanColumns: ["autoApproved"] }));
1317
+ }
1318
+ /**
1319
+ * @param {HumanRequestRow} row
1320
+ * @returns {RunnableEffect<void, SmithersError>}
1321
+ */
1322
+ insertHumanRequest(row) {
1323
+ return this.write(`insert human request ${row.requestId}`, () => this.internalStorage.insertIgnore("_smithers_human_requests", row));
1324
+ }
1325
+ /**
1326
+ * @param {string} requestId
1327
+ * @returns {RunnableEffect<HumanRequestRow | undefined, SmithersError>}
1328
+ */
1329
+ getHumanRequest(requestId) {
1330
+ return this.read(`get human request ${requestId}`, () => this.internalStorage.queryOne(`SELECT *
1331
+ FROM _smithers_human_requests
1332
+ WHERE request_id = ?
1333
+ LIMIT 1`, [requestId]));
1334
+ }
1335
+ /**
1336
+ * @param {string} requestId
1337
+ * @returns {RunnableEffect<void, SmithersError>}
1338
+ */
1339
+ reopenHumanRequest(requestId) {
1340
+ return this.write(`reopen human request ${requestId}`, () => this.internalStorage.updateWhere("_smithers_human_requests", {
1341
+ status: "pending",
1342
+ responseJson: null,
1343
+ answeredAtMs: null,
1344
+ answeredBy: null,
1345
+ }, "request_id = ? AND status = ?", [requestId, "answered"]));
1346
+ }
1347
+ /**
1348
+ * @param {number} [nowMs]
1349
+ * @returns {RunnableEffect<void, SmithersError>}
1350
+ */
1351
+ expireStaleHumanRequests(nowMs = Date.now()) {
1352
+ return this.write(`expire stale human requests before ${nowMs}`, () => this.internalStorage.updateWhere("_smithers_human_requests", {
1353
+ status: "expired",
1354
+ responseJson: null,
1355
+ answeredAtMs: null,
1356
+ answeredBy: null,
1357
+ }, "status = ? AND timeout_at_ms IS NOT NULL AND timeout_at_ms <= ?", ["pending", nowMs]));
1358
+ }
1359
+ /**
1360
+ * @param {number} [nowMs]
1361
+ * @returns {RunnableEffect<PendingHumanRequestRow[], SmithersError>}
1362
+ */
1363
+ listPendingHumanRequests(nowMs = Date.now()) {
1364
+ const self = this;
1365
+ return runnableEffect(Effect.gen(function* () {
1366
+ yield* self.expireStaleHumanRequests(nowMs);
1367
+ return yield* self.read("list pending human requests", () => self.internalStorage.queryAll(`SELECT
1368
+ h.request_id,
1369
+ h.run_id,
1370
+ h.node_id,
1371
+ h.iteration,
1372
+ h.kind,
1373
+ h.status,
1374
+ h.prompt,
1375
+ h.schema_json,
1376
+ h.options_json,
1377
+ h.response_json,
1378
+ h.requested_at_ms,
1379
+ h.answered_at_ms,
1380
+ h.answered_by,
1381
+ h.timeout_at_ms,
1382
+ r.workflow_name,
1383
+ r.status AS run_status,
1384
+ n.label AS node_label
1385
+ FROM _smithers_human_requests h
1386
+ LEFT JOIN _smithers_runs r ON h.run_id = r.run_id
1387
+ LEFT JOIN _smithers_nodes n
1388
+ ON h.run_id = n.run_id
1389
+ AND h.node_id = n.node_id
1390
+ AND h.iteration = n.iteration
1391
+ WHERE h.status = ?
1392
+ ORDER BY h.requested_at_ms ASC, h.run_id, h.node_id, h.iteration`, ["pending"]));
1393
+ }));
1394
+ }
1395
+ /**
1396
+ * @param {string} requestId
1397
+ * @param {string} responseJson
1398
+ * @param {number} answeredAtMs
1399
+ * @param {string | null} [answeredBy]
1400
+ * @returns {RunnableEffect<void, SmithersError>}
1401
+ */
1402
+ answerHumanRequest(requestId, responseJson, answeredAtMs, answeredBy) {
1403
+ return this.write(`answer human request ${requestId}`, () => this.internalStorage.updateWhere("_smithers_human_requests", {
1404
+ status: "answered",
1405
+ responseJson,
1406
+ answeredAtMs,
1407
+ answeredBy: answeredBy ?? null,
1408
+ }, "request_id = ? AND status = ?", [requestId, "pending"]));
1409
+ }
1410
+ /**
1411
+ * @param {string} requestId
1412
+ * @returns {RunnableEffect<void, SmithersError>}
1413
+ */
1414
+ cancelHumanRequest(requestId) {
1415
+ return this.write(`cancel human request ${requestId}`, () => this.internalStorage.updateWhere("_smithers_human_requests", {
1416
+ status: "cancelled",
1417
+ }, "request_id = ? AND status = ?", [requestId, "pending"]));
1418
+ }
1419
+ /**
1420
+ * @param {AlertRow} row
1421
+ * @returns {Promise<AlertRow | undefined>}
1422
+ */
1423
+ insertAlert(row) {
1424
+ validateAlertRow(row);
1425
+ const self = this;
1426
+ return this.withTransaction(`insert alert ${row.alertId}`, Effect.gen(function* () {
1427
+ const existing = yield* self.getAlert(row.alertId);
1428
+ if (existing) {
1429
+ return existing;
1430
+ }
1431
+ yield* self.write(`insert alert ${row.alertId}`, () => self.internalStorage.insertIgnore("_smithers_alerts", row));
1432
+ yield* Metric.increment(Metric.tagged(Metric.tagged(alertsFiredTotal, "policy", row.policyName), "severity", row.severity));
1433
+ if (isAlertActiveStatus(row.status)) {
1434
+ yield* Metric.update(alertsActive, 1);
1435
+ }
1436
+ return yield* self.getAlert(row.alertId);
1437
+ }));
1438
+ }
1439
+ /**
1440
+ * @param {string} alertId
1441
+ * @returns {RunnableEffect<AlertRow | undefined, SmithersError>}
1442
+ */
1443
+ getAlert(alertId) {
1444
+ return this.read(`get alert ${alertId}`, () => this.internalStorage.queryOne(`SELECT *
1445
+ FROM _smithers_alerts
1446
+ WHERE alert_id = ?
1447
+ LIMIT 1`, [alertId]));
1448
+ }
1449
+ /**
1450
+ * @param {readonly AlertStatus[]} [statuses]
1451
+ * @returns {RunnableEffect<AlertRow[], SmithersError>}
1452
+ */
1453
+ listAlerts(limit = 100, statuses) {
1454
+ if (statuses) {
1455
+ for (const status of statuses) {
1456
+ validateAlertStatus(status);
1457
+ }
1458
+ }
1459
+ const normalizedLimit = Math.max(1, Math.floor(limit));
1460
+ return this.read("list alerts", () => {
1461
+ const clauses = [];
1462
+ const params = [];
1463
+ if (statuses && statuses.length > 0) {
1464
+ clauses.push(`status IN (${statuses.map(() => "?").join(", ")})`);
1465
+ params.push(...statuses);
1466
+ }
1467
+ const whereSql = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
1468
+ return this.internalStorage.queryAll(`SELECT *
1469
+ FROM _smithers_alerts
1470
+ ${whereSql}
1471
+ ORDER BY
1472
+ CASE status
1473
+ WHEN 'firing' THEN 0
1474
+ WHEN 'acknowledged' THEN 1
1475
+ WHEN 'silenced' THEN 2
1476
+ WHEN 'resolved' THEN 3
1477
+ ELSE 4
1478
+ END,
1479
+ fired_at_ms DESC,
1480
+ alert_id ASC
1481
+ LIMIT ?`, [...params, normalizedLimit]);
1482
+ });
1483
+ }
1484
+ /**
1485
+ * @param {string} alertId
1486
+ * @returns {Promise<AlertRow | undefined>}
1487
+ */
1488
+ acknowledgeAlert(alertId, acknowledgedAtMs = Date.now()) {
1489
+ validateOptionalPositiveTimestamp({ acknowledgedAtMs }, "acknowledgedAtMs");
1490
+ const self = this;
1491
+ return this.withTransaction(`acknowledge alert ${alertId}`, Effect.gen(function* () {
1492
+ const alert = yield* self.getAlert(alertId);
1493
+ if (!alert) {
1494
+ return undefined;
1495
+ }
1496
+ if (alert.status !== "firing") {
1497
+ return alert;
1498
+ }
1499
+ yield* self.write(`acknowledge alert ${alertId}`, () => self.internalStorage.updateWhere("_smithers_alerts", {
1500
+ status: "acknowledged",
1501
+ acknowledgedAtMs,
1502
+ }, "alert_id = ? AND status = ?", [alertId, "firing"]));
1503
+ yield* Metric.increment(Metric.tagged(alertsAcknowledgedTotal, "policy", alert.policyName));
1504
+ return yield* self.getAlert(alertId);
1505
+ }));
1506
+ }
1507
+ /**
1508
+ * @param {string} alertId
1509
+ * @returns {Promise<AlertRow | undefined>}
1510
+ */
1511
+ resolveAlert(alertId, resolvedAtMs = Date.now()) {
1512
+ validateOptionalPositiveTimestamp({ resolvedAtMs }, "resolvedAtMs");
1513
+ const self = this;
1514
+ return this.withTransaction(`resolve alert ${alertId}`, Effect.gen(function* () {
1515
+ const alert = yield* self.getAlert(alertId);
1516
+ if (!alert) {
1517
+ return undefined;
1518
+ }
1519
+ if (alert.status === "resolved") {
1520
+ return alert;
1521
+ }
1522
+ yield* self.write(`resolve alert ${alertId}`, () => self.internalStorage.updateWhere("_smithers_alerts", {
1523
+ status: "resolved",
1524
+ resolvedAtMs,
1525
+ }, "alert_id = ? AND status != ?", [alertId, "resolved"]));
1526
+ if (isAlertActiveStatus(alert.status)) {
1527
+ yield* Metric.update(alertsActive, -1);
1528
+ }
1529
+ return yield* self.getAlert(alertId);
1530
+ }));
1531
+ }
1532
+ /**
1533
+ * @param {string} alertId
1534
+ * @returns {Promise<AlertRow | undefined>}
1535
+ */
1536
+ silenceAlert(alertId) {
1537
+ const self = this;
1538
+ return this.withTransaction(`silence alert ${alertId}`, Effect.gen(function* () {
1539
+ const alert = yield* self.getAlert(alertId);
1540
+ if (!alert) {
1541
+ return undefined;
1542
+ }
1543
+ if (alert.status === "resolved" || alert.status === "silenced") {
1544
+ return alert;
1545
+ }
1546
+ yield* self.write(`silence alert ${alertId}`, () => self.internalStorage.updateWhere("_smithers_alerts", {
1547
+ status: "silenced",
1548
+ }, "alert_id = ? AND status != ? AND status != ?", [alertId, "resolved", "silenced"]));
1549
+ return yield* self.getAlert(alertId);
1550
+ }));
1551
+ }
1552
+ /**
1553
+ * @param {{ runId: string; signalName: string; correlationId: string | null; payloadJson: string; receivedAtMs: number; receivedBy?: string | null; }} row
1554
+ * @returns {RunnableEffect<number, SmithersError>}
1555
+ */
1556
+ insertSignalWithNextSeq(row) {
1557
+ const label = `insert signal ${row.signalName}`;
1558
+ const self = this;
1559
+ return runnableEffect(withSqliteWriteRetryEffect(() => Effect.gen(function* () {
1560
+ const existing = yield* self.read(label, () => self.internalStorage.queryOne(`SELECT seq
1561
+ FROM _smithers_signals
1562
+ WHERE run_id = ?
1563
+ AND signal_name = ?
1564
+ AND ${row.correlationId === null ? "correlation_id IS NULL" : "correlation_id = ?"}
1565
+ AND payload_json = ?
1566
+ AND received_at_ms = ?
1567
+ AND ${row.receivedBy == null ? "received_by IS NULL" : "received_by = ?"}
1568
+ ORDER BY seq DESC
1569
+ LIMIT 1`, [
1570
+ row.runId,
1571
+ row.signalName,
1572
+ ...(row.correlationId === null ? [] : [row.correlationId]),
1573
+ row.payloadJson,
1574
+ row.receivedAtMs,
1575
+ ...(row.receivedBy == null ? [] : [row.receivedBy]),
1576
+ ]));
1577
+ if (existing?.seq !== undefined) {
1578
+ return existing.seq;
1579
+ }
1580
+ const client = self.db.$client;
1581
+ if (!client ||
1582
+ typeof client.exec !== "function" ||
1583
+ typeof client.query !== "function") {
1584
+ const lastSeq = (yield* self.getLastSignalSeq(row.runId)) ?? -1;
1585
+ const seq = lastSeq + 1;
1586
+ yield* Effect.tryPromise({
1587
+ try: () => self.internalStorage.insertIgnore("_smithers_signals", {
1588
+ ...row,
1589
+ receivedBy: row.receivedBy ?? null,
1590
+ seq,
1591
+ }),
1592
+ catch: (cause) => toSmithersError(cause, "insert fallback signal row"),
1593
+ });
1594
+ return seq;
1595
+ }
1596
+ return yield* Effect.try({
1597
+ try: () => {
1598
+ client.run("BEGIN IMMEDIATE");
1599
+ try {
1600
+ const res = client
1601
+ .query("SELECT COALESCE(MAX(seq), -1) + 1 AS seq FROM _smithers_signals WHERE run_id = ?")
1602
+ .get(row.runId);
1603
+ const seq = Number(res?.seq ?? 0);
1604
+ client
1605
+ .query("INSERT INTO _smithers_signals (run_id, seq, signal_name, correlation_id, payload_json, received_at_ms, received_by) VALUES (?, ?, ?, ?, ?, ?, ?)")
1606
+ .run(row.runId, seq, row.signalName, row.correlationId, row.payloadJson, row.receivedAtMs, row.receivedBy ?? null);
1607
+ client.run("COMMIT");
1608
+ return seq;
1609
+ }
1610
+ catch (error) {
1611
+ try {
1612
+ client.run("ROLLBACK");
1613
+ }
1614
+ catch {
1615
+ // ignore rollback failures
1616
+ }
1617
+ throw error;
1618
+ }
1619
+ },
1620
+ catch: (cause) => toSmithersError(cause, "insert signal transaction"),
1621
+ });
1622
+ }), { label }).pipe(Effect.annotateLogs({
1623
+ runId: row.runId,
1624
+ signalName: row.signalName,
1625
+ correlationId: row.correlationId ?? null,
1626
+ }), Effect.withLogSpan(`db:${label}`)));
1627
+ }
1628
+ /**
1629
+ * @param {string} runId
1630
+ * @returns {RunnableEffect<number | undefined, SmithersError>}
1631
+ */
1632
+ getLastSignalSeq(runId) {
1633
+ return this.read(`get last signal seq ${runId}`, () => this.internalStorage.getLastSignalSeq(runId));
1634
+ }
1635
+ /**
1636
+ * @param {string} runId
1637
+ * @param {SignalQuery} [query]
1638
+ * @returns {RunnableEffect<SignalRow[], SmithersError>}
1639
+ */
1640
+ listSignals(runId, query = {}) {
1641
+ const limit = Math.max(1, Math.floor(query.limit ?? 200));
1642
+ return this.read(`list signals ${runId}`, () => {
1643
+ const clauses = ["run_id = ?"];
1644
+ const params = [runId];
1645
+ if (query.signalName) {
1646
+ clauses.push("signal_name = ?");
1647
+ params.push(query.signalName);
1648
+ }
1649
+ if (query.correlationId !== undefined) {
1650
+ if (query.correlationId === null) {
1651
+ clauses.push("correlation_id IS NULL");
1652
+ }
1653
+ else {
1654
+ clauses.push("correlation_id = ?");
1655
+ params.push(query.correlationId);
1656
+ }
1657
+ }
1658
+ if (typeof query.receivedAfterMs === "number") {
1659
+ clauses.push("received_at_ms >= ?");
1660
+ params.push(query.receivedAfterMs);
1661
+ }
1662
+ return this.internalStorage.queryAll(`SELECT *
1663
+ FROM _smithers_signals
1664
+ WHERE ${clauses.join(" AND ")}
1665
+ ORDER BY seq ASC
1666
+ LIMIT ?`, [...params, limit]);
1667
+ });
1668
+ }
1669
+ /**
1670
+ * @param {Record<string, unknown>} row
1671
+ * @returns {RunnableEffect<void, SmithersError>}
1672
+ */
1673
+ insertToolCall(row) {
1674
+ return this.write(`insert tool call ${row.toolName}`, () => this.internalStorage.insertIgnore("_smithers_tool_calls", row));
1675
+ }
1676
+ /**
1677
+ * @param {Record<string, unknown>} row
1678
+ * @returns {RunnableEffect<void, SmithersError>}
1679
+ */
1680
+ upsertSandbox(row) {
1681
+ return this.write(`upsert sandbox ${row.sandboxId}`, () => this.internalStorage.upsert("_smithers_sandboxes", row, ["runId", "sandboxId"]));
1682
+ }
1683
+ /**
1684
+ * @param {string} runId
1685
+ * @param {string} sandboxId
1686
+ * @returns {RunnableEffect<Record<string, unknown> | undefined, SmithersError>}
1687
+ */
1688
+ getSandbox(runId, sandboxId) {
1689
+ return this.read(`get sandbox ${sandboxId}`, () => this.internalStorage.queryOne(`SELECT *
1690
+ FROM _smithers_sandboxes
1691
+ WHERE run_id = ? AND sandbox_id = ?
1692
+ LIMIT 1`, [runId, sandboxId]));
1693
+ }
1694
+ /**
1695
+ * @param {string} runId
1696
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
1697
+ */
1698
+ listSandboxes(runId) {
1699
+ return this.read(`list sandboxes ${runId}`, () => this.internalStorage.queryAll(`SELECT *
1700
+ FROM _smithers_sandboxes
1701
+ WHERE run_id = ?`, [runId]));
1702
+ }
1703
+ /**
1704
+ * @param {string} runId
1705
+ * @param {string} nodeId
1706
+ * @param {number} iteration
1707
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
1708
+ */
1709
+ listToolCalls(runId, nodeId, iteration) {
1710
+ return this.read(`list tool calls ${nodeId}`, () => this.internalStorage.queryAll(`SELECT *
1711
+ FROM _smithers_tool_calls
1712
+ WHERE run_id = ? AND node_id = ? AND iteration = ?
1713
+ ORDER BY attempt ASC, seq ASC`, [runId, nodeId, iteration]));
1714
+ }
1715
+ /**
1716
+ * @param {Record<string, unknown>} row
1717
+ * @returns {RunnableEffect<void, SmithersError>}
1718
+ */
1719
+ insertEvent(row) {
1720
+ return this.write(`insert event ${row.type}`, () => this.internalStorage.insertIgnore("_smithers_events", row));
1721
+ }
1722
+ /**
1723
+ * @param {{ runId: string; timestampMs: number; type: string; payloadJson: string; }} row
1724
+ * @returns {RunnableEffect<number, SmithersError>}
1725
+ */
1726
+ insertEventWithNextSeq(row) {
1727
+ const label = `insert event ${row.type}`;
1728
+ const self = this;
1729
+ return runnableEffect(withSqliteWriteRetryEffect(() => Effect.gen(function* () {
1730
+ const existing = yield* self.read(label, () => self.internalStorage.queryOne(`SELECT seq
1731
+ FROM _smithers_events
1732
+ WHERE run_id = ?
1733
+ AND timestamp_ms = ?
1734
+ AND type = ?
1735
+ AND payload_json = ?
1736
+ ORDER BY seq DESC
1737
+ LIMIT 1`, [row.runId, row.timestampMs, row.type, row.payloadJson]));
1738
+ if (existing?.seq !== undefined) {
1739
+ return existing.seq;
1740
+ }
1741
+ const client = self.db.$client;
1742
+ if (!client ||
1743
+ typeof client.exec !== "function" ||
1744
+ typeof client.query !== "function") {
1745
+ const lastSeq = (yield* self.getLastEventSeq(row.runId)) ?? -1;
1746
+ const seq = lastSeq + 1;
1747
+ yield* Effect.tryPromise({
1748
+ try: () => self.internalStorage.insertIgnore("_smithers_events", { ...row, seq }),
1749
+ catch: (cause) => toSmithersError(cause, "insert fallback event row"),
1750
+ });
1751
+ return seq;
1752
+ }
1753
+ return yield* Effect.try({
1754
+ try: () => {
1755
+ client.run("BEGIN IMMEDIATE");
1756
+ try {
1757
+ const res = client
1758
+ .query("SELECT COALESCE(MAX(seq), -1) + 1 AS seq FROM _smithers_events WHERE run_id = ?")
1759
+ .get(row.runId);
1760
+ const seq = Number(res?.seq ?? 0);
1761
+ client
1762
+ .query("INSERT INTO _smithers_events (run_id, seq, timestamp_ms, type, payload_json) VALUES (?, ?, ?, ?, ?)")
1763
+ .run(row.runId, seq, row.timestampMs, row.type, row.payloadJson);
1764
+ client.run("COMMIT");
1765
+ return seq;
1766
+ }
1767
+ catch (error) {
1768
+ try {
1769
+ client.run("ROLLBACK");
1770
+ }
1771
+ catch {
1772
+ // ignore rollback failures
1773
+ }
1774
+ throw error;
1775
+ }
1776
+ },
1777
+ catch: (cause) => toSmithersError(cause, "insert event transaction"),
1778
+ });
1779
+ }), { label }).pipe(Effect.annotateLogs({ dbOperation: label }), Effect.withLogSpan(`db:${label}`)));
1780
+ }
1781
+ /**
1782
+ * @param {string} runId
1783
+ * @returns {RunnableEffect<number | undefined, SmithersError>}
1784
+ */
1785
+ getLastEventSeq(runId) {
1786
+ return this.read(`get last event seq ${runId}`, () => this.internalStorage.getLastEventSeq(runId));
1787
+ }
1788
+ /**
1789
+ * @param {string} runId
1790
+ * @param {EventHistoryQuery} [query]
1791
+ * @returns {{ whereSql: string; params: Array<string | number> }}
1792
+ */
1793
+ buildEventHistoryWhere(runId, query = {}) {
1794
+ const clauses = ["run_id = ?", "seq > ?"];
1795
+ const params = [runId, query.afterSeq ?? -1];
1796
+ if (typeof query.sinceTimestampMs === "number") {
1797
+ clauses.push("timestamp_ms >= ?");
1798
+ params.push(query.sinceTimestampMs);
1799
+ }
1800
+ if (query.types && query.types.length > 0) {
1801
+ clauses.push(`type IN (${query.types.map(() => "?").join(", ")})`);
1802
+ params.push(...query.types);
1803
+ }
1804
+ if (query.nodeId) {
1805
+ clauses.push("json_extract(payload_json, '$.nodeId') = ?");
1806
+ params.push(query.nodeId);
1807
+ }
1808
+ return {
1809
+ whereSql: clauses.join(" AND "),
1810
+ params,
1811
+ };
1812
+ }
1813
+ /**
1814
+ * @param {string} runId
1815
+ * @param {EventHistoryQuery} [query]
1816
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
1817
+ */
1818
+ listEventHistory(runId, query = {}) {
1819
+ return this.read(`list event history ${runId}`, () => this.internalStorage.listEventHistory(runId, query));
1820
+ }
1821
+ /**
1822
+ * @param {string} runId
1823
+ * @param {EventHistoryQuery} [query]
1824
+ * @returns {RunnableEffect<number, SmithersError>}
1825
+ */
1826
+ countEventHistory(runId, query = {}) {
1827
+ return this.read(`count event history ${runId}`, () => this.internalStorage.countEventHistory(runId, query));
1828
+ }
1829
+ /**
1830
+ * @param {string} runId
1831
+ * @param {number} afterSeq
1832
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
1833
+ */
1834
+ listEvents(runId, afterSeq, limit = 200) {
1835
+ return this.listEventHistory(runId, { afterSeq, limit });
1836
+ }
1837
+ /**
1838
+ * @param {string} runId
1839
+ * @param {string} type
1840
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
1841
+ */
1842
+ listEventsByType(runId, type) {
1843
+ return this.read(`list events by type ${type}`, () => this.internalStorage.listEventsByType(runId, type));
1844
+ }
1845
+ /**
1846
+ * @param {Record<string, unknown>} row
1847
+ * @returns {RunnableEffect<void, SmithersError>}
1848
+ */
1849
+ insertOrUpdateRalph(row) {
1850
+ return this.write(`upsert ralph ${row.ralphId}`, () => this.internalStorage.upsert("_smithers_ralph", row, ["runId", "ralphId"]));
1851
+ }
1852
+ /**
1853
+ * @param {string} runId
1854
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
1855
+ */
1856
+ listRalph(runId) {
1857
+ return this.read(`list ralph ${runId}`, () => this.internalStorage.queryAll(`SELECT *
1858
+ FROM _smithers_ralph
1859
+ WHERE run_id = ?`, [runId], { booleanColumns: ["done"] }));
1860
+ }
1861
+ /**
1862
+ * @param {string} runId
1863
+ * @returns {RunnableEffect<ApprovalRow[], SmithersError>}
1864
+ */
1865
+ listPendingApprovals(runId) {
1866
+ return this.read(`list pending approvals ${runId}`, () => this.internalStorage.queryAll(`SELECT *
1867
+ FROM _smithers_approvals
1868
+ WHERE run_id = ? AND status = ?`, [runId, "requested"], { booleanColumns: ["autoApproved"] }));
1869
+ }
1870
+ /**
1871
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
1872
+ */
1873
+ listAllPendingApprovals() {
1874
+ return this.read("list all pending approvals", () => this.internalStorage.queryAll(`SELECT
1875
+ a.run_id,
1876
+ a.node_id,
1877
+ a.iteration,
1878
+ a.status,
1879
+ a.requested_at_ms,
1880
+ a.note,
1881
+ a.decided_by,
1882
+ r.workflow_name,
1883
+ r.status AS run_status,
1884
+ n.label AS node_label
1885
+ FROM _smithers_approvals a
1886
+ LEFT JOIN _smithers_runs r ON a.run_id = r.run_id
1887
+ LEFT JOIN _smithers_nodes n
1888
+ ON a.run_id = n.run_id
1889
+ AND a.node_id = n.node_id
1890
+ AND a.iteration = n.iteration
1891
+ WHERE a.status = ?
1892
+ ORDER BY COALESCE(a.requested_at_ms, 0) ASC, a.run_id, a.node_id, a.iteration`, ["requested"]));
1893
+ }
1894
+ /**
1895
+ * @param {string} workflowName
1896
+ * @param {string} nodeId
1897
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
1898
+ */
1899
+ listApprovalHistoryForNode(workflowName, nodeId, limit = 50) {
1900
+ return this.read(`list approval history ${workflowName}:${nodeId}`, () => this.internalStorage.queryAll(`SELECT
1901
+ a.run_id,
1902
+ a.node_id,
1903
+ a.iteration,
1904
+ a.status,
1905
+ a.requested_at_ms,
1906
+ a.decided_at_ms,
1907
+ a.note,
1908
+ a.decided_by,
1909
+ a.request_json,
1910
+ a.decision_json,
1911
+ a.auto_approved,
1912
+ r.workflow_name,
1913
+ r.created_at_ms AS run_created_at_ms
1914
+ FROM _smithers_approvals a
1915
+ INNER JOIN _smithers_runs r ON a.run_id = r.run_id
1916
+ WHERE r.workflow_name = ? AND a.node_id = ?
1917
+ ORDER BY r.created_at_ms DESC, a.decided_at_ms DESC
1918
+ LIMIT ?`, [workflowName, nodeId, limit], { booleanColumns: ["autoApproved"] }));
1919
+ }
1920
+ /**
1921
+ * @param {string} runId
1922
+ * @param {string} ralphId
1923
+ * @returns {RunnableEffect<Record<string, unknown> | undefined, SmithersError>}
1924
+ */
1925
+ getRalph(runId, ralphId) {
1926
+ return this.read(`get ralph ${ralphId}`, () => this.internalStorage.queryOne(`SELECT *
1927
+ FROM _smithers_ralph
1928
+ WHERE run_id = ? AND ralph_id = ?
1929
+ LIMIT 1`, [runId, ralphId], { booleanColumns: ["done"] }));
1930
+ }
1931
+ /**
1932
+ * @param {Record<string, unknown>} row
1933
+ * @returns {RunnableEffect<void, SmithersError>}
1934
+ */
1935
+ insertCache(row) {
1936
+ return this.write(`insert cache ${row.cacheKey}`, () => this.internalStorage.insertIgnore("_smithers_cache", row));
1937
+ }
1938
+ /**
1939
+ * @param {Record<string, unknown>} row
1940
+ * @returns {RunnableEffect<void, SmithersError>}
1941
+ */
1942
+ insertCacheEffect(row) {
1943
+ return this.insertCache(row);
1944
+ }
1945
+ /**
1946
+ * @param {string} cacheKey
1947
+ * @returns {RunnableEffect<CacheRow | undefined, SmithersError>}
1948
+ */
1949
+ getCache(cacheKey) {
1950
+ return this.read(`get cache ${cacheKey}`, () => this.internalStorage.queryOne(`SELECT *
1951
+ FROM _smithers_cache
1952
+ WHERE cache_key = ?
1953
+ LIMIT 1`, [cacheKey]));
1954
+ }
1955
+ /**
1956
+ * @param {string} nodeId
1957
+ * @param {string} [outputTable]
1958
+ * @returns {RunnableEffect<CacheRow[], SmithersError>}
1959
+ */
1960
+ listCacheByNode(nodeId, outputTable, limit = 20) {
1961
+ return this.read(`list cache by node ${nodeId}`, () => this.internalStorage.queryAll(`SELECT *
1962
+ FROM _smithers_cache
1963
+ WHERE node_id = ?${outputTable ? " AND output_table = ?" : ""}
1964
+ ORDER BY created_at_ms DESC
1965
+ LIMIT ?`, outputTable ? [nodeId, outputTable, limit] : [nodeId, limit]));
1966
+ }
1967
+ /**
1968
+ * @param {string} runId
1969
+ * @param {number} frameNo
1970
+ * @returns {RunnableEffect<void, SmithersError>}
1971
+ */
1972
+ deleteFramesAfter(runId, frameNo) {
1973
+ const self = this;
1974
+ return runnableEffect(Effect.gen(function* () {
1975
+ yield* self.write(`delete frames after ${frameNo}`, () => self.internalStorage.deleteWhere("_smithers_frames", "run_id = ? AND frame_no > ?", [runId, frameNo]));
1976
+ self.clearFrameCacheForRun(runId);
1977
+ }));
1978
+ }
1979
+ /**
1980
+ * @param {string} runId
1981
+ * @param {number} limit
1982
+ * @param {number} [afterFrameNo]
1983
+ * @returns {RunnableEffect<FrameRow[], SmithersError>}
1984
+ */
1985
+ listFrames(runId, limit, afterFrameNo) {
1986
+ const self = this;
1987
+ return runnableEffect(Effect.gen(function* () {
1988
+ const rows = (yield* self.read(`list frames ${runId}`, () => self.internalStorage.queryAll(`SELECT *
1989
+ FROM _smithers_frames
1990
+ WHERE run_id = ?${afterFrameNo !== undefined ? " AND frame_no > ?" : ""}
1991
+ ORDER BY frame_no DESC
1992
+ LIMIT ?`, afterFrameNo !== undefined
1993
+ ? [runId, afterFrameNo, limit]
1994
+ : [runId, limit])));
1995
+ const localCache = new Map();
1996
+ const expanded = [];
1997
+ for (const row of rows) {
1998
+ expanded.push(yield* self.inflateFrameRow(row, localCache));
1999
+ }
2000
+ return expanded;
2001
+ }));
2002
+ }
2003
+ /**
2004
+ * @param {string} runId
2005
+ * @returns {RunnableEffect<Array<{ state: string; count: number }>, SmithersError>}
2006
+ */
2007
+ countNodesByState(runId) {
2008
+ return this.read(`count nodes by state ${runId}`, () => this.internalStorage.queryAll(`SELECT state, COUNT(*) AS count
2009
+ FROM _smithers_nodes
2010
+ WHERE run_id = ?
2011
+ GROUP BY state`, [runId]));
2012
+ }
2013
+ /**
2014
+ * @param {Record<string, unknown>} row
2015
+ * @returns {RunnableEffect<void, SmithersError>}
2016
+ */
2017
+ upsertCron(row) {
2018
+ return this.write("upsert cron", () => this.internalStorage.upsert("_smithers_cron", row, ["cronId"], ["pattern", "workflowPath", "enabled", "nextRunAtMs"]));
2019
+ }
2020
+ /**
2021
+ * @param {boolean} [enabledOnly]
2022
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
2023
+ */
2024
+ listCrons(enabledOnly = true) {
2025
+ return this.read("list crons", () => this.internalStorage.queryAll(`SELECT *
2026
+ FROM _smithers_cron${enabledOnly ? " WHERE enabled = ?" : ""}`, enabledOnly ? [true] : [], { booleanColumns: ["enabled"] }));
2027
+ }
2028
+ /**
2029
+ * @param {string} cronId
2030
+ * @param {number} lastRunAtMs
2031
+ * @param {number} nextRunAtMs
2032
+ * @param {string | null} [errorJson]
2033
+ * @returns {RunnableEffect<void, SmithersError>}
2034
+ */
2035
+ updateCronRunTime(cronId, lastRunAtMs, nextRunAtMs, errorJson) {
2036
+ return this.write(`update cron run time ${cronId}`, () => this.internalStorage.updateWhere("_smithers_cron", { lastRunAtMs, nextRunAtMs, errorJson: errorJson ?? null }, "cron_id = ?", [cronId]));
2037
+ }
2038
+ /**
2039
+ * @param {string} cronId
2040
+ * @returns {RunnableEffect<void, SmithersError>}
2041
+ */
2042
+ deleteCron(cronId) {
2043
+ return this.write(`delete cron ${cronId}`, () => this.internalStorage.deleteWhere("_smithers_cron", "cron_id = ?", [cronId]));
2044
+ }
2045
+ // ---------------------------------------------------------------------------
2046
+ // Scorer results
2047
+ // ---------------------------------------------------------------------------
2048
+ /**
2049
+ * @param {Record<string, unknown>} row
2050
+ * @returns {RunnableEffect<void, SmithersError>}
2051
+ */
2052
+ insertScorerResult(row) {
2053
+ return this.write(`insert scorer result ${row.scorerId}`, () => this.internalStorage.insertIgnore("_smithers_scorers", row));
2054
+ }
2055
+ /**
2056
+ * @param {string} runId
2057
+ * @param {string} [nodeId]
2058
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
2059
+ */
2060
+ listScorerResults(runId, nodeId) {
2061
+ return this.read(`list scorer results ${runId}`, () => this.internalStorage.queryAll(`SELECT *
2062
+ FROM _smithers_scorers
2063
+ WHERE run_id = ?${nodeId ? " AND node_id = ?" : ""}
2064
+ ORDER BY scored_at_ms ASC`, nodeId ? [runId, nodeId] : [runId]));
2065
+ }
2066
+ /**
2067
+ * @param {string} runId
2068
+ * @returns {RunnableEffect<RunRow | undefined, SmithersError>}
2069
+ */
2070
+ getRunEffect(runId) {
2071
+ return this.getRun(runId);
2072
+ }
2073
+ /**
2074
+ * @param {string} [status]
2075
+ * @returns {RunnableEffect<RunRow[], SmithersError>}
2076
+ */
2077
+ listRunsEffect(limit = 50, status) {
2078
+ return this.listRuns(limit, status);
2079
+ }
2080
+ /**
2081
+ * @param {number} staleBeforeMs
2082
+ * @returns {RunnableEffect<StaleRunRecord[], SmithersError>}
2083
+ */
2084
+ listStaleRunningRunsEffect(staleBeforeMs, limit = 1000) {
2085
+ return this.listStaleRunningRuns(staleBeforeMs, limit);
2086
+ }
2087
+ /**
2088
+ * @param {Parameters<SmithersDb["claimRunForResume"]>[0]} params
2089
+ * @returns {RunnableEffect<boolean, SmithersError>}
2090
+ */
2091
+ claimRunForResumeEffect(params) {
2092
+ return this.claimRunForResume(params);
2093
+ }
2094
+ /**
2095
+ * @param {Parameters<SmithersDb["releaseRunResumeClaim"]>[0]} params
2096
+ * @returns {RunnableEffect<void, SmithersError>}
2097
+ */
2098
+ releaseRunResumeClaimEffect(params) {
2099
+ return this.releaseRunResumeClaim(params);
2100
+ }
2101
+ /**
2102
+ * @param {string} runId
2103
+ * @param {string} nodeId
2104
+ * @returns {RunnableEffect<NodeRow[], SmithersError>}
2105
+ */
2106
+ listNodeIterationsEffect(runId, nodeId) {
2107
+ return this.listNodeIterations(runId, nodeId);
2108
+ }
2109
+ /**
2110
+ * @param {string} runId
2111
+ * @returns {RunnableEffect<NodeRow[], SmithersError>}
2112
+ */
2113
+ listNodesEffect(runId) {
2114
+ return this.listNodes(runId);
2115
+ }
2116
+ /**
2117
+ * @param {string} runId
2118
+ * @param {string} nodeId
2119
+ * @param {number} iteration
2120
+ * @returns {RunnableEffect<AttemptRow[], SmithersError>}
2121
+ */
2122
+ listAttemptsEffect(runId, nodeId, iteration) {
2123
+ return this.listAttempts(runId, nodeId, iteration);
2124
+ }
2125
+ /**
2126
+ * @param {string} runId
2127
+ * @returns {RunnableEffect<AttemptRow[], SmithersError>}
2128
+ */
2129
+ listAttemptsForRunEffect(runId) {
2130
+ return this.listAttemptsForRun(runId);
2131
+ }
2132
+ /**
2133
+ * @param {string} runId
2134
+ * @param {string} nodeId
2135
+ * @param {number} iteration
2136
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
2137
+ */
2138
+ listToolCallsEffect(runId, nodeId, iteration) {
2139
+ return this.listToolCalls(runId, nodeId, iteration);
2140
+ }
2141
+ /**
2142
+ * @param {string} tableName
2143
+ * @param {string} runId
2144
+ * @param {string} nodeId
2145
+ * @param {number} iteration
2146
+ * @returns {RunnableEffect<Record<string, unknown> | null, SmithersError>}
2147
+ */
2148
+ getRawNodeOutputForIterationEffect(tableName, runId, nodeId, iteration) {
2149
+ return this.getRawNodeOutputForIteration(tableName, runId, nodeId, iteration);
2150
+ }
2151
+ /**
2152
+ * @param {Parameters<SmithersDb["insertEventWithNextSeq"]>[0]} row
2153
+ * @returns {RunnableEffect<number, SmithersError>}
2154
+ */
2155
+ insertEventWithNextSeqEffect(row) {
2156
+ return this.insertEventWithNextSeq(row);
2157
+ }
2158
+ /**
2159
+ * @param {string} runId
2160
+ * @returns {RunnableEffect<number | undefined, SmithersError>}
2161
+ */
2162
+ getLastEventSeqEffect(runId) {
2163
+ return this.getLastEventSeq(runId);
2164
+ }
2165
+ /**
2166
+ * @param {string} runId
2167
+ * @param {EventHistoryQuery} [query]
2168
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
2169
+ */
2170
+ listEventHistoryEffect(runId, query = {}) {
2171
+ return this.listEventHistory(runId, query);
2172
+ }
2173
+ /**
2174
+ * @param {string} runId
2175
+ * @param {EventHistoryQuery} [query]
2176
+ * @returns {RunnableEffect<number, SmithersError>}
2177
+ */
2178
+ countEventHistoryEffect(runId, query = {}) {
2179
+ return this.countEventHistory(runId, query);
2180
+ }
2181
+ /**
2182
+ * @param {string} runId
2183
+ * @param {string} type
2184
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
2185
+ */
2186
+ listEventsByTypeEffect(runId, type) {
2187
+ return this.listEventsByType(runId, type);
2188
+ }
2189
+ /**
2190
+ * @param {string} runId
2191
+ * @returns {RunnableEffect<ApprovalRow[], SmithersError>}
2192
+ */
2193
+ listPendingApprovalsEffect(runId) {
2194
+ return this.listPendingApprovals(runId);
2195
+ }
2196
+ /**
2197
+ * @param {string} runId
2198
+ * @returns {RunnableEffect<FrameRow | undefined, SmithersError>}
2199
+ */
2200
+ getLastFrameEffect(runId) {
2201
+ return this.getLastFrame(runId);
2202
+ }
2203
+ /**
2204
+ * @param {string} nodeId
2205
+ * @param {string} [outputTable]
2206
+ * @returns {RunnableEffect<CacheRow[], SmithersError>}
2207
+ */
2208
+ listCacheByNodeEffect(nodeId, outputTable, limit = 20) {
2209
+ return this.listCacheByNode(nodeId, outputTable, limit);
2210
+ }
2211
+ /**
2212
+ * @param {boolean} [enabledOnly]
2213
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
2214
+ */
2215
+ listCronsEffect(enabledOnly = true) {
2216
+ return this.listCrons(enabledOnly);
2217
+ }
2218
+ /**
2219
+ * @param {string} cronId
2220
+ * @param {number} lastRunAtMs
2221
+ * @param {number} nextRunAtMs
2222
+ * @param {string | null} [errorJson]
2223
+ * @returns {RunnableEffect<void, SmithersError>}
2224
+ */
2225
+ updateCronRunTimeEffect(cronId, lastRunAtMs, nextRunAtMs, errorJson) {
2226
+ return this.updateCronRunTime(cronId, lastRunAtMs, nextRunAtMs, errorJson);
2227
+ }
2228
+ /**
2229
+ * @param {string} runId
2230
+ * @param {string} [nodeId]
2231
+ * @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
2232
+ */
2233
+ listScorerResultsEffect(runId, nodeId) {
2234
+ return this.listScorerResults(runId, nodeId);
2235
+ }
2236
+ }