@relayflows/sdk 2.0.44 → 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.
Files changed (111) hide show
  1. package/dist/authored-flow-error.d.ts +1 -1
  2. package/dist/authored-flow-error.d.ts.map +1 -1
  3. package/dist/authored-flow-error.js.map +1 -1
  4. package/dist/authored-helper-effect.d.ts.map +1 -1
  5. package/dist/authored-helper-effect.js +9 -0
  6. package/dist/authored-helper-effect.js.map +1 -1
  7. package/dist/authored-root.d.ts.map +1 -1
  8. package/dist/authored-root.js +17 -0
  9. package/dist/authored-root.js.map +1 -1
  10. package/dist/cli/direct-run.d.ts.map +1 -1
  11. package/dist/cli/direct-run.js +7 -0
  12. package/dist/cli/direct-run.js.map +1 -1
  13. package/dist/cli/run.d.ts.map +1 -1
  14. package/dist/cli/run.js +31 -2
  15. package/dist/cli/run.js.map +1 -1
  16. package/dist/cloud-mirror-step.d.ts.map +1 -1
  17. package/dist/cloud-mirror-step.js +6 -1
  18. package/dist/cloud-mirror-step.js.map +1 -1
  19. package/dist/compile.d.ts +1 -0
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +2 -1
  22. package/dist/compile.js.map +1 -1
  23. package/dist/failure-kinds.d.ts +1 -1
  24. package/dist/failure-kinds.d.ts.map +1 -1
  25. package/dist/failure-kinds.js +1 -0
  26. package/dist/failure-kinds.js.map +1 -1
  27. package/dist/flow-tool-adapters.d.ts +56 -0
  28. package/dist/flow-tool-adapters.d.ts.map +1 -0
  29. package/dist/flow-tool-adapters.js +42 -0
  30. package/dist/flow-tool-adapters.js.map +1 -0
  31. package/dist/flow-tool-client.d.ts +38 -0
  32. package/dist/flow-tool-client.d.ts.map +1 -0
  33. package/dist/flow-tool-client.js +105 -0
  34. package/dist/flow-tool-client.js.map +1 -0
  35. package/dist/flow-tool-contract.d.ts +103 -0
  36. package/dist/flow-tool-contract.d.ts.map +1 -0
  37. package/dist/flow-tool-contract.js +75 -0
  38. package/dist/flow-tool-contract.js.map +1 -0
  39. package/dist/flow-tool-definitions.d.ts +19 -0
  40. package/dist/flow-tool-definitions.d.ts.map +1 -0
  41. package/dist/flow-tool-definitions.js +12 -0
  42. package/dist/flow-tool-definitions.js.map +1 -0
  43. package/dist/flow-tool-http.d.ts +14 -0
  44. package/dist/flow-tool-http.d.ts.map +1 -0
  45. package/dist/flow-tool-http.js +175 -0
  46. package/dist/flow-tool-http.js.map +1 -0
  47. package/dist/flow-tool-kernel.d.ts +27 -0
  48. package/dist/flow-tool-kernel.d.ts.map +1 -0
  49. package/dist/flow-tool-kernel.js +310 -0
  50. package/dist/flow-tool-kernel.js.map +1 -0
  51. package/dist/flow-tool-manifest.d.ts +30 -0
  52. package/dist/flow-tool-manifest.d.ts.map +1 -0
  53. package/dist/flow-tool-manifest.js +79 -0
  54. package/dist/flow-tool-manifest.js.map +1 -0
  55. package/dist/flow-tool-schema.d.ts +15 -0
  56. package/dist/flow-tool-schema.d.ts.map +1 -0
  57. package/dist/flow-tool-schema.js +70 -0
  58. package/dist/flow-tool-schema.js.map +1 -0
  59. package/dist/flow-tool-wire.d.ts +26 -0
  60. package/dist/flow-tool-wire.d.ts.map +1 -0
  61. package/dist/flow-tool-wire.js +195 -0
  62. package/dist/flow-tool-wire.js.map +1 -0
  63. package/dist/helper-park.d.ts +14 -0
  64. package/dist/helper-park.d.ts.map +1 -0
  65. package/dist/helper-park.js +40 -0
  66. package/dist/helper-park.js.map +1 -0
  67. package/dist/helper-receipt.d.ts +11 -0
  68. package/dist/helper-receipt.d.ts.map +1 -0
  69. package/dist/helper-receipt.js +100 -0
  70. package/dist/helper-receipt.js.map +1 -0
  71. package/dist/helper-writeback.d.ts.map +1 -1
  72. package/dist/helper-writeback.js +7 -2
  73. package/dist/helper-writeback.js.map +1 -1
  74. package/dist/index.d.ts +9 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +9 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/journal-client.d.ts +7 -0
  79. package/dist/journal-client.d.ts.map +1 -1
  80. package/dist/journal-client.js +22 -1
  81. package/dist/journal-client.js.map +1 -1
  82. package/dist/protocol.d.ts +7 -0
  83. package/dist/protocol.d.ts.map +1 -1
  84. package/dist/yaml-helper-effect.d.ts.map +1 -1
  85. package/dist/yaml-helper-effect.js +32 -23
  86. package/dist/yaml-helper-effect.js.map +1 -1
  87. package/package.json +2 -2
  88. package/src/authored-flow-error.ts +1 -0
  89. package/src/authored-helper-effect.ts +9 -0
  90. package/src/authored-root.ts +17 -0
  91. package/src/cli/direct-run.ts +7 -0
  92. package/src/cli/run.ts +30 -2
  93. package/src/cloud-mirror-step.ts +6 -1
  94. package/src/compile.ts +2 -1
  95. package/src/failure-kinds.ts +1 -0
  96. package/src/flow-tool-adapters.ts +43 -0
  97. package/src/flow-tool-client.ts +115 -0
  98. package/src/flow-tool-contract.ts +153 -0
  99. package/src/flow-tool-definitions.ts +27 -0
  100. package/src/flow-tool-http.ts +134 -0
  101. package/src/flow-tool-kernel.ts +319 -0
  102. package/src/flow-tool-manifest.ts +104 -0
  103. package/src/flow-tool-schema.ts +66 -0
  104. package/src/flow-tool-wire.ts +152 -0
  105. package/src/helper-park.ts +45 -0
  106. package/src/helper-receipt.ts +90 -0
  107. package/src/helper-writeback.ts +8 -3
  108. package/src/index.ts +21 -0
  109. package/src/journal-client.ts +21 -1
  110. package/src/protocol.ts +7 -0
  111. 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
+ }
@@ -1,11 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { join } from 'node:path';
3
- import { writeJsonFile, readJsonFile, listJsonFiles, type WritebackResult } from '@relayfile/adapter-core/vfs-client';
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) { throw new HelperDeliveryError(cause instanceof Error ? cause.message : String(cause), { 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 writeJsonFile(options(), call.provider, `write.${request.resource}`, draft, body); }
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';
@@ -132,12 +132,32 @@ export class JournalClient extends JournalConnection {
132
132
  ...(reuseFromRunId === undefined ? {} : { reuse_from_run_id: reuseFromRunId }),
133
133
  ...(admissionKey === undefined ? {} : { admission_key: admissionKey }),
134
134
  ...(watch ? { watch: true } : {}),
135
+ ...this.stepEnv(),
135
136
  }, null);
136
137
  }
137
138
 
138
139
  /** §3 memoized resume. */
139
140
  runResume(runId: string, allowHumanInfluenced = false): Promise<VerbContract['run.resume']['result']> {
140
- return this.request('run.resume', { run_id: runId, ...(allowHumanInfluenced ? { allow_human_influenced: true } : {}) }, null);
141
+ return this.request('run.resume', {
142
+ run_id: runId,
143
+ ...(allowHumanInfluenced ? { allow_human_influenced: true } : {}),
144
+ ...this.stepEnv(),
145
+ }, null);
146
+ }
147
+
148
+ /**
149
+ * This process's environment, for the run's `f.run` steps. A shared daemon
150
+ * otherwise spawns them in the environment of whichever CLI started it
151
+ * (kernel/DAEMON-LIFECYCLE.md §3). Sent only to a daemon that lists
152
+ * `step_env`, so an older one never sees an unknown field.
153
+ */
154
+ private stepEnv(): { env?: Record<string, string> } {
155
+ if (!this.features.has('step_env')) return {};
156
+ const env: Record<string, string> = {};
157
+ for (const [key, value] of Object.entries(process.env)) {
158
+ if (value !== undefined) env[key] = value;
159
+ }
160
+ return { env };
141
161
  }
142
162
 
143
163
  /** Durably request cancellation and return the terminal run fact. */
package/src/protocol.ts CHANGED
@@ -126,6 +126,11 @@ export interface RunStartParams {
126
126
  admission_key?: string;
127
127
  /** Push the new run's entries to this connection from `run.spawned` on, as `run.watch` does. */
128
128
  watch?: boolean;
129
+ /**
130
+ * Environment for this run's deterministic steps, replacing the daemon's.
131
+ * Daemon feature `step_env`. Held in daemon memory only, never journaled.
132
+ */
133
+ env?: Record<string, string>;
129
134
  reuse_from_run_id?: string;
130
135
  /**
131
136
  * The kernel spec dialect — the ONE boundary shape `RunSpec::parse`
@@ -148,6 +153,8 @@ export type RunStartResult = RunOutcome;
148
153
 
149
154
  export interface RunResumeParams {
150
155
  allow_human_influenced?: boolean;
156
+ /** Replaces the run's step environment (feature `step_env`); see `RunStartParams.env`. */
157
+ env?: Record<string, string>;
151
158
  run_id: string;
152
159
  }
153
160
  export type RunResumeResult = RunOutcome;
@@ -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 { writeJsonFile } from '@relayfile/adapter-core/vfs-client';
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 writeJsonFile({ relayfileMountRoot: mount }, request.provider,
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
- const output = await withWorkerLease(client, dispatch, async signal => {
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 }] });