@smithers-orchestrator/db 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +43 -0
  3. package/src/JsonBounds.ts +6 -0
  4. package/src/SchemaRegistryEntry.ts +6 -0
  5. package/src/SqlMessageStorage.js +818 -0
  6. package/src/SqlMessageStorageEventHistoryQuery.ts +7 -0
  7. package/src/SqliteWriteRetryOptions.ts +7 -0
  8. package/src/adapter/AlertRow.ts +29 -0
  9. package/src/adapter/AlertSeverity.ts +2 -0
  10. package/src/adapter/AlertStatus.ts +2 -0
  11. package/src/adapter/ApprovalRow.ts +13 -0
  12. package/src/adapter/AttemptRow.ts +17 -0
  13. package/src/adapter/CacheRow.ts +12 -0
  14. package/src/adapter/DB_ALERT_ALLOWED_SEVERITIES.js +5 -0
  15. package/src/adapter/DB_ALERT_ALLOWED_STATUSES.js +6 -0
  16. package/src/adapter/DB_ALERT_ID_MAX_LENGTH.js +1 -0
  17. package/src/adapter/DB_ALERT_MESSAGE_MAX_LENGTH.js +1 -0
  18. package/src/adapter/DB_ALERT_POLICY_NAME_MAX_LENGTH.js +1 -0
  19. package/src/adapter/DB_RUN_ALLOWED_STATUSES.js +10 -0
  20. package/src/adapter/DB_RUN_ID_MAX_LENGTH.js +1 -0
  21. package/src/adapter/DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js +1 -0
  22. package/src/adapter/EventHistoryQuery.ts +7 -0
  23. package/src/adapter/HumanRequestRow.ts +19 -0
  24. package/src/adapter/NodeDiffCacheRow.ts +9 -0
  25. package/src/adapter/NodeRow.ts +10 -0
  26. package/src/adapter/PendingHumanRequestRow.ts +7 -0
  27. package/src/adapter/RunAncestryRow.ts +5 -0
  28. package/src/adapter/RunRow.ts +21 -0
  29. package/src/adapter/SignalQuery.ts +6 -0
  30. package/src/adapter/SignalRow.ts +9 -0
  31. package/src/adapter/SmithersDb.js +2236 -0
  32. package/src/adapter/StaleRunRecord.ts +7 -0
  33. package/src/adapter/index.js +27 -0
  34. package/src/adapter.js +2359 -0
  35. package/src/assertJsonPayloadWithinBounds.js +94 -0
  36. package/src/assertMaxBytes.js +23 -0
  37. package/src/assertMaxJsonDepth.js +40 -0
  38. package/src/assertMaxStringLength.js +16 -0
  39. package/src/assertOptionalArrayMaxLength.js +16 -0
  40. package/src/assertOptionalStringMaxLength.js +11 -0
  41. package/src/assertPositiveFiniteInteger.js +14 -0
  42. package/src/assertPositiveFiniteNumber.js +12 -0
  43. package/src/buildHumanRequestId.js +9 -0
  44. package/src/cache/nodeDiffCache.js +124 -0
  45. package/src/ensure.js +18 -0
  46. package/src/ensureSqlMessageStorage.js +11 -0
  47. package/src/ensureSqlMessageStorageEffect.js +12 -0
  48. package/src/frame-codec/FRAME_KEYFRAME_INTERVAL.js +1 -0
  49. package/src/frame-codec/FrameDelta.ts +6 -0
  50. package/src/frame-codec/FrameDeltaOp.ts +20 -0
  51. package/src/frame-codec/FrameEncoding.ts +1 -0
  52. package/src/frame-codec/JsonPath.ts +3 -0
  53. package/src/frame-codec/JsonPathSegment.ts +1 -0
  54. package/src/frame-codec/applyFrameDelta.js +143 -0
  55. package/src/frame-codec/applyFrameDeltaJson.js +10 -0
  56. package/src/frame-codec/encodeFrameDelta.js +247 -0
  57. package/src/frame-codec/index.js +15 -0
  58. package/src/frame-codec/normalizeFrameEncoding.js +13 -0
  59. package/src/frame-codec/parseFrameDelta.js +27 -0
  60. package/src/frame-codec/serializeFrameDelta.js +9 -0
  61. package/src/frame-codec.js +409 -0
  62. package/src/getSqlMessageStorage.js +11 -0
  63. package/src/index.d.ts +5203 -0
  64. package/src/index.js +20 -0
  65. package/src/input-bounds.js +12 -0
  66. package/src/input.js +17 -0
  67. package/src/internal-schema/index.js +19 -0
  68. package/src/internal-schema/smithersAlerts.js +27 -0
  69. package/src/internal-schema/smithersApprovals.js +18 -0
  70. package/src/internal-schema/smithersAttempts.js +20 -0
  71. package/src/internal-schema/smithersCache.js +13 -0
  72. package/src/internal-schema/smithersCron.js +11 -0
  73. package/src/internal-schema/smithersEvents.js +10 -0
  74. package/src/internal-schema/smithersFrames.js +14 -0
  75. package/src/internal-schema/smithersHumanRequests.js +17 -0
  76. package/src/internal-schema/smithersNodeDiffs.js +12 -0
  77. package/src/internal-schema/smithersNodes.js +13 -0
  78. package/src/internal-schema/smithersRalph.js +10 -0
  79. package/src/internal-schema/smithersRuns.js +22 -0
  80. package/src/internal-schema/smithersSandboxes.js +16 -0
  81. package/src/internal-schema/smithersSignals.js +12 -0
  82. package/src/internal-schema/smithersTimeTravelAudit.js +12 -0
  83. package/src/internal-schema/smithersToolCalls.js +19 -0
  84. package/src/internal-schema/smithersVectors.js +12 -0
  85. package/src/internal-schema.js +245 -0
  86. package/src/isRetryableSqliteWriteError.js +53 -0
  87. package/src/loadInputEffect.js +28 -0
  88. package/src/loadOutputsEffect.js +87 -0
  89. package/src/output/OutputKey.ts +1 -0
  90. package/src/output/buildKeyWhere.js +17 -0
  91. package/src/output/buildOutputRow.js +34 -0
  92. package/src/output/describeSchemaShape.js +70 -0
  93. package/src/output/getAgentOutputSchema.js +13 -0
  94. package/src/output/getKeyColumns.js +19 -0
  95. package/src/output/index.js +14 -0
  96. package/src/output/selectOutputRowEffect.js +30 -0
  97. package/src/output/stripAutoColumns.js +10 -0
  98. package/src/output/upsertOutputRowEffect.js +38 -0
  99. package/src/output/validateExistingOutput.js +17 -0
  100. package/src/output/validateOutput.js +17 -0
  101. package/src/output-schema-descriptor.js +163 -0
  102. package/src/output.js +240 -0
  103. package/src/react-output.js +10 -0
  104. package/src/runState/ComputeRunStateOptions.ts +4 -0
  105. package/src/runState/DeriveRunStateInput.ts +10 -0
  106. package/src/runState/RUN_STATE_HEARTBEAT_STALE_MS.js +1 -0
  107. package/src/runState/ReasonBlocked.ts +10 -0
  108. package/src/runState/ReasonUnhealthy.ts +6 -0
  109. package/src/runState/RunState.ts +12 -0
  110. package/src/runState/RunStateView.ts +11 -0
  111. package/src/runState/computeRunState.js +22 -0
  112. package/src/runState/computeRunStateFromRow.js +102 -0
  113. package/src/runState/deriveRunState.js +109 -0
  114. package/src/runState/parseEventMeta.js +18 -0
  115. package/src/runState/parseTimerMeta.js +16 -0
  116. package/src/runState-types.ts +23 -0
  117. package/src/runState.js +7 -0
  118. package/src/schema-signature.js +22 -0
  119. package/src/snapshot.js +125 -0
  120. package/src/sql-message-storage.js +839 -0
  121. package/src/storage/InMemoryStorage.js +484 -0
  122. package/src/storage/StorageService.js +7 -0
  123. package/src/storage/StorageServiceShape.ts +122 -0
  124. package/src/storage/StorageServiceTypes.ts +150 -0
  125. package/src/unwrapZodType.js +17 -0
  126. package/src/utils/camelToSnake.js +6 -0
  127. package/src/withSqliteWriteRetryEffect.js +110 -0
  128. package/src/write-retry.js +49 -0
  129. package/src/zodToCreateTableSQL.js +41 -0
  130. package/src/zodToTable.js +60 -0
