@smithers-orchestrator/server 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.
@@ -0,0 +1,504 @@
1
+ import { getTableName } from "drizzle-orm";
2
+ import { Effect, Metric, MetricBoundaries } from "effect";
3
+ import { getAgentOutputSchema, selectOutputRow, stripAutoColumns } from "@smithers-orchestrator/db/output";
4
+ import { buildOutputSchemaDescriptor } from "@smithers-orchestrator/db/output-schema-descriptor";
5
+ import { runPromise } from "../smithersRuntime.js";
6
+ import { NodeOutputRouteError } from "./NodeOutputRouteError.js";
7
+ import { NODE_OUTPUT_WARN_BYTES } from "./NODE_OUTPUT_WARN_BYTES.js";
8
+ import { NODE_OUTPUT_MAX_BYTES } from "./NODE_OUTPUT_MAX_BYTES.js";
9
+
10
+ /** @typedef {import("./NodeOutputResponse.js").NodeOutputResponse} NodeOutputResponse */
11
+
12
+ const RUN_ID_PATTERN = /^[a-z0-9_-]{1,64}$/;
13
+ const NODE_ID_PATTERN = /^[a-zA-Z0-9:_-]{1,128}$/;
14
+ const INT32_MAX = 2_147_483_647;
15
+
16
+ const fastBucketsMs = MetricBoundaries.exponential({ start: 1, factor: 2, count: 12 });
17
+ const sizeBuckets = MetricBoundaries.exponential({ start: 100, factor: 2, count: 16 });
18
+
19
+ const nodeOutputRequestTotal = Metric.counter("smithers_node_output_request_total");
20
+ const nodeOutputBytes = Metric.histogram("smithers_node_output_bytes", sizeBuckets);
21
+ const nodeOutputDurationMs = Metric.histogram("smithers_node_output_duration_ms", fastBucketsMs);
22
+ const nodeOutputSchemaConversionErrorTotal = Metric.counter("smithers_node_output_schema_conversion_error_total");
23
+
24
+ /**
25
+ * Wrap a promise-returning function in a real tracing span with attributes.
26
+ * Mirrors the pattern used by getNodeDiffRoute: one Effect.withSpan, plus a
27
+ * debug log carrying the span name and duration so log-only backends still
28
+ * get a record of the child span.
29
+ *
30
+ * @template T
31
+ * @param {(effect: Effect.Effect<void>) => Promise<unknown>} emitEffect
32
+ * @param {string} spanName
33
+ * @param {Record<string, unknown>} attrs
34
+ * @param {() => Promise<T>} run
35
+ * @returns {Promise<T>}
36
+ */
37
+ async function emitEffectSpan(emitEffect, spanName, attrs, run) {
38
+ const startedAt = Date.now();
39
+ try {
40
+ const result = await runPromise(Effect.promise(() => run()).pipe(Effect.withSpan(spanName, { attributes: attrs })));
41
+ await swallow(() => emitEffect(Effect.logDebug(spanName).pipe(Effect.annotateLogs({
42
+ ...attrs,
43
+ span: spanName,
44
+ durationMs: Date.now() - startedAt,
45
+ }))));
46
+ return result;
47
+ }
48
+ catch (error) {
49
+ await swallow(() => emitEffect(Effect.logError(`${spanName} failed`).pipe(Effect.annotateLogs({
50
+ ...attrs,
51
+ span: spanName,
52
+ durationMs: Date.now() - startedAt,
53
+ error: error instanceof Error ? error.message : String(error),
54
+ }))));
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * @param {() => Promise<void>} run
61
+ */
62
+ async function swallow(run) {
63
+ try {
64
+ await run();
65
+ }
66
+ catch {
67
+ // Observability must never break RPC responses.
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Resolve per-node output row plus schema hints for DevTools rendering.
73
+ *
74
+ * @param {{
75
+ * runId: unknown;
76
+ * nodeId: unknown;
77
+ * iteration: unknown;
78
+ * resolveRun: (runId: string) => Promise<{ workflow: import("@smithers-orchestrator/components/SmithersWorkflow").SmithersWorkflow<unknown>; adapter: import("@smithers-orchestrator/db/adapter").SmithersDb } | null>;
79
+ * selectOutputRowImpl?: typeof selectOutputRow;
80
+ * emitEffect?: (effect: Effect.Effect<void>) => Promise<unknown>;
81
+ * }} params
82
+ * @returns {Promise<NodeOutputResponse>}
83
+ */
84
+ export async function getNodeOutputRoute(params) {
85
+ const emitEffect = params.emitEffect ?? ((effect) => runPromise(effect));
86
+ const startedAt = performance.now();
87
+ let statusForMetrics = "error";
88
+ let rowBytes = 0;
89
+ let logRunId = asString(params.runId) ?? null;
90
+ let logNodeId = asString(params.nodeId) ?? null;
91
+ let logIteration = coerceOptionalInteger(params.iteration);
92
+ let logErrorCode = null;
93
+ const rootSpanAttrs = {
94
+ runId: logRunId ?? "",
95
+ nodeId: logNodeId ?? "",
96
+ iteration: logIteration ?? -1,
97
+ status: "unknown",
98
+ bytes: 0,
99
+ };
100
+ try {
101
+ const runId = parseRunId(params.runId);
102
+ const nodeId = parseNodeId(params.nodeId);
103
+ const iteration = parseIteration(params.iteration);
104
+ logRunId = runId;
105
+ logNodeId = nodeId;
106
+ logIteration = iteration;
107
+ rootSpanAttrs.runId = runId;
108
+ rootSpanAttrs.nodeId = nodeId;
109
+ rootSpanAttrs.iteration = iteration;
110
+
111
+ const resolved = await params.resolveRun(runId);
112
+ if (!resolved) {
113
+ throw new NodeOutputRouteError("RunNotFound", `Run not found: ${runId}`);
114
+ }
115
+
116
+ const nodeIterations = await resolved.adapter.listNodeIterations(runId, nodeId);
117
+ if (!Array.isArray(nodeIterations) || nodeIterations.length === 0) {
118
+ throw new NodeOutputRouteError("NodeNotFound", `Node not found: ${nodeId}`);
119
+ }
120
+
121
+ const node = nodeIterations.find((entry) => (entry?.iteration ?? 0) === iteration);
122
+ if (!node) {
123
+ throw new NodeOutputRouteError("IterationNotFound", `Iteration not found: ${iteration}`);
124
+ }
125
+
126
+ const outputTableName = asString(node.outputTable)?.trim() ?? "";
127
+ if (!outputTableName) {
128
+ throw new NodeOutputRouteError("NodeHasNoOutput", `Node ${nodeId} has no output table.`);
129
+ }
130
+
131
+ const output = resolveOutputDefinition(resolved.workflow, outputTableName);
132
+ if (!output?.table) {
133
+ throw new NodeOutputRouteError("NodeHasNoOutput", `Output table ${outputTableName} is not registered.`);
134
+ }
135
+
136
+ const { descriptor, warnings } = await emitEffectSpan(
137
+ emitEffect,
138
+ "devtools.buildSchemaDescriptor",
139
+ { runId, nodeId, iteration },
140
+ async () => {
141
+ const collected = [];
142
+ const schemaForDescriptor = isDescriptorSchema(output.zodSchema)
143
+ ? output.zodSchema
144
+ : getAgentOutputSchema(output.table);
145
+ const builtDescriptor = buildOutputSchemaDescriptor(schemaForDescriptor, {
146
+ onWarning: (warning) => collected.push(warning),
147
+ });
148
+ return { descriptor: builtDescriptor, warnings: collected };
149
+ },
150
+ );
151
+
152
+ if (warnings.length > 0) {
153
+ for (const warning of warnings) {
154
+ await swallow(() => emitEffect(Effect.all([
155
+ Metric.increment(nodeOutputSchemaConversionErrorTotal),
156
+ Effect.logWarning("getNodeOutput schema conversion warning").pipe(Effect.annotateLogs({
157
+ runId,
158
+ nodeId,
159
+ iteration,
160
+ errorCode: warning.code,
161
+ field: warning.field,
162
+ construct: warning.construct,
163
+ })),
164
+ ], { discard: true })));
165
+ }
166
+ }
167
+
168
+ const selectOutputRowImpl = params.selectOutputRowImpl ?? selectOutputRow;
169
+ let selectedRow;
170
+ try {
171
+ selectedRow = await emitEffectSpan(
172
+ emitEffect,
173
+ "db.outputs.select",
174
+ { runId, nodeId, iteration },
175
+ () => selectOutputRowImpl(resolved.workflow.db, output.table, {
176
+ runId,
177
+ nodeId,
178
+ iteration,
179
+ }),
180
+ );
181
+ }
182
+ catch (error) {
183
+ if (looksLikeMalformedOutputRow(error)) {
184
+ throw new NodeOutputRouteError("MalformedOutputRow", "Output row is not parseable JSON.");
185
+ }
186
+ throw error;
187
+ }
188
+
189
+ const hasRow = selectedRow !== undefined;
190
+ let normalizedRow = null;
191
+ if (hasRow) {
192
+ normalizedRow = normalizeOutputRow(selectedRow);
193
+ if (normalizedRow !== null && !isPlainObject(normalizedRow)) {
194
+ throw new NodeOutputRouteError("MalformedOutputRow", "Output row must be a JSON object or null.");
195
+ }
196
+ rowBytes = byteLengthOfJson(normalizedRow);
197
+ rootSpanAttrs.bytes = rowBytes;
198
+ if (rowBytes > NODE_OUTPUT_MAX_BYTES) {
199
+ throw new NodeOutputRouteError("PayloadTooLarge", `Output payload exceeds ${NODE_OUTPUT_MAX_BYTES} bytes.`);
200
+ }
201
+ if (rowBytes > NODE_OUTPUT_WARN_BYTES) {
202
+ await swallow(() => emitEffect(Effect.logWarning("getNodeOutput large payload").pipe(Effect.annotateLogs({
203
+ runId,
204
+ nodeId,
205
+ iteration,
206
+ rowBytes,
207
+ }))));
208
+ }
209
+ }
210
+
211
+ if (hasRow) {
212
+ statusForMetrics = "produced";
213
+ rootSpanAttrs.status = "produced";
214
+ return {
215
+ status: "produced",
216
+ row: normalizedRow,
217
+ schema: descriptor,
218
+ };
219
+ }
220
+
221
+ const attempts = await resolved.adapter.listAttempts(runId, nodeId, iteration);
222
+ const latestAttempt = Array.isArray(attempts) ? attempts[0] : undefined;
223
+ const failed =
224
+ node.state === "failed" ||
225
+ latestAttempt?.state === "failed" ||
226
+ (typeof latestAttempt?.errorJson === "string" && latestAttempt.errorJson.length > 0);
227
+
228
+ if (failed) {
229
+ statusForMetrics = "failed";
230
+ rootSpanAttrs.status = "failed";
231
+ return {
232
+ status: "failed",
233
+ row: null,
234
+ schema: descriptor,
235
+ partial: parsePartialHeartbeat(latestAttempt?.heartbeatDataJson),
236
+ };
237
+ }
238
+
239
+ statusForMetrics = "pending";
240
+ rootSpanAttrs.status = "pending";
241
+ return {
242
+ status: "pending",
243
+ row: null,
244
+ schema: descriptor,
245
+ };
246
+ }
247
+ catch (error) {
248
+ if (error instanceof NodeOutputRouteError) {
249
+ logErrorCode = error.code;
250
+ if (error.code === "MalformedOutputRow") {
251
+ await swallow(() => emitEffect(Effect.logError("getNodeOutput malformed row").pipe(Effect.annotateLogs({
252
+ runId: logRunId,
253
+ nodeId: logNodeId,
254
+ iteration: logIteration,
255
+ errorCode: error.code,
256
+ }))));
257
+ }
258
+ throw error;
259
+ }
260
+ logErrorCode = asString(error?.code) ?? "ServerError";
261
+ await swallow(() => emitEffect(Effect.logError("getNodeOutput failed").pipe(Effect.annotateLogs({
262
+ runId: logRunId,
263
+ nodeId: logNodeId,
264
+ iteration: logIteration,
265
+ errorCode: logErrorCode,
266
+ errorMessage: asString(error?.message) ?? String(error),
267
+ }))));
268
+ throw error;
269
+ }
270
+ finally {
271
+ const durationMs = Math.max(0, performance.now() - startedAt);
272
+ rootSpanAttrs.bytes = rowBytes;
273
+ // Root span wrapping the finalisation (one span per RPC).
274
+ await swallow(() => runPromise(Effect.sync(() => {
275
+ // No-op body; the span records start/end times and attributes.
276
+ }).pipe(Effect.withSpan("devtools.getNodeOutput", {
277
+ attributes: {
278
+ ...rootSpanAttrs,
279
+ status: statusForMetrics,
280
+ bytes: rowBytes,
281
+ durationMs,
282
+ ...(logErrorCode ? { errorCode: logErrorCode } : {}),
283
+ },
284
+ }))));
285
+ await swallow(() => emitEffect(Effect.all([
286
+ Metric.increment(Metric.tagged(nodeOutputRequestTotal, "status", statusForMetrics)),
287
+ Metric.update(nodeOutputBytes, rowBytes),
288
+ Metric.update(nodeOutputDurationMs, durationMs),
289
+ Effect.logInfo("getNodeOutput completed").pipe(Effect.annotateLogs({
290
+ runId: logRunId,
291
+ nodeId: logNodeId,
292
+ iteration: logIteration,
293
+ status: statusForMetrics,
294
+ rowBytes,
295
+ durationMs,
296
+ ...(logErrorCode ? { errorCode: logErrorCode } : {}),
297
+ })),
298
+ ], { discard: true })));
299
+ }
300
+ }
301
+
302
+ /**
303
+ * @param {unknown} value
304
+ */
305
+ function parseRunId(value) {
306
+ const runId = asString(value);
307
+ if (!runId || !RUN_ID_PATTERN.test(runId)) {
308
+ throw new NodeOutputRouteError("InvalidRunId", "runId must match /^[a-z0-9_-]{1,64}$/.");
309
+ }
310
+ return runId;
311
+ }
312
+
313
+ /**
314
+ * @param {unknown} value
315
+ */
316
+ function parseNodeId(value) {
317
+ const nodeId = asString(value);
318
+ if (!nodeId || !NODE_ID_PATTERN.test(nodeId)) {
319
+ throw new NodeOutputRouteError("InvalidNodeId", "nodeId must match /^[a-zA-Z0-9:_-]{1,128}$/.");
320
+ }
321
+ return nodeId;
322
+ }
323
+
324
+ /**
325
+ * @param {unknown} value
326
+ */
327
+ function parseIteration(value) {
328
+ const normalized = coerceOptionalInteger(value);
329
+ if (normalized === undefined || normalized < 0 || normalized > INT32_MAX) {
330
+ throw new NodeOutputRouteError("InvalidIteration", "iteration must be a non-negative 32-bit integer.");
331
+ }
332
+ return normalized;
333
+ }
334
+
335
+ /**
336
+ * @param {unknown} value
337
+ * @returns {number | undefined}
338
+ */
339
+ function coerceOptionalInteger(value) {
340
+ if (value === undefined || value === null || value === "") {
341
+ return undefined;
342
+ }
343
+ const numeric = typeof value === "number" ? value : Number(value);
344
+ if (!Number.isFinite(numeric) || !Number.isInteger(numeric)) {
345
+ return undefined;
346
+ }
347
+ return numeric;
348
+ }
349
+
350
+ /**
351
+ * @param {unknown} workflow
352
+ * @param {string} outputTableName
353
+ * @returns {{ table: unknown; zodSchema?: unknown } | null}
354
+ */
355
+ function resolveOutputDefinition(workflow, outputTableName) {
356
+ const wf = /** @type {Record<string, unknown> | null | undefined} */ (
357
+ workflow && typeof workflow === "object" ? workflow : null
358
+ );
359
+ const schemaRegistry = /** @type {{ get?: (key: string) => { table?: unknown; zodSchema?: unknown } | undefined; values?: () => Iterable<{ table?: unknown; zodSchema?: unknown }>; } | undefined} */ (
360
+ wf?.schemaRegistry
361
+ );
362
+ if (schemaRegistry && typeof schemaRegistry.get === "function") {
363
+ const hit = schemaRegistry.get(outputTableName);
364
+ if (hit?.table) {
365
+ return {
366
+ table: hit.table,
367
+ zodSchema: hit.zodSchema,
368
+ };
369
+ }
370
+ if (typeof schemaRegistry.values === "function") {
371
+ for (const entry of schemaRegistry.values()) {
372
+ if (!entry?.table) {
373
+ continue;
374
+ }
375
+ try {
376
+ if (getTableName(entry.table) === outputTableName) {
377
+ return {
378
+ table: entry.table,
379
+ zodSchema: entry.zodSchema,
380
+ };
381
+ }
382
+ }
383
+ catch { }
384
+ }
385
+ }
386
+ }
387
+
388
+ const db = /** @type {Record<string, unknown> | undefined} */ (
389
+ wf?.db && typeof wf.db === "object" ? wf.db : undefined
390
+ );
391
+ const dbInternal = /** @type {Record<string, unknown> | undefined} */ (
392
+ db?._ && typeof db._ === "object" ? db._ : undefined
393
+ );
394
+ const candidates = [
395
+ dbInternal?.fullSchema,
396
+ dbInternal?.schema,
397
+ db?.schema,
398
+ ];
399
+ for (const candidate of candidates) {
400
+ if (!candidate || typeof candidate !== "object") {
401
+ continue;
402
+ }
403
+ const candidateRecord = /** @type {Record<string, unknown>} */ (candidate);
404
+ const direct = candidateRecord[outputTableName];
405
+ if (direct) {
406
+ return { table: direct };
407
+ }
408
+ for (const table of Object.values(candidateRecord)) {
409
+ try {
410
+ if (getTableName(table) === outputTableName) {
411
+ return { table };
412
+ }
413
+ }
414
+ catch { }
415
+ }
416
+ }
417
+
418
+ return null;
419
+ }
420
+
421
+ /**
422
+ * @param {unknown} value
423
+ */
424
+ function isDescriptorSchema(value) {
425
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && "shape" in value;
426
+ }
427
+
428
+ /**
429
+ * @param {unknown} row
430
+ * @returns {unknown}
431
+ */
432
+ function normalizeOutputRow(row) {
433
+ if (!row || typeof row !== "object") {
434
+ return row ?? null;
435
+ }
436
+ const r = /** @type {Record<string, unknown>} */ (row);
437
+ const keys = Object.keys(r);
438
+ const payloadOnly =
439
+ "payload" in r &&
440
+ keys.every((key) => key === "runId" || key === "nodeId" || key === "iteration" || key === "payload");
441
+ if (payloadOnly) {
442
+ return r.payload ?? null;
443
+ }
444
+ return stripAutoColumns(r);
445
+ }
446
+
447
+ /**
448
+ * @param {unknown} value
449
+ */
450
+ function parsePartialHeartbeat(value) {
451
+ if (typeof value !== "string" || value.length === 0) {
452
+ return null;
453
+ }
454
+ try {
455
+ const parsed = JSON.parse(value);
456
+ return isPlainObject(parsed) ? parsed : null;
457
+ }
458
+ catch {
459
+ return null;
460
+ }
461
+ }
462
+
463
+ /**
464
+ * @param {unknown} value
465
+ */
466
+ function byteLengthOfJson(value) {
467
+ let json;
468
+ try {
469
+ json = JSON.stringify(value);
470
+ }
471
+ catch {
472
+ throw new NodeOutputRouteError("MalformedOutputRow", "Output row is not valid JSON.");
473
+ }
474
+ if (typeof json !== "string") {
475
+ throw new NodeOutputRouteError("MalformedOutputRow", "Output row is not valid JSON.");
476
+ }
477
+ return Buffer.byteLength(json, "utf8");
478
+ }
479
+
480
+ /**
481
+ * @param {unknown} error
482
+ */
483
+ function looksLikeMalformedOutputRow(error) {
484
+ if (error instanceof SyntaxError) {
485
+ return true;
486
+ }
487
+ const message = asString(error?.message)?.toLowerCase() ?? "";
488
+ return message.includes("json") || message.includes("parse") || message.includes("malformed");
489
+ }
490
+
491
+ /**
492
+ * @param {unknown} value
493
+ * @returns {value is Record<string, unknown>}
494
+ */
495
+ function isPlainObject(value) {
496
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
497
+ }
498
+
499
+ /**
500
+ * @param {unknown} value
501
+ */
502
+ function asString(value) {
503
+ return typeof value === "string" ? value : undefined;
504
+ }
@@ -0,0 +1,84 @@
1
+ import { jumpToFrame, JumpToFrameError } from "@smithers-orchestrator/time-travel/jumpToFrame";
2
+
3
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
4
+ /** @typedef {import("@smithers-orchestrator/observability/SmithersEvent").SmithersEvent} SmithersEvent */
5
+ /** @typedef {import("@smithers-orchestrator/time-travel/jumpToFrame").JumpResult} JumpResult */
6
+
7
+ /**
8
+ * Gateway wrapper around time-travel jump orchestration.
9
+ *
10
+ * The gateway has no direct hook into the engine's in-memory reconciler
11
+ * (reconciler state is DB-backed: frames, nodes, attempts). We wire real
12
+ * capture/restore/rebuild functions that operate on the run's DB state so
13
+ * that the transaction rollback path inside jumpToFrame has meaningful
14
+ * inputs, and callers can plug in an in-memory reconciler if they have one.
15
+ *
16
+ * @param {{
17
+ * adapter: SmithersDb;
18
+ * runId: unknown;
19
+ * frameNo: unknown;
20
+ * confirm?: unknown;
21
+ * caller?: string;
22
+ * pauseRunLoop?: () => Promise<void> | void;
23
+ * resumeRunLoop?: () => Promise<void> | void;
24
+ * emitEvent?: (event: SmithersEvent) => Promise<void> | void;
25
+ * captureReconcilerState?: () => Promise<unknown> | unknown;
26
+ * restoreReconcilerState?: (snapshot: unknown) => Promise<void> | void;
27
+ * rebuildReconcilerState?: (xmlJson: string) => Promise<void> | void;
28
+ * onLog?: (level: "info" | "warn" | "error", message: string, fields?: Record<string, unknown>) => Promise<void> | void;
29
+ * }} input
30
+ * @returns {Promise<JumpResult>}
31
+ */
32
+ export async function jumpToFrameRoute(input) {
33
+ const adapter = input.adapter;
34
+ const runId = typeof input.runId === "string" ? input.runId : null;
35
+
36
+ // Default reconciler hooks: DB-backed snapshot of latest frame + no-op
37
+ // restore (frames are rolled back by the main transaction) + a rebuild
38
+ // that simply annotates the run record with the target frame xml hash,
39
+ // making it observable that a rewind happened.
40
+ const defaultCapture = async () => {
41
+ if (!runId) {
42
+ return null;
43
+ }
44
+ try {
45
+ const latest = await adapter.getLastFrame(runId);
46
+ if (!latest) {
47
+ return null;
48
+ }
49
+ return {
50
+ frameNo: Number(latest.frameNo),
51
+ createdAtMs: Number(latest.createdAtMs),
52
+ xmlHash: typeof latest.xmlHash === "string" ? latest.xmlHash : null,
53
+ };
54
+ } catch {
55
+ return null;
56
+ }
57
+ };
58
+ const defaultRestore = async () => {
59
+ // Frames/attempts/nodes rollback is handled by the main transaction; no
60
+ // separate in-memory restore is required for the DB-driven engine.
61
+ };
62
+ const defaultRebuild = async () => {
63
+ // Rebuild is a no-op for the DB-driven engine; the next resume reads
64
+ // state directly from _smithers_frames. Callers that have an in-memory
65
+ // reconciler can inject their own rebuildReconcilerState hook.
66
+ };
67
+
68
+ return await jumpToFrame({
69
+ adapter: input.adapter,
70
+ runId: input.runId,
71
+ frameNo: input.frameNo,
72
+ confirm: input.confirm,
73
+ caller: input.caller,
74
+ pauseRunLoop: input.pauseRunLoop,
75
+ resumeRunLoop: input.resumeRunLoop,
76
+ emitEvent: input.emitEvent,
77
+ captureReconcilerState: input.captureReconcilerState ?? defaultCapture,
78
+ restoreReconcilerState: input.restoreReconcilerState ?? defaultRestore,
79
+ rebuildReconcilerState: input.rebuildReconcilerState ?? defaultRebuild,
80
+ onLog: input.onLog,
81
+ });
82
+ }
83
+
84
+ export { JumpToFrameError };