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