@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,352 @@
1
+ import { applyPatch as applyUnifiedPatch } from "diff";
2
+ import { spawn } from "node:child_process";
3
+ import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
6
+ /** @typedef {import("./DiffBundle.ts").DiffBundle} DiffBundle */
7
+ /** @typedef {import("./FilePatch.ts").FilePatch} FilePatch */
8
+
9
+ /**
10
+ * @param {string} cwd
11
+ * @param {string[]} args
12
+ * @param {{ input?: string; allowExitCodes?: ReadonlySet<number>; }} [options]
13
+ * @returns {Promise<GitCommandResult>}
14
+ */
15
+ async function runGit(cwd, args, options) {
16
+ return new Promise((resolve, reject) => {
17
+ const child = spawn("git", args, {
18
+ cwd,
19
+ stdio: ["pipe", "pipe", "pipe"],
20
+ });
21
+ let stdout = "";
22
+ let stderr = "";
23
+ child.stdout.setEncoding("utf8");
24
+ child.stderr.setEncoding("utf8");
25
+ child.stdout.on("data", (chunk) => {
26
+ stdout += chunk;
27
+ });
28
+ child.stderr.on("data", (chunk) => {
29
+ stderr += chunk;
30
+ });
31
+ child.once("error", reject);
32
+ child.once("close", (code) => {
33
+ const allowExitCodes = options?.allowExitCodes;
34
+ if (code === 0 || (typeof code === "number" && allowExitCodes?.has(code))) {
35
+ resolve({ stdout, stderr });
36
+ return;
37
+ }
38
+ reject(new SmithersError("INVALID_INPUT", `git ${args.join(" ")} failed`, { cwd, args, code, stderr: stderr.trim(), stdout: stdout.trim() }));
39
+ });
40
+ if (options?.input) {
41
+ child.stdin.write(options.input);
42
+ }
43
+ child.stdin.end();
44
+ });
45
+ }
46
+ /**
47
+ * @param {string} diff
48
+ * @returns {string[]}
49
+ */
50
+ function splitGitDiff(diff) {
51
+ const normalized = diff.trim();
52
+ if (normalized.length === 0) {
53
+ return [];
54
+ }
55
+ return normalized
56
+ .split(/^diff --git /m)
57
+ .filter((chunk) => chunk.length > 0)
58
+ .map((chunk) => `diff --git ${chunk}`.trimEnd() + "\n");
59
+ }
60
+ /**
61
+ * @param {string} chunk
62
+ * @returns {string}
63
+ */
64
+ function extractPatchPath(chunk) {
65
+ const renameTo = chunk.match(/^rename to (.+)$/m)?.[1];
66
+ if (renameTo) {
67
+ return renameTo.trim();
68
+ }
69
+ const plusPath = chunk.match(/^\+\+\+ b\/(.+)$/m)?.[1];
70
+ if (plusPath) {
71
+ return plusPath.trim();
72
+ }
73
+ const minusPath = chunk.match(/^--- a\/(.+)$/m)?.[1];
74
+ if (minusPath) {
75
+ return minusPath.trim();
76
+ }
77
+ const diffHeader = chunk.match(/^diff --git a\/(.+?) b\/(.+)$/m);
78
+ if (diffHeader) {
79
+ return diffHeader[2].trim();
80
+ }
81
+ throw new SmithersError("INVALID_INPUT", "Unable to determine file path from diff chunk", { chunk: chunk.slice(0, 200) });
82
+ }
83
+ /**
84
+ * @param {string} chunk
85
+ * @returns {FilePatch["operation"]}
86
+ */
87
+ function extractOperation(chunk) {
88
+ if (/^new file mode /m.test(chunk)) {
89
+ return "add";
90
+ }
91
+ if (/^deleted file mode /m.test(chunk)) {
92
+ return "delete";
93
+ }
94
+ return "modify";
95
+ }
96
+ /**
97
+ * @param {string} chunk
98
+ * @returns {boolean}
99
+ */
100
+ function isBinaryPatch(chunk) {
101
+ return /(^GIT binary patch$)|(^Binary files )/m.test(chunk);
102
+ }
103
+ /**
104
+ * @param {string} path
105
+ * @returns {Promise<boolean>}
106
+ */
107
+ async function fileExists(path) {
108
+ try {
109
+ await access(path);
110
+ return true;
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ }
116
+ /**
117
+ * @param {string} baseRef
118
+ * @param {string} currentDir
119
+ * @returns {Promise<Set<string>>}
120
+ */
121
+ async function listBinaryPaths(baseRef, currentDir) {
122
+ const { stdout } = await runGit(currentDir, [
123
+ "diff",
124
+ "--numstat",
125
+ "--find-renames=100%",
126
+ baseRef,
127
+ "--",
128
+ ".",
129
+ ]);
130
+ const binaryPaths = new Set();
131
+ for (const line of stdout.split("\n")) {
132
+ const trimmed = line.trim();
133
+ if (!trimmed)
134
+ continue;
135
+ const [added, removed, ...rest] = trimmed.split("\t");
136
+ if (added === "-" && removed === "-" && rest.length > 0) {
137
+ binaryPaths.add(rest.join("\t"));
138
+ }
139
+ }
140
+ return binaryPaths;
141
+ }
142
+ /**
143
+ * @param {string} currentDir
144
+ * @returns {Promise<string[]>}
145
+ */
146
+ async function listUntrackedFiles(currentDir) {
147
+ const { stdout } = await runGit(currentDir, [
148
+ "ls-files",
149
+ "--others",
150
+ "--exclude-standard",
151
+ "--",
152
+ ".",
153
+ ]);
154
+ return stdout
155
+ .split("\n")
156
+ .map((line) => line.trim())
157
+ .filter((line) => line.length > 0);
158
+ }
159
+ /**
160
+ * @param {string} currentDir
161
+ * @returns {Promise<string[]>}
162
+ */
163
+ async function computeUntrackedDiffs(currentDir) {
164
+ const untracked = await listUntrackedFiles(currentDir);
165
+ const diffs = [];
166
+ for (const relativePath of untracked) {
167
+ const { stdout } = await runGit(currentDir, ["diff", "--no-index", "--binary", "--", "/dev/null", relativePath], { allowExitCodes: new Set([1]) });
168
+ if (stdout.trim().length > 0) {
169
+ diffs.push(stdout.trimEnd() + "\n");
170
+ }
171
+ }
172
+ return diffs;
173
+ }
174
+ /**
175
+ * Compute a diff bundle strictly between two immutable refs.
176
+ *
177
+ * Unlike {@link computeDiffBundle}, this variant does NOT read the working
178
+ * tree or untracked files. It is the preferred entry point for historical
179
+ * diffs (e.g. the `getNodeDiff` RPC) because it is read-only and cannot be
180
+ * contaminated by concurrent runs mutating the checkout.
181
+ *
182
+ * @param {string} baseRef
183
+ * @param {string} targetRef
184
+ * @param {string} currentDir
185
+ * @param {number} [seq]
186
+ * @returns {Promise<DiffBundle>}
187
+ */
188
+ export async function computeDiffBundleBetweenRefs(baseRef, targetRef, currentDir, seq = 1) {
189
+ const [{ stdout: trackedDiff }, { stdout: numstat }] = await Promise.all([
190
+ runGit(currentDir, [
191
+ "diff",
192
+ "--binary",
193
+ "--find-renames=100%",
194
+ "--no-ext-diff",
195
+ baseRef,
196
+ targetRef,
197
+ "--",
198
+ ".",
199
+ ]),
200
+ runGit(currentDir, [
201
+ "diff",
202
+ "--numstat",
203
+ "--find-renames=100%",
204
+ baseRef,
205
+ targetRef,
206
+ "--",
207
+ ".",
208
+ ]),
209
+ ]);
210
+ const binaryPaths = new Set();
211
+ for (const line of numstat.split("\n")) {
212
+ const trimmed = line.trim();
213
+ if (!trimmed)
214
+ continue;
215
+ const [added, removed, ...rest] = trimmed.split("\t");
216
+ if (added === "-" && removed === "-" && rest.length > 0) {
217
+ binaryPaths.add(rest.join("\t"));
218
+ }
219
+ }
220
+ const patches = [];
221
+ const chunks = splitGitDiff(trackedDiff);
222
+ for (const chunk of chunks) {
223
+ const path = extractPatchPath(chunk);
224
+ const operation = extractOperation(chunk);
225
+ const binary = isBinaryPatch(chunk) || binaryPaths.has(path);
226
+ let binaryContent;
227
+ if (binary && operation !== "delete") {
228
+ try {
229
+ const { stdout } = await runGit(currentDir, [
230
+ "show",
231
+ `${targetRef}:${path}`,
232
+ ]);
233
+ binaryContent = Buffer.from(stdout, "binary").toString("base64");
234
+ }
235
+ catch {
236
+ // File may not be readable as a blob at targetRef; fall through
237
+ // without binaryContent. Caller receives operation + diff only.
238
+ binaryContent = undefined;
239
+ }
240
+ }
241
+ patches.push({
242
+ path,
243
+ operation,
244
+ diff: chunk,
245
+ binaryContent,
246
+ });
247
+ }
248
+ return {
249
+ seq,
250
+ baseRef,
251
+ patches,
252
+ };
253
+ }
254
+ /**
255
+ * @param {string} baseRef
256
+ * @param {string} currentDir
257
+ * @returns {Promise<DiffBundle>}
258
+ */
259
+ export async function computeDiffBundle(baseRef, currentDir, seq = 1) {
260
+ const [{ stdout: trackedDiff }, binaryPaths, untrackedDiffs] = await Promise.all([
261
+ runGit(currentDir, [
262
+ "diff",
263
+ "--binary",
264
+ "--find-renames=100%",
265
+ "--no-ext-diff",
266
+ baseRef,
267
+ "--",
268
+ ".",
269
+ ]),
270
+ listBinaryPaths(baseRef, currentDir),
271
+ computeUntrackedDiffs(currentDir),
272
+ ]);
273
+ const patches = [];
274
+ const chunks = [
275
+ ...splitGitDiff(trackedDiff),
276
+ ...untrackedDiffs.flatMap(splitGitDiff),
277
+ ];
278
+ for (const chunk of chunks) {
279
+ const path = extractPatchPath(chunk);
280
+ const operation = extractOperation(chunk);
281
+ const binary = isBinaryPatch(chunk) || binaryPaths.has(path);
282
+ const fullPath = join(currentDir, path);
283
+ patches.push({
284
+ path,
285
+ operation,
286
+ diff: chunk,
287
+ binaryContent: binary && operation !== "delete" && await fileExists(fullPath)
288
+ ? (await readFile(fullPath)).toString("base64")
289
+ : undefined,
290
+ });
291
+ }
292
+ return {
293
+ seq,
294
+ baseRef,
295
+ patches,
296
+ };
297
+ }
298
+ /**
299
+ * @param {FilePatch} patch
300
+ * @param {string} targetDir
301
+ * @returns {Promise<void>}
302
+ */
303
+ async function applyPatchFallback(patch, targetDir) {
304
+ const targetPath = join(targetDir, patch.path);
305
+ const targetExists = await fileExists(targetPath);
306
+ if (patch.binaryContent) {
307
+ if (patch.operation === "delete") {
308
+ await rm(targetPath, { force: true });
309
+ return;
310
+ }
311
+ await mkdir(dirname(targetPath), { recursive: true });
312
+ await writeFile(targetPath, Buffer.from(patch.binaryContent, "base64"));
313
+ return;
314
+ }
315
+ if (patch.operation === "delete" && !targetExists) {
316
+ return;
317
+ }
318
+ const current = patch.operation === "add" || !targetExists
319
+ ? ""
320
+ : await readFile(targetPath, "utf8");
321
+ const updated = applyUnifiedPatch(current, patch.diff);
322
+ if (updated === false) {
323
+ throw new SmithersError("TOOL_PATCH_FAILED", `Failed to apply patch for ${patch.path}`, { path: patch.path, operation: patch.operation });
324
+ }
325
+ if (patch.operation === "delete") {
326
+ await rm(targetPath, { force: true });
327
+ return;
328
+ }
329
+ await mkdir(dirname(targetPath), { recursive: true });
330
+ await writeFile(targetPath, updated, "utf8");
331
+ }
332
+ /**
333
+ * @param {DiffBundle} bundle
334
+ * @param {string} targetDir
335
+ * @returns {Promise<void>}
336
+ */
337
+ export async function applyDiffBundle(bundle, targetDir) {
338
+ if (bundle.patches.length === 0) {
339
+ return;
340
+ }
341
+ await mkdir(targetDir, { recursive: true });
342
+ const fullPatch = bundle.patches.map((patch) => patch.diff).join("");
343
+ try {
344
+ await runGit(targetDir, ["apply", "--binary", "--whitespace=nowarn", "--unsafe-paths", "-"], { input: fullPatch });
345
+ return;
346
+ }
347
+ catch (error) {
348
+ for (const patch of bundle.patches) {
349
+ await applyPatchFallback(patch, targetDir);
350
+ }
351
+ }
352
+ }
@@ -0,0 +1,282 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./ApprovalDurableDeferredResolution.ts").ApprovalDurableDeferredResolution} ApprovalDurableDeferredResolution */
3
+ /** @typedef {import("./WaitForEventDurableDeferredResolution.ts").WaitForEventDurableDeferredResolution} WaitForEventDurableDeferredResolution */
4
+ // @smithers-type-exports-end
5
+
6
+ import * as DurableDeferred from "@effect/workflow/DurableDeferred";
7
+ import * as Workflow from "@effect/workflow/Workflow";
8
+ import { resolve as resolvePath } from "node:path";
9
+ import { Effect, Exit, Schema } from "effect";
10
+ import { updateAsyncExternalWaitPending } from "@smithers-orchestrator/observability/metrics";
11
+ /**
12
+ * @typedef {{ _tag: "Complete"; exit: Exit.Exit<any, any>; } | { _tag: "Pending"; }} BridgeDeferredResult
13
+ */
14
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} _SmithersDb */
15
+ /**
16
+ * @typedef {{ signalName: string; correlationId: string | null; payloadJson: string; seq: number; receivedAtMs: number; }} WaitForEventSignalInput
17
+ */
18
+
19
+ export const DurableDeferredBridgeWorkflow = Workflow.make({
20
+ name: "SmithersDurableDeferredBridge",
21
+ payload: { executionId: Schema.String },
22
+ success: Schema.Unknown,
23
+ idempotencyKey: ({ executionId }) => executionId,
24
+ });
25
+ const adapterNamespaces = new WeakMap();
26
+ let nextAdapterNamespace = 0;
27
+ /**
28
+ * @param {_SmithersDb} adapter
29
+ * @returns {string}
30
+ */
31
+ const getAdapterNamespace = (adapter) => {
32
+ const filename = adapter?.db?.$client?.filename;
33
+ if (typeof filename === "string" && filename.length > 0 && filename !== ":memory:") {
34
+ return `sqlite:${resolvePath(filename)}`;
35
+ }
36
+ const existing = adapterNamespaces.get(adapter);
37
+ if (existing) {
38
+ return existing;
39
+ }
40
+ const created = `adapter-${++nextAdapterNamespace}`;
41
+ adapterNamespaces.set(adapter, created);
42
+ return created;
43
+ };
44
+ export const approvalDurableDeferredSuccessSchema = Schema.Struct({
45
+ approved: Schema.Boolean,
46
+ note: Schema.NullOr(Schema.String),
47
+ decidedBy: Schema.NullOr(Schema.String),
48
+ decisionJson: Schema.NullOr(Schema.String),
49
+ autoApproved: Schema.Boolean,
50
+ });
51
+ export const waitForEventDurableDeferredSuccessSchema = Schema.Struct({
52
+ signalName: Schema.String,
53
+ correlationId: Schema.NullOr(Schema.String),
54
+ payloadJson: Schema.String,
55
+ seq: Schema.Number,
56
+ receivedAtMs: Schema.Number,
57
+ });
58
+ /**
59
+ * @param {string | null | undefined} value
60
+ * @returns {string | null}
61
+ */
62
+ function normalizeCorrelationId(value) {
63
+ const normalized = typeof value === "string" ? value.trim() : "";
64
+ return normalized.length > 0 ? normalized : null;
65
+ }
66
+ /**
67
+ * @param {unknown} value
68
+ * @returns {number | undefined}
69
+ */
70
+ function parseOptionalFiniteNumber(value) {
71
+ if (value == null || value === "") {
72
+ return undefined;
73
+ }
74
+ const parsed = Number(value);
75
+ return Number.isFinite(parsed) ? parsed : undefined;
76
+ }
77
+ /**
78
+ * @param {string | null} [metaJson]
79
+ * @returns {WaitForEventAttemptSnapshot | null}
80
+ */
81
+ function parseWaitForEventAttemptSnapshot(metaJson) {
82
+ if (!metaJson)
83
+ return null;
84
+ try {
85
+ const parsed = JSON.parse(metaJson);
86
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
87
+ return null;
88
+ }
89
+ const waitForEvent = parsed?.waitForEvent;
90
+ if (!waitForEvent || typeof waitForEvent !== "object" || Array.isArray(waitForEvent)) {
91
+ return null;
92
+ }
93
+ const signalName = typeof waitForEvent.signalName === "string"
94
+ ? waitForEvent.signalName.trim()
95
+ : "";
96
+ if (!signalName) {
97
+ return null;
98
+ }
99
+ return {
100
+ meta: parsed,
101
+ signalName,
102
+ correlationId: normalizeCorrelationId(waitForEvent.correlationId),
103
+ waitAsync: waitForEvent.waitAsync === true,
104
+ resolvedSignalSeq: parseOptionalFiniteNumber(waitForEvent.resolvedSignalSeq),
105
+ receivedAtMs: parseOptionalFiniteNumber(waitForEvent.receivedAtMs),
106
+ };
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ /**
113
+ * @param {WaitForEventAttemptSnapshot} snapshot
114
+ * @param {WaitForEventSignalInput} signal
115
+ * @returns {string}
116
+ */
117
+ function buildResolvedWaitForEventMetaJson(snapshot, signal) {
118
+ const waitForEvent = snapshot.meta.waitForEvent &&
119
+ typeof snapshot.meta.waitForEvent === "object" &&
120
+ !Array.isArray(snapshot.meta.waitForEvent)
121
+ ? snapshot.meta.waitForEvent
122
+ : {};
123
+ return JSON.stringify({
124
+ ...snapshot.meta,
125
+ kind: typeof snapshot.meta.kind === "string"
126
+ ? snapshot.meta.kind
127
+ : "wait-for-event",
128
+ waitForEvent: {
129
+ ...waitForEvent,
130
+ signalName: snapshot.signalName,
131
+ correlationId: snapshot.correlationId,
132
+ waitAsync: snapshot.waitAsync,
133
+ resolvedSignalSeq: signal.seq,
134
+ receivedAtMs: signal.receivedAtMs,
135
+ },
136
+ });
137
+ }
138
+ /**
139
+ * @param {_SmithersDb} adapter
140
+ * @param {string} runId
141
+ * @param {string} nodeId
142
+ * @param {number} iteration
143
+ * @param {WaitForEventSignalInput} signal
144
+ */
145
+ async function markWaitForEventResolved(adapter, runId, nodeId, iteration, signal) {
146
+ const attempts = await Effect.runPromise(adapter.listAttempts(runId, nodeId, iteration));
147
+ const waitingAttempt = attempts.find((attempt) => attempt.state === "waiting-event") ??
148
+ attempts[0];
149
+ const snapshot = parseWaitForEventAttemptSnapshot(waitingAttempt?.metaJson);
150
+ if (!waitingAttempt || !snapshot || snapshot.resolvedSignalSeq !== undefined) {
151
+ return;
152
+ }
153
+ await Effect.runPromise(adapter.updateAttempt(runId, nodeId, iteration, waitingAttempt.attempt, {
154
+ metaJson: buildResolvedWaitForEventMetaJson(snapshot, signal),
155
+ }));
156
+ if (snapshot.waitAsync) {
157
+ try {
158
+ await Effect.runPromise(updateAsyncExternalWaitPending("event", -1));
159
+ }
160
+ catch { }
161
+ }
162
+ }
163
+ const deferredResolutions = new Map();
164
+ /**
165
+ * @template Success, Error
166
+ * @param {string} executionId
167
+ * @param {DurableDeferred.DurableDeferred<Success, Error>} _deferred
168
+ * @returns {Promise<BridgeDeferredResult>}
169
+ */
170
+ const awaitBridgeDeferred = async (executionId, _deferred) => {
171
+ const exit = deferredResolutions.get(executionId);
172
+ return exit ? { _tag: "Complete", exit } : { _tag: "Pending" };
173
+ };
174
+ /**
175
+ * @template Success, Error
176
+ * @param {string} executionId
177
+ * @param {DurableDeferred.DurableDeferred<Success, Error>} _deferred
178
+ * @param {Exit.Exit<Success["Type"], Error["Type"]>} exit
179
+ */
180
+ const resolveBridgeDeferred = async (executionId, _deferred, exit) => {
181
+ deferredResolutions.set(executionId, exit);
182
+ };
183
+ /**
184
+ * @param {_SmithersDb} adapter
185
+ * @param {string} runId
186
+ * @param {string} nodeId
187
+ * @param {number} iteration
188
+ * @returns {string}
189
+ */
190
+ export const makeDurableDeferredBridgeExecutionId = (adapter, runId, nodeId, iteration) => [
191
+ "smithers-durable-deferred-bridge",
192
+ getAdapterNamespace(adapter),
193
+ runId,
194
+ nodeId,
195
+ String(iteration),
196
+ ].join(":");
197
+ /**
198
+ * @param {string} nodeId
199
+ */
200
+ export const makeApprovalDurableDeferred = (nodeId) => DurableDeferred.make(`approval:${nodeId}`, {
201
+ success: approvalDurableDeferredSuccessSchema,
202
+ });
203
+ /**
204
+ * @param {string} nodeId
205
+ */
206
+ export const makeWaitForEventDurableDeferred = (nodeId) => DurableDeferred.make(`wait-for-event:${nodeId}`, {
207
+ success: waitForEventDurableDeferredSuccessSchema,
208
+ });
209
+ /**
210
+ * @param {_SmithersDb} adapter
211
+ * @param {string} runId
212
+ * @param {string} nodeId
213
+ * @param {number} iteration
214
+ */
215
+ export const awaitApprovalDurableDeferred = (adapter, runId, nodeId, iteration) => awaitBridgeDeferred(makeDurableDeferredBridgeExecutionId(adapter, runId, nodeId, iteration), makeApprovalDurableDeferred(nodeId));
216
+ /**
217
+ * @param {_SmithersDb} adapter
218
+ * @param {string} runId
219
+ * @param {string} nodeId
220
+ * @param {number} iteration
221
+ */
222
+ export const awaitWaitForEventDurableDeferred = (adapter, runId, nodeId, iteration) => awaitBridgeDeferred(makeDurableDeferredBridgeExecutionId(adapter, runId, nodeId, iteration), makeWaitForEventDurableDeferred(nodeId));
223
+ /**
224
+ * @param {_SmithersDb} adapter
225
+ * @param {string} runId
226
+ * @param {string} nodeId
227
+ * @param {number} iteration
228
+ * @param {{ approved: boolean; note?: string | null; decidedBy?: string | null; decisionJson?: string | null; autoApproved?: boolean; }} resolution
229
+ */
230
+ export const bridgeApprovalResolve = async (adapter, runId, nodeId, iteration, resolution) => {
231
+ await resolveBridgeDeferred(makeDurableDeferredBridgeExecutionId(adapter, runId, nodeId, iteration), makeApprovalDurableDeferred(nodeId), Exit.succeed({
232
+ approved: resolution.approved,
233
+ note: resolution.note ?? null,
234
+ decidedBy: resolution.decidedBy ?? null,
235
+ decisionJson: resolution.decisionJson ?? null,
236
+ autoApproved: resolution.autoApproved ?? false,
237
+ }));
238
+ };
239
+ /**
240
+ * @param {_SmithersDb} adapter
241
+ * @param {string} runId
242
+ * @param {string} nodeId
243
+ * @param {number} iteration
244
+ * @param {WaitForEventSignalInput} signal
245
+ */
246
+ export const bridgeWaitForEventResolve = async (adapter, runId, nodeId, iteration, signal) => {
247
+ await markWaitForEventResolved(adapter, runId, nodeId, iteration, signal);
248
+ await resolveBridgeDeferred(makeDurableDeferredBridgeExecutionId(adapter, runId, nodeId, iteration), makeWaitForEventDurableDeferred(nodeId), Exit.succeed({
249
+ signalName: signal.signalName,
250
+ correlationId: normalizeCorrelationId(signal.correlationId),
251
+ payloadJson: signal.payloadJson,
252
+ seq: signal.seq,
253
+ receivedAtMs: signal.receivedAtMs,
254
+ }));
255
+ };
256
+ /**
257
+ * @param {_SmithersDb} adapter
258
+ * @param {string} runId
259
+ * @param {WaitForEventSignalInput} signal
260
+ */
261
+ export const bridgeSignalResolve = async (adapter, runId, signal) => {
262
+ const nodes = await Effect.runPromise(adapter.listNodes(runId));
263
+ const normalizedCorrelationId = normalizeCorrelationId(signal.correlationId);
264
+ for (const node of nodes) {
265
+ if (node.state !== "waiting-event")
266
+ continue;
267
+ const iteration = node.iteration ?? 0;
268
+ const attempts = await Effect.runPromise(adapter.listAttempts(runId, node.nodeId, iteration));
269
+ const waitingAttempt = attempts.find((attempt) => attempt.state === "waiting-event") ??
270
+ attempts[0];
271
+ if (!waitingAttempt)
272
+ continue;
273
+ const snapshot = parseWaitForEventAttemptSnapshot(waitingAttempt.metaJson);
274
+ if (!snapshot)
275
+ continue;
276
+ if (snapshot.signalName !== signal.signalName)
277
+ continue;
278
+ if (snapshot.correlationId !== normalizedCorrelationId)
279
+ continue;
280
+ await bridgeWaitForEventResolve(adapter, runId, node.nodeId, iteration, signal);
281
+ }
282
+ };