@relayflows/sdk 2.0.35 → 2.0.36

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 (138) hide show
  1. package/dist/adapters/base.d.ts +16 -0
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +15 -0
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/codex.d.ts.map +1 -1
  7. package/dist/adapters/codex.js +4 -0
  8. package/dist/adapters/codex.js.map +1 -1
  9. package/dist/adapters/usage-limit.d.ts +2 -0
  10. package/dist/adapters/usage-limit.d.ts.map +1 -0
  11. package/dist/adapters/usage-limit.js +17 -0
  12. package/dist/adapters/usage-limit.js.map +1 -0
  13. package/dist/authored-activity.d.ts +17 -0
  14. package/dist/authored-activity.d.ts.map +1 -0
  15. package/dist/authored-activity.js +218 -0
  16. package/dist/authored-activity.js.map +1 -0
  17. package/dist/authored-child-run.d.ts +34 -0
  18. package/dist/authored-child-run.d.ts.map +1 -0
  19. package/dist/authored-child-run.js +128 -0
  20. package/dist/authored-child-run.js.map +1 -0
  21. package/dist/authored-flow-error.d.ts +32 -3
  22. package/dist/authored-flow-error.d.ts.map +1 -1
  23. package/dist/authored-flow-error.js +3 -1
  24. package/dist/authored-flow-error.js.map +1 -1
  25. package/dist/authored-flow-executor.d.ts +9 -1
  26. package/dist/authored-flow-executor.d.ts.map +1 -1
  27. package/dist/authored-flow-executor.js +30 -0
  28. package/dist/authored-flow-executor.js.map +1 -1
  29. package/dist/authored-mcp.js +2 -2
  30. package/dist/authored-mcp.js.map +1 -1
  31. package/dist/authored-node-entry.js +3 -1
  32. package/dist/authored-node-entry.js.map +1 -1
  33. package/dist/authored-node-runner.d.ts.map +1 -1
  34. package/dist/authored-node-runner.js +40 -8
  35. package/dist/authored-node-runner.js.map +1 -1
  36. package/dist/authored-plugin-effect.js +2 -2
  37. package/dist/authored-plugin-effect.js.map +1 -1
  38. package/dist/authored-root.d.ts +10 -7
  39. package/dist/authored-root.d.ts.map +1 -1
  40. package/dist/authored-root.js +19 -0
  41. package/dist/authored-root.js.map +1 -1
  42. package/dist/authored-step-output.d.ts +24 -8
  43. package/dist/authored-step-output.d.ts.map +1 -1
  44. package/dist/authored-step-output.js +81 -15
  45. package/dist/authored-step-output.js.map +1 -1
  46. package/dist/authored-subscription-park.d.ts +5 -0
  47. package/dist/authored-subscription-park.d.ts.map +1 -0
  48. package/dist/authored-subscription-park.js +47 -0
  49. package/dist/authored-subscription-park.js.map +1 -0
  50. package/dist/authored-worker-step.d.ts.map +1 -1
  51. package/dist/authored-worker-step.js +7 -2
  52. package/dist/authored-worker-step.js.map +1 -1
  53. package/dist/cli/check-activities.d.ts +8 -0
  54. package/dist/cli/check-activities.d.ts.map +1 -0
  55. package/dist/cli/check-activities.js +155 -0
  56. package/dist/cli/check-activities.js.map +1 -0
  57. package/dist/cli/cli-probe.d.ts.map +1 -1
  58. package/dist/cli/cli-probe.js +14 -2
  59. package/dist/cli/cli-probe.js.map +1 -1
  60. package/dist/cli/direct-run.d.ts.map +1 -1
  61. package/dist/cli/direct-run.js +10 -2
  62. package/dist/cli/direct-run.js.map +1 -1
  63. package/dist/cli/run.d.ts +18 -5
  64. package/dist/cli/run.d.ts.map +1 -1
  65. package/dist/cli/run.js +24 -5
  66. package/dist/cli/run.js.map +1 -1
  67. package/dist/cli/subscription-report.d.ts +5 -0
  68. package/dist/cli/subscription-report.d.ts.map +1 -0
  69. package/dist/cli/subscription-report.js +48 -0
  70. package/dist/cli/subscription-report.js.map +1 -0
  71. package/dist/cli-adapter.d.ts +4 -2
  72. package/dist/cli-adapter.d.ts.map +1 -1
  73. package/dist/cli-adapter.js +4 -0
  74. package/dist/cli-adapter.js.map +1 -1
  75. package/dist/cli.d.ts +1 -1
  76. package/dist/cli.d.ts.map +1 -1
  77. package/dist/cli.js +13 -5
  78. package/dist/cli.js.map +1 -1
  79. package/dist/failure-kinds.d.ts +2 -2
  80. package/dist/failure-kinds.d.ts.map +1 -1
  81. package/dist/failure-kinds.js +2 -0
  82. package/dist/failure-kinds.js.map +1 -1
  83. package/dist/index.d.ts +1 -1
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/journal-client.d.ts +17 -2
  87. package/dist/journal-client.d.ts.map +1 -1
  88. package/dist/journal-client.js +33 -2
  89. package/dist/journal-client.js.map +1 -1
  90. package/dist/journal-reader.d.ts.map +1 -1
  91. package/dist/journal-reader.js +2 -0
  92. package/dist/journal-reader.js.map +1 -1
  93. package/dist/local-agent.d.ts +4 -1
  94. package/dist/local-agent.d.ts.map +1 -1
  95. package/dist/local-agent.js +14 -4
  96. package/dist/local-agent.js.map +1 -1
  97. package/dist/preflight.d.ts +6 -0
  98. package/dist/preflight.d.ts.map +1 -1
  99. package/dist/preflight.js +36 -10
  100. package/dist/preflight.js.map +1 -1
  101. package/dist/protocol.d.ts +166 -1
  102. package/dist/protocol.d.ts.map +1 -1
  103. package/dist/step-settling.d.ts +11 -0
  104. package/dist/step-settling.d.ts.map +1 -0
  105. package/dist/step-settling.js +22 -0
  106. package/dist/step-settling.js.map +1 -0
  107. package/package.json +2 -2
  108. package/src/adapters/base.ts +19 -0
  109. package/src/adapters/claude.ts +15 -0
  110. package/src/adapters/codex.ts +6 -0
  111. package/src/adapters/usage-limit.ts +17 -0
  112. package/src/authored-activity.ts +251 -0
  113. package/src/authored-child-run.ts +138 -0
  114. package/src/authored-flow-error.ts +33 -0
  115. package/src/authored-flow-executor.ts +40 -0
  116. package/src/authored-mcp.ts +2 -2
  117. package/src/authored-node-entry.ts +3 -1
  118. package/src/authored-node-runner.ts +41 -8
  119. package/src/authored-plugin-effect.ts +2 -2
  120. package/src/authored-root.ts +27 -4
  121. package/src/authored-step-output.ts +88 -15
  122. package/src/authored-subscription-park.ts +46 -0
  123. package/src/authored-worker-step.ts +7 -2
  124. package/src/cli/check-activities.ts +144 -0
  125. package/src/cli/cli-probe.ts +15 -3
  126. package/src/cli/direct-run.ts +9 -0
  127. package/src/cli/run.ts +46 -9
  128. package/src/cli/subscription-report.ts +51 -0
  129. package/src/cli-adapter.ts +8 -2
  130. package/src/cli.ts +13 -6
  131. package/src/failure-kinds.ts +2 -0
  132. package/src/index.ts +6 -0
  133. package/src/journal-client.ts +47 -3
  134. package/src/journal-reader.ts +2 -0
  135. package/src/local-agent.ts +17 -3
  136. package/src/preflight.ts +46 -10
  137. package/src/protocol.ts +129 -0
  138. package/src/step-settling.ts +19 -0
@@ -25,6 +25,7 @@ import {
25
25
  emptyReport,
26
26
  fromCheckReport,
27
27
  protocolFailure,
28
+ suspendedExecution,
28
29
  socketFor,
29
30
  type RunExecution,
30
31
  type RunLifecycleOptions,
@@ -92,6 +93,7 @@ export async function runDirectFlow(
92
93
  {
93
94
  dataDir,
94
95
  admissionKey: admissionIdentity,
96
+ onAdmitted: runId => { base.rootRunId = runId; },
95
97
  localAgentStream: localAgent?.stream,
96
98
  ...(localAgent === undefined ? {} : { workerCapacity }),
97
99
  lifecycle: {
@@ -102,6 +104,9 @@ export async function runDirectFlow(
102
104
  },
103
105
  },
104
106
  );
107
+ if (result.state === 'suspended') {
108
+ return suspendedExecution('run', base, socketPath, result.rootRunId, result);
109
+ }
105
110
  const terminal = result.journalSteps.at(-1);
106
111
  if (terminal === undefined) {
107
112
  return protocolFailure('run', base, socketPath, new Error(
@@ -139,6 +144,8 @@ export async function runDirectFlow(
139
144
  && (error.code === 'helper_slack.credential_missing'
140
145
  || error.code === 'helper_slack.mount_required'
141
146
  || error.code === 'budget_syntax_invalid'
147
+ || error.code === 'cli_outdated'
148
+ || error.code === 'provider_usage_limited'
142
149
  || error.code === 'unsupported_promise_lifecycle'
143
150
  || error.code === 'unsupported_header'
144
151
  || error.code === 'agent_cli_unresolved'
@@ -152,6 +159,8 @@ export async function runDirectFlow(
152
159
  error.code === 'helper_slack.credential_missing'
153
160
  || error.code === 'helper_slack.mount_required'
154
161
  || error.code === 'budget_syntax_invalid'
162
+ || error.code === 'cli_outdated'
163
+ || error.code === 'provider_usage_limited'
155
164
  ) ? error.code : 'invalid_spec',
156
165
  message: error.message,
157
166
  }, path)),
package/src/cli/run.ts CHANGED
@@ -23,13 +23,15 @@ import {
23
23
  import type { ParkCause, RunFailureKind, RunWarningKind, StepFailedDetails } from '../failure-kinds.js';
24
24
  import { inspectionHint, renderInspection, renderStepEvidence, stepFailureDetails } from './step-failure.js';
25
25
  import { JournalClient, JournalProtocolError } from '../journal-client.js';
26
- import { attachLocalAgent } from '../local-agent.js';
26
+ import { attachLocalAgent, declaredLocalAgentStreams } from '../local-agent.js';
27
27
  import { LlmWorker } from '../llm-worker.js';
28
28
  import { readAuthoredRootMetadata, resumeDurableAuthoredFlow, type AuthoredRootMetadata } from '../authored-root.js';
29
+ import type { AuthoredFlowSuspendedResult } from '../authored-flow-executor.js';
29
30
  import type {
30
31
  RunCompletionReason,
31
32
  RunOutcome,
32
33
  RunStatus,
34
+ SubscriptionSnapshot,
33
35
  } from '../protocol.js';
34
36
  import type { StepType } from '../spec.js';
35
37
  import {
@@ -42,7 +44,7 @@ import {
42
44
  type CheckReport,
43
45
  } from './check.js';
44
46
 
45
- export type RunExitCode = 0 | 1 | 2 | 3;
47
+ export type RunExitCode = 0 | 1 | 2 | 3 | 4;
46
48
  export type RunCommand = 'run' | 'resume' | 'answer';
47
49
 
48
50
  export interface ParkedStep {
@@ -52,7 +54,7 @@ export interface ParkedStep {
52
54
 
53
55
  export interface RunDiagnostic extends StepFailedDetails {
54
56
  severity: 'refusal' | 'failure' | 'parked' | 'warning' | 'declined';
55
- kind: RunFailureKind | RunWarningKind | RunCompletionReason;
57
+ kind: RunFailureKind | RunWarningKind | RunCompletionReason | 'subscription_suspended';
56
58
  message: string;
57
59
  }
58
60
 
@@ -61,10 +63,14 @@ export interface RunReport {
61
63
  command: RunCommand;
62
64
  path?: string;
63
65
  runId?: string;
64
- /** Authored root to resume/collect when runId identifies a failed child. */
66
+ /** Authored root owning subscriptions when runId names a failed child. */
65
67
  rootRunId?: string;
66
68
  socketPath?: string;
67
- status?: RunStatus;
69
+ status?: RunStatus | 'suspended';
70
+ /** Cloud consumes this exact durable boundary before launching a resume. */
71
+ suspension?: AuthoredFlowSuspendedResult['suspension'] & { settleMs?: number; idleAtMs?: number };
72
+ /** Durable subscription projection for Cloud routing and cleanup. */
73
+ subscriptions?: SubscriptionSnapshot[];
68
74
  completionReason?: RunCompletionReason;
69
75
  /**
70
76
  * What the body passed to `done(reason, { detail })`, normalized: redacted,
@@ -103,6 +109,26 @@ export interface RunExecution {
103
109
  report: RunReport;
104
110
  }
105
111
 
112
+ export function suspendedExecution(
113
+ command: RunCommand,
114
+ base: CheckReport | RunReport,
115
+ socketPath: string,
116
+ runId: string,
117
+ result: AuthoredFlowSuspendedResult & { readonly rootRunId: string },
118
+ ): RunExecution {
119
+ return {
120
+ exitCode: 4,
121
+ report: {
122
+ ...fromBase(command, base), ok: false, runId, socketPath, status: 'suspended',
123
+ suspension: result.suspension, completedSteps: result.journalSteps.length,
124
+ diagnostics: [...base.diagnostics, {
125
+ severity: 'warning', kind: 'subscription_suspended',
126
+ message: `Flow "${result.name}" suspended for ${result.suspension.kind}.`,
127
+ }],
128
+ },
129
+ };
130
+ }
131
+
106
132
  export interface RunProgress {
107
133
  runId: string;
108
134
  stepId: string;
@@ -185,7 +211,10 @@ async function executeCheckedFlow(
185
211
  // Use the checked CLI/model and declared surfaces unchanged. The worker
186
212
  // advertises its existing pins; the daemon still owns surface matching.
187
213
  if (options.localAgent) {
188
- localAgent = await attachLocalAgent(client, dataDir, options.onPtyReady, undefined, options.agentCapacity);
214
+ localAgent = await attachLocalAgent(
215
+ client, dataDir, options.onPtyReady, undefined, options.agentCapacity,
216
+ undefined, declaredLocalAgentStreams(spec),
217
+ );
189
218
  }
190
219
  if (options.localAgent && spec.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
191
220
  const { attachCommunicationWorkers } = await import('../communication/local.js');
@@ -271,6 +300,7 @@ export async function resumeFlow(
271
300
  try {
272
301
  authoredRoot = await readAuthoredRootMetadata(client, runId);
273
302
  if (authoredRoot !== undefined) {
303
+ base.rootRunId = runId;
274
304
  // Both worker-surface mismatches are refusals, not protocol failures.
275
305
  // They used to throw bare `Error`s, which landed on `protocol_error`
276
306
  // ("RUN <id> unknown") and told nobody what to do instead; and the
@@ -311,6 +341,9 @@ export async function resumeFlow(
311
341
  lifecycle: options,
312
342
  });
313
343
  if (result === undefined) throw new Error('authored root disappeared during resume');
344
+ if (result.state === 'suspended') {
345
+ return suspendedExecution('resume', base, socketPath, runId, result);
346
+ }
314
347
  return authoredCompletion('resume', base, socketPath, result, runId);
315
348
  }
316
349
  // resumeHelperEffect subsumes the old resumeSlackEffect: it handles the
@@ -318,9 +351,13 @@ export async function resumeFlow(
318
351
  // second call the earlier rebase left is a stale reference from before
319
352
  // the helper fanout renamed the API.
320
353
  if (options.localAgent) {
321
- authoredAgent = await attachLocalAgent(client, dataDir, options.onPtyReady, undefined, options.agentCapacity);
322
354
  const entries = (await client.journalRead(runId, 1)).entries as Array<{ entry_type: string; payload?: { spec?: import('../spec.js').KernelRunSpec } }>;
323
355
  const spec = entries.find(entry => entry.entry_type === 'run.spawned')?.payload?.spec;
356
+ if (!spec) throw new Error('Cannot attach a local worker: the journaled run spec is missing');
357
+ authoredAgent = await attachLocalAgent(
358
+ client, dataDir, options.onPtyReady, undefined, options.agentCapacity,
359
+ undefined, declaredLocalAgentStreams(spec),
360
+ );
324
361
  if (spec?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
325
362
  const { attachCommunicationWorkers } = await import('../communication/local.js');
326
363
  communicationWorkers = await attachCommunicationWorkers(spec, socketPath, dataDir);
@@ -955,7 +992,7 @@ async function inspectOutOfBandStep(
955
992
  }
956
993
 
957
994
  // Match kernel/relayflowd/src/server/client.rs: allow the lease sweep to dispatch a retry.
958
- const LEASE_SWEEP_GRACE_MS = 5_000;
995
+ export const LEASE_SWEEP_GRACE_MS = 5_000;
959
996
 
960
997
  async function waitForRunningStep(
961
998
  client: JournalClient,
@@ -1056,7 +1093,7 @@ function throwIfCanceled(signal: AbortSignal | undefined, stepId: string): void
1056
1093
  if (signal?.aborted === true) throw new Error(`waiting for running step "${stepId}" was canceled`);
1057
1094
  }
1058
1095
 
1059
- function delay(ms: number, signal?: AbortSignal): Promise<void> {
1096
+ export function delay(ms: number, signal?: AbortSignal): Promise<void> {
1060
1097
  return new Promise((resolveDelay, rejectDelay) => {
1061
1098
  const finish = (): void => {
1062
1099
  signal?.removeEventListener('abort', cancel);
@@ -0,0 +1,51 @@
1
+ import { JournalClient } from '../journal-client.js';
2
+ import type { SubscriptionInspectResult } from '../protocol.js';
3
+ import type { RunExecution } from './run.js';
4
+
5
+ /** One JSON emission boundary covers authored/declarative run and resume outcomes. */
6
+ export async function withSubscriptionMetadata(
7
+ execution: RunExecution,
8
+ inspect: (socket: string, runId: string) => Promise<SubscriptionInspectResult> = inspectSubscriptions,
9
+ ): Promise<RunExecution> {
10
+ const { report } = execution;
11
+ const rootRunId = report.rootRunId ?? report.runId;
12
+ if (rootRunId === undefined || execution.exitCode === 2) {
13
+ return { ...execution, report: { ...report, subscriptions: [] } };
14
+ }
15
+ try {
16
+ if (report.socketPath === undefined) throw new Error('run report has no daemon socket');
17
+ const { subscriptions } = await inspect(report.socketPath, rootRunId);
18
+ let suspension = report.suspension;
19
+ if (suspension?.kind === 'event_wait') {
20
+ const state = subscriptions.find(item => item.subscriptionId === suspension!.subscriptionId);
21
+ if (state?.state !== 'active' || !Number.isSafeInteger(state.idleAtMs)
22
+ || !Number.isSafeInteger(state.settleMs) || !Number.isSafeInteger(state.deadlineAtMs)) {
23
+ throw new Error('event wait has no active durable timing snapshot');
24
+ }
25
+ suspension = { ...suspension, settleMs: state.settleMs,
26
+ idleAtMs: state.idleAtMs, deadlineAtMs: state.deadlineAtMs };
27
+ }
28
+ return { ...execution, report: { ...report, subscriptions,
29
+ ...(suspension === undefined ? {} : { suspension }) } };
30
+ } catch (error) {
31
+ // Missing authority is not an empty snapshot: Cloud must not close bindings
32
+ // or schedule timers from invented state. Preserve the run's diagnostic,
33
+ // but fail this report instead of presenting a usable suspension/success.
34
+ const { suspension: _suspension, subscriptions: _subscriptions, ...failed } = report;
35
+ return { exitCode: 1, report: { ...failed, ok: false, diagnostics: [
36
+ ...report.diagnostics, { severity: 'failure', kind: 'protocol_error',
37
+ message: `Cannot inspect durable subscriptions: ${error instanceof Error ? error.message : 'unknown error'}` },
38
+ ] } };
39
+ }
40
+ }
41
+
42
+ async function inspectSubscriptions(socket: string, runId: string): Promise<SubscriptionInspectResult> {
43
+ const client = new JournalClient(socket);
44
+ try {
45
+ await client.connect();
46
+ await client.hello('flows-subscription-report');
47
+ return await client.subscriptionInspect({ run_id: runId });
48
+ } finally {
49
+ client.close();
50
+ }
51
+ }
@@ -10,10 +10,11 @@ import {
10
10
  registeredAdapters,
11
11
  } from './adapters/index.js';
12
12
  import type { CliAdapterKind } from './adapters/index.js';
13
- import type { CliInvocation, CliAdapterIdentification } from './adapters/base.js';
13
+ import type { CliInvocation, CliAdapterIdentification, ModelProbeFailure } from './adapters/base.js';
14
14
 
15
15
  export type { CliAdapterKind } from './adapters/index.js';
16
- export type { CliInvocation, CliAdapterIdentification, HeadlessAdapter } from './adapters/base.js';
16
+ export type { CliInvocation, CliAdapterIdentification, HeadlessAdapter, ModelProbeFailure,
17
+ ModelProbeFailureCause } from './adapters/base.js';
17
18
 
18
19
  export {
19
20
  WRAPPER_IDENTIFY_ARG,
@@ -73,6 +74,11 @@ export function modelReadinessProbe(kind: CliAdapterKind, model: string): CliInv
73
74
  return registeredAdapters()[kind].buildModelReadinessProbe(model);
74
75
  }
75
76
 
77
+ /** Why a failed readiness probe failed, when the adapter recognises its output. */
78
+ export function classifyModelProbeFailure(kind: CliAdapterKind, output: string): ModelProbeFailure | undefined {
79
+ return registeredAdapters()[kind].classifyModelProbeFailure?.(output);
80
+ }
81
+
76
82
  /** Build the actual worker argv; this is shared contract, not probe-only lore. */
77
83
  export function agentExecution(
78
84
  kind: CliAdapterKind,
package/src/cli.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { withSubscriptionMetadata } from './cli/subscription-report.js';
2
3
  import { addPlugin } from './cli/add.js';
3
4
  import { parsePluginArgs, runPluginCommand, type PluginArgs } from './cli/plugin.js';
4
5
  import { watchCheck } from './cli-watch.js';
@@ -77,7 +78,7 @@ export interface CliIo {
77
78
  tty?: boolean;
78
79
  }
79
80
 
80
- type CliExitCode = 0 | 1 | 2 | 3;
81
+ type CliExitCode = 0 | 1 | 2 | 3 | 4;
81
82
 
82
83
  /**
83
84
  * Every shape `parseArgs` can produce. Exported for `cli-commands.ts`, whose
@@ -413,13 +414,14 @@ export async function runCli(
413
414
  // preflight) is worse than printing `Observer:` on a later line, so we
414
415
  // emit the run report immediately and finalize the observer link after.
415
416
  if (parsed.json) {
417
+ const reported = await withSubscriptionMetadata(execution);
416
418
  const observerUrl = await observerUrlFrom(observerMint, logged);
417
- emitRunReport(execution, parsed.json, logged, observerUrl, await mirror?.receipt());
419
+ emitRunReport(reported, parsed.json, logged, observerUrl, await mirror?.receipt());
418
420
  // Last, and after the report: the run's own output is what the mirror
419
421
  // uploads, and a run's exit code has never waited on Cloud. The mirror
420
422
  // bounds itself and never rejects.
421
- await mirror?.finish(execution.report);
422
- return execution.exitCode;
423
+ await mirror?.finish(reported.report);
424
+ return reported.exitCode;
423
425
  }
424
426
  emitRunReport(execution, parsed.json, logged);
425
427
  await finalizeObserverLine(observerMint, logged);
@@ -437,6 +439,11 @@ export async function runCli(
437
439
  async function checkAuthoredFlowComposed(path: string): Promise<{ report: CheckReport }> {
438
440
  const helper = await checkHelperBody(path);
439
441
  if (!helper.report.ok) return helper;
442
+ // The activity checker loads the TypeScript compiler. YAML checks and
443
+ // unrelated CLI commands should not pay that startup cost on every run.
444
+ const { checkAuthoredActivities } = await import('./cli/check-activities.js');
445
+ const activities = await checkAuthoredActivities(path);
446
+ if (!activities.report.ok) return activities;
440
447
  const mcp = await checkTypeScriptFlow(path);
441
448
  const triggers = isAuthoredFlowPath(path)
442
449
  ? await checkAuthoredTriggers(path)
@@ -452,8 +459,8 @@ async function checkAuthoredFlowComposed(path: string): Promise<{ report: CheckR
452
459
  // The authored definition sees helper flags, body use and `cli:`
453
460
  // declarations; the compiled view underneath knows only its steps.
454
461
  ...(triggers?.report.requirements === undefined ? {} : { requirements: triggers.report.requirements }),
455
- diagnostics: [...helper.report.diagnostics, ...mcp.report.diagnostics, ...triggerDiagnostics],
456
- ok: helper.report.ok && mcp.report.ok && triggerOk,
462
+ diagnostics: [...helper.report.diagnostics, ...activities.report.diagnostics, ...mcp.report.diagnostics, ...triggerDiagnostics],
463
+ ok: helper.report.ok && activities.report.ok && mcp.report.ok && triggerOk,
457
464
  },
458
465
  };
459
466
  }
@@ -15,6 +15,7 @@ const PREFLIGHT_ENVIRONMENT_FAILURE_KINDS = [
15
15
  'mcp_unreachable',
16
16
  'budget_syntax_invalid',
17
17
  'cli_missing',
18
+ 'cli_outdated',
18
19
  'cli_unauthenticated',
19
20
  'cli_unresolved',
20
21
  'cli_unsupported',
@@ -24,6 +25,7 @@ const PREFLIGHT_ENVIRONMENT_FAILURE_KINDS = [
24
25
  'memory_unreachable',
25
26
  'no_executor',
26
27
  'probe_failed',
28
+ 'provider_usage_limited',
27
29
  'scope_syntax_invalid',
28
30
  'mount_unknown',
29
31
  'scope_ungrantable',
package/src/index.ts CHANGED
@@ -173,6 +173,12 @@ export type {
173
173
  EffectRef,
174
174
  EventEmitParams,
175
175
  EventEmitResult,
176
+ SubscriptionCloseParams,
177
+ SubscriptionCloseResult,
178
+ SubscriptionNextParams,
179
+ SubscriptionNextResult,
180
+ SubscriptionOpenParams,
181
+ SubscriptionOpenResult,
176
182
  HelloParams,
177
183
  HelloResult,
178
184
  JournalReadParams,
@@ -13,7 +13,7 @@ import { EventEmitter } from 'node:events';
13
13
  export { walkJournal, JournalReadError, type JournalEvent, type JournalReadFailure } from './journal-reader.js';
14
14
  import { randomUUID } from 'node:crypto';
15
15
  import { createConnection, type Socket } from 'node:net';
16
- import type { VerbContract, EventSubmitParams } from './protocol.js';
16
+ import type { VerbContract, EventEmitParams, EventSubmitParams } from './protocol.js';
17
17
  import {
18
18
  PROTOCOL_VERSION,
19
19
  type CompletionReason,
@@ -442,8 +442,17 @@ export class JournalClient extends EventEmitter {
442
442
  });
443
443
  }
444
444
 
445
- eventEmit(runId: string, eventKey: string, payload: unknown): Promise<VerbContract['event.emit']['result']> {
446
- return this.request('event.emit', { run_id: runId, event_key: eventKey, payload });
445
+ eventEmit(
446
+ runId: string,
447
+ eventKey: string,
448
+ payload: unknown,
449
+ options: Pick<EventEmitParams, 'delivery_id' | 'actor'> = {},
450
+ ): Promise<VerbContract['event.emit']['result']> {
451
+ return this.request('event.emit', { run_id: runId, event_key: eventKey, payload, ...options });
452
+ }
453
+
454
+ subscriptionPark(params: VerbContract['subscription.park']['params']): Promise<VerbContract['subscription.park']['result']> {
455
+ return this.request('subscription.park', params);
447
456
  }
448
457
 
449
458
  /**
@@ -457,6 +466,41 @@ export class JournalClient extends EventEmitter {
457
466
  return this.request('event.submit', { spec, event });
458
467
  }
459
468
 
469
+ /** Prepare a body subscription. Cloud must activate the returned request before a body can observe it. */
470
+ subscriptionOpen(params: VerbContract['subscription.open']['params']): Promise<VerbContract['subscription.open']['result']> {
471
+ return this.request('subscription.open', params, null);
472
+ }
473
+
474
+ /** Commit Cloud's durable binding receipt and ingress fence. */
475
+ subscriptionActivate(params: VerbContract['subscription.activate']['params']): Promise<VerbContract['subscription.activate']['result']> {
476
+ return this.request('subscription.activate', params, null);
477
+ }
478
+
479
+ /** Append to one activated subscription under its immutable router receipt. */
480
+ subscriptionDeliver(params: VerbContract['subscription.deliver']['params']): Promise<VerbContract['subscription.deliver']['result']> {
481
+ return this.request('subscription.deliver', params, null);
482
+ }
483
+
484
+ /** Read durable subscription state without changing timers or ingress. */
485
+ subscriptionInspect(params: VerbContract['subscription.inspect']['params']): Promise<VerbContract['subscription.inspect']['result']> {
486
+ return this.request('subscription.inspect', params);
487
+ }
488
+
489
+ /** Fence the exact activated receipt after router-side overflow. */
490
+ subscriptionFenceOverflow(params: VerbContract['subscription.fence_overflow']['params']): Promise<VerbContract['subscription.fence_overflow']['result']> {
491
+ return this.request('subscription.fence_overflow', params, null);
492
+ }
493
+
494
+ /** Return a durable wake, or an explicit suspension with no daemon-side sleep. */
495
+ subscriptionNext(params: VerbContract['subscription.next']['params']): Promise<VerbContract['subscription.next']['result']> {
496
+ return this.request('subscription.next', params, null);
497
+ }
498
+
499
+ /** Close a body subscription; the server refuses later external appends. */
500
+ subscriptionClose(params: VerbContract['subscription.close']['params']): Promise<VerbContract['subscription.close']['result']> {
501
+ return this.request('subscription.close', params, null);
502
+ }
503
+
460
504
  channelAppend(params: VerbContract['channel.append']['params']): Promise<VerbContract['channel.append']['result']> {
461
505
  return this.request('channel.append', params);
462
506
  }
@@ -8,6 +8,8 @@ import { journalRecordOffset } from './journal-offset.js';
8
8
  // Journal version 1's closed vocabulary (relayflowd-core/src/entry.rs).
9
9
  const ENTRY_TYPES = new Set([
10
10
  'run.spawned', 'run.cancel.requested', 'event.received', 'subscription.registered',
11
+ 'subscription.prepared', 'subscription.opened', 'subscription.closed',
12
+ 'subscription.overflow.fenced', 'subscription.acknowledged',
11
13
  'subscription.matched', 'subscription.stale', 'step.routed', 'step.attempt.started',
12
14
  'step.completed', 'wait.event', 'wait.human', 'sleep.until', 'wait.completed',
13
15
  'stream.appended', 'memory.injected', 'channel.appended', 'channel.delivered',
@@ -2,6 +2,8 @@ import { onWorkerFailure } from './worker-lease.js';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import type { JournalClient } from './journal-client.js';
4
4
  import { AgentWorker } from './worker.js';
5
+ import type { KernelRunSpec } from './spec.js';
6
+ import { communicationInstruction } from './communication/spec.js';
5
7
  import { DEFAULT_LOCAL_AGENT_CAPACITY } from './worker-slots.js';
6
8
 
7
9
  /** A local worker for stream-only steps; no workspace recovery is claimed. */
@@ -12,13 +14,16 @@ export async function attachLocalAgent(
12
14
  requestedStream?: string,
13
15
  capacity: number = DEFAULT_LOCAL_AGENT_CAPACITY,
14
16
  runRoot?: string,
17
+ declaredStreams: readonly string[] = [],
15
18
  ): Promise<{
16
19
  stream: string;
17
20
  readonly failure: unknown;
18
21
  close(): Promise<void>;
19
22
  }> {
20
- // A fresh, unconsumed stream has offset zero. The executor declares exactly
21
- // this stream on its agent steps. No worktree revision is invented.
23
+ // This worker has consumed no messages: its read offsets are zero, including
24
+ // named declarative streams. The kernel retains ownership of dispatch pins
25
+ // and rejects a resume whose required offsets this worker does not hold.
26
+ // No worktree revision is invented.
22
27
  const stream = requestedStream ?? `local-agent-${randomUUID()}`;
23
28
  const worker = new AgentWorker(client, {
24
29
  workerId: stream,
@@ -27,7 +32,8 @@ export async function attachLocalAgent(
27
32
  // this same number (worker-slots.ts), so they never ask for more.
28
33
  capacity,
29
34
  dataDir, onPtyReady, runRoot,
30
- pins: { workspace: [], streams: [{ stream, read_offset: 0 }] },
35
+ pins: { workspace: [], streams: [...new Set([stream, ...declaredStreams])]
36
+ .map(stream => ({ stream, read_offset: 0 })) },
31
37
  });
32
38
  let failure: unknown;
33
39
  // The cause travels with the close, so the flow's next request names it.
@@ -41,3 +47,11 @@ export async function attachLocalAgent(
41
47
  },
42
48
  };
43
49
  }
50
+
51
+ /** Conversation workers own their streams and registration; do not steal their dispatches. */
52
+ export function declaredLocalAgentStreams(spec: KernelRunSpec): string[] {
53
+ return [...new Set(spec.steps.flatMap(step =>
54
+ step.type === 'agent' && !communicationInstruction(step.instruction)
55
+ ? step.surfaces?.streams?.map(({ stream }) => stream) ?? []
56
+ : []))];
57
+ }
package/src/preflight.ts CHANGED
@@ -52,6 +52,12 @@ export interface CliProbeResult {
52
52
  * unauthenticated CLI.
53
53
  */
54
54
  authFailureDetail?: string;
55
+ /** Exit code of a FAILED model-scoped readiness probe. */
56
+ modelExitCode?: number | null;
57
+ /** Cause the adapter read from a failed readiness probe's output, if any. */
58
+ modelFailure?: import('./adapters/base.js').ModelProbeFailure;
59
+ /** Redacted output of a FAILED model-scoped readiness probe. */
60
+ modelFailureDetail?: string;
55
61
  }
56
62
 
57
63
  export type CliProbeFailureDetail =
@@ -603,17 +609,47 @@ function probeResolvedCli(
603
609
  message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero${exitCode}; authenticate it or repair that adapter's authentication probe.${detail}`,
604
610
  });
605
611
  } else if (resolution.model !== undefined && result.modelAvailable !== true) {
606
- diagnostics.push({
607
- severity: 'refusal',
608
- kind: 'model_unavailable',
609
- stepId: resolution.stepId,
610
- cli: resolution.cli,
611
- model: resolution.model,
612
- message: describeEffectiveModel(resolution.stepId, resolution.model, resolution.cli, resolution.modelSource)
613
- + `, but its model-scoped "${result.modelCommand ?? `${resolution.cli} auth status`}" probe exited non-zero;`
614
- + ` verify the model name and this credential's access.`,
615
- });
612
+ diagnostics.push(modelProbeRefusal(resolution as CliResolution & { model: string }, result));
613
+ }
614
+ }
615
+
616
+ // Only a probe whose output says the model is unknown (or the adapter gave no
617
+ // output to read) is told to check the model name. An outdated CLI and a
618
+ // usage-limited credential name a different remedy, and "verify the model
619
+ // name" sends the author after a typo that is not there.
620
+ function modelProbeRefusal(
621
+ resolution: CliResolution & { model: string },
622
+ result: CliProbeResult,
623
+ ): PreflightDiagnostic {
624
+ const { stepId, cli, model } = resolution;
625
+ const effective = describeEffectiveModel(stepId, model, cli, resolution.modelSource);
626
+ const command = `"${result.modelCommand ?? `${cli} auth status`}"`;
627
+ const exit = result.modelExitCode === undefined || result.modelExitCode === null ? '' : ` (exit ${result.modelExitCode})`;
628
+ const reported = result.modelFailureDetail === undefined ? '' : ` It reported: ${result.modelFailureDetail}`;
629
+ const base = { severity: 'refusal' as const, stepId, cli, model };
630
+ const cause = result.modelFailure?.cause;
631
+ if (cause === 'cli_outdated') {
632
+ const required = result.modelFailure?.requiredVersion;
633
+ return { ...base, kind: 'cli_outdated',
634
+ message: `${effective}, but this "${cli}" is too old to use that model`
635
+ + (required === undefined ? '' : `; version ${required} or newer is required`)
636
+ + `. Upgrade the CLI in the environment that runs this flow; the model name is not the problem.${reported}` };
637
+ }
638
+ if (cause === 'provider_usage_limited') {
639
+ return { ...base, kind: 'provider_usage_limited',
640
+ message: `${effective}, but the provider refused the model-scoped ${command} probe${exit} because this credential`
641
+ + ` has reached a usage or rate limit. Retry after the limit resets or use a credential with capacity;`
642
+ + ` the model name is not the problem.${reported}` };
616
643
  }
644
+ // A probe that printed nothing is judged on its exit code alone, which is
645
+ // how a wrapper refuses a model it cannot serve.
646
+ const unknownModel = cause === 'model_unknown' || result.modelFailureDetail === undefined;
647
+ return { ...base, kind: 'model_unavailable',
648
+ message: `${effective}, but its model-scoped ${command} probe exited non-zero${exit}`
649
+ + (unknownModel
650
+ ? `; verify the model name and this credential's access.`
651
+ : ` for a reason Relayflows does not recognise.`)
652
+ + reported };
617
653
  }
618
654
 
619
655
  function probeFailedMessage(