@relayflows/sdk 2.0.45 → 2.0.47
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/journal-client.d.ts +5 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +8 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.d.ts.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/journal-client.ts +8 -2
- package/src/protocol.ts +6 -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';
|
package/src/journal-client.ts
CHANGED
|
@@ -57,6 +57,8 @@ export class JournalClient extends JournalConnection {
|
|
|
57
57
|
private readonly budgetHandshake: boolean;
|
|
58
58
|
/** Additive request capabilities the daemon advertised at `hello`; none until then. */
|
|
59
59
|
private features: ReadonlySet<string> = new Set();
|
|
60
|
+
/** `hello.det_withheld`: names whose values never go into a sent env. */
|
|
61
|
+
private withheld: ReadonlySet<string> = new Set();
|
|
60
62
|
|
|
61
63
|
constructor(socketPath: string, options: JournalClientOptions = {}) {
|
|
62
64
|
super(socketPath, options.requestTimeoutMs ?? 30_000, options.connectTimeoutMs ?? 2_000);
|
|
@@ -112,6 +114,8 @@ export class JournalClient extends JournalConnection {
|
|
|
112
114
|
const result = this.budgeted === undefined || !this.budgetHandshake ? await this.request('hello', params)
|
|
113
115
|
: await this.budgeted.handshake(params, this.requestTimeoutMs);
|
|
114
116
|
this.features = new Set(Array.isArray(result.features) ? result.features.filter(f => typeof f === 'string') : []);
|
|
117
|
+
this.withheld = new Set(Array.isArray(result.det_withheld)
|
|
118
|
+
? result.det_withheld.filter(name => typeof name === 'string') : []);
|
|
115
119
|
return result;
|
|
116
120
|
}
|
|
117
121
|
|
|
@@ -149,13 +153,15 @@ export class JournalClient extends JournalConnection {
|
|
|
149
153
|
* This process's environment, for the run's `f.run` steps. A shared daemon
|
|
150
154
|
* otherwise spawns them in the environment of whichever CLI started it
|
|
151
155
|
* (kernel/DAEMON-LIFECYCLE.md §3). Sent only to a daemon that lists
|
|
152
|
-
* `step_env`, so an older one never sees an unknown field.
|
|
156
|
+
* `step_env`, so an older one never sees an unknown field. Names the
|
|
157
|
+
* daemon withholds (`hello.det_withheld`) are left out, on start and resume
|
|
158
|
+
* alike, so their values never cross the socket from this client.
|
|
153
159
|
*/
|
|
154
160
|
private stepEnv(): { env?: Record<string, string> } {
|
|
155
161
|
if (!this.features.has('step_env')) return {};
|
|
156
162
|
const env: Record<string, string> = {};
|
|
157
163
|
for (const [key, value] of Object.entries(process.env)) {
|
|
158
|
-
if (value !== undefined) env[key] = value;
|
|
164
|
+
if (value !== undefined && !this.withheld.has(key)) env[key] = value;
|
|
159
165
|
}
|
|
160
166
|
return { env };
|
|
161
167
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -84,6 +84,12 @@ export interface HelloResult {
|
|
|
84
84
|
* predate it, which then accept none of the optional fields it gates.
|
|
85
85
|
*/
|
|
86
86
|
features?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* Env names the daemon strips from every deterministic step
|
|
89
|
+
* (`relayflowd serve --det-withhold`). Names only. A client must leave
|
|
90
|
+
* their values out of the `env` it sends; the daemon strips them anyway.
|
|
91
|
+
*/
|
|
92
|
+
det_withheld?: string[];
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
export interface StepSpend {
|
|
@@ -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 }] });
|