@@ -0,0 +1,484 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { StorageService } from "./StorageService.js";
3
+ /** @typedef {import("./StorageServiceShape.ts").StorageServiceShape} StorageServiceShape */
4
+
5
+ /**
6
+ * @param {string} runId
7
+ * @param {string} nodeId
8
+ * @param {number} iteration
9
+ */
10
+ function nodeKey(runId, nodeId, iteration) {
11
+ return `${runId}::${nodeId}::${iteration}`;
12
+ }
13
+ /**
14
+ * @param {string} runId
15
+ * @param {string} nodeId
16
+ * @param {number} iteration
17
+ * @param {number} attempt
18
+ */
19
+ function attemptKey(runId, nodeId, iteration, attempt) {
20
+ return `${nodeKey(runId, nodeId, iteration)}::${attempt}`;
21
+ }
22
+ /**
23
+ * @param {OutputKey} key
24
+ * @returns {string}
25
+ */
26
+ function outputKey(key) {
27
+ return JSON.stringify(Object.entries(key).sort(([left], [right]) => left.localeCompare(right)));
28
+ }
29
+ /**
30
+ * @param {string} runId
31
+ * @param {string} nodeId
32
+ * @param {number} iteration
33
+ */
34
+ function approvalKey(runId, nodeId, iteration) {
35
+ return nodeKey(runId, nodeId, iteration);
36
+ }
37
+ /**
38
+ * @param {string} runId
39
+ * @param {string} ralphId
40
+ */
41
+ function ralphKey(runId, ralphId) {
42
+ return `${runId}::${ralphId}`;
43
+ }
44
+ /**
45
+ * @param {string} runId
46
+ * @param {string} sandboxId
47
+ */
48
+ function sandboxKey(runId, sandboxId) {
49
+ return `${runId}::${sandboxId}`;
50
+ }
51
+ /**
52
+ * @template T
53
+ * @param {readonly T[]} rows
54
+ * @returns {T[]}
55
+ */
56
+ function applyLimit(rows, limit = rows.length) {
57
+ return rows.slice(0, Math.max(0, Math.floor(limit)));
58
+ }
59
+ /**
60
+ * @param {EventRow} row
61
+ * @param {EventHistoryQuery} [query]
62
+ * @returns {boolean}
63
+ */
64
+ function eventMatchesQuery(row, query = {}) {
65
+ if (query.afterSeq !== undefined && row.seq <= query.afterSeq)
66
+ return false;
67
+ if (query.sinceTimestampMs !== undefined && row.timestampMs < query.sinceTimestampMs) {
68
+ return false;
69
+ }
70
+ if (query.types && query.types.length > 0 && !query.types.includes(row.type)) {
71
+ return false;
72
+ }
73
+ if (query.nodeId) {
74
+ try {
75
+ const payload = JSON.parse(row.payloadJson);
76
+ if (payload.nodeId !== query.nodeId)
77
+ return false;
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ }
83
+ return true;
84
+ }
85
+ /**
86
+ * @returns {StorageServiceShape}
87
+ */
88
+ export function makeInMemoryStorageService() {
89
+ const runs = new Map();
90
+ const frames = [];
91
+ const nodes = new Map();
92
+ const outputs = new Map();
93
+ const attempts = new Map();
94
+ const approvals = new Map();
95
+ const humanRequests = new Map();
96
+ const alerts = new Map();
97
+ const signals = [];
98
+ const toolCalls = [];
99
+ const sandboxes = new Map();
100
+ const events = [];
101
+ const ralphs = new Map();
102
+ const caches = new Map();
103
+ const crons = new Map();
104
+ const scorerResults = [];
105
+ /**
106
+ * @param {string} tableName
107
+ */
108
+ const table = (tableName) => {
109
+ const existing = outputs.get(tableName);
110
+ if (existing)
111
+ return existing;
112
+ const created = new Map();
113
+ outputs.set(tableName, created);
114
+ return created;
115
+ };
116
+ return {
117
+ rawQuery: (_queryString) => Effect.succeed([]),
118
+ insertRun: (run) => Effect.sync(() => {
119
+ runs.set(run.runId, run);
120
+ }),
121
+ updateRun: (runId, patch) => Effect.sync(() => {
122
+ const current = runs.get(runId) ?? { runId, status: "running" };
123
+ runs.set(runId, { ...current, ...patch });
124
+ }),
125
+ heartbeatRun: (runId, runtimeOwnerId, heartbeatAtMs) => Effect.sync(() => {
126
+ const current = runs.get(runId) ?? { runId, status: "running" };
127
+ runs.set(runId, { ...current, runtimeOwnerId, heartbeatAtMs });
128
+ }),
129
+ requestRunCancel: (runId, cancelRequestedAtMs) => Effect.sync(() => {
130
+ const current = runs.get(runId) ?? { runId, status: "running" };
131
+ runs.set(runId, { ...current, cancelRequestedAtMs });
132
+ }),
133
+ requestRunHijack: (runId, hijackRequestedAtMs, hijackTarget = null) => Effect.sync(() => {
134
+ const current = runs.get(runId) ?? { runId, status: "running" };
135
+ runs.set(runId, { ...current, hijackRequestedAtMs, hijackTarget });
136
+ }),
137
+ clearRunHijack: (runId) => Effect.sync(() => {
138
+ const current = runs.get(runId);
139
+ if (current) {
140
+ runs.set(runId, { ...current, hijackRequestedAtMs: null, hijackTarget: null });
141
+ }
142
+ }),
143
+ getRun: (runId) => Effect.sync(() => runs.get(runId) ?? null),
144
+ listRunAncestry: (runId, limit = 1000) => Effect.sync(() => {
145
+ const rows = [];
146
+ let current = runs.get(runId);
147
+ let depth = 0;
148
+ while (current && rows.length < limit) {
149
+ rows.push({
150
+ runId: current.runId,
151
+ parentRunId: current.parentRunId ?? null,
152
+ depth,
153
+ });
154
+ current = current.parentRunId ? runs.get(current.parentRunId) : undefined;
155
+ depth += 1;
156
+ }
157
+ return rows;
158
+ }),
159
+ getLatestChildRun: (parentRunId) => Effect.sync(() => [...runs.values()]
160
+ .filter((run) => run.parentRunId === parentRunId)
161
+ .sort((left, right) => (right.createdAtMs ?? 0) - (left.createdAtMs ?? 0))[0] ??
162
+ null),
163
+ listRuns: (limit = 50, status) => Effect.sync(() => applyLimit([...runs.values()]
164
+ .filter((run) => !status || run.status === status)
165
+ .sort((left, right) => (right.createdAtMs ?? 0) - (left.createdAtMs ?? 0)), limit)),
166
+ listStaleRunningRuns: (staleBeforeMs, limit = 1000) => Effect.sync(() => applyLimit([...runs.values()]
167
+ .filter((run) => run.status === "running" &&
168
+ (run.heartbeatAtMs == null || run.heartbeatAtMs < staleBeforeMs))
169
+ .map((run) => ({
170
+ runId: run.runId,
171
+ workflowPath: run.workflowPath ?? null,
172
+ heartbeatAtMs: run.heartbeatAtMs ?? null,
173
+ runtimeOwnerId: run.runtimeOwnerId ?? null,
174
+ status: run.status,
175
+ })), limit)),
176
+ claimRunForResume: (params) => Effect.sync(() => {
177
+ const run = runs.get(params.runId);
178
+ if (!run)
179
+ return false;
180
+ const expectedStatus = params.expectedStatus ?? "running";
181
+ const requireStale = params.requireStale ?? expectedStatus === "running";
182
+ const stale = run.heartbeatAtMs == null || run.heartbeatAtMs < params.staleBeforeMs;
183
+ if (run.status !== expectedStatus)
184
+ return false;
185
+ if ((run.runtimeOwnerId ?? null) !== params.expectedRuntimeOwnerId)
186
+ return false;
187
+ if ((run.heartbeatAtMs ?? null) !== params.expectedHeartbeatAtMs)
188
+ return false;
189
+ if (requireStale && !stale)
190
+ return false;
191
+ runs.set(params.runId, {
192
+ ...run,
193
+ runtimeOwnerId: params.claimOwnerId,
194
+ heartbeatAtMs: params.claimHeartbeatAtMs,
195
+ });
196
+ return true;
197
+ }),
198
+ releaseRunResumeClaim: (params) => Effect.sync(() => {
199
+ const run = runs.get(params.runId);
200
+ if (run?.runtimeOwnerId === params.claimOwnerId) {
201
+ runs.set(params.runId, {
202
+ ...run,
203
+ runtimeOwnerId: params.restoreRuntimeOwnerId,
204
+ heartbeatAtMs: params.restoreHeartbeatAtMs,
205
+ });
206
+ }
207
+ }),
208
+ updateClaimedRun: (params) => Effect.sync(() => {
209
+ const run = runs.get(params.runId);
210
+ if (!run)
211
+ return false;
212
+ if (run.runtimeOwnerId !== params.expectedRuntimeOwnerId)
213
+ return false;
214
+ if ((run.heartbeatAtMs ?? null) !== params.expectedHeartbeatAtMs)
215
+ return false;
216
+ runs.set(params.runId, { ...run, ...params.patch });
217
+ return true;
218
+ }),
219
+ insertNode: (node) => Effect.sync(() => {
220
+ nodes.set(nodeKey(node.runId, node.nodeId, node.iteration), node);
221
+ }),
222
+ getNode: (runId, nodeId, iteration) => Effect.sync(() => nodes.get(nodeKey(runId, nodeId, iteration)) ?? null),
223
+ listNodeIterations: (runId, nodeId) => Effect.sync(() => [...nodes.values()]
224
+ .filter((node) => node.runId === runId && node.nodeId === nodeId)
225
+ .sort((left, right) => left.iteration - right.iteration)),
226
+ listNodes: (runId) => Effect.sync(() => [...nodes.values()]
227
+ .filter((node) => node.runId === runId)
228
+ .sort((left, right) => left.updatedAtMs - right.updatedAtMs)),
229
+ countNodesByState: (runId) => Effect.sync(() => {
230
+ const counts = new Map();
231
+ for (const node of nodes.values()) {
232
+ if (node.runId !== runId)
233
+ continue;
234
+ counts.set(node.state, (counts.get(node.state) ?? 0) + 1);
235
+ }
236
+ return [...counts.entries()].map(([state, count]) => ({ state, count }));
237
+ }),
238
+ upsertOutputRow: (tableName, key, row) => Effect.sync(() => {
239
+ table(tableName).set(outputKey(key), { ...row, ...key });
240
+ }),
241
+ deleteOutputRow: (tableName, key) => Effect.sync(() => {
242
+ table(tableName).delete(outputKey(key));
243
+ }),
244
+ getRawNodeOutput: (tableName, runId, nodeId) => Effect.sync(() => [...table(tableName).values()].find((row) => row.runId === runId && row.nodeId === nodeId) ?? null),
245
+ getRawNodeOutputForIteration: (tableName, runId, nodeId, iteration) => Effect.sync(() => [...table(tableName).values()].find((row) => row.runId === runId &&
246
+ row.nodeId === nodeId &&
247
+ Number(row.iteration) === iteration) ?? null),
248
+ insertAttempt: (attempt) => Effect.sync(() => {
249
+ attempts.set(attemptKey(attempt.runId, attempt.nodeId, attempt.iteration, attempt.attempt), attempt);
250
+ }),
251
+ updateAttempt: (runId, nodeId, iteration, attempt, patch) => Effect.sync(() => {
252
+ const key = attemptKey(runId, nodeId, iteration, attempt);
253
+ const current = attempts.get(key);
254
+ if (current)
255
+ attempts.set(key, { ...current, ...patch });
256
+ }),
257
+ heartbeatAttempt: (runId, nodeId, iteration, attempt, heartbeatAtMs, heartbeatDataJson = null) => Effect.sync(() => {
258
+ const key = attemptKey(runId, nodeId, iteration, attempt);
259
+ const current = attempts.get(key);
260
+ if (current)
261
+ attempts.set(key, { ...current, heartbeatAtMs, heartbeatDataJson });
262
+ }),
263
+ listAttempts: (runId, nodeId, iteration) => Effect.sync(() => [...attempts.values()]
264
+ .filter((attempt) => attempt.runId === runId &&
265
+ attempt.nodeId === nodeId &&
266
+ attempt.iteration === iteration)
267
+ .sort((left, right) => left.attempt - right.attempt)),
268
+ listAttemptsForRun: (runId) => Effect.sync(() => [...attempts.values()]
269
+ .filter((attempt) => attempt.runId === runId)
270
+ .sort((left, right) => left.startedAtMs - right.startedAtMs)),
271
+ getAttempt: (runId, nodeId, iteration, attempt) => Effect.sync(() => attempts.get(attemptKey(runId, nodeId, iteration, attempt)) ?? null),
272
+ listInProgressAttempts: (runId) => Effect.sync(() => [...attempts.values()].filter((attempt) => attempt.runId === runId && attempt.state === "in-progress")),
273
+ listAllInProgressAttempts: () => Effect.sync(() => [...attempts.values()].filter((attempt) => attempt.state === "in-progress")),
274
+ insertFrame: (frame) => Effect.sync(() => {
275
+ frames.push(frame);
276
+ }),
277
+ getLastFrame: (runId) => Effect.sync(() => frames
278
+ .filter((frame) => frame.runId === runId)
279
+ .sort((left, right) => right.frameNo - left.frameNo)[0] ?? null),
280
+ deleteFramesAfter: (runId, frameNo) => Effect.sync(() => {
281
+ for (let index = frames.length - 1; index >= 0; index -= 1) {
282
+ const frame = frames[index];
283
+ if (frame.runId === runId && frame.frameNo > frameNo) {
284
+ frames.splice(index, 1);
285
+ }
286
+ }
287
+ }),
288
+ listFrames: (runId, limit, afterFrameNo) => Effect.sync(() => applyLimit(frames
289
+ .filter((frame) => frame.runId === runId &&
290
+ (afterFrameNo === undefined || frame.frameNo > afterFrameNo))
291
+ .sort((left, right) => right.frameNo - left.frameNo), limit)),
292
+ insertOrUpdateApproval: (approval) => Effect.sync(() => {
293
+ approvals.set(approvalKey(approval.runId, approval.nodeId, approval.iteration), approval);
294
+ }),
295
+ getApproval: (runId, nodeId, iteration) => Effect.sync(() => approvals.get(approvalKey(runId, nodeId, iteration)) ?? null),
296
+ listPendingApprovals: (runId) => Effect.sync(() => [...approvals.values()].filter((approval) => approval.runId === runId && approval.status === "pending")),
297
+ listAllPendingApprovals: () => Effect.sync(() => [...approvals.values()].filter((approval) => approval.status === "pending")),
298
+ listApprovalHistoryForNode: (workflowName, nodeId, limit = 50) => Effect.sync(() => {
299
+ const runIds = new Set([...runs.values()]
300
+ .filter((run) => run.workflowName === workflowName)
301
+ .map((run) => run.runId));
302
+ return applyLimit([...approvals.values()]
303
+ .filter((approval) => approval.nodeId === nodeId && runIds.has(approval.runId))
304
+ .sort((left, right) => (right.requestedAtMs ?? 0) - (left.requestedAtMs ?? 0)), limit);
305
+ }),
306
+ insertHumanRequest: (row) => Effect.sync(() => {
307
+ humanRequests.set(row.requestId, row);
308
+ }),
309
+ getHumanRequest: (requestId) => Effect.sync(() => humanRequests.get(requestId) ?? null),
310
+ reopenHumanRequest: (requestId) => Effect.sync(() => {
311
+ const current = humanRequests.get(requestId);
312
+ if (current) {
313
+ humanRequests.set(requestId, {
314
+ ...current,
315
+ status: "pending",
316
+ responseJson: null,
317
+ answeredAtMs: null,
318
+ answeredBy: null,
319
+ });
320
+ }
321
+ }),
322
+ expireStaleHumanRequests: (nowMs = Date.now()) => Effect.sync(() => {
323
+ const expired = [];
324
+ for (const current of humanRequests.values()) {
325
+ if (current.status === "pending" &&
326
+ current.timeoutAtMs != null &&
327
+ current.timeoutAtMs <= nowMs) {
328
+ const next = { ...current, status: "expired" };
329
+ humanRequests.set(current.requestId, next);
330
+ expired.push(next);
331
+ }
332
+ }
333
+ return expired;
334
+ }),
335
+ listPendingHumanRequests: (nowMs = Date.now()) => Effect.sync(() => [...humanRequests.values()]
336
+ .filter((request) => request.status === "pending" &&
337
+ (request.timeoutAtMs == null || request.timeoutAtMs > nowMs))
338
+ .map((request) => {
339
+ const run = runs.get(request.runId);
340
+ const node = nodes.get(nodeKey(request.runId, request.nodeId, request.iteration));
341
+ return {
342
+ ...request,
343
+ workflowName: run?.workflowName ?? null,
344
+ runStatus: run?.status ?? null,
345
+ nodeLabel: node?.label ?? null,
346
+ };
347
+ })),
348
+ answerHumanRequest: (requestId, responseJson, answeredBy = null, answeredAtMs = Date.now()) => Effect.sync(() => {
349
+ const current = humanRequests.get(requestId);
350
+ if (current) {
351
+ humanRequests.set(requestId, {
352
+ ...current,
353
+ status: "answered",
354
+ responseJson,
355
+ answeredBy,
356
+ answeredAtMs,
357
+ });
358
+ }
359
+ }),
360
+ cancelHumanRequest: (requestId) => Effect.sync(() => {
361
+ const current = humanRequests.get(requestId);
362
+ if (current)
363
+ humanRequests.set(requestId, { ...current, status: "cancelled" });
364
+ }),
365
+ insertAlert: (row) => Effect.sync(() => {
366
+ alerts.set(row.alertId, row);
367
+ }),
368
+ getAlert: (alertId) => Effect.sync(() => alerts.get(alertId) ?? null),
369
+ listAlerts: (limit = 100, statuses) => Effect.sync(() => applyLimit([...alerts.values()]
370
+ .filter((alert) => !statuses || statuses.includes(alert.status))
371
+ .sort((left, right) => right.firedAtMs - left.firedAtMs), limit)),
372
+ acknowledgeAlert: (alertId, acknowledgedAtMs = Date.now()) => Effect.sync(() => {
373
+ const current = alerts.get(alertId);
374
+ if (current)
375
+ alerts.set(alertId, { ...current, status: "acknowledged", acknowledgedAtMs });
376
+ }),
377
+ resolveAlert: (alertId, resolvedAtMs = Date.now()) => Effect.sync(() => {
378
+ const current = alerts.get(alertId);
379
+ if (current)
380
+ alerts.set(alertId, { ...current, status: "resolved", resolvedAtMs });
381
+ }),
382
+ silenceAlert: (alertId) => Effect.sync(() => {
383
+ const current = alerts.get(alertId);
384
+ if (current)
385
+ alerts.set(alertId, { ...current, status: "silenced" });
386
+ }),
387
+ insertSignalWithNextSeq: (row) => Effect.sync(() => {
388
+ const existing = signals.find((signal) => signal.runId === row.runId &&
389
+ signal.signalName === row.signalName &&
390
+ signal.correlationId === row.correlationId &&
391
+ signal.payloadJson === row.payloadJson &&
392
+ signal.receivedAtMs === row.receivedAtMs &&
393
+ (signal.receivedBy ?? null) === (row.receivedBy ?? null));
394
+ if (existing)
395
+ return existing.seq;
396
+ const seq = Math.max(-1, ...signals.filter((signal) => signal.runId === row.runId).map((signal) => signal.seq)) + 1;
397
+ signals.push({ ...row, receivedBy: row.receivedBy ?? null, seq });
398
+ return seq;
399
+ }),
400
+ getLastSignalSeq: (runId) => Effect.sync(() => {
401
+ const values = signals.filter((signal) => signal.runId === runId).map((signal) => signal.seq);
402
+ return values.length > 0 ? Math.max(...values) : null;
403
+ }),
404
+ listSignals: (runId, query = {}) => Effect.sync(() => applyLimit(signals
405
+ .filter((signal) => signal.runId === runId &&
406
+ (!query.signalName || signal.signalName === query.signalName) &&
407
+ (query.correlationId === undefined ||
408
+ signal.correlationId === query.correlationId) &&
409
+ (query.receivedAfterMs === undefined ||
410
+ signal.receivedAtMs >= query.receivedAfterMs))
411
+ .sort((left, right) => left.seq - right.seq), query.limit ?? 200)),
412
+ insertToolCall: (row) => Effect.sync(() => {
413
+ toolCalls.push(row);
414
+ }),
415
+ listToolCalls: (runId, nodeId, iteration) => Effect.sync(() => toolCalls.filter((row) => row.runId === runId && row.nodeId === nodeId && row.iteration === iteration)),
416
+ upsertSandbox: (row) => Effect.sync(() => {
417
+ sandboxes.set(sandboxKey(row.runId, row.sandboxId), row);
418
+ }),
419
+ getSandbox: (runId, sandboxId) => Effect.sync(() => sandboxes.get(sandboxKey(runId, sandboxId)) ?? null),
420
+ listSandboxes: (runId) => Effect.sync(() => [...sandboxes.values()].filter((row) => row.runId === runId)),
421
+ insertEvent: (row) => Effect.sync(() => {
422
+ events.push(row);
423
+ }),
424
+ insertEventWithNextSeq: (row) => Effect.sync(() => {
425
+ const existing = events.find((event) => event.runId === row.runId &&
426
+ event.timestampMs === row.timestampMs &&
427
+ event.type === row.type &&
428
+ event.payloadJson === row.payloadJson);
429
+ if (existing)
430
+ return existing.seq;
431
+ const seq = Math.max(-1, ...events.filter((event) => event.runId === row.runId).map((event) => event.seq)) + 1;
432
+ events.push({ ...row, seq });
433
+ return seq;
434
+ }),
435
+ getLastEventSeq: (runId) => Effect.sync(() => {
436
+ const values = events.filter((event) => event.runId === runId).map((event) => event.seq);
437
+ return values.length > 0 ? Math.max(...values) : null;
438
+ }),
439
+ listEventHistory: (runId, query = {}) => Effect.sync(() => applyLimit(events
440
+ .filter((event) => event.runId === runId && eventMatchesQuery(event, query))
441
+ .sort((left, right) => left.seq - right.seq), query.limit ?? 200)),
442
+ countEventHistory: (runId, query = {}) => Effect.sync(() => events.filter((event) => event.runId === runId && eventMatchesQuery(event, query))
443
+ .length),
444
+ listEvents: (runId, afterSeq, limit = 200) => Effect.sync(() => applyLimit(events
445
+ .filter((event) => event.runId === runId && event.seq > afterSeq)
446
+ .sort((left, right) => left.seq - right.seq), limit)),
447
+ listEventsByType: (runId, type) => Effect.sync(() => events
448
+ .filter((event) => event.runId === runId && event.type === type)
449
+ .sort((left, right) => left.seq - right.seq)),
450
+ insertOrUpdateRalph: (row) => Effect.sync(() => {
451
+ ralphs.set(ralphKey(row.runId, row.ralphId), row);
452
+ }),
453
+ listRalph: (runId) => Effect.sync(() => [...ralphs.values()].filter((row) => row.runId === runId)),
454
+ getRalph: (runId, ralphId) => Effect.sync(() => ralphs.get(ralphKey(runId, ralphId)) ?? null),
455
+ insertCache: (row) => Effect.sync(() => {
456
+ caches.set(row.cacheKey, row);
457
+ }),
458
+ getCache: (cacheKey) => Effect.sync(() => caches.get(cacheKey) ?? null),
459
+ listCacheByNode: (nodeId, outputTable, limit = 20) => Effect.sync(() => applyLimit([...caches.values()]
460
+ .filter((cache) => cache.nodeId === nodeId &&
461
+ (!outputTable || cache.outputTable === outputTable))
462
+ .sort((left, right) => right.createdAtMs - left.createdAtMs), limit)),
463
+ upsertCron: (row) => Effect.sync(() => {
464
+ crons.set(row.cronId, row);
465
+ }),
466
+ listCrons: (enabledOnly = true) => Effect.sync(() => [...crons.values()].filter((cron) => !enabledOnly || cron.enabled !== false)),
467
+ updateCronRunTime: (cronId, lastRunAtMs, nextRunAtMs, errorJson = null) => Effect.sync(() => {
468
+ const current = crons.get(cronId);
469
+ if (current)
470
+ crons.set(cronId, { ...current, lastRunAtMs, nextRunAtMs, errorJson });
471
+ }),
472
+ deleteCron: (cronId) => Effect.sync(() => {
473
+ crons.delete(cronId);
474
+ }),
475
+ insertScorerResult: (row) => Effect.sync(() => {
476
+ scorerResults.push(row);
477
+ }),
478
+ listScorerResults: (runId, nodeId) => Effect.sync(() => scorerResults
479
+ .filter((row) => row.runId === runId && (!nodeId || row.nodeId === nodeId))
480
+ .sort((left, right) => (left.scoredAtMs ?? 0) - (right.scoredAtMs ?? 0))),
481
+ withTransaction: (_label, effect) => effect,
482
+ };
483
+ }
484
+ export const InMemoryStorageLive = Layer.sync(StorageService, makeInMemoryStorageService);
@@ -0,0 +1,7 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./StorageServiceShape.ts").StorageServiceShape} StorageServiceShape */
3
+ // @smithers-type-exports-end
4
+
5
+ import { Context, Effect } from "effect";
6
+ export class StorageService extends Context.Tag("StorageService")() {
7
+ }
@@ -0,0 +1,122 @@
1
+ import type { Effect } from "effect";
2
+ import type { AlertRow } from "../adapter/AlertRow.ts";
3
+ import type { AlertStatus } from "../adapter/AlertStatus.ts";
4
+ import type { AttemptRow } from "../adapter/AttemptRow.ts";
5
+ import type { ApprovalRow } from "../adapter/ApprovalRow.ts";
6
+ import type { CacheRow } from "../adapter/CacheRow.ts";
7
+ import type { EventHistoryQuery } from "../adapter/EventHistoryQuery.ts";
8
+ import type { HumanRequestRow } from "../adapter/HumanRequestRow.ts";
9
+ import type { NodeRow } from "../adapter/NodeRow.ts";
10
+ import type { PendingHumanRequestRow } from "../adapter/PendingHumanRequestRow.ts";
11
+ import type { RunAncestryRow } from "../adapter/RunAncestryRow.ts";
12
+ import type { SignalQuery } from "../adapter/SignalQuery.ts";
13
+ import type { SignalRow } from "../adapter/SignalRow.ts";
14
+ import type { StaleRunRecord } from "../adapter/StaleRunRecord.ts";
15
+ import type {
16
+ Attempt,
17
+ AttemptPatch,
18
+ ClaimRunForResumeParams,
19
+ CronRow,
20
+ EventInsertRow,
21
+ EventRow,
22
+ FrameRow,
23
+ JsonRecord,
24
+ OutputKey,
25
+ RalphRow,
26
+ ReleaseRunResumeClaimParams,
27
+ Run,
28
+ RunPatch,
29
+ SandboxRow,
30
+ ScorerResultRow,
31
+ SignalInsertRow,
32
+ ToolCallRow,
33
+ UpdateClaimedRunParams,
34
+ } from "./StorageServiceTypes.ts";
35
+
36
+ export type StorageServiceShape = {
37
+ readonly rawQuery: (queryString: string) => Effect.Effect<readonly JsonRecord[]>;
38
+ readonly insertRun: (run: Run) => Effect.Effect<void>;
39
+ readonly updateRun: (runId: string, patch: RunPatch) => Effect.Effect<void>;
40
+ readonly heartbeatRun: (runId: string, runtimeOwnerId: string, heartbeatAtMs: number) => Effect.Effect<void>;
41
+ readonly requestRunCancel: (runId: string, cancelRequestedAtMs: number) => Effect.Effect<void>;
42
+ readonly requestRunHijack: (runId: string, hijackRequestedAtMs: number, hijackTarget?: string | null) => Effect.Effect<void>;
43
+ readonly clearRunHijack: (runId: string) => Effect.Effect<void>;
44
+ readonly getRun: (runId: string) => Effect.Effect<Run | null>;
45
+ readonly listRunAncestry: (runId: string, limit?: number) => Effect.Effect<readonly RunAncestryRow[]>;
46
+ readonly getLatestChildRun: (parentRunId: string) => Effect.Effect<Run | null>;
47
+ readonly listRuns: (limit?: number, status?: string) => Effect.Effect<readonly Run[]>;
48
+ readonly listStaleRunningRuns: (staleBeforeMs: number, limit?: number) => Effect.Effect<readonly StaleRunRecord[]>;
49
+ readonly claimRunForResume: (params: ClaimRunForResumeParams) => Effect.Effect<boolean>;
50
+ readonly releaseRunResumeClaim: (params: ReleaseRunResumeClaimParams) => Effect.Effect<void>;
51
+ readonly updateClaimedRun: (params: UpdateClaimedRunParams) => Effect.Effect<boolean>;
52
+ readonly insertNode: (node: NodeRow) => Effect.Effect<void>;
53
+ readonly getNode: (runId: string, nodeId: string, iteration: number) => Effect.Effect<NodeRow | null>;
54
+ readonly listNodeIterations: (runId: string, nodeId: string) => Effect.Effect<readonly NodeRow[]>;
55
+ readonly listNodes: (runId: string) => Effect.Effect<readonly NodeRow[]>;
56
+ readonly countNodesByState: (runId: string) => Effect.Effect<readonly {
57
+ readonly state: string;
58
+ readonly count: number;
59
+ }[]>;
60
+ readonly upsertOutputRow: (tableName: string, key: OutputKey, row: JsonRecord) => Effect.Effect<void>;
61
+ readonly deleteOutputRow: (tableName: string, key: OutputKey) => Effect.Effect<void>;
62
+ readonly getRawNodeOutput: (tableName: string, runId: string, nodeId: string) => Effect.Effect<JsonRecord | null>;
63
+ readonly getRawNodeOutputForIteration: (tableName: string, runId: string, nodeId: string, iteration: number) => Effect.Effect<JsonRecord | null>;
64
+ readonly insertAttempt: (attempt: AttemptRow) => Effect.Effect<void>;
65
+ readonly updateAttempt: (runId: string, nodeId: string, iteration: number, attempt: number, patch: AttemptPatch) => Effect.Effect<void>;
66
+ readonly heartbeatAttempt: (runId: string, nodeId: string, iteration: number, attempt: number, heartbeatAtMs: number, heartbeatDataJson?: string | null) => Effect.Effect<void>;
67
+ readonly listAttempts: (runId: string, nodeId: string, iteration: number) => Effect.Effect<readonly Attempt[]>;
68
+ readonly listAttemptsForRun: (runId: string) => Effect.Effect<readonly Attempt[]>;
69
+ readonly getAttempt: (runId: string, nodeId: string, iteration: number, attempt: number) => Effect.Effect<Attempt | null>;
70
+ readonly listInProgressAttempts: (runId: string) => Effect.Effect<readonly Attempt[]>;
71
+ readonly listAllInProgressAttempts: () => Effect.Effect<readonly Attempt[]>;
72
+ readonly insertFrame: (frame: FrameRow) => Effect.Effect<void>;
73
+ readonly getLastFrame: (runId: string) => Effect.Effect<FrameRow | null>;
74
+ readonly deleteFramesAfter: (runId: string, frameNo: number) => Effect.Effect<void>;
75
+ readonly listFrames: (runId: string, limit: number, afterFrameNo?: number) => Effect.Effect<readonly FrameRow[]>;
76
+ readonly insertOrUpdateApproval: (approval: ApprovalRow) => Effect.Effect<void>;
77
+ readonly getApproval: (runId: string, nodeId: string, iteration: number) => Effect.Effect<ApprovalRow | null>;
78
+ readonly listPendingApprovals: (runId: string) => Effect.Effect<readonly ApprovalRow[]>;
79
+ readonly listAllPendingApprovals: () => Effect.Effect<readonly ApprovalRow[]>;
80
+ readonly listApprovalHistoryForNode: (workflowName: string, nodeId: string, limit?: number) => Effect.Effect<readonly ApprovalRow[]>;
81
+ readonly insertHumanRequest: (row: HumanRequestRow) => Effect.Effect<void>;
82
+ readonly getHumanRequest: (requestId: string) => Effect.Effect<HumanRequestRow | null>;
83
+ readonly reopenHumanRequest: (requestId: string) => Effect.Effect<void>;
84
+ readonly expireStaleHumanRequests: (nowMs?: number) => Effect.Effect<readonly HumanRequestRow[]>;
85
+ readonly listPendingHumanRequests: (nowMs?: number) => Effect.Effect<readonly PendingHumanRequestRow[]>;
86
+ readonly answerHumanRequest: (requestId: string, responseJson: string, answeredBy?: string | null, answeredAtMs?: number) => Effect.Effect<void>;
87
+ readonly cancelHumanRequest: (requestId: string) => Effect.Effect<void>;
88
+ readonly insertAlert: (row: AlertRow) => Effect.Effect<void>;
89
+ readonly getAlert: (alertId: string) => Effect.Effect<AlertRow | null>;
90
+ readonly listAlerts: (limit?: number, statuses?: readonly AlertStatus[]) => Effect.Effect<readonly AlertRow[]>;
91
+ readonly acknowledgeAlert: (alertId: string, acknowledgedAtMs?: number) => Effect.Effect<void>;
92
+ readonly resolveAlert: (alertId: string, resolvedAtMs?: number) => Effect.Effect<void>;
93
+ readonly silenceAlert: (alertId: string) => Effect.Effect<void>;
94
+ readonly insertSignalWithNextSeq: (row: SignalInsertRow) => Effect.Effect<number>;
95
+ readonly getLastSignalSeq: (runId: string) => Effect.Effect<number | null>;
96
+ readonly listSignals: (runId: string, query?: SignalQuery) => Effect.Effect<readonly SignalRow[]>;
97
+ readonly insertToolCall: (row: ToolCallRow) => Effect.Effect<void>;
98
+ readonly listToolCalls: (runId: string, nodeId: string, iteration: number) => Effect.Effect<readonly ToolCallRow[]>;
99
+ readonly upsertSandbox: (row: SandboxRow) => Effect.Effect<void>;
100
+ readonly getSandbox: (runId: string, sandboxId: string) => Effect.Effect<SandboxRow | null>;
101
+ readonly listSandboxes: (runId: string) => Effect.Effect<readonly SandboxRow[]>;
102
+ readonly insertEvent: (row: EventRow) => Effect.Effect<void>;
103
+ readonly insertEventWithNextSeq: (row: EventInsertRow) => Effect.Effect<number>;
104
+ readonly getLastEventSeq: (runId: string) => Effect.Effect<number | null>;
105
+ readonly listEventHistory: (runId: string, query?: EventHistoryQuery) => Effect.Effect<readonly EventRow[]>;
106
+ readonly countEventHistory: (runId: string, query?: EventHistoryQuery) => Effect.Effect<number>;
107
+ readonly listEvents: (runId: string, afterSeq: number, limit?: number) => Effect.Effect<readonly EventRow[]>;
108
+ readonly listEventsByType: (runId: string, type: string) => Effect.Effect<readonly EventRow[]>;
109
+ readonly insertOrUpdateRalph: (row: RalphRow) => Effect.Effect<void>;
110
+ readonly listRalph: (runId: string) => Effect.Effect<readonly RalphRow[]>;
111
+ readonly getRalph: (runId: string, ralphId: string) => Effect.Effect<RalphRow | null>;
112
+ readonly insertCache: (row: CacheRow) => Effect.Effect<void>;
113
+ readonly getCache: (cacheKey: string) => Effect.Effect<CacheRow | null>;
114
+ readonly listCacheByNode: (nodeId: string, outputTable?: string, limit?: number) => Effect.Effect<readonly CacheRow[]>;
115
+ readonly upsertCron: (row: CronRow) => Effect.Effect<void>;
116
+ readonly listCrons: (enabledOnly?: boolean) => Effect.Effect<readonly CronRow[]>;
117
+ readonly updateCronRunTime: (cronId: string, lastRunAtMs: number, nextRunAtMs: number, errorJson?: string | null) => Effect.Effect<void>;
118
+ readonly deleteCron: (cronId: string) => Effect.Effect<void>;
119
+ readonly insertScorerResult: (row: ScorerResultRow) => Effect.Effect<void>;
120
+ readonly listScorerResults: (runId: string, nodeId?: string) => Effect.Effect<readonly ScorerResultRow[]>;
121
+ readonly withTransaction: <A, E, R>(label: string, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
122
+ };