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