@relayflows/sdk 2.0.45 → 2.0.46
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/dist/authored-flow-error.d.ts +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +9 -0
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +17 -0
- package/dist/authored-root.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +7 -0
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +31 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cloud-mirror-step.d.ts.map +1 -1
- package/dist/cloud-mirror-step.js +6 -1
- package/dist/cloud-mirror-step.js.map +1 -1
- package/dist/compile.d.ts +1 -0
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +2 -1
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +1 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +1 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-tool-adapters.d.ts +56 -0
- package/dist/flow-tool-adapters.d.ts.map +1 -0
- package/dist/flow-tool-adapters.js +42 -0
- package/dist/flow-tool-adapters.js.map +1 -0
- package/dist/flow-tool-client.d.ts +38 -0
- package/dist/flow-tool-client.d.ts.map +1 -0
- package/dist/flow-tool-client.js +105 -0
- package/dist/flow-tool-client.js.map +1 -0
- package/dist/flow-tool-contract.d.ts +103 -0
- package/dist/flow-tool-contract.d.ts.map +1 -0
- package/dist/flow-tool-contract.js +75 -0
- package/dist/flow-tool-contract.js.map +1 -0
- package/dist/flow-tool-definitions.d.ts +19 -0
- package/dist/flow-tool-definitions.d.ts.map +1 -0
- package/dist/flow-tool-definitions.js +12 -0
- package/dist/flow-tool-definitions.js.map +1 -0
- package/dist/flow-tool-http.d.ts +14 -0
- package/dist/flow-tool-http.d.ts.map +1 -0
- package/dist/flow-tool-http.js +175 -0
- package/dist/flow-tool-http.js.map +1 -0
- package/dist/flow-tool-kernel.d.ts +27 -0
- package/dist/flow-tool-kernel.d.ts.map +1 -0
- package/dist/flow-tool-kernel.js +310 -0
- package/dist/flow-tool-kernel.js.map +1 -0
- package/dist/flow-tool-manifest.d.ts +30 -0
- package/dist/flow-tool-manifest.d.ts.map +1 -0
- package/dist/flow-tool-manifest.js +79 -0
- package/dist/flow-tool-manifest.js.map +1 -0
- package/dist/flow-tool-schema.d.ts +15 -0
- package/dist/flow-tool-schema.d.ts.map +1 -0
- package/dist/flow-tool-schema.js +70 -0
- package/dist/flow-tool-schema.js.map +1 -0
- package/dist/flow-tool-wire.d.ts +26 -0
- package/dist/flow-tool-wire.d.ts.map +1 -0
- package/dist/flow-tool-wire.js +195 -0
- package/dist/flow-tool-wire.js.map +1 -0
- package/dist/helper-park.d.ts +14 -0
- package/dist/helper-park.d.ts.map +1 -0
- package/dist/helper-park.js +40 -0
- package/dist/helper-park.js.map +1 -0
- package/dist/helper-receipt.d.ts +11 -0
- package/dist/helper-receipt.d.ts.map +1 -0
- package/dist/helper-receipt.js +100 -0
- package/dist/helper-receipt.js.map +1 -0
- package/dist/helper-writeback.d.ts.map +1 -1
- package/dist/helper-writeback.js +7 -2
- package/dist/helper-writeback.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/yaml-helper-effect.d.ts.map +1 -1
- package/dist/yaml-helper-effect.js +32 -23
- package/dist/yaml-helper-effect.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-flow-error.ts +1 -0
- package/src/authored-helper-effect.ts +9 -0
- package/src/authored-root.ts +17 -0
- package/src/cli/direct-run.ts +7 -0
- package/src/cli/run.ts +30 -2
- package/src/cloud-mirror-step.ts +6 -1
- package/src/compile.ts +2 -1
- package/src/failure-kinds.ts +1 -0
- package/src/flow-tool-adapters.ts +43 -0
- package/src/flow-tool-client.ts +115 -0
- package/src/flow-tool-contract.ts +153 -0
- package/src/flow-tool-definitions.ts +27 -0
- package/src/flow-tool-http.ts +134 -0
- package/src/flow-tool-kernel.ts +319 -0
- package/src/flow-tool-manifest.ts +104 -0
- package/src/flow-tool-schema.ts +66 -0
- package/src/flow-tool-wire.ts +152 -0
- package/src/helper-park.ts +45 -0
- package/src/helper-receipt.ts +90 -0
- package/src/helper-writeback.ts +8 -3
- package/src/index.ts +21 -0
- package/src/yaml-helper-effect.ts +12 -5
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { JournalClient } from './journal-client.js';
|
|
2
|
+
import type { StepDispatchEvent } from './protocol.js';
|
|
3
|
+
import { HelperWritebackPending } from './helper-receipt.js';
|
|
4
|
+
|
|
5
|
+
export function helperReceiptWaitId(stepId: string, attempt: number): string {
|
|
6
|
+
return `helper-receipt:${stepId}:${attempt}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Journal a human-wait park before the worker connection closes.
|
|
10
|
+
* Closing without this is a transport failure, and the default budget is one
|
|
11
|
+
* extra retry: two receipt timeouts then fail the accepted write.
|
|
12
|
+
*/
|
|
13
|
+
export async function parkHelperReceipt(
|
|
14
|
+
client: JournalClient, dispatch: StepDispatchEvent, error: HelperWritebackPending,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
await client.stepWait(dispatch.run_id, dispatch.step_id, dispatch.attempt, dispatch.idempotency_key, {
|
|
17
|
+
wait_id: helperReceiptWaitId(dispatch.step_id, dispatch.attempt),
|
|
18
|
+
prompt: error.message,
|
|
19
|
+
requested_of: 'relayfile-receipt',
|
|
20
|
+
options: ['resume'],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Resume continues the same accepted write. Answering the park redispatches
|
|
25
|
+
* the attempt; it does not submit another provider draft.
|
|
26
|
+
*/
|
|
27
|
+
export async function releaseHelperReceiptWaits(client: JournalClient, runId: string): Promise<void> {
|
|
28
|
+
type WaitEntry = { seq?: number; entry_type: string; payload?: { wait_id?: string } };
|
|
29
|
+
const entries: WaitEntry[] = [];
|
|
30
|
+
let from = 1;
|
|
31
|
+
for (;;) {
|
|
32
|
+
const page = (await client.journalRead(runId, from, 1000)).entries as WaitEntry[];
|
|
33
|
+
if (page.length === 0) break;
|
|
34
|
+
entries.push(...page);
|
|
35
|
+
const last = page[page.length - 1]?.seq;
|
|
36
|
+
if (last === undefined || page.length < 1000) break;
|
|
37
|
+
from = last + 1;
|
|
38
|
+
}
|
|
39
|
+
const closed = new Set(entries.filter(entry => entry.entry_type === 'wait.completed').map(entry => entry.payload?.wait_id));
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const waitId = entry.payload?.wait_id;
|
|
42
|
+
if (entry.entry_type !== 'wait.human' || waitId === undefined || !waitId.startsWith('helper-receipt:') || closed.has(waitId)) continue;
|
|
43
|
+
await client.eventEmit(runId, waitId, { answer: 'resume', answeredBy: 'flows-resume' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, relative, isAbsolute } from 'node:path';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { writeJsonFile, type WritebackReceipt, type WritebackResult } from '@relayfile/adapter-core/vfs-client';
|
|
5
|
+
import { atomicJson, receiptPath, readHelperReceipt } from './helper-storage.js';
|
|
6
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
7
|
+
|
|
8
|
+
export class HelperWritebackPending extends AuthoredFlowExecutionError {
|
|
9
|
+
constructor(readonly writeId: string, readonly path: string, runId: string) {
|
|
10
|
+
super('helper_writeback_pending', `write ${writeId} accepted or submission interrupted; delivery receipt pending at ${path}. Resume to wait; do not re-issue the write.`, undefined, runId);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function helperReceiptTimeoutMs(): number {
|
|
15
|
+
const value = process.env.RELAYFLOW_HELPER_RECEIPT_TIMEOUT_MS;
|
|
16
|
+
if (value === undefined || value.trim() === '') return 60_000;
|
|
17
|
+
const timeout = Number(value);
|
|
18
|
+
if (!Number.isSafeInteger(timeout) || timeout <= 0) throw new Error('RELAYFLOW_HELPER_RECEIPT_TIMEOUT_MS must be a positive integer');
|
|
19
|
+
return timeout;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Persist intent before mount I/O: an interrupted submission is never blindly repeated. */
|
|
23
|
+
export async function writeHelperDraft(mount: string, provider: string, operation: string,
|
|
24
|
+
path: string, body: Record<string, unknown>, dataDir: string, runId: string, stepId: string,
|
|
25
|
+
signal: AbortSignal): Promise<WritebackResult> {
|
|
26
|
+
const timeout = helperReceiptTimeoutMs();
|
|
27
|
+
const writeId = `${runId}:${stepId}`;
|
|
28
|
+
const file = receiptPath(dataDir, runId, stepId) + '.pending';
|
|
29
|
+
let intent: { path: string; absolutePath: string; body: Record<string, unknown> };
|
|
30
|
+
try { intent = await readHelperReceipt(file) as typeof intent; }
|
|
31
|
+
catch (error) {
|
|
32
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
33
|
+
const absolutePath = resolve(mount, path.replace(/^\/+/, ''));
|
|
34
|
+
const inside = relative(mount, absolutePath);
|
|
35
|
+
if (inside === '..' || inside.startsWith('../') || isAbsolute(inside)) throw new Error('Helper path escapes mount root');
|
|
36
|
+
intent = { path, absolutePath, body };
|
|
37
|
+
await atomicJson(file, intent);
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
const submitted = await writeJsonFile({ relayfileMountRoot: mount, writebackTimeoutMs: 0 }, provider, operation, path, body);
|
|
40
|
+
if (providerReceipt(submitted.receipt, intent.body)) {
|
|
41
|
+
return { ...submitted, deliveryStatus: 'confirmed', receipt: stringReceipt(submitted.receipt) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const deadline = Date.now() + timeout;
|
|
45
|
+
do {
|
|
46
|
+
signal.throwIfAborted();
|
|
47
|
+
let receipt: WritebackReceipt | undefined;
|
|
48
|
+
try { receipt = JSON.parse(await readFile(intent.absolutePath, 'utf8')) as WritebackReceipt; }
|
|
49
|
+
catch (error) {
|
|
50
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
|
|
51
|
+
}
|
|
52
|
+
// Identity fields only. A path, timestamp, or error object written while
|
|
53
|
+
// the adapter is still flushing is not a provider delivery.
|
|
54
|
+
if (providerReceipt(receipt, intent.body)) {
|
|
55
|
+
return { path: intent.path, absolutePath: intent.absolutePath, deliveryStatus: 'confirmed', receipt: stringReceipt(receipt) };
|
|
56
|
+
}
|
|
57
|
+
const remaining = deadline - Date.now();
|
|
58
|
+
if (remaining <= 0) break;
|
|
59
|
+
await delay(Math.min(250, remaining), undefined, { signal });
|
|
60
|
+
} while (Date.now() <= deadline);
|
|
61
|
+
throw new HelperWritebackPending(writeId, intent.path, runId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A provider id the draft did not already carry. Path and created are not delivery. */
|
|
65
|
+
function providerReceipt(receipt: WritebackReceipt | undefined, draft: Record<string, unknown>): receipt is WritebackReceipt {
|
|
66
|
+
if (receipt === undefined || typeof receipt !== 'object' || Array.isArray(receipt)) return false;
|
|
67
|
+
return (['id', 'externalId', 'ts'] as const).some(key => {
|
|
68
|
+
const value = receiptIdentity(receipt[key]);
|
|
69
|
+
return value !== undefined && value !== receiptIdentity(draft[key]);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** GitHub mount files store comment and issue ids as JSON numbers. */
|
|
74
|
+
function receiptIdentity(value: unknown): string | undefined {
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
78
|
+
}
|
|
79
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) return String(value);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function stringReceipt(receipt: WritebackReceipt): WritebackReceipt {
|
|
84
|
+
const next: WritebackReceipt = { ...receipt };
|
|
85
|
+
for (const key of ['id', 'externalId', 'ts'] as const) {
|
|
86
|
+
const value = receiptIdentity(receipt[key]);
|
|
87
|
+
if (value !== undefined) next[key] = value;
|
|
88
|
+
}
|
|
89
|
+
return next;
|
|
90
|
+
}
|
package/src/helper-writeback.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { readJsonFile, listJsonFiles, type WritebackResult } from '@relayfile/adapter-core/vfs-client';
|
|
4
4
|
import type { RelayTransport, RelayTransportRequest } from '@relayfile/relay-helpers/transport';
|
|
5
5
|
import { helperClients, helperProviders, invokeHelper, type HelperCall } from '@relayflows/surface/runtime';
|
|
6
6
|
import { providerMount } from './slack-preflight.js';
|
|
7
7
|
import { atomicJson } from './helper-storage.js';
|
|
8
8
|
import type { SlackCall } from './slack-writeback.js';
|
|
9
|
+
import { writeHelperDraft, HelperWritebackPending } from './helper-receipt.js';
|
|
9
10
|
|
|
10
11
|
export class HelperDeliveryError extends Error {}
|
|
11
12
|
|
|
@@ -14,7 +15,10 @@ export async function helperWriteback(call: HelperCall, dataDir: string, runId:
|
|
|
14
15
|
if (!factory) throw new Error(`No writeback client for ${call.provider}`);
|
|
15
16
|
const { transport } = helperTransport(call, dataDir, runId, stepId, signal);
|
|
16
17
|
try { return await invokeHelper(factory, call, transport); }
|
|
17
|
-
catch (cause) {
|
|
18
|
+
catch (cause) {
|
|
19
|
+
if (cause instanceof HelperWritebackPending) throw cause;
|
|
20
|
+
throw new HelperDeliveryError(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
21
|
+
}
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/** One transport for every provider, including Slack. Only confirmed writes succeed. */
|
|
@@ -65,8 +69,9 @@ export function helperTransport(call: HelperCall | SlackCall, dataDir: string, r
|
|
|
65
69
|
runId, stepId, request: { ...request, body }, receipt: result.receipt,
|
|
66
70
|
});
|
|
67
71
|
} else {
|
|
68
|
-
try { result = await
|
|
72
|
+
try { result = await writeHelperDraft(options().relayfileMountRoot, call.provider, `write.${request.resource}`, draft, body, dataDir, runId, stepId, signal); }
|
|
69
73
|
catch (cause) {
|
|
74
|
+
if (cause instanceof HelperWritebackPending) throw cause;
|
|
70
75
|
// Upstream created() converts pending/terminal adapter errors into values.
|
|
71
76
|
// A journal effect must fail instead of confirming an undelivered write.
|
|
72
77
|
throw new Error(`${call.provider} writeback failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
package/src/index.ts
CHANGED
|
@@ -131,6 +131,16 @@ export { localFlowEvalExecutor, type LocalFlowEvalExecutorOptions } from './flow
|
|
|
131
131
|
export { cloudFlowEvalExecutor, type CloudFlowEvalExecutorOptions } from './flow-eval-cloud.js';
|
|
132
132
|
|
|
133
133
|
export { canonicalize, specHash } from './canonical.js';
|
|
134
|
+
export {
|
|
135
|
+
createFlowToolManifest, parseFlowToolManifest, canonicalFlowToolManifest,
|
|
136
|
+
validateFlowToolInput, validateFlowToolResult,
|
|
137
|
+
type FlowToolDeclarationV1, type FlowToolManifestV1, type FlowToolDigest,
|
|
138
|
+
} from './flow-tool-manifest.js';
|
|
139
|
+
export { FLOW_TOOL_SCHEMA_DIALECT, type FlowToolJson, type FlowToolObjectSchema } from './flow-tool-schema.js';
|
|
140
|
+
export {
|
|
141
|
+
flowToolFunctionDefinition, flowToolMcpDefinition,
|
|
142
|
+
type FlowToolFunctionDefinition, type FlowToolMcpDefinition,
|
|
143
|
+
} from './flow-tool-definitions.js';
|
|
134
144
|
export {
|
|
135
145
|
compileAndHash,
|
|
136
146
|
compileSpec,
|
|
@@ -295,6 +305,17 @@ export {
|
|
|
295
305
|
|
|
296
306
|
export { createFlow, type CreateFlowOptions, type CreatedFlow } from './create-flow.js';
|
|
297
307
|
|
|
308
|
+
export * from './flow-tool-contract.js';
|
|
309
|
+
export { parseFlowToolCatalog, canonicalFlowToolCatalog, parseFlowToolEntry, parseFlowToolRun, parseFlowToolEvent,
|
|
310
|
+
parseFlowToolEvidence, parseFlowToolInvocation, flowToolInputDigest, flowToolRunLinks } from './flow-tool-wire.js';
|
|
311
|
+
export { FlowToolClient, type FlowToolTransport, type FlowToolRequest, type FlowToolInvocationOptions } from './flow-tool-client.js';
|
|
312
|
+
export { createFlowToolHttpTransport, type FlowToolHttpOptions } from './flow-tool-http.js';
|
|
313
|
+
export { createFlowToolAdapters } from './flow-tool-adapters.js';
|
|
314
|
+
export {
|
|
315
|
+
createKernelFlowToolControlPlane, FLOW_TOOL_INPUT_PLACEHOLDER,
|
|
316
|
+
type KernelFlowToolDeployment, type KernelFlowToolControlPlaneOptions,
|
|
317
|
+
} from './flow-tool-kernel.js';
|
|
318
|
+
|
|
298
319
|
export { renderProgress, type ProgressEvent } from './progress.js';
|
|
299
320
|
export { webhookTriggerSpec } from './trigger-executor.js';
|
|
300
321
|
export { scheduleTriggerSpec, scheduleLowering, SCHEDULE_EXECUTOR, type ScheduleLowering } from './schedule-trigger.js';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { HelperWritebackPending, writeHelperDraft } from './helper-receipt.js';
|
|
5
|
+
import { parkHelperReceipt } from './helper-park.js';
|
|
5
6
|
import type { RelayTransport } from '@relayfile/relay-helpers';
|
|
6
7
|
import type { JournalClient } from './journal-client.js';
|
|
7
8
|
import type { StepDispatchEvent } from './protocol.js';
|
|
@@ -37,8 +38,8 @@ async function writeback(call: HelperCall, dataDir: string, runId: string, stepI
|
|
|
37
38
|
// Item updates keep the client's canonical path. Creates use a stable draft.
|
|
38
39
|
const path = request.path.endsWith('.json') ? request.path
|
|
39
40
|
: `${request.path}/draft-${createHash('sha256').update(idempotencyKey).digest('hex')}.json`;
|
|
40
|
-
const result = await
|
|
41
|
-
`write.${request.resource}`, path, body);
|
|
41
|
+
const result = await writeHelperDraft(mount, request.provider,
|
|
42
|
+
`write.${request.resource}`, path, body, dataDir, runId, stepId, signal);
|
|
42
43
|
if (result.deliveryStatus !== 'confirmed' || !result.receipt) {
|
|
43
44
|
throw new Error(`${call.provider} writeback is pending; no delivery receipt`);
|
|
44
45
|
}
|
|
@@ -54,7 +55,9 @@ export async function completeHelperDispatch(
|
|
|
54
55
|
client: JournalClient, dispatch: StepDispatchEvent, call: HelperCall, dataDir: string,
|
|
55
56
|
): Promise<void> {
|
|
56
57
|
const surfacePath = `/${call.provider}`;
|
|
57
|
-
|
|
58
|
+
let output: { idempotencyKey: string; receipt: unknown };
|
|
59
|
+
try {
|
|
60
|
+
output = await withWorkerLease(client, dispatch, async signal => {
|
|
58
61
|
const file = receiptPath(dataDir, dispatch.run_id, dispatch.step_id);
|
|
59
62
|
let receipt: unknown;
|
|
60
63
|
await client.performEffect({
|
|
@@ -72,7 +75,11 @@ export async function completeHelperDispatch(
|
|
|
72
75
|
});
|
|
73
76
|
if (receipt === undefined) receipt = await readSlackReceipt(file);
|
|
74
77
|
return { ...call, idempotencyKey: `${dispatch.run_id}:${dispatch.step_id}`, receipt };
|
|
75
|
-
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof HelperWritebackPending) await parkHelperReceipt(client, dispatch, error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
76
83
|
await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt,
|
|
77
84
|
dispatch.idempotency_key, 'success', { output, started_pins: dispatch.pins, end_pins: dispatch.pins, reported_cost: NO_MODEL_COST,
|
|
78
85
|
effects: [{ surface_path: surfacePath, idempotency_key: dispatch.idempotency_key }] });
|