@smithers-orchestrator/engine 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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +50 -0
  3. package/src/AlertHumanRequestOptions.ts +8 -0
  4. package/src/AlertRuntimeServices.ts +10 -0
  5. package/src/ChildWorkflowDefinition.ts +5 -0
  6. package/src/ChildWorkflowExecuteOptions.ts +14 -0
  7. package/src/ContinuationRequest.ts +3 -0
  8. package/src/HijackState.ts +19 -0
  9. package/src/HumanRequestKind.ts +1 -0
  10. package/src/HumanRequestStatus.ts +1 -0
  11. package/src/PlanNode.ts +29 -0
  12. package/src/RalphMeta.ts +7 -0
  13. package/src/RalphState.ts +4 -0
  14. package/src/RalphStateMap.ts +3 -0
  15. package/src/ScheduleResult.ts +15 -0
  16. package/src/SignalRunOptions.ts +5 -0
  17. package/src/alert-runtime.js +22 -0
  18. package/src/approvals.js +220 -0
  19. package/src/child-workflow.js +163 -0
  20. package/src/effect/ApprovalDeferredResolution.ts +13 -0
  21. package/src/effect/ApprovalDurableDeferredResolution.ts +11 -0
  22. package/src/effect/ApprovalPayload.ts +7 -0
  23. package/src/effect/ApprovalResult.ts +6 -0
  24. package/src/effect/BuilderNode.ts +52 -0
  25. package/src/effect/BuilderStepHandle.ts +47 -0
  26. package/src/effect/CancelPayload.ts +3 -0
  27. package/src/effect/CancelResult.ts +4 -0
  28. package/src/effect/DeferredResolution.ts +7 -0
  29. package/src/effect/DiffBundle.ts +7 -0
  30. package/src/effect/ExecuteTaskActivityOptions.ts +7 -0
  31. package/src/effect/FilePatch.ts +6 -0
  32. package/src/effect/GetRunPayload.ts +3 -0
  33. package/src/effect/GetRunResult.ts +3 -0
  34. package/src/effect/LegacyExecuteTaskFn.ts +24 -0
  35. package/src/effect/ListRunsPayload.ts +6 -0
  36. package/src/effect/RunStatusSchema.ts +9 -0
  37. package/src/effect/RunSummary.ts +23 -0
  38. package/src/effect/SignalPayload.ts +7 -0
  39. package/src/effect/SignalResult.ts +6 -0
  40. package/src/effect/SmithersSqliteOptions.ts +3 -0
  41. package/src/effect/SqlMessageStorageEventHistoryQuery.ts +7 -0
  42. package/src/effect/TaggedWorkerError.ts +46 -0
  43. package/src/effect/TaskActivityContext.ts +4 -0
  44. package/src/effect/TaskActivityRetryOptions.ts +4 -0
  45. package/src/effect/TaskBridgeToolConfig.ts +6 -0
  46. package/src/effect/TaskFailure.ts +3 -0
  47. package/src/effect/TaskResult.ts +5 -0
  48. package/src/effect/UnknownWorkerError.ts +5 -0
  49. package/src/effect/WaitForEventDurableDeferredResolution.ts +11 -0
  50. package/src/effect/WorkerDispatchKind.ts +1 -0
  51. package/src/effect/WorkerTask.ts +14 -0
  52. package/src/effect/WorkerTaskError.ts +4 -0
  53. package/src/effect/WorkerTaskKind.ts +1 -0
  54. package/src/effect/WorkflowPatchDecisionRecord.ts +4 -0
  55. package/src/effect/WorkflowPatchDecisions.ts +1 -0
  56. package/src/effect/WorkflowVersioningRuntime.ts +7 -0
  57. package/src/effect/activity-bridge.js +131 -0
  58. package/src/effect/bridge-utils.js +45 -0
  59. package/src/effect/builder.js +837 -0
  60. package/src/effect/compute-task-bridge.js +734 -0
  61. package/src/effect/deferred-bridge.js +63 -0
  62. package/src/effect/deferred-state-bridge.js +1343 -0
  63. package/src/effect/diff-bundle.js +352 -0
  64. package/src/effect/durable-deferred-bridge.js +282 -0
  65. package/src/effect/entity-worker.js +154 -0
  66. package/src/effect/http-runner.js +86 -0
  67. package/src/effect/rpc-schema.js +101 -0
  68. package/src/effect/single-runner.js +189 -0
  69. package/src/effect/sql-message-storage.js +817 -0
  70. package/src/effect/static-task-bridge.js +308 -0
  71. package/src/effect/versioning.js +123 -0
  72. package/src/effect/workflow-bridge.js +260 -0
  73. package/src/effect/workflow-make-bridge.js +233 -0
  74. package/src/engine.js +6933 -0
  75. package/src/events.js +237 -0
  76. package/src/external/json-schema-to-zod.js +214 -0
  77. package/src/getDefinedToolMetadata.js +10 -0
  78. package/src/hot/HotReloadEvent.ts +21 -0
  79. package/src/hot/HotWorkflowController.js +220 -0
  80. package/src/hot/OverlayOptions.ts +4 -0
  81. package/src/hot/WatchTreeOptions.ts +6 -0
  82. package/src/hot/index.js +9 -0
  83. package/src/hot/overlay.js +177 -0
  84. package/src/hot/watch.js +174 -0
  85. package/src/human-requests.js +120 -0
  86. package/src/index.d.ts +1597 -0
  87. package/src/index.js +41 -0
  88. package/src/runtime-owner.js +36 -0
  89. package/src/scheduler.js +31 -0
  90. package/src/signals.js +82 -0
@@ -0,0 +1,1343 @@
1
+ import React from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { Effect, Exit } from "effect";
4
+ import { buildOutputRow, describeSchemaShape, selectOutputRow, stripAutoColumns, validateExistingOutput, validateOutput, } from "@smithers-orchestrator/db/output";
5
+ import { awaitApprovalDurableDeferred, awaitWaitForEventDurableDeferred, bridgeApprovalResolve, bridgeWaitForEventResolve, } from "./durable-deferred-bridge.js";
6
+ import { EventBus } from "../events.js";
7
+ import { buildHumanRequestId, getHumanTaskPrompt as getStoredHumanTaskPrompt, isHumanTaskMeta, } from "../human-requests.js";
8
+ import { parseAttemptMetaJson } from "./bridge-utils.js";
9
+ import { updateAsyncExternalWaitPending } from "@smithers-orchestrator/observability/metrics";
10
+ import { markdownComponents } from "@smithers-orchestrator/components/markdownComponents";
11
+ import { errorToJson } from "@smithers-orchestrator/errors/errorToJson";
12
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
13
+ import { nowMs } from "@smithers-orchestrator/scheduler/nowMs";
14
+ /**
15
+ * @typedef {"pending" | "waiting-approval" | "waiting-event" | "waiting-timer" | "finished" | "failed" | "skipped"} DeferredBridgeState
16
+ */
17
+ /**
18
+ * @typedef {{ handled: false; } | { handled: true; state: DeferredBridgeState; }} DeferredBridgeResolution
19
+ */
20
+ /**
21
+ * @typedef {(state: "pending" | "failed" | "skipped") => Promise<void>} DeferredBridgeStateEmitter
22
+ */
23
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} _SmithersDb */
24
+ /** @typedef {import("@smithers-orchestrator/db/adapter/ApprovalRow").ApprovalRow} ApprovalRow */
25
+ /** @typedef {import("@smithers-orchestrator/graph/TaskDescriptor").TaskDescriptor} _TaskDescriptor */
26
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase<Record<string, unknown>>} BunSQLiteDatabase */
27
+
28
+ const timerDurationMultipliers = {
29
+ ms: 1,
30
+ s: 1_000,
31
+ m: 60_000,
32
+ h: 3_600_000,
33
+ d: 86_400_000,
34
+ };
35
+ /**
36
+ * @param {"approval" | "event"} kind
37
+ * @param {number} delta
38
+ */
39
+ async function updateAsyncExternalWaitPendingSafe(kind, delta) {
40
+ try {
41
+ await Effect.runPromise(updateAsyncExternalWaitPending(kind, delta));
42
+ }
43
+ catch { }
44
+ }
45
+ /**
46
+ * @param {Pick<WaitForEventSnapshot, "waitAsync" | "resolvedSignalSeq"> | null | undefined} snapshot
47
+ */
48
+ function shouldClearAsyncWaitMetric(snapshot) {
49
+ return Boolean(snapshot?.waitAsync &&
50
+ !Number.isFinite(Number(snapshot.resolvedSignalSeq)));
51
+ }
52
+ /**
53
+ * @param {_TaskDescriptor} desc
54
+ */
55
+ function buildApprovalRequestJson(desc) {
56
+ return JSON.stringify({
57
+ mode: desc.approvalMode ?? "gate",
58
+ waitAsync: desc.waitAsync === true,
59
+ title: desc.label ?? null,
60
+ summary: desc.meta && typeof desc.meta.requestSummary === "string"
61
+ ? desc.meta.requestSummary
62
+ : null,
63
+ options: desc.approvalOptions ?? [],
64
+ allowedScopes: desc.approvalAllowedScopes ?? [],
65
+ allowedUsers: desc.approvalAllowedUsers ?? [],
66
+ autoApprove: desc.approvalAutoApprove ?? null,
67
+ });
68
+ }
69
+ /**
70
+ * @param {_TaskDescriptor} desc
71
+ * @returns {string | null}
72
+ */
73
+ function buildHumanRequestSchemaJson(desc) {
74
+ if (!desc.outputSchema && !desc.outputTable) {
75
+ return null;
76
+ }
77
+ return describeSchemaShape((desc.outputSchema ?? desc.outputTable), desc.outputSchema);
78
+ }
79
+ /**
80
+ * @param {unknown} prompt
81
+ * @returns {string}
82
+ */
83
+ function renderHumanPromptToText(prompt) {
84
+ if (prompt == null)
85
+ return "";
86
+ if (typeof prompt === "string")
87
+ return prompt;
88
+ if (typeof prompt === "number")
89
+ return String(prompt);
90
+ try {
91
+ let element;
92
+ if (React.isValidElement(prompt)) {
93
+ element = React.cloneElement(prompt, {
94
+ components: markdownComponents,
95
+ });
96
+ }
97
+ else {
98
+ element = React.createElement(React.Fragment, null, prompt);
99
+ }
100
+ return renderToStaticMarkup(element)
101
+ .replace(/\n{3,}/g, "\n\n")
102
+ .trim();
103
+ }
104
+ catch {
105
+ const result = String(prompt ?? "");
106
+ if (result === "[object Object]") {
107
+ throw new SmithersError("MDX_PRELOAD_INACTIVE", "HumanTask prompt could not be rendered because the MDX preload is inactive.");
108
+ }
109
+ return result;
110
+ }
111
+ }
112
+ /**
113
+ * @param {Record<string, unknown> | null | undefined} meta
114
+ * @param {string} fallback
115
+ * @returns {string}
116
+ */
117
+ function getHumanTaskPrompt(meta, fallback) {
118
+ const renderedPrompt = renderHumanPromptToText(meta?.prompt);
119
+ return renderedPrompt.trim().length > 0
120
+ ? renderedPrompt
121
+ : getStoredHumanTaskPrompt(meta, fallback);
122
+ }
123
+ /**
124
+ * @param {_SmithersDb} adapter
125
+ * @param {string} runId
126
+ * @param {_TaskDescriptor} desc
127
+ * @param {number} requestedAtMs
128
+ */
129
+ async function ensurePendingHumanRequest(adapter, runId, desc, requestedAtMs) {
130
+ if (!isHumanTaskMeta(desc.meta)) {
131
+ return;
132
+ }
133
+ const requestId = buildHumanRequestId(runId, desc.nodeId, desc.iteration);
134
+ const existing = await Effect.runPromise(adapter.getHumanRequest(requestId));
135
+ if (existing) {
136
+ return;
137
+ }
138
+ await Effect.runPromise(adapter.insertHumanRequest({
139
+ requestId,
140
+ runId,
141
+ nodeId: desc.nodeId,
142
+ iteration: desc.iteration,
143
+ kind: "json",
144
+ status: "pending",
145
+ prompt: getHumanTaskPrompt(desc.meta, desc.label ?? desc.nodeId),
146
+ schemaJson: buildHumanRequestSchemaJson(desc),
147
+ optionsJson: null,
148
+ responseJson: null,
149
+ requestedAtMs,
150
+ answeredAtMs: null,
151
+ answeredBy: null,
152
+ timeoutAtMs: typeof desc.timeoutMs === "number" ? requestedAtMs + desc.timeoutMs : null,
153
+ }));
154
+ }
155
+ const HUMAN_REQUEST_REOPEN_ERROR_CODES = new Set([
156
+ "HUMAN_TASK_INVALID_JSON",
157
+ "HUMAN_TASK_VALIDATION_FAILED",
158
+ ]);
159
+ /**
160
+ * @param {string | null} [errorJson]
161
+ * @returns {string | null}
162
+ */
163
+ function parseAttemptErrorCode(errorJson) {
164
+ if (!errorJson) {
165
+ return null;
166
+ }
167
+ try {
168
+ const parsed = JSON.parse(errorJson);
169
+ return typeof parsed?.code === "string" ? parsed.code : null;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ /**
176
+ * @param {_SmithersDb} adapter
177
+ * @param {string} runId
178
+ * @param {_TaskDescriptor} desc
179
+ */
180
+ async function reconcileHumanRequestValidationFailure(adapter, runId, desc) {
181
+ if (!isHumanTaskMeta(desc.meta)) {
182
+ return undefined;
183
+ }
184
+ const requestId = buildHumanRequestId(runId, desc.nodeId, desc.iteration);
185
+ const request = await Effect.runPromise(adapter.getHumanRequest(requestId));
186
+ if (!request || request.status !== "answered") {
187
+ return request;
188
+ }
189
+ const attempts = await Effect.runPromise(adapter.listAttempts(runId, desc.nodeId, desc.iteration));
190
+ const latestAttempt = attempts[0];
191
+ if (latestAttempt?.state !== "failed" ||
192
+ !HUMAN_REQUEST_REOPEN_ERROR_CODES.has(parseAttemptErrorCode(latestAttempt?.errorJson) ?? "")) {
193
+ return request;
194
+ }
195
+ if (typeof request.answeredAtMs === "number" &&
196
+ typeof latestAttempt?.finishedAtMs === "number" &&
197
+ request.answeredAtMs > latestAttempt.finishedAtMs) {
198
+ return request;
199
+ }
200
+ await Effect.runPromise(adapter.reopenHumanRequest(requestId));
201
+ return {
202
+ ...request,
203
+ status: "pending",
204
+ responseJson: null,
205
+ answeredAtMs: null,
206
+ answeredBy: null,
207
+ };
208
+ }
209
+ /**
210
+ * @param {_TaskDescriptor} desc
211
+ */
212
+ function defaultAutoApprovalDecision(desc) {
213
+ if (desc.approvalMode === "select") {
214
+ const selected = desc.approvalOptions?.[0]?.key;
215
+ return selected ? { selected, notes: "Automatically selected" } : null;
216
+ }
217
+ if (desc.approvalMode === "rank") {
218
+ const ranked = desc.approvalOptions?.map((option) => option.key) ?? [];
219
+ return { ranked, notes: "Automatically ranked" };
220
+ }
221
+ return null;
222
+ }
223
+ /**
224
+ * @param {_SmithersDb} adapter
225
+ * @param {string} runId
226
+ * @param {_TaskDescriptor} desc
227
+ */
228
+ async function shouldAutoApprove(adapter, runId, desc) {
229
+ const config = desc.approvalAutoApprove;
230
+ if (!config) {
231
+ return false;
232
+ }
233
+ if (config.revertOnMet) {
234
+ return false;
235
+ }
236
+ if (config.conditionMet === false) {
237
+ return false;
238
+ }
239
+ const after = typeof config.after === "number" ? config.after : 0;
240
+ if (after <= 0) {
241
+ return true;
242
+ }
243
+ const run = await Effect.runPromise(adapter.getRun(runId));
244
+ if (!run?.workflowName) {
245
+ return false;
246
+ }
247
+ const history = await Effect.runPromise(adapter.listApprovalHistoryForNode(run.workflowName, desc.nodeId, after + 10));
248
+ let consecutive = 0;
249
+ for (const entry of history) {
250
+ if (entry.runId === runId) {
251
+ continue;
252
+ }
253
+ if (entry.autoApproved) {
254
+ continue;
255
+ }
256
+ if (entry.status === "approved") {
257
+ consecutive += 1;
258
+ if (consecutive >= after) {
259
+ return true;
260
+ }
261
+ continue;
262
+ }
263
+ if (entry.status === "denied") {
264
+ return false;
265
+ }
266
+ }
267
+ return false;
268
+ }
269
+ /**
270
+ * @param {_TaskDescriptor} desc
271
+ * @returns {boolean}
272
+ */
273
+ export function isBridgeManagedTimerTask(desc) {
274
+ return Boolean(desc.meta && desc.meta.__timer);
275
+ }
276
+ /**
277
+ * @param {_TaskDescriptor} desc
278
+ * @returns {boolean}
279
+ */
280
+ export function isBridgeManagedWaitForEventTask(desc) {
281
+ return Boolean(desc.meta && desc.meta.__waitForEvent);
282
+ }
283
+ /**
284
+ * @param {_TaskDescriptor} desc
285
+ * @returns {TimerType}
286
+ */
287
+ function parseTimerType(desc) {
288
+ const raw = desc.meta?.__timerType;
289
+ return raw === "absolute" ? "absolute" : "duration";
290
+ }
291
+ /**
292
+ * @param {_TaskDescriptor} desc
293
+ * @returns {string}
294
+ */
295
+ function parseWaitForEventSignalName(desc) {
296
+ const signalName = String(desc.meta?.__eventName ?? "").trim();
297
+ if (!signalName) {
298
+ throw new SmithersError("INVALID_INPUT", `WaitForEvent ${desc.nodeId} is missing event metadata.`, { nodeId: desc.nodeId });
299
+ }
300
+ return signalName;
301
+ }
302
+ /**
303
+ * @param {_TaskDescriptor} desc
304
+ * @returns {string | undefined}
305
+ */
306
+ function parseWaitForEventCorrelationId(desc) {
307
+ const raw = desc.meta?.__correlationId;
308
+ return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : undefined;
309
+ }
310
+ /**
311
+ * @param {_TaskDescriptor} desc
312
+ * @returns {WaitForEventOnTimeout}
313
+ */
314
+ function parseWaitForEventOnTimeout(desc) {
315
+ const raw = desc.meta?.__onTimeout;
316
+ return raw === "continue" || raw === "skip" ? raw : "fail";
317
+ }
318
+ /**
319
+ * @param {unknown} value
320
+ * @returns {number | undefined}
321
+ */
322
+ function parseOptionalFiniteNumber(value) {
323
+ if (value == null || value === "") {
324
+ return undefined;
325
+ }
326
+ const parsed = Number(value);
327
+ return Number.isFinite(parsed) ? parsed : undefined;
328
+ }
329
+ /**
330
+ * @param {_TaskDescriptor} desc
331
+ * @param {number} startedAtMs
332
+ * @returns {WaitForEventSnapshot}
333
+ */
334
+ function buildWaitForEventSnapshot(desc, startedAtMs) {
335
+ return {
336
+ signalName: parseWaitForEventSignalName(desc),
337
+ correlationId: parseWaitForEventCorrelationId(desc),
338
+ onTimeout: parseWaitForEventOnTimeout(desc),
339
+ timeoutMs: typeof desc.timeoutMs === "number" && Number.isFinite(desc.timeoutMs)
340
+ ? desc.timeoutMs
341
+ : null,
342
+ waitAsync: desc.waitAsync === true,
343
+ startedAtMs,
344
+ };
345
+ }
346
+ /**
347
+ * @param {string | null} [metaJson]
348
+ * @returns {WaitForEventSnapshot | null}
349
+ */
350
+ function parseWaitForEventSnapshot(metaJson) {
351
+ const meta = parseAttemptMetaJson(metaJson);
352
+ const waitForEvent = meta.waitForEvent;
353
+ if (!waitForEvent ||
354
+ typeof waitForEvent !== "object" ||
355
+ Array.isArray(waitForEvent)) {
356
+ return null;
357
+ }
358
+ const signalName = typeof waitForEvent.signalName === "string"
359
+ ? waitForEvent.signalName
360
+ : null;
361
+ const startedAtMs = Number(waitForEvent.startedAtMs);
362
+ if (!signalName || !Number.isFinite(startedAtMs)) {
363
+ return null;
364
+ }
365
+ const timeoutMsRaw = waitForEvent.timeoutMs;
366
+ const timeoutMs = timeoutMsRaw == null || timeoutMsRaw === ""
367
+ ? null
368
+ : Number.isFinite(Number(timeoutMsRaw))
369
+ ? Number(timeoutMsRaw)
370
+ : null;
371
+ const resolvedSignalSeqRaw = waitForEvent.resolvedSignalSeq;
372
+ const receivedAtMsRaw = waitForEvent.receivedAtMs;
373
+ const timedOutAtMsRaw = waitForEvent.timedOutAtMs;
374
+ return {
375
+ signalName,
376
+ correlationId: typeof waitForEvent.correlationId === "string"
377
+ ? waitForEvent.correlationId
378
+ : undefined,
379
+ onTimeout: waitForEvent.onTimeout === "continue" ||
380
+ waitForEvent.onTimeout === "skip"
381
+ ? waitForEvent.onTimeout
382
+ : "fail",
383
+ timeoutMs,
384
+ waitAsync: waitForEvent.waitAsync === true,
385
+ startedAtMs,
386
+ resolvedSignalSeq: parseOptionalFiniteNumber(resolvedSignalSeqRaw),
387
+ receivedAtMs: parseOptionalFiniteNumber(receivedAtMsRaw),
388
+ timedOutAtMs: parseOptionalFiniteNumber(timedOutAtMsRaw),
389
+ };
390
+ }
391
+ /**
392
+ * @param {WaitForEventSnapshot} snapshot
393
+ * @returns {Record<string, unknown>}
394
+ */
395
+ function buildWaitForEventAttemptMeta(snapshot) {
396
+ return {
397
+ kind: "wait-for-event",
398
+ waitForEvent: {
399
+ signalName: snapshot.signalName,
400
+ correlationId: snapshot.correlationId ?? null,
401
+ onTimeout: snapshot.onTimeout,
402
+ timeoutMs: snapshot.timeoutMs,
403
+ waitAsync: snapshot.waitAsync === true,
404
+ startedAtMs: snapshot.startedAtMs,
405
+ resolvedSignalSeq: snapshot.resolvedSignalSeq ?? null,
406
+ receivedAtMs: snapshot.receivedAtMs ?? null,
407
+ timedOutAtMs: snapshot.timedOutAtMs ?? null,
408
+ },
409
+ };
410
+ }
411
+ /**
412
+ * @param {string} raw
413
+ * @param {string} nodeId
414
+ * @returns {number}
415
+ */
416
+ function parseTimerDurationMs(raw, nodeId) {
417
+ const input = raw.trim().toLowerCase();
418
+ const match = input.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/);
419
+ if (!match) {
420
+ throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} has invalid duration "${raw}". Use formats like 500ms, 10s, 2m.`, { nodeId, duration: raw });
421
+ }
422
+ const value = Number(match[1]);
423
+ const unit = match[2] ?? "ms";
424
+ const multiplier = timerDurationMultipliers[unit];
425
+ const ms = Math.floor(value * multiplier);
426
+ if (!Number.isFinite(ms) || ms < 0) {
427
+ throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} duration "${raw}" is not valid.`, { nodeId, duration: raw });
428
+ }
429
+ return ms;
430
+ }
431
+ /**
432
+ * @param {string} raw
433
+ * @param {string} nodeId
434
+ * @returns {number}
435
+ */
436
+ function parseTimerUntilMs(raw, nodeId) {
437
+ const parsed = Date.parse(raw);
438
+ if (!Number.isFinite(parsed)) {
439
+ throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} has invalid "until" timestamp "${raw}".`, { nodeId, until: raw });
440
+ }
441
+ return Math.floor(parsed);
442
+ }
443
+ /**
444
+ * @param {_TaskDescriptor} desc
445
+ * @param {number} createdAtMs
446
+ * @returns {TimerSnapshot}
447
+ */
448
+ function buildTimerSnapshot(desc, createdAtMs) {
449
+ const timerType = parseTimerType(desc);
450
+ const timerId = desc.nodeId;
451
+ if (timerType === "duration") {
452
+ const duration = String(desc.meta?.__timerDuration ?? "").trim();
453
+ if (!duration) {
454
+ throw new SmithersError("INVALID_INPUT", `Timer ${timerId} is missing duration metadata.`, { nodeId: timerId });
455
+ }
456
+ const delayMs = parseTimerDurationMs(duration, timerId);
457
+ return {
458
+ timerId,
459
+ timerType,
460
+ duration,
461
+ createdAtMs,
462
+ firesAtMs: createdAtMs + delayMs,
463
+ };
464
+ }
465
+ const until = String(desc.meta?.__timerUntil ?? "").trim();
466
+ if (!until) {
467
+ throw new SmithersError("INVALID_INPUT", `Timer ${timerId} is missing until metadata.`, { nodeId: timerId });
468
+ }
469
+ return {
470
+ timerId,
471
+ timerType,
472
+ until,
473
+ createdAtMs,
474
+ firesAtMs: parseTimerUntilMs(until, timerId),
475
+ };
476
+ }
477
+ /**
478
+ * @param {string | null} [metaJson]
479
+ * @returns {TimerSnapshot | null}
480
+ */
481
+ function parseTimerSnapshot(metaJson) {
482
+ const meta = parseAttemptMetaJson(metaJson);
483
+ const timer = meta.timer;
484
+ if (!timer || typeof timer !== "object" || Array.isArray(timer))
485
+ return null;
486
+ const timerId = typeof timer.timerId === "string" ? timer.timerId : null;
487
+ const timerType = timer.timerType === "absolute" ? "absolute" : "duration";
488
+ const createdAtMs = Number(timer.createdAtMs);
489
+ const firesAtMs = Number(timer.firesAtMs);
490
+ if (!timerId || !Number.isFinite(createdAtMs) || !Number.isFinite(firesAtMs)) {
491
+ return null;
492
+ }
493
+ const firedAtRaw = timer.firedAtMs;
494
+ const firedAtMs = Number.isFinite(Number(firedAtRaw))
495
+ ? Number(firedAtRaw)
496
+ : undefined;
497
+ return {
498
+ timerId,
499
+ timerType,
500
+ createdAtMs,
501
+ firesAtMs,
502
+ firedAtMs,
503
+ duration: typeof timer.duration === "string"
504
+ ? timer.duration
505
+ : undefined,
506
+ until: typeof timer.until === "string"
507
+ ? timer.until
508
+ : undefined,
509
+ };
510
+ }
511
+ /**
512
+ * @param {TimerSnapshot} snapshot
513
+ * @returns {Record<string, unknown>}
514
+ */
515
+ function buildTimerAttemptMeta(snapshot) {
516
+ return {
517
+ kind: "timer",
518
+ timer: {
519
+ timerId: snapshot.timerId,
520
+ timerType: snapshot.timerType,
521
+ duration: snapshot.duration ?? null,
522
+ until: snapshot.until ?? null,
523
+ createdAtMs: snapshot.createdAtMs,
524
+ firesAtMs: snapshot.firesAtMs,
525
+ firedAtMs: snapshot.firedAtMs ?? null,
526
+ },
527
+ };
528
+ }
529
+ /**
530
+ * @param {_TaskDescriptor} desc
531
+ * @param {string} runId
532
+ * @param {unknown} payload
533
+ * @returns {Record<string, unknown>}
534
+ */
535
+ function validateDeferredOutputPayload(desc, runId, payload) {
536
+ if (!desc.outputTable) {
537
+ throw new SmithersError("TASK_MISSING_OUTPUT", `Task ${desc.nodeId} is missing a resolved output table.`, { nodeId: desc.nodeId });
538
+ }
539
+ const cleanPayload = stripAutoColumns(payload);
540
+ const payloadWithKeys = buildOutputRow(desc.outputTable, runId, desc.nodeId, desc.iteration, cleanPayload);
541
+ let validation = validateOutput(desc.outputTable, payloadWithKeys);
542
+ if (validation.ok && desc.outputSchema) {
543
+ const zodResult = desc.outputSchema.safeParse(cleanPayload);
544
+ if (!zodResult.success) {
545
+ validation = { ok: false, error: zodResult.error };
546
+ }
547
+ }
548
+ if (!validation.ok) {
549
+ throw validation.error;
550
+ }
551
+ return validation.data;
552
+ }
553
+ /**
554
+ * @param {_SmithersDb} adapter
555
+ * @param {string} runId
556
+ * @param {_TaskDescriptor} desc
557
+ * @param {EventBus} eventBus
558
+ * @returns {Promise<DeferredBridgeResolution>}
559
+ */
560
+ async function resolveTimerTaskStateBridge(adapter, runId, desc, eventBus) {
561
+ if (!isBridgeManagedTimerTask(desc)) {
562
+ return { handled: false };
563
+ }
564
+ const now = nowMs();
565
+ const attempts = await Effect.runPromise(adapter.listAttempts(runId, desc.nodeId, desc.iteration));
566
+ const latest = attempts[0];
567
+ const latestTimerSnapshot = parseTimerSnapshot(latest?.metaJson);
568
+ if (!latest) {
569
+ const snapshot = buildTimerSnapshot(desc, now);
570
+ const attemptNo = 1;
571
+ const immediateFire = snapshot.firesAtMs <= now;
572
+ const initialState = immediateFire ? "finished" : "waiting-timer";
573
+ const firedAtMs = immediateFire ? now : undefined;
574
+ const metaJson = JSON.stringify(buildTimerAttemptMeta({
575
+ ...snapshot,
576
+ firedAtMs,
577
+ }));
578
+ const nodeState = immediateFire ? "finished" : "waiting-timer";
579
+ await adapter.withTransaction("timer-start", Effect.gen(function* () {
580
+ yield* adapter.insertAttempt({
581
+ runId,
582
+ nodeId: desc.nodeId,
583
+ iteration: desc.iteration,
584
+ attempt: attemptNo,
585
+ state: initialState,
586
+ startedAtMs: now,
587
+ finishedAtMs: immediateFire ? now : null,
588
+ errorJson: null,
589
+ jjPointer: null,
590
+ jjCwd: null,
591
+ cached: false,
592
+ metaJson,
593
+ responseText: null,
594
+ });
595
+ yield* adapter.insertNode({
596
+ runId,
597
+ nodeId: desc.nodeId,
598
+ iteration: desc.iteration,
599
+ state: nodeState,
600
+ lastAttempt: attemptNo,
601
+ updatedAtMs: now,
602
+ outputTable: desc.outputTableName,
603
+ label: desc.label ?? null,
604
+ });
605
+ }));
606
+ await Effect.runPromise(eventBus.emitEventWithPersist({
607
+ type: "TimerCreated",
608
+ runId,
609
+ timerId: desc.nodeId,
610
+ firesAtMs: snapshot.firesAtMs,
611
+ timerType: snapshot.timerType,
612
+ timestampMs: now,
613
+ }));
614
+ if (immediateFire) {
615
+ await Effect.runPromise(eventBus.emitEventWithPersist({
616
+ type: "TimerFired",
617
+ runId,
618
+ timerId: desc.nodeId,
619
+ firesAtMs: snapshot.firesAtMs,
620
+ firedAtMs: now,
621
+ delayMs: Math.max(0, now - snapshot.firesAtMs),
622
+ timestampMs: now,
623
+ }));
624
+ await Effect.runPromise(eventBus.emitEventWithPersist({
625
+ type: "NodeFinished",
626
+ runId,
627
+ nodeId: desc.nodeId,
628
+ iteration: desc.iteration,
629
+ attempt: attemptNo,
630
+ timestampMs: now,
631
+ }));
632
+ }
633
+ else {
634
+ await Effect.runPromise(eventBus.emitEventWithPersist({
635
+ type: "NodeWaitingTimer",
636
+ runId,
637
+ nodeId: desc.nodeId,
638
+ iteration: desc.iteration,
639
+ firesAtMs: snapshot.firesAtMs,
640
+ timestampMs: now,
641
+ }));
642
+ }
643
+ return { handled: true, state: nodeState };
644
+ }
645
+ if (latest.state === "waiting-timer") {
646
+ const snapshot = latestTimerSnapshot ?? buildTimerSnapshot(desc, now);
647
+ if (snapshot.firesAtMs > now) {
648
+ await Effect.runPromise(adapter.insertNode({
649
+ runId,
650
+ nodeId: desc.nodeId,
651
+ iteration: desc.iteration,
652
+ state: "waiting-timer",
653
+ lastAttempt: latest.attempt,
654
+ updatedAtMs: now,
655
+ outputTable: desc.outputTableName,
656
+ label: desc.label ?? null,
657
+ }));
658
+ return { handled: true, state: "waiting-timer" };
659
+ }
660
+ const firedAtMs = now;
661
+ const firedSnapshot = {
662
+ ...snapshot,
663
+ firedAtMs,
664
+ };
665
+ await adapter.withTransaction("timer-fire", Effect.gen(function* () {
666
+ yield* adapter.updateAttempt(runId, desc.nodeId, desc.iteration, latest.attempt, {
667
+ state: "finished",
668
+ finishedAtMs: firedAtMs,
669
+ metaJson: JSON.stringify(buildTimerAttemptMeta(firedSnapshot)),
670
+ });
671
+ yield* adapter.insertNode({
672
+ runId,
673
+ nodeId: desc.nodeId,
674
+ iteration: desc.iteration,
675
+ state: "finished",
676
+ lastAttempt: latest.attempt,
677
+ updatedAtMs: firedAtMs,
678
+ outputTable: desc.outputTableName,
679
+ label: desc.label ?? null,
680
+ });
681
+ }));
682
+ await Effect.runPromise(eventBus.emitEventWithPersist({
683
+ type: "TimerFired",
684
+ runId,
685
+ timerId: desc.nodeId,
686
+ firesAtMs: snapshot.firesAtMs,
687
+ firedAtMs,
688
+ delayMs: Math.max(0, firedAtMs - snapshot.firesAtMs),
689
+ timestampMs: firedAtMs,
690
+ }));
691
+ await Effect.runPromise(eventBus.emitEventWithPersist({
692
+ type: "NodeFinished",
693
+ runId,
694
+ nodeId: desc.nodeId,
695
+ iteration: desc.iteration,
696
+ attempt: latest.attempt,
697
+ timestampMs: firedAtMs,
698
+ }));
699
+ return { handled: true, state: "finished" };
700
+ }
701
+ if (latest.state === "finished") {
702
+ await Effect.runPromise(adapter.insertNode({
703
+ runId,
704
+ nodeId: desc.nodeId,
705
+ iteration: desc.iteration,
706
+ state: "finished",
707
+ lastAttempt: latest.attempt,
708
+ updatedAtMs: now,
709
+ outputTable: desc.outputTableName,
710
+ label: desc.label ?? null,
711
+ }));
712
+ return { handled: true, state: "finished" };
713
+ }
714
+ if (latest.state === "cancelled") {
715
+ await Effect.runPromise(adapter.insertNode({
716
+ runId,
717
+ nodeId: desc.nodeId,
718
+ iteration: desc.iteration,
719
+ state: "skipped",
720
+ lastAttempt: latest.attempt,
721
+ updatedAtMs: now,
722
+ outputTable: desc.outputTableName,
723
+ label: desc.label ?? null,
724
+ }));
725
+ return { handled: true, state: "skipped" };
726
+ }
727
+ if (latest.state === "failed") {
728
+ await Effect.runPromise(adapter.insertNode({
729
+ runId,
730
+ nodeId: desc.nodeId,
731
+ iteration: desc.iteration,
732
+ state: "failed",
733
+ lastAttempt: latest.attempt,
734
+ updatedAtMs: now,
735
+ outputTable: desc.outputTableName,
736
+ label: desc.label ?? null,
737
+ }));
738
+ return { handled: true, state: "failed" };
739
+ }
740
+ return { handled: false };
741
+ }
742
+ /**
743
+ * @param {_SmithersDb} adapter
744
+ * @param {string} runId
745
+ * @param {_TaskDescriptor} desc
746
+ * @param {number} attemptNo
747
+ * @param {unknown} error
748
+ * @param {WaitForEventSnapshot} snapshot
749
+ * @param {DeferredBridgeStateEmitter} [emitStateEvent]
750
+ * @returns {Promise<DeferredBridgeResolution>}
751
+ */
752
+ async function failWaitForEventTaskBridge(adapter, runId, desc, attemptNo, error, snapshot, emitStateEvent) {
753
+ const finishedAtMs = nowMs();
754
+ const errorJson = JSON.stringify(errorToJson(error));
755
+ await adapter.withTransaction("wait-event-fail", Effect.gen(function* () {
756
+ yield* adapter.updateAttempt(runId, desc.nodeId, desc.iteration, attemptNo, {
757
+ state: "failed",
758
+ finishedAtMs,
759
+ errorJson,
760
+ metaJson: JSON.stringify(buildWaitForEventAttemptMeta(snapshot)),
761
+ });
762
+ yield* adapter.insertNode({
763
+ runId,
764
+ nodeId: desc.nodeId,
765
+ iteration: desc.iteration,
766
+ state: "failed",
767
+ lastAttempt: attemptNo,
768
+ updatedAtMs: finishedAtMs,
769
+ outputTable: desc.outputTableName,
770
+ label: desc.label ?? null,
771
+ });
772
+ }));
773
+ if (shouldClearAsyncWaitMetric(snapshot)) {
774
+ await updateAsyncExternalWaitPendingSafe("event", -1);
775
+ }
776
+ await emitStateEvent?.("failed");
777
+ return { handled: true, state: "failed" };
778
+ }
779
+ /**
780
+ * @param {_SmithersDb} adapter
781
+ * @param {string} runId
782
+ * @param {_TaskDescriptor} desc
783
+ * @param {number} attemptNo
784
+ * @param {unknown} payload
785
+ * @param {WaitForEventSnapshot} snapshot
786
+ * @returns {Promise<DeferredBridgeResolution>}
787
+ */
788
+ async function finishWaitForEventTaskBridge(adapter, runId, desc, attemptNo, payload, snapshot) {
789
+ const outputPayload = validateDeferredOutputPayload(desc, runId, payload);
790
+ const finishedAtMs = nowMs();
791
+ await adapter.withTransaction("wait-event-finish", Effect.gen(function* () {
792
+ yield* adapter.upsertOutputRow(desc.outputTable, { runId, nodeId: desc.nodeId, iteration: desc.iteration }, outputPayload);
793
+ yield* adapter.updateAttempt(runId, desc.nodeId, desc.iteration, attemptNo, {
794
+ state: "finished",
795
+ finishedAtMs,
796
+ errorJson: null,
797
+ metaJson: JSON.stringify(buildWaitForEventAttemptMeta(snapshot)),
798
+ });
799
+ yield* adapter.insertNode({
800
+ runId,
801
+ nodeId: desc.nodeId,
802
+ iteration: desc.iteration,
803
+ state: "finished",
804
+ lastAttempt: attemptNo,
805
+ updatedAtMs: finishedAtMs,
806
+ outputTable: desc.outputTableName,
807
+ label: desc.label ?? null,
808
+ });
809
+ }));
810
+ if (shouldClearAsyncWaitMetric(snapshot)) {
811
+ await updateAsyncExternalWaitPendingSafe("event", -1);
812
+ }
813
+ return { handled: true, state: "finished" };
814
+ }
815
+ /**
816
+ * @param {_SmithersDb} adapter
817
+ * @param {string} runId
818
+ * @param {_TaskDescriptor} desc
819
+ * @param {number} attemptNo
820
+ * @param {WaitForEventSnapshot} snapshot
821
+ * @param {DeferredBridgeStateEmitter} [emitStateEvent]
822
+ * @returns {Promise<DeferredBridgeResolution>}
823
+ */
824
+ async function resolveWaitForEventTimeoutBridge(adapter, runId, desc, attemptNo, snapshot, emitStateEvent) {
825
+ const finishedAtMs = nowMs();
826
+ const timeoutSnapshot = {
827
+ ...snapshot,
828
+ timedOutAtMs: finishedAtMs,
829
+ };
830
+ if (snapshot.onTimeout === "continue") {
831
+ try {
832
+ return await finishWaitForEventTaskBridge(adapter, runId, desc, attemptNo, null, timeoutSnapshot);
833
+ }
834
+ catch (error) {
835
+ return failWaitForEventTaskBridge(adapter, runId, desc, attemptNo, error, timeoutSnapshot, emitStateEvent);
836
+ }
837
+ }
838
+ if (snapshot.onTimeout === "skip") {
839
+ await adapter.withTransaction("wait-event-skip", Effect.gen(function* () {
840
+ yield* adapter.updateAttempt(runId, desc.nodeId, desc.iteration, attemptNo, {
841
+ state: "skipped",
842
+ finishedAtMs,
843
+ errorJson: null,
844
+ metaJson: JSON.stringify(buildWaitForEventAttemptMeta(timeoutSnapshot)),
845
+ });
846
+ yield* adapter.insertNode({
847
+ runId,
848
+ nodeId: desc.nodeId,
849
+ iteration: desc.iteration,
850
+ state: "skipped",
851
+ lastAttempt: attemptNo,
852
+ updatedAtMs: finishedAtMs,
853
+ outputTable: desc.outputTableName,
854
+ label: desc.label ?? null,
855
+ });
856
+ }));
857
+ if (shouldClearAsyncWaitMetric(timeoutSnapshot)) {
858
+ await updateAsyncExternalWaitPendingSafe("event", -1);
859
+ }
860
+ await emitStateEvent?.("skipped");
861
+ return { handled: true, state: "skipped" };
862
+ }
863
+ return failWaitForEventTaskBridge(adapter, runId, desc, attemptNo, new SmithersError("TASK_TIMEOUT", `WaitForEvent ${desc.nodeId} timed out after ${snapshot.timeoutMs ?? 0}ms.`, {
864
+ nodeId: desc.nodeId,
865
+ signalName: snapshot.signalName,
866
+ correlationId: snapshot.correlationId ?? null,
867
+ timeoutMs: snapshot.timeoutMs ?? 0,
868
+ }), timeoutSnapshot, emitStateEvent);
869
+ }
870
+ /**
871
+ * @param {_SmithersDb} adapter
872
+ * @param {string} runId
873
+ * @param {_TaskDescriptor} desc
874
+ * @param {WaitForEventSnapshot} snapshot
875
+ * @param {number} [startedAtMs]
876
+ */
877
+ async function syncWaitForEventDurableDeferredFromDb(adapter, runId, desc, snapshot, startedAtMs) {
878
+ const [signal] = await Effect.runPromise(adapter.listSignals(runId, {
879
+ signalName: snapshot.signalName,
880
+ correlationId: snapshot.correlationId ?? null,
881
+ receivedAfterMs: typeof startedAtMs === "number" ? startedAtMs : undefined,
882
+ limit: 1,
883
+ }));
884
+ if (!signal) {
885
+ return;
886
+ }
887
+ await bridgeWaitForEventResolve(adapter, runId, desc.nodeId, desc.iteration, {
888
+ signalName: signal.signalName,
889
+ correlationId: signal.correlationId ?? null,
890
+ payloadJson: signal.payloadJson,
891
+ seq: signal.seq,
892
+ receivedAtMs: signal.receivedAtMs,
893
+ });
894
+ }
895
+ /**
896
+ * @param {_SmithersDb} adapter
897
+ * @param {string} runId
898
+ * @param {_TaskDescriptor} desc
899
+ * @param {ApprovalRow | null | undefined} approval
900
+ */
901
+ async function syncApprovalDurableDeferredFromDb(adapter, runId, desc, approval) {
902
+ if (approval?.status !== "approved" && approval?.status !== "denied") {
903
+ return;
904
+ }
905
+ await bridgeApprovalResolve(adapter, runId, desc.nodeId, desc.iteration, {
906
+ approved: approval.status === "approved",
907
+ note: approval.note ?? null,
908
+ decidedBy: approval.decidedBy ?? null,
909
+ decisionJson: approval.decisionJson ?? null,
910
+ autoApproved: approval.autoApproved ?? false,
911
+ });
912
+ }
913
+ /**
914
+ * @param {_SmithersDb} adapter
915
+ * @param {BunSQLiteDatabase} db
916
+ * @param {string} runId
917
+ * @param {_TaskDescriptor} desc
918
+ * @param {EventBus} _eventBus
919
+ * @param {DeferredBridgeStateEmitter} [emitStateEvent]
920
+ * @returns {Promise<DeferredBridgeResolution>}
921
+ */
922
+ async function resolveWaitForEventTaskStateBridge(adapter, db, runId, desc, _eventBus, emitStateEvent) {
923
+ if (!isBridgeManagedWaitForEventTask(desc)) {
924
+ return { handled: false };
925
+ }
926
+ const now = nowMs();
927
+ const attempts = await Effect.runPromise(adapter.listAttempts(runId, desc.nodeId, desc.iteration));
928
+ let latest = attempts[0];
929
+ let latestSnapshot = parseWaitForEventSnapshot(latest?.metaJson);
930
+ if (!latest) {
931
+ const snapshot = buildWaitForEventSnapshot(desc, now);
932
+ const metaJson = JSON.stringify(buildWaitForEventAttemptMeta(snapshot));
933
+ await adapter.withTransaction("wait-event-start", Effect.gen(function* () {
934
+ yield* adapter.insertAttempt({
935
+ runId,
936
+ nodeId: desc.nodeId,
937
+ iteration: desc.iteration,
938
+ attempt: 1,
939
+ state: "waiting-event",
940
+ startedAtMs: now,
941
+ finishedAtMs: null,
942
+ errorJson: null,
943
+ jjPointer: null,
944
+ jjCwd: null,
945
+ cached: false,
946
+ metaJson,
947
+ responseText: null,
948
+ });
949
+ yield* adapter.insertNode({
950
+ runId,
951
+ nodeId: desc.nodeId,
952
+ iteration: desc.iteration,
953
+ state: "waiting-event",
954
+ lastAttempt: 1,
955
+ updatedAtMs: now,
956
+ outputTable: desc.outputTableName,
957
+ label: desc.label ?? null,
958
+ });
959
+ }));
960
+ if (snapshot.waitAsync) {
961
+ await updateAsyncExternalWaitPendingSafe("event", 1);
962
+ }
963
+ latest = {
964
+ attempt: 1,
965
+ state: "waiting-event",
966
+ startedAtMs: now,
967
+ metaJson,
968
+ };
969
+ latestSnapshot = snapshot;
970
+ if (snapshot.timeoutMs === null || snapshot.timeoutMs > 0) {
971
+ return { handled: true, state: "waiting-event" };
972
+ }
973
+ }
974
+ if (desc.outputTable) {
975
+ const outputRow = await selectOutputRow(db, desc.outputTable, {
976
+ runId,
977
+ nodeId: desc.nodeId,
978
+ iteration: desc.iteration,
979
+ });
980
+ if (outputRow) {
981
+ const valid = validateExistingOutput(desc.outputTable, outputRow);
982
+ if (valid.ok) {
983
+ await Effect.runPromise(adapter.insertNode({
984
+ runId,
985
+ nodeId: desc.nodeId,
986
+ iteration: desc.iteration,
987
+ state: "finished",
988
+ lastAttempt: latest?.attempt ?? null,
989
+ updatedAtMs: nowMs(),
990
+ outputTable: desc.outputTableName,
991
+ label: desc.label ?? null,
992
+ }));
993
+ return { handled: true, state: "finished" };
994
+ }
995
+ }
996
+ }
997
+ if (latest.state === "waiting-event") {
998
+ const snapshot = latestSnapshot ?? buildWaitForEventSnapshot(desc, latest.startedAtMs ?? now);
999
+ await syncWaitForEventDurableDeferredFromDb(adapter, runId, desc, snapshot, latest.startedAtMs);
1000
+ const awaited = await awaitWaitForEventDurableDeferred(adapter, runId, desc.nodeId, desc.iteration);
1001
+ if (awaited._tag === "Complete" && Exit.isSuccess(awaited.exit)) {
1002
+ const signal = awaited.exit.value;
1003
+ const resolvedSnapshot = {
1004
+ ...snapshot,
1005
+ resolvedSignalSeq: signal.seq,
1006
+ receivedAtMs: signal.receivedAtMs,
1007
+ };
1008
+ try {
1009
+ return await finishWaitForEventTaskBridge(adapter, runId, desc, latest.attempt, JSON.parse(signal.payloadJson), resolvedSnapshot);
1010
+ }
1011
+ catch (error) {
1012
+ return failWaitForEventTaskBridge(adapter, runId, desc, latest.attempt, error, resolvedSnapshot, emitStateEvent);
1013
+ }
1014
+ }
1015
+ const timeoutMs = typeof snapshot.timeoutMs === "number" && Number.isFinite(snapshot.timeoutMs)
1016
+ ? snapshot.timeoutMs
1017
+ : null;
1018
+ if (timeoutMs !== null &&
1019
+ typeof latest.startedAtMs === "number" &&
1020
+ latest.startedAtMs + timeoutMs <= now) {
1021
+ return resolveWaitForEventTimeoutBridge(adapter, runId, desc, latest.attempt, snapshot, emitStateEvent);
1022
+ }
1023
+ await Effect.runPromise(adapter.insertNode({
1024
+ runId,
1025
+ nodeId: desc.nodeId,
1026
+ iteration: desc.iteration,
1027
+ state: "waiting-event",
1028
+ lastAttempt: latest.attempt,
1029
+ updatedAtMs: now,
1030
+ outputTable: desc.outputTableName,
1031
+ label: desc.label ?? null,
1032
+ }));
1033
+ return { handled: true, state: "waiting-event" };
1034
+ }
1035
+ if (latest.state === "finished") {
1036
+ await Effect.runPromise(adapter.insertNode({
1037
+ runId,
1038
+ nodeId: desc.nodeId,
1039
+ iteration: desc.iteration,
1040
+ state: "finished",
1041
+ lastAttempt: latest.attempt,
1042
+ updatedAtMs: now,
1043
+ outputTable: desc.outputTableName,
1044
+ label: desc.label ?? null,
1045
+ }));
1046
+ return { handled: true, state: "finished" };
1047
+ }
1048
+ if (latest.state === "skipped") {
1049
+ await Effect.runPromise(adapter.insertNode({
1050
+ runId,
1051
+ nodeId: desc.nodeId,
1052
+ iteration: desc.iteration,
1053
+ state: "skipped",
1054
+ lastAttempt: latest.attempt,
1055
+ updatedAtMs: now,
1056
+ outputTable: desc.outputTableName,
1057
+ label: desc.label ?? null,
1058
+ }));
1059
+ await emitStateEvent?.("skipped");
1060
+ return { handled: true, state: "skipped" };
1061
+ }
1062
+ if (latest.state === "cancelled") {
1063
+ await Effect.runPromise(adapter.insertNode({
1064
+ runId,
1065
+ nodeId: desc.nodeId,
1066
+ iteration: desc.iteration,
1067
+ state: "skipped",
1068
+ lastAttempt: latest.attempt,
1069
+ updatedAtMs: now,
1070
+ outputTable: desc.outputTableName,
1071
+ label: desc.label ?? null,
1072
+ }));
1073
+ await emitStateEvent?.("skipped");
1074
+ return { handled: true, state: "skipped" };
1075
+ }
1076
+ if (latest.state === "failed") {
1077
+ await Effect.runPromise(adapter.insertNode({
1078
+ runId,
1079
+ nodeId: desc.nodeId,
1080
+ iteration: desc.iteration,
1081
+ state: "failed",
1082
+ lastAttempt: latest.attempt,
1083
+ updatedAtMs: now,
1084
+ outputTable: desc.outputTableName,
1085
+ label: desc.label ?? null,
1086
+ }));
1087
+ await emitStateEvent?.("failed");
1088
+ return { handled: true, state: "failed" };
1089
+ }
1090
+ return { handled: false };
1091
+ }
1092
+ /**
1093
+ * @param {_SmithersDb} adapter
1094
+ * @param {BunSQLiteDatabase} db
1095
+ * @param {string} runId
1096
+ * @param {_TaskDescriptor} desc
1097
+ * @param {EventBus} eventBus
1098
+ * @param {DeferredBridgeStateEmitter} [emitStateEvent]
1099
+ * @returns {Promise<DeferredBridgeResolution>}
1100
+ */
1101
+ async function resolveApprovalTaskStateBridge(adapter, db, runId, desc, eventBus, emitStateEvent) {
1102
+ if (!desc.needsApproval) {
1103
+ return { handled: false };
1104
+ }
1105
+ let approval = await Effect.runPromise(adapter.getApproval(runId, desc.nodeId, desc.iteration));
1106
+ if (!approval) {
1107
+ const requestedAtMs = nowMs();
1108
+ const requestJson = buildApprovalRequestJson(desc);
1109
+ if (await shouldAutoApprove(adapter, runId, desc)) {
1110
+ const decisionJson = JSON.stringify(defaultAutoApprovalDecision(desc));
1111
+ approval = {
1112
+ runId,
1113
+ nodeId: desc.nodeId,
1114
+ iteration: desc.iteration,
1115
+ status: "approved",
1116
+ requestedAtMs: desc.approvalAutoApprove?.audit ? requestedAtMs : null,
1117
+ decidedAtMs: requestedAtMs,
1118
+ note: "Auto-approved",
1119
+ decidedBy: "smithers:auto",
1120
+ requestJson,
1121
+ decisionJson,
1122
+ autoApproved: true,
1123
+ };
1124
+ await Effect.runPromise(adapter.insertOrUpdateApproval(approval));
1125
+ await bridgeApprovalResolve(adapter, runId, desc.nodeId, desc.iteration, {
1126
+ approved: true,
1127
+ note: approval.note,
1128
+ decidedBy: approval.decidedBy,
1129
+ decisionJson,
1130
+ autoApproved: true,
1131
+ });
1132
+ await Effect.runPromise(eventBus.emitEventWithPersist({
1133
+ type: "ApprovalAutoApproved",
1134
+ runId,
1135
+ nodeId: desc.nodeId,
1136
+ iteration: desc.iteration,
1137
+ timestampMs: requestedAtMs,
1138
+ }));
1139
+ }
1140
+ else {
1141
+ approval = {
1142
+ runId,
1143
+ nodeId: desc.nodeId,
1144
+ iteration: desc.iteration,
1145
+ status: "requested",
1146
+ requestedAtMs,
1147
+ decidedAtMs: null,
1148
+ note: null,
1149
+ decidedBy: null,
1150
+ requestJson,
1151
+ decisionJson: null,
1152
+ autoApproved: false,
1153
+ };
1154
+ await Effect.runPromise(adapter.insertOrUpdateApproval(approval));
1155
+ if (desc.waitAsync) {
1156
+ await updateAsyncExternalWaitPendingSafe("approval", 1);
1157
+ }
1158
+ await Effect.runPromise(eventBus.emitEventWithPersist({
1159
+ type: "ApprovalRequested",
1160
+ runId,
1161
+ nodeId: desc.nodeId,
1162
+ iteration: desc.iteration,
1163
+ timestampMs: requestedAtMs,
1164
+ }));
1165
+ await Effect.runPromise(eventBus.emitEventWithPersist({
1166
+ type: "NodeWaitingApproval",
1167
+ runId,
1168
+ nodeId: desc.nodeId,
1169
+ iteration: desc.iteration,
1170
+ timestampMs: requestedAtMs,
1171
+ }));
1172
+ await ensurePendingHumanRequest(adapter, runId, desc, requestedAtMs);
1173
+ }
1174
+ }
1175
+ if (approval?.status === "requested") {
1176
+ await ensurePendingHumanRequest(adapter, runId, desc, approval.requestedAtMs ?? nowMs());
1177
+ }
1178
+ const humanRequest = await reconcileHumanRequestValidationFailure(adapter, runId, desc);
1179
+ if (approval?.status === "approved" && humanRequest?.status === "pending") {
1180
+ await Effect.runPromise(adapter.insertNode({
1181
+ runId,
1182
+ nodeId: desc.nodeId,
1183
+ iteration: desc.iteration,
1184
+ state: "waiting-approval",
1185
+ lastAttempt: null,
1186
+ updatedAtMs: nowMs(),
1187
+ outputTable: desc.outputTableName,
1188
+ label: desc.label ?? null,
1189
+ }));
1190
+ return { handled: true, state: "waiting-approval" };
1191
+ }
1192
+ await syncApprovalDurableDeferredFromDb(adapter, runId, desc, approval);
1193
+ const awaited = await awaitApprovalDurableDeferred(adapter, runId, desc.nodeId, desc.iteration);
1194
+ if (awaited._tag !== "Complete" || !Exit.isSuccess(awaited.exit)) {
1195
+ await Effect.runPromise(adapter.insertNode({
1196
+ runId,
1197
+ nodeId: desc.nodeId,
1198
+ iteration: desc.iteration,
1199
+ state: "waiting-approval",
1200
+ lastAttempt: null,
1201
+ updatedAtMs: nowMs(),
1202
+ outputTable: desc.outputTableName,
1203
+ label: desc.label ?? null,
1204
+ }));
1205
+ return { handled: true, state: "waiting-approval" };
1206
+ }
1207
+ approval = (await Effect.runPromise(adapter.getApproval(runId, desc.nodeId, desc.iteration)) ?? approval);
1208
+ if (approval?.status === "denied") {
1209
+ if (desc.approvalMode !== "gate" && desc.approvalOnDeny !== "fail") {
1210
+ const outputRow = await selectOutputRow(db, desc.outputTable, {
1211
+ runId,
1212
+ nodeId: desc.nodeId,
1213
+ iteration: desc.iteration,
1214
+ });
1215
+ if (outputRow) {
1216
+ const valid = validateExistingOutput(desc.outputTable, outputRow);
1217
+ if (valid.ok) {
1218
+ await Effect.runPromise(adapter.insertNode({
1219
+ runId,
1220
+ nodeId: desc.nodeId,
1221
+ iteration: desc.iteration,
1222
+ state: "finished",
1223
+ lastAttempt: null,
1224
+ updatedAtMs: nowMs(),
1225
+ outputTable: desc.outputTableName,
1226
+ label: desc.label ?? null,
1227
+ }));
1228
+ return { handled: true, state: "finished" };
1229
+ }
1230
+ }
1231
+ await Effect.runPromise(adapter.insertNode({
1232
+ runId,
1233
+ nodeId: desc.nodeId,
1234
+ iteration: desc.iteration,
1235
+ state: "pending",
1236
+ lastAttempt: null,
1237
+ updatedAtMs: nowMs(),
1238
+ outputTable: desc.outputTableName,
1239
+ label: desc.label ?? null,
1240
+ }));
1241
+ await emitStateEvent?.("pending");
1242
+ return { handled: true, state: "pending" };
1243
+ }
1244
+ const state = desc.continueOnFail
1245
+ ? "skipped"
1246
+ : "failed";
1247
+ await Effect.runPromise(adapter.insertNode({
1248
+ runId,
1249
+ nodeId: desc.nodeId,
1250
+ iteration: desc.iteration,
1251
+ state,
1252
+ lastAttempt: null,
1253
+ updatedAtMs: nowMs(),
1254
+ outputTable: desc.outputTableName,
1255
+ label: desc.label ?? null,
1256
+ }));
1257
+ await emitStateEvent?.(state);
1258
+ return { handled: true, state };
1259
+ }
1260
+ if (approval?.status === "approved") {
1261
+ return { handled: false };
1262
+ }
1263
+ await Effect.runPromise(adapter.insertNode({
1264
+ runId,
1265
+ nodeId: desc.nodeId,
1266
+ iteration: desc.iteration,
1267
+ state: "waiting-approval",
1268
+ lastAttempt: null,
1269
+ updatedAtMs: nowMs(),
1270
+ outputTable: desc.outputTableName,
1271
+ label: desc.label ?? null,
1272
+ }));
1273
+ return { handled: true, state: "waiting-approval" };
1274
+ }
1275
+ /**
1276
+ * @param {_SmithersDb} adapter
1277
+ * @param {BunSQLiteDatabase} db
1278
+ * @param {string} runId
1279
+ * @param {_TaskDescriptor} desc
1280
+ * @param {EventBus} eventBus
1281
+ * @param {DeferredBridgeStateEmitter} [emitStateEvent]
1282
+ * @returns {Promise<DeferredBridgeResolution>}
1283
+ */
1284
+ export async function resolveDeferredTaskStateBridge(adapter, db, runId, desc, eventBus, emitStateEvent) {
1285
+ const timer = await resolveTimerTaskStateBridge(adapter, runId, desc, eventBus);
1286
+ if (timer.handled) {
1287
+ return timer;
1288
+ }
1289
+ const waitForEvent = await resolveWaitForEventTaskStateBridge(adapter, db, runId, desc, eventBus, emitStateEvent);
1290
+ if (waitForEvent.handled) {
1291
+ return waitForEvent;
1292
+ }
1293
+ return resolveApprovalTaskStateBridge(adapter, db, runId, desc, eventBus, emitStateEvent);
1294
+ }
1295
+ /**
1296
+ * @param {_SmithersDb} adapter
1297
+ * @param {string} runId
1298
+ * @param {EventBus} eventBus
1299
+ * @param {string} reason
1300
+ */
1301
+ export async function cancelPendingTimersBridge(adapter, runId, eventBus, reason) {
1302
+ const nodes = await Effect.runPromise(adapter.listNodes(runId));
1303
+ for (const node of nodes) {
1304
+ if (node.state !== "waiting-timer")
1305
+ continue;
1306
+ const attempts = await Effect.runPromise(adapter.listAttempts(runId, node.nodeId, node.iteration ?? 0));
1307
+ const waiting = attempts.find((attempt) => attempt.state === "waiting-timer");
1308
+ if (!waiting)
1309
+ continue;
1310
+ const cancelledAtMs = nowMs();
1311
+ await adapter.withTransaction("cancel-pending-timer", Effect.gen(function* () {
1312
+ yield* adapter.updateAttempt(runId, node.nodeId, node.iteration ?? 0, waiting.attempt, {
1313
+ state: "cancelled",
1314
+ finishedAtMs: cancelledAtMs,
1315
+ });
1316
+ yield* adapter.insertNode({
1317
+ runId,
1318
+ nodeId: node.nodeId,
1319
+ iteration: node.iteration ?? 0,
1320
+ state: "cancelled",
1321
+ lastAttempt: waiting.attempt,
1322
+ updatedAtMs: cancelledAtMs,
1323
+ outputTable: node.outputTable ?? "",
1324
+ label: node.label ?? null,
1325
+ });
1326
+ }));
1327
+ await Effect.runPromise(eventBus.emitEventWithPersist({
1328
+ type: "TimerCancelled",
1329
+ runId,
1330
+ timerId: node.nodeId,
1331
+ timestampMs: cancelledAtMs,
1332
+ }));
1333
+ await Effect.runPromise(eventBus.emitEventWithPersist({
1334
+ type: "NodeCancelled",
1335
+ runId,
1336
+ nodeId: node.nodeId,
1337
+ iteration: node.iteration ?? 0,
1338
+ attempt: waiting.attempt,
1339
+ reason,
1340
+ timestampMs: cancelledAtMs,
1341
+ }));
1342
+ }
1343
+ }