@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.
- package/LICENSE +21 -0
- package/package.json +50 -0
- package/src/AlertHumanRequestOptions.ts +8 -0
- package/src/AlertRuntimeServices.ts +10 -0
- package/src/ChildWorkflowDefinition.ts +5 -0
- package/src/ChildWorkflowExecuteOptions.ts +14 -0
- package/src/ContinuationRequest.ts +3 -0
- package/src/HijackState.ts +19 -0
- package/src/HumanRequestKind.ts +1 -0
- package/src/HumanRequestStatus.ts +1 -0
- package/src/PlanNode.ts +29 -0
- package/src/RalphMeta.ts +7 -0
- package/src/RalphState.ts +4 -0
- package/src/RalphStateMap.ts +3 -0
- package/src/ScheduleResult.ts +15 -0
- package/src/SignalRunOptions.ts +5 -0
- package/src/alert-runtime.js +22 -0
- package/src/approvals.js +220 -0
- package/src/child-workflow.js +163 -0
- package/src/effect/ApprovalDeferredResolution.ts +13 -0
- package/src/effect/ApprovalDurableDeferredResolution.ts +11 -0
- package/src/effect/ApprovalPayload.ts +7 -0
- package/src/effect/ApprovalResult.ts +6 -0
- package/src/effect/BuilderNode.ts +52 -0
- package/src/effect/BuilderStepHandle.ts +47 -0
- package/src/effect/CancelPayload.ts +3 -0
- package/src/effect/CancelResult.ts +4 -0
- package/src/effect/DeferredResolution.ts +7 -0
- package/src/effect/DiffBundle.ts +7 -0
- package/src/effect/ExecuteTaskActivityOptions.ts +7 -0
- package/src/effect/FilePatch.ts +6 -0
- package/src/effect/GetRunPayload.ts +3 -0
- package/src/effect/GetRunResult.ts +3 -0
- package/src/effect/LegacyExecuteTaskFn.ts +24 -0
- package/src/effect/ListRunsPayload.ts +6 -0
- package/src/effect/RunStatusSchema.ts +9 -0
- package/src/effect/RunSummary.ts +23 -0
- package/src/effect/SignalPayload.ts +7 -0
- package/src/effect/SignalResult.ts +6 -0
- package/src/effect/SmithersSqliteOptions.ts +3 -0
- package/src/effect/SqlMessageStorageEventHistoryQuery.ts +7 -0
- package/src/effect/TaggedWorkerError.ts +46 -0
- package/src/effect/TaskActivityContext.ts +4 -0
- package/src/effect/TaskActivityRetryOptions.ts +4 -0
- package/src/effect/TaskBridgeToolConfig.ts +6 -0
- package/src/effect/TaskFailure.ts +3 -0
- package/src/effect/TaskResult.ts +5 -0
- package/src/effect/UnknownWorkerError.ts +5 -0
- package/src/effect/WaitForEventDurableDeferredResolution.ts +11 -0
- package/src/effect/WorkerDispatchKind.ts +1 -0
- package/src/effect/WorkerTask.ts +14 -0
- package/src/effect/WorkerTaskError.ts +4 -0
- package/src/effect/WorkerTaskKind.ts +1 -0
- package/src/effect/WorkflowPatchDecisionRecord.ts +4 -0
- package/src/effect/WorkflowPatchDecisions.ts +1 -0
- package/src/effect/WorkflowVersioningRuntime.ts +7 -0
- package/src/effect/activity-bridge.js +131 -0
- package/src/effect/bridge-utils.js +45 -0
- package/src/effect/builder.js +837 -0
- package/src/effect/compute-task-bridge.js +734 -0
- package/src/effect/deferred-bridge.js +63 -0
- package/src/effect/deferred-state-bridge.js +1343 -0
- package/src/effect/diff-bundle.js +352 -0
- package/src/effect/durable-deferred-bridge.js +282 -0
- package/src/effect/entity-worker.js +154 -0
- package/src/effect/http-runner.js +86 -0
- package/src/effect/rpc-schema.js +101 -0
- package/src/effect/single-runner.js +189 -0
- package/src/effect/sql-message-storage.js +817 -0
- package/src/effect/static-task-bridge.js +308 -0
- package/src/effect/versioning.js +123 -0
- package/src/effect/workflow-bridge.js +260 -0
- package/src/effect/workflow-make-bridge.js +233 -0
- package/src/engine.js +6933 -0
- package/src/events.js +237 -0
- package/src/external/json-schema-to-zod.js +214 -0
- package/src/getDefinedToolMetadata.js +10 -0
- package/src/hot/HotReloadEvent.ts +21 -0
- package/src/hot/HotWorkflowController.js +220 -0
- package/src/hot/OverlayOptions.ts +4 -0
- package/src/hot/WatchTreeOptions.ts +6 -0
- package/src/hot/index.js +9 -0
- package/src/hot/overlay.js +177 -0
- package/src/hot/watch.js +174 -0
- package/src/human-requests.js +120 -0
- package/src/index.d.ts +1597 -0
- package/src/index.js +41 -0
- package/src/runtime-owner.js +36 -0
- package/src/scheduler.js +31 -0
- 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
|
+
}
|