@voltro/runtime 0.79.1 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -39,6 +39,85 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.80.0] — 2026-09-24
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **The inspect stream's replica selection is decided before the response** — `@voltro/runtime`, `@voltro/voltro`
47
+
48
+ `InspectStream.select` returns `Effect<InspectStreamSelection | undefined>` instead of a `Stream`, where `InspectStreamSelection` is either `{ kind: 'stream', stream }` or `{ kind: 'refused', status, body }`. A stream response is committed as `200` from its first byte, so a refusal can only be expressed before it starts. The option is wired by `voltro dev`, `voltro serve` and `voltro start`; code that passes its own `inspectStream` to `startRpcServer` wraps its stream in `Effect.succeed({ kind: 'stream', stream })`. `voltro update` lists every line that sets `inspectStream`.
49
+ - **`JiraService.listBoards` finds boards by project, name and type** — `@voltro/plugin-atlassian`
50
+
51
+ Every board method of `JiraService` took a board id, so a board picker had nothing to list and people looked ids up in Jira by hand. `listBoards({ projectKeyOrId?, name?, type? })` calls `GET /rest/agile/1.0/board`, follows the pages itself, and returns each board as `{ id, name, type, location? }`, where `location` names the project the board is located in. `name` is a contains match on Jira's side. It runs on the caller's own credentials like every other method, so a board the caller may not see is not listed. The break is for code that provides the service itself: a test stub written against the full `JiraServiceShape` needs the new method.
52
+
53
+ **`voltro update` carries you across this** — codemod `0.80.0/03_jira-service-lists-boards`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.80.0).
54
+ - **A loader has `ctx.query` in the browser, so a clicked-to page keeps its title** — `@voltro/web`, `@voltro/client`, `@voltro/protocol`, `@voltro/cli`
55
+
56
+ `ctx.query` was `undefined` when a loader ran in the browser. A page whose loader resolved an entity's name for `meta` therefore showed it in the title on a fresh load and dropped it after a click inside the app: the same URL, two titles. The browser loader now gets `ctx.query` too. It reads through the page's own api client, so it carries the visitor's session. A query resolves to its first snapshot, as on the server, and an in-band error rejects with the typed error on `voltroError`. A newer navigation that replaces the one in flight now aborts the loader's `signal`, as `LoaderContext.signal` always promised, and that closes the read. What changes for existing code is a check on `query`'s presence: `if (query)`, `query ? … : …` and `query?.(…)` now take the resolving branch in the browser too. Branch on `ctx.isServer` where the check meant "this is the server". `voltro update` lists every such line in your pages and layouts.
57
+
58
+ Bundle size: the browser read path adds 1.6 KB gz to a web app's first load (194.6 → 196.2 KB gz on the framework's zero-procedure fixture). Because the router now imports the api client, the shared chunk that carries the Effect runtime is emitted as `router-*.js` instead of `index-*.js`; the bytes moved between the two files, they did not grow.
59
+ - **No framework loading spinner; useNavigation() exposes navigation state** — `@voltro/web`
60
+
61
+ `FrameworkBoot` mounted a corner `NavigationIndicator` in every web app, in development and production, so a spinner appeared at the bottom right of pages whose design never included one — also in apps that had switched the devtools off. It is no longer mounted, and `NavigationIndicator` is removed from `@voltro/web`. In development the devtools button shows navigation progress as before. To build your own cue, read the new `useNavigation()` hook: `state` is `'loading'` while the router keeps the current page on screen for a target whose page module or loaders are not ready, and `pendingPathname` names that target; a server render is always `'idle'`. For a reconnecting api use `useConnectionStatus(apiName)` from `@voltro/client`. `voltro update` lists every file that still imports `NavigationIndicator`.
62
+ - **DB_MAX_CONNECTIONS is what the whole process holds, not the size of each pool** — `@voltro/cli`, `@voltro/workflow`, `@voltro/database`
63
+
64
+ Every framework user of the database opened its own pool: the store, the plugin bind context, the workflow runtime, aggregates, analytics, and the boot steps that migrate or check the schema. Each was sized by `DB_MAX_CONNECTIONS`, so one process could hold several times the number its boot line printed. Measured on postgres at `DB_MAX_CONNECTIONS=3`: the line promised 4 and the database saw 8 connections from `voltro dev` and from `voltro serve`. Two pods budgeted at 5 against a pooler of 15 both failed to acquire in the same second.
65
+
66
+ The pools were built by dialect-independent code, and the repair is measured the same way on every dialect: under boot and a burst of concurrent requests, postgres, mariadb, mysql and mssql each hold exactly what the line prints.
67
+
68
+ In `voltro dev` and `voltro serve` they now share one pool per database target, so the boot line's number is what the process holds. Short-lived commands such as `voltro data restore` keep their own pool, because they close it to get fresh sessions. A value tuned per pool buys fewer connections in total. Read replicas, a separate migration URL (`DB_DIRECT_URL` / `DB_MIGRATE_URL`), a provisioned tenant's database and pools your own code builds stay separate.
69
+
70
+ The workflow runtime needs 3 connections at once (2 with `VOLTRO_WORKFLOW_RUNNER_STORAGE=memory`). Below that the boot now refuses and names the number; it used to wait out the acquire timeout and exit on `Failed to acquire connection`. `sqlPoolCapacity` is exported from `@voltro/database/sql` beside `registerSqlPoolCapacity`, an addition.
71
+
72
+ **`voltro update` carries you across this** — codemod `0.80.0/01_db-max-connections-is-the-process-budget`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.80.0).
73
+
74
+ ### Added
75
+
76
+ - **security.clientAddressHeader reads a proxy's single-address header** — `@voltro/runtime`, `@voltro/cli`
77
+
78
+ `security.clientAddressHeader` (env `VOLTRO_CLIENT_ADDRESS_HEADER`) names a header a trusted proxy sets to the client's address, such as `cf-connecting-ip` or `x-real-ip`. It is believed only from a peer covered by `security.trustedProxies` and only when it holds one valid IP; otherwise `x-forwarded-for` applies as before. Naming a header without declaring a trusted proxy refuses the boot.
79
+ - **Rate-limit rules can key on the caller's address** — `@voltro/plugin-ratelimit`, `@voltro/protocol`, `@voltro/runtime`, `@voltro/cli`
80
+
81
+ A rule can now use `by: 'ip'`, one bucket per client address. Before sign-in every caller is the same anonymous subject, so `'subject'` put them all in one bucket and one client could use up a sign-in or invite endpoint for everyone. The address is the one the listener resolves through `security.trustedProxies`, and plugin interceptors receive it as `RpcInterceptorContext.remoteAddr` on WebSocket, `POST /rpc` and `publicApi` REST calls; REST handlers get it as `RestRouteContext.remoteAddr`. A client cannot set it. Calls that do not arrive over the network share one `unknown` bucket. `KeyBy` gains the `'ip'` member: every value that type-checked before still does; only code that exhaustively switches over `KeyBy` needs the new case.
82
+
83
+ ### Changed
84
+
85
+ - **Server-side rpc calls carry the visitor's address** — `@voltro/cli`
86
+
87
+ A loader's `ctx.query`, a page's `preload`, and the no-JavaScript form post now send `x-forwarded-for` to the api: the chain the request arrived with plus the address that delivered it to the web server. An api whose `security.trustedProxies` covers the web tier therefore rate-limits and audits these calls per visitor instead of counting every visitor against the web server. An api that trusts no proxy ignores the header, so its behaviour is unchanged. A visitor-written `x-forwarded-for` is never forwarded on its own.
88
+
89
+ ### Fixed
90
+
91
+ - **The boot's database connection line names the dialect in use** — `@voltro/cli`
92
+
93
+ A database configured with `DB_HOST` and friends was logged as `db connection: postgres://…` whatever the dialect, so a SQL Server or MySQL connection read as a postgres one. The line now starts with the dialect's name.
94
+ - **The dashboard's live streams share one browser connection** — `@voltro/cli`
95
+
96
+ The DevTools dashboard opened one event-stream connection per app. Over HTTP/1.1 a browser holds at most six connections per origin across all tabs, so with six apps — or three in two tabs — the logs and overview pages held every connection the dashboard had, and each later request waited indefinitely: a click on another page changed the URL, kept the old page on screen and left the navigation indicator spinning. Every stream a page holds now travels on one connection: the page posts its targets, each with its own app bearer, to `/api/dashboard/proxy/streams`, and `voltro dev` and `voltro start` fan the apps in behind it, each reconnecting on its own backoff. `/api/dashboard/proxy/stream` is removed; `@voltro/dashboard` of the same release uses the new route.
97
+ - **`meta` keeps the rendered language after hydration without a locale cookie** — `@voltro/web`, `@voltro/cli`
98
+
99
+ In the browser, `meta` received the `voltro:locale` cookie's locale and otherwise a fixed `'en'`. A first visit carries no cookie, so the server rendered the title in the language it negotiated (`Accept-Language`, else `defaultLocale`), and after hydration the tab switched to English. `meta` now falls back to the locale the server rendered the document with, which it stamps on `<html lang>`, and the cookie still wins when a language switch has written one. A page `voltro serve` answers with the unrendered shell (a page the prerender did not produce) declared `lang="en"` in every app, and the client took that for the server's choice. The shell now declares the app's `defaultLocale`, or its first declared locale.
100
+ - **The first `voltro dev` on a fresh SQL Server database boots with CDC on** — `@voltro/sql-mssql`
101
+
102
+ With `changeStrategy: 'cdc'`, the default, the Change Tracking reader started before auto-migrate created the schema and enabled tracking on tables that did not exist yet. The boot died on `Failed to execute statement`, so every new project on SQL Server failed its first `voltro dev`. The same database booted once with `CDC=0` worked afterwards.
103
+
104
+ The reader now enables tracking on the tables that exist and picks up the rest on the poll that first finds them, reading each from its own tracking floor. A table the migration creates during the boot is tracked from then on. `ensureChangeTracking` resolves to the tables it could not enable yet.
105
+ - **Replica observations of ended processes are deleted after a day** — `@voltro/cli`, `@voltro/runtime`, `@voltro/database`
106
+
107
+ `_voltro_replica_observations` had no retention. A replica deletes its own rows on a graceful shutdown, but a process that crashed or was killed left them behind, and a replica ID that does not survive a restart — `dev-<pid>` under `voltro dev`, a pod replaced under a new name — never overwrites them. Every such row stayed listed as `departed` in each fleet read indefinitely. The retention sweep now deletes rows not rewritten for 24 hours on both boot paths and every dialect; live replicas rewrite theirs every 30 seconds. `VOLTRO_REPLICA_OBSERVATIONS_TTL_HOURS` changes the window, with a floor of one hour, and the boot line lists the policy with the others.
108
+ - **A live stream for an ended replica is refused instead of reconnecting forever** — `@voltro/cli`, `@voltro/devtools-ui`
109
+
110
+ `/_voltro/inspect/stream?replica=<id>` now resolves the named replica and verifies its handshake before it answers, with the same refusals as a read: `404` for an unknown ID or one without a reachable address (no request leaves the process), `504` for a peer that does not accept the connection, `502` for a peer that answers with another process's feed. Previously the API answered `200 text/event-stream` first and found the missing peer inside the body, so a dashboard still naming a process from before a restart received a stream that closed at once, reconnected on backoff indefinitely, and left a `Fiber terminated with an unhandled error` in the target's log on every attempt. The replica selector in the local and Cloud dashboards now also removes a selected replica once the fleet no longer lists it — the process ended and its ID never answers again — names the removed IDs, and returns an emptied selection to All replicas. A replica that is only unavailable (stale, without a reachable address, or expected by the fleet without an answer) stays selected.
111
+ - **A controlled workflow start without an acknowledgement returns its receipt** — `@voltro/runtime`, `@voltro/voltro`, `@voltro/workflow`, `@voltro/cli`, `@voltro/protocol`
112
+
113
+ When the engine dispatch of a start with flow controls failed without a definite answer, such as an exhausted database pool or an acknowledgement lost after the engine took the run, `start()` threw `Submission … remains unacknowledged`. The submission stayed durable and recovery retried it, so a caller that reported the failure could see the run happen anyway.
114
+
115
+ `start()` now returns `status: 'queued'` with `deferral.mode: 'unacknowledged'`, and `handle.id` is the receipt recovery binds to the resulting run. `start(…, { wait: true })` and `run(…)` follow it. A definite rejection still throws.
116
+
117
+ `WorkflowAdmissionOutcome`'s `accept` may now resolve to a `WorkflowQueuedAdmission` as well as an execution ID. Custom admission adapters only produce that outcome; the framework's facade is what calls `accept`, so adapter code keeps compiling. `isUnacknowledgedSubmission` is exported from `@voltro/workflow`.
118
+
119
+ ---
120
+
42
121
  ## [0.79.1] — 2026-09-24
43
122
 
44
123
  ### Changed
package/dist/index.d.ts CHANGED
@@ -2245,6 +2245,8 @@ export declare interface ClientAddressInput {
2245
2245
  readonly socketAddress: string | undefined;
2246
2246
  /** Raw `x-forwarded-for` header, if present. */
2247
2247
  readonly xForwardedFor: string | undefined;
2248
+ /** Raw value of `config.clientAddressHeader`, if the request carried it. */
2249
+ readonly clientAddressHeader?: string | undefined;
2248
2250
  readonly config: TrustedProxyConfig;
2249
2251
  }
2250
2252
 
@@ -5453,13 +5455,12 @@ export declare const forbiddenColumnsForSubject: (tableName: string, subject: Su
5453
5455
  export declare const forgetCredential: (store: VaultStore, connectionId: string, subjectId: string) => Promise<boolean>;
5454
5456
 
5455
5457
  /**
5456
- * Drop rows for a replica GENERATION that is gone.
5458
+ * Drop this replica's rows on a graceful shutdown.
5457
5459
  *
5458
- * Not a retention sweep — the table has no history to age out. This removes
5459
- * the row of a process that restarted, because the replica's NEW row is
5460
- * written under the same `(replicaId, kind)` key and the old generation is
5461
- * simply overwritten. What this handles is a replica that went away for good
5462
- * (a scale-down), whose row would otherwise be counted forever.
5460
+ * A restart under the same replica ID overwrites its row anyway; this handles
5461
+ * a replica that goes away for good (a scale-down). It cannot run for a process
5462
+ * that crashed or was killed — those rows are aged out by the retention sweep
5463
+ * on `writtenAt`, which every live replica refreshes each publish interval.
5463
5464
  */
5464
5465
  export declare const forgetReplicaObservations: (store: DataStore, replicaId: string) => Promise<void>;
5465
5466
 
@@ -5862,7 +5863,10 @@ export { InspectStore }
5862
5863
  */
5863
5864
  export declare interface InspectStream {
5864
5865
  readonly replicaId?: string;
5865
- readonly select?: (url: string, headers: Readonly<Record<string, string>>) => Stream.Stream<Uint8Array, unknown> | undefined;
5866
+ /** Undefined serves this process's own feed. Decided before the response
5867
+ * starts: an event stream is a 200 from its first byte, so a refusal found
5868
+ * inside the body could only reach the client as a feed that closes. */
5869
+ readonly select?: (url: string, headers: Readonly<Record<string, string>>) => Effect.Effect<InspectStreamSelection | undefined>;
5866
5870
  /** Same shape as the inspect auth resolver — returns ok or reason. */
5867
5871
  readonly authorize: (headers: Readonly<Record<string, string>>) => {
5868
5872
  readonly ok: true;
@@ -5884,6 +5888,16 @@ export declare interface InspectStream {
5884
5888
  readonly keepAliveMs?: number;
5885
5889
  }
5886
5890
 
5891
+ /** A named process's feed, or the reasoned refusal to open it. */
5892
+ export declare type InspectStreamSelection = {
5893
+ readonly kind: 'stream';
5894
+ readonly stream: Stream.Stream<Uint8Array>;
5895
+ } | {
5896
+ readonly kind: 'refused';
5897
+ readonly status: number;
5898
+ readonly body: unknown;
5899
+ };
5900
+
5887
5901
  export { inspectWorkflow }
5888
5902
 
5889
5903
  export declare const installApprovalGate: (gate: ApprovalGateApi) => void;
@@ -9425,6 +9439,14 @@ export declare const resolveReactiveConfig: (config?: ReactiveConfigInput | unde
9425
9439
 
9426
9440
  export declare const resolveRedirectUri: (definition: OAuth2ConnectionDefinition, publicUrl: string) => string;
9427
9441
 
9442
+ /** The client address of a request, read from its headers per `config`. The
9443
+ * one call every listener makes — which headers matter is decided here. */
9444
+ export declare const resolveRequestClientAddress: (input: {
9445
+ readonly socketAddress: string | undefined;
9446
+ readonly headers: Readonly<Record<string, string | undefined>>;
9447
+ readonly config: TrustedProxyConfig;
9448
+ }) => string | undefined;
9449
+
9428
9450
  /**
9429
9451
  * Whether the request reached the app over TLS.
9430
9452
  *
@@ -9513,7 +9535,7 @@ export declare const resolveTransactionSubject: (subject: Subject, tenantId: str
9513
9535
  * rebuild; the config field exists because it is a tunable and tunables belong
9514
9536
  * in `app.config.ts`.
9515
9537
  */
9516
- export declare const resolveTrustedProxies: (configured?: ReadonlyArray<string> | undefined, env?: Readonly<Record<string, string | undefined>>) => TrustedProxyConfig;
9538
+ export declare const resolveTrustedProxies: (configured?: ReadonlyArray<string> | undefined, env?: Readonly<Record<string, string | undefined>>, configuredHeader?: string | undefined) => TrustedProxyConfig;
9517
9539
 
9518
9540
  /** Per-resource-type policy. `actions` maps an action to the relations that
9519
9541
  * grant it (ANY-of). `implies` is relation implication (owner ⇒ editor ⇒
@@ -9944,6 +9966,9 @@ export declare interface RowHistoryEntry {
9944
9966
  readonly traceId?: string | null;
9945
9967
  }
9946
9968
 
9969
+ /** Internal header name. Its value is meaningless without the process mark. */
9970
+ export declare const RPC_CLIENT_ADDRESS_HEADER = "x-voltro-client-address";
9971
+
9947
9972
  /** Server-owned lifetime, not a user identity or a client-supplied claim.
9948
9973
  * WebSockets retain it across calls; HTTP retains it only within one request.
9949
9974
  * Reconnects and server restarts receive a new id. */
@@ -9951,6 +9976,11 @@ export declare interface RpcConnection {
9951
9976
  readonly id: string;
9952
9977
  readonly transport: 'websocket' | 'http';
9953
9978
  readonly signal: AbortSignal;
9979
+ /** The caller's address, resolved by the rpc listener through
9980
+ * `security.trustedProxies` / `clientAddressHeader` — the same value its
9981
+ * per-IP shield counts. Absent when the call did not arrive through that
9982
+ * listener (an internal invocation, an embedder's own transport). */
9983
+ readonly remoteAddr?: string;
9954
9984
  }
9955
9985
 
9956
9986
  export declare interface RpcServerOptions<Rpcs extends Rpc.Any> {
@@ -10250,6 +10280,9 @@ export declare interface RuntimeContext {
10250
10280
  /** Server-owned transport identity and close signal. Not authentication;
10251
10281
  * session membership remains an application's explicit access decision. */
10252
10282
  readonly connection?: RpcConnection;
10283
+ /** `connection.remoteAddr`, lifted so the serve pipeline — which also serves
10284
+ * REST calls that have no connection — reads one field for both. */
10285
+ readonly remoteAddr?: string;
10253
10286
  }
10254
10287
 
10255
10288
  /**
@@ -11099,6 +11132,11 @@ export declare interface ServeRequestContext {
11099
11132
  readonly surface?: 'rest';
11100
11133
  /** The descriptor's `publicApi` spec, on a projected call. */
11101
11134
  readonly publicApi?: PublicApiSpec;
11135
+ /** The caller's address as the listener resolved it through
11136
+ * `security.trustedProxies` — carried to the plugin interceptor so a
11137
+ * per-endpoint limit can count per client. Absent for calls that did not
11138
+ * arrive over the network. */
11139
+ readonly remoteAddr?: string;
11102
11140
  /**
11103
11141
  * Caller-side cancellation (the gRPC deadline / client-cancel path).
11104
11142
  * Honoured at the ONE place the executor effect runs to a promise, so an
@@ -11163,6 +11201,8 @@ export declare type ServeRpcInterceptor = (base: Effect.Effect<unknown, unknown,
11163
11201
  /** See `ServeRequestContext.surface` — present on a REST-projected call. */
11164
11202
  readonly surface?: 'rest';
11165
11203
  readonly publicApi?: PublicApiSpec;
11204
+ /** See `ServeRequestContext.remoteAddr`. */
11205
+ readonly remoteAddr?: string;
11166
11206
  }) => Effect.Effect<unknown, unknown, never>;
11167
11207
 
11168
11208
  /** The published tracer, or `undefined` when tracing is off / no server booted. */
@@ -12182,6 +12222,12 @@ export declare interface TransportSecurityOptions {
12182
12222
  * CIDRs, the presets `loopback` / `private`, a hop count (`['2']`), or
12183
12223
  * `['*']` to trust any peer. Env override: `VOLTRO_TRUSTED_PROXIES`. */
12184
12224
  readonly trustedProxies?: ReadonlyArray<string>;
12225
+ /** A header a trusted proxy SETS to the client's single address
12226
+ * (`'cf-connecting-ip'`, `'x-real-ip'`). Believed only from a peer
12227
+ * `trustedProxies` covers, and only when it holds one valid IP; otherwise
12228
+ * the `x-forwarded-for` chain applies. Env override:
12229
+ * `VOLTRO_CLIENT_ADDRESS_HEADER`. */
12230
+ readonly clientAddressHeader?: string;
12185
12231
  /** Security response headers. Env override: `VOLTRO_SECURITY_HEADERS`
12186
12232
  * (`off|default|strict`), plus `VOLTRO_CSP` / `VOLTRO_CSP_HTML` /
12187
12233
  * `VOLTRO_HSTS`. */
@@ -12227,6 +12273,10 @@ export declare interface TrustedProxyConfig {
12227
12273
  readonly trustAll: boolean;
12228
12274
  /** Hop count (express's numeric `trust proxy`). `undefined` → not hop mode. */
12229
12275
  readonly hops?: number;
12276
+ /** Lower-cased name of a single-address header a trusted proxy SETS
12277
+ * (`cf-connecting-ip`, `x-real-ip`). Preferred over the forwarded chain when
12278
+ * it holds one valid IP; `x-forwarded-for` remains the fallback. */
12279
+ readonly clientAddressHeader?: string;
12230
12280
  }
12231
12281
 
12232
12282
  /**
@@ -12806,15 +12856,12 @@ export declare type WorkflowAdmissionOutcome = {
12806
12856
  * that filled up on this arrival. */
12807
12857
  readonly payload: unknown;
12808
12858
  /** Dispatch the resident submission and return its durably acknowledged
12809
- * execution ID. Recovery owns retries; callers must not execute separately. */
12810
- readonly accept: () => Promise<string>;
12811
- } | {
12812
- readonly kind: 'queued';
12813
- readonly receiptId: string;
12814
- readonly intentId: string;
12815
- readonly mode: string;
12816
- readonly dueAt: number;
12817
- } | {
12859
+ * execution ID — or, when the dispatch went unacknowledged, the receipt
12860
+ * recovery completes it under (`mode: 'unacknowledged'`). That start may
12861
+ * still run, so it is neither an execution ID nor a failure. Recovery owns
12862
+ * retries; callers must not execute separately. */
12863
+ readonly accept: () => Promise<string | WorkflowQueuedAdmission>;
12864
+ } | WorkflowQueuedAdmission | {
12818
12865
  readonly kind: 'dropped';
12819
12866
  readonly retryAfterMs: number;
12820
12867
  } | {
@@ -13118,6 +13165,15 @@ export declare class WorkflowPayloadError extends Error {
13118
13165
  missingFields: ReadonlyArray<string>, detail: string);
13119
13166
  }
13120
13167
 
13168
+ /** A start the caller follows through its receipt instead of an execution ID. */
13169
+ export declare interface WorkflowQueuedAdmission {
13170
+ readonly kind: 'queued';
13171
+ readonly receiptId: string;
13172
+ readonly intentId: string;
13173
+ readonly mode: string;
13174
+ readonly dueAt: number;
13175
+ }
13176
+
13121
13177
  export declare interface WorkflowQueuedControlOptions {
13122
13178
  readonly callerContext?: WorkflowCallerContext;
13123
13179
  readonly cause?: NonNullable<WorkflowControlRequest['cause']>;