@voltro/runtime 0.29.0 → 0.30.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/dist/index.d.ts CHANGED
@@ -1274,7 +1274,30 @@ export declare interface BindEventInput {
1274
1274
  readonly guards?: Guards<unknown> | undefined;
1275
1275
  }
1276
1276
 
1277
- export declare const bindMutation: <Input, Output, E = never>(execute: (input: Input, context: RuntimeContext) => Output | Promise<Output> | Effect.Effect<Output, E, never>, input: Input, spanName?: string, codec?: MutationOutputCodec<unknown>, idempotency?: IdempotencyBinding) => Effect.Effect<Output, E, SubjectService | ConnectionInfo>;
1277
+ export declare const bindMutation: <Input, Output, E = never>(execute: (input: Input, context: RuntimeContext) => Output | Promise<Output> | Effect.Effect<Output, E, never>, input: Input, spanName?: string, codec?: MutationOutputCodec<unknown>, idempotency?: IdempotencyBinding,
1278
+ /**
1279
+ * This procedure's DECLARED error union (`descriptor.error`).
1280
+ *
1281
+ * The third category of failure, and the one two guards below could not see.
1282
+ * A value that is TAGGED but not representable by this union is undeclared by
1283
+ * construction: the untagged catch skips it (it has a `_tag`), the infra list
1284
+ * skips it (it is not on a curated list), and the rpc encoder then cannot
1285
+ * match it and ships the whole `ExitEncoded<…>` decode tree to the browser —
1286
+ * ~2 KB for a one-line cause, with the message at the END so every tool that
1287
+ * truncates shows the useless half.
1288
+ *
1289
+ * A consumer met it with `TenantScopeViolation`. Adding that tag to
1290
+ * `INFRA_ERROR_TAGS` would have been wrong: `effectStore.ts` documents
1291
+ * `error: Schema.Union(TenantScopeViolation, StoreOperationFailed, …)` as a
1292
+ * supported declaration, so an app that DECLARES it must still receive it
1293
+ * typed. The question is therefore not "is this tag infra" but "can THIS
1294
+ * descriptor represent it" — which only the descriptor can answer.
1295
+ *
1296
+ * Omitted ⇒ the check is skipped entirely. A call site that cannot supply a
1297
+ * schema keeps exactly the old behaviour rather than collapsing errors it
1298
+ * cannot classify.
1299
+ */
1300
+ declaredError?: Schema.Schema.All) => Effect.Effect<Output, E, SubjectService | ConnectionInfo>;
1278
1301
 
1279
1302
  /**
1280
1303
  * Bind a non-reactive server→client stream (A3). The executor builds a
@@ -5723,6 +5746,26 @@ export declare type ReactionAct = {
5723
5746
  } | {
5724
5747
  readonly kind: 'workflow';
5725
5748
  readonly workflow: string;
5749
+ /**
5750
+ * Shape the workflow's payload from the change. Omit and the CHANGED ROW is
5751
+ * the payload, as before.
5752
+ *
5753
+ * Without this the workflow's payload schema is dictated by the watched
5754
+ * TABLE's row shape rather than by what the workflow needs, which drags two
5755
+ * problems into every reaction-started workflow:
5756
+ *
5757
+ * • every column travels, including ones the workflow has no business
5758
+ * seeing — a reaction on a users table hands the whole row to a
5759
+ * narration workflow;
5760
+ * • the row shape is DIALECT-dependent at the edges. A `timestamp()`
5761
+ * column arrives as a `Date` on MariaDB and a number elsewhere, so an
5762
+ * app normalising on both sides of an idempotency key is doing the
5763
+ * framework's job.
5764
+ *
5765
+ * A mapper fixes both at the declaration: the workflow declares the payload
5766
+ * it wants and the reaction adapts.
5767
+ */
5768
+ readonly payload?: (event: ReactionEvent) => unknown;
5726
5769
  };
5727
5770
 
5728
5771
  export declare interface ReactionDefinition {
@@ -5749,10 +5792,35 @@ export declare interface ReactionGuards {
5749
5792
  /** Per-tenant AI spend ceiling (USD). Checked via the injected `checkBudget`
5750
5793
  * (the app wires `requireAiBudget`). Over budget → the reaction refuses. */
5751
5794
  readonly costBudgetUsd?: number;
5752
- /** Per-reaction rate cap — at most `limit` firings per `windowMs`. */
5795
+ /**
5796
+ * Rate cap — at most `limit` firings per `windowMs`, PER KEY.
5797
+ *
5798
+ * ── Two defects this shape replaces, both reported from production ────────
5799
+ *
5800
+ * It read as a per-key cap and was neither. The runner keyed the limiter on
5801
+ * the REACTION NAME, so one cap covered every row and every tenant the
5802
+ * reaction watched: an app with a hundred tenants got a hundredth of the
5803
+ * throughput it declared, and the busiest tenant starved the rest.
5804
+ *
5805
+ * And the limiter was in-memory, per process. With three replicas the
5806
+ * effective cap was 3×, and nothing about the declaration said so — the same
5807
+ * config produced a different limit depending on how many pods happened to be
5808
+ * running.
5809
+ *
5810
+ * `key` is now REQUIRED to get per-entity behaviour, and the limiter is
5811
+ * DURABLE (a claim in the shared store, the same INSERT-wins arbiter the cron
5812
+ * scheduler uses), so the cap is the cap regardless of replica count.
5813
+ *
5814
+ * Omitting `key` keeps the old GLOBAL meaning — which is a legitimate thing to
5815
+ * want (a cap on a scarce downstream), just not what the old field appeared to
5816
+ * offer.
5817
+ */
5753
5818
  readonly rateLimit?: {
5754
5819
  readonly limit: number;
5755
5820
  readonly windowMs: number;
5821
+ /** Partitions the cap. `(event) => event.new.tenantId` for per-tenant,
5822
+ * `(event) => String(event.new.id)` for per-row. */
5823
+ readonly key?: (event: ReactionEvent) => string;
5756
5824
  };
5757
5825
  }
5758
5826
 
@@ -5760,7 +5828,17 @@ export declare type ReactionOp = 'insert' | 'update' | 'delete';
5760
5828
 
5761
5829
  export declare type ReactionOutcome = 'acted' | 'skipped-op' | 'skipped-when' | 'skipped-dedupe' | 'skipped-ratelimit' | 'skipped-budget';
5762
5830
 
5763
- /** A simple per-key sliding-window rate limiter (in-memory, per process). */
5831
+ /**
5832
+ * A per-key sliding-window rate limiter — IN-MEMORY, PER PROCESS.
5833
+ *
5834
+ * Correct for a single-process deployment and wrong for every other one: with N
5835
+ * replicas the effective cap is N×, silently. It stays as the FALLBACK the
5836
+ * runner uses when no durable claimer is wired (dev on the memory store), and
5837
+ * the runner logs once when it falls back, because "my limit is 3× what I
5838
+ * declared" is not something anyone discovers by reading config.
5839
+ *
5840
+ * Prefer {@link ReactionRunDeps.claimRateSlot}.
5841
+ */
5764
5842
  export declare class ReactionRateLimiter {
5765
5843
  private readonly hits;
5766
5844
  allow(key: string, limit: number, windowMs: number, now: number): boolean;
@@ -5781,6 +5859,14 @@ export declare interface ReactionRunDeps {
5781
5859
  /** Per-tenant AI budget check — true = within budget. Injected (wraps
5782
5860
  * requireAiBudget). Only consulted when `guards.costBudgetUsd` is set. */
5783
5861
  readonly checkBudget?: (tenantId: string | null, budgetUsd: number) => boolean | Promise<boolean>;
5862
+ /**
5863
+ * Atomically claim ONE rate slot. `true` = we got it and may fire.
5864
+ *
5865
+ * Injected by the serve layer over the shared store, so the cap holds across
5866
+ * replicas. Absent → the runner falls back to {@link ReactionRateLimiter},
5867
+ * which is per-process and therefore N× on N replicas.
5868
+ */
5869
+ readonly claimRateSlot?: (key: string) => Promise<boolean>;
5784
5870
  readonly rateLimiter?: ReactionRateLimiter;
5785
5871
  readonly now?: () => number;
5786
5872
  }
@@ -8757,6 +8843,44 @@ export declare type WhereOp = '=' | '!=' | '<>' | '>' | '>=' | '<' | '<=' | 'in'
8757
8843
  */
8758
8844
  export declare const withRowFilter: <R extends ServeRequestContext>(request: R) => Promise<R>;
8759
8845
 
8846
+ /**
8847
+ * What the gate decided. `passthrough` is the common case — no controls
8848
+ * declared — and is distinct from `start` so the facade can skip the commit
8849
+ * bookkeeping entirely rather than calling a no-op.
8850
+ */
8851
+ export declare type WorkflowAdmissionOutcome = {
8852
+ readonly kind: 'passthrough';
8853
+ } | {
8854
+ readonly kind: 'start';
8855
+ /** What to start WITH. Differs from the caller's payload only for a batch
8856
+ * that filled up on this arrival. */
8857
+ readonly payload: unknown;
8858
+ /** Called once the engine returns an execution id: links the ledger row,
8859
+ * consumes any batched intents, evicts a singleton incumbent. */
8860
+ readonly commit: (executionId: string) => Promise<void>;
8861
+ } | {
8862
+ readonly kind: 'queued';
8863
+ readonly intentId: string;
8864
+ readonly mode: string;
8865
+ readonly dueAt: number;
8866
+ } | {
8867
+ readonly kind: 'dropped';
8868
+ readonly retryAfterMs: number;
8869
+ } | {
8870
+ readonly kind: 'skipped';
8871
+ readonly executionId: string;
8872
+ };
8873
+
8874
+ export declare interface WorkflowAdmissionRequest {
8875
+ readonly workflowName: string;
8876
+ readonly payload: unknown;
8877
+ readonly callerContext: WorkflowCallerContext | undefined;
8878
+ /** `true` for `run(...)` and `start(..., { wait: true })` — a caller blocking
8879
+ * for the RESULT. A deferring control has no coherent answer for one, so the
8880
+ * gate refuses it by name rather than doing something surprising. */
8881
+ readonly waiting: boolean;
8882
+ }
8883
+
8760
8884
  export declare interface WorkflowCallerContext {
8761
8885
  readonly subject?: unknown;
8762
8886
  readonly traceId?: string | null;
@@ -8771,6 +8895,19 @@ export declare interface WorkflowChildOptions {
8771
8895
  /* Excluded from this release type: callerContext */
8772
8896
  }
8773
8897
 
8898
+ /**
8899
+ * Reject a blocking caller on a workflow whose flow control can DEFER.
8900
+ *
8901
+ * `run(...)` and `start({ wait: true })` block for the run's RESULT, and there
8902
+ * is no result to return for a start that was collapsed into a future run.
8903
+ * Silently starting it anyway would break the declared limit; silently
8904
+ * returning nothing would break the caller's type. So it is an error that names
8905
+ * both halves.
8906
+ */
8907
+ export declare class WorkflowDeferredResultError extends Error {
8908
+ constructor(workflowName: string, method: string, mode: string);
8909
+ }
8910
+
8774
8911
  export declare interface WorkflowDefinitionLike {
8775
8912
  readonly name: string;
8776
8913
  readonly payloadSchema: Schema.Schema.Any;
@@ -8858,6 +8995,17 @@ export declare interface WorkflowFacadeOptions {
8858
8995
  * waiting for its next storage tick. No-op when unset (single replica / no
8859
8996
  * broker → the poll interval covers it). */
8860
8997
  readonly onEnqueue?: () => void;
8998
+ /**
8999
+ * Declarative flow control — the admission boundary every start passes
9000
+ * through when the workflow declares `debounce` / `singleton` / `concurrency`
9001
+ * / `throttle` / `rateLimit` / `batch`, or when an operator paused it.
9002
+ *
9003
+ * Injected, and absent by default, for the same reason `listRuns` is: the
9004
+ * runtime stays storage-agnostic and the CLI owns the tables. Absent ⇒ every
9005
+ * start takes exactly the path it took before flow control existed, which is
9006
+ * what makes this feature free for a workflow that declares none.
9007
+ */
9008
+ readonly admitStart?: (input: WorkflowAdmissionRequest) => Promise<WorkflowAdmissionOutcome>;
8861
9009
  }
8862
9010
 
8863
9011
  export declare interface WorkflowLayerExecutionContext {
@@ -8898,6 +9046,20 @@ export declare class WorkflowPayloadError extends Error {
8898
9046
  missingFields: ReadonlyArray<string>, detail: string);
8899
9047
  }
8900
9048
 
9049
+ /**
9050
+ * A blocking caller hit a `rateLimit` cap.
9051
+ *
9052
+ * `start()` reports a drop through the handle, because a fire-and-forget caller
9053
+ * has somewhere to put it. `run()` has no such place — its return type is the
9054
+ * workflow's success value — so the drop has to be an error, or it would look
9055
+ * like a run that returned `undefined`.
9056
+ */
9057
+ export declare class WorkflowRateLimitedError extends Error {
9058
+ readonly workflowName: string;
9059
+ readonly retryAfterMs: number;
9060
+ constructor(workflowName: string, retryAfterMs: number);
9061
+ }
9062
+
8901
9063
  /** Result of {@link WorkflowsAppContext.redrive} — whether the failed run's
8902
9064
  * durable journal was re-driven, how many failed step attempts were reset so
8903
9065
  * they re-execute, and a `reason` when it declined (no journal / still
@@ -8929,6 +9091,24 @@ export declare interface WorkflowRunListFilter {
8929
9091
  readonly workflowName?: string;
8930
9092
  readonly tag?: string;
8931
9093
  readonly status?: WorkflowRunRecordStatus;
9094
+ /** Several statuses at once (`['failed', 'cancelled']`). Ignored when the
9095
+ * single `status` is also set — one of them has to win, and the singular,
9096
+ * older spelling is the one existing callers already rely on. */
9097
+ readonly statuses?: ReadonlyArray<WorkflowRunRecordStatus>;
9098
+ /** Case-insensitive substring match on the workflow tag — the search-box
9099
+ * semantic, where `tag`/`workflowName` are exact. */
9100
+ readonly tagContains?: string;
9101
+ /** Exact match on the run's recorded `source` (`workflow-rpc`,
9102
+ * `app-context`, `inspect`, `schedule:<name>`, …). */
9103
+ readonly source?: string;
9104
+ /** Prefix match against the run id OR the execution id — what a human
9105
+ * pastes from a log line. Case-sensitive, because ids are. */
9106
+ readonly idPrefix?: string;
9107
+ /** Only runs started at/after this instant. With `startedBefore` this is
9108
+ * the time-range view; each bound works alone too. */
9109
+ readonly startedAfter?: Date;
9110
+ /** Only runs started strictly before this instant. */
9111
+ readonly startedBefore?: Date;
8932
9112
  /** The DEAD-LETTER view: failed runs an operator has NOT yet discarded
8933
9113
  * (`status = 'failed' AND discardedAt IS NULL`). Since the framework applies no
8934
9114
  * retry, a `failed` run is terminal — this is the queue of unhandled failures.
@@ -9035,6 +9215,14 @@ export declare interface WorkflowSignalTarget {
9035
9215
  readonly workflowName?: string;
9036
9216
  }
9037
9217
 
9218
+ /** A blocking caller lost a `singleton: { mode: 'skip' }` race. Carries the
9219
+ * incumbent's execution id, so the caller can wait on THAT run instead. */
9220
+ export declare class WorkflowSingletonHeldError extends Error {
9221
+ readonly workflowName: string;
9222
+ readonly executionId: string;
9223
+ constructor(workflowName: string, executionId: string);
9224
+ }
9225
+
9038
9226
  export declare interface WorkflowStartOptions {
9039
9227
  readonly wait?: boolean;
9040
9228
  /* Excluded from this release type: callerContext */
@@ -9043,8 +9231,14 @@ export declare interface WorkflowStartOptions {
9043
9231
  export declare const workflowToRpc: (workflow: WorkflowDefinitionLike) => Rpc.Rpc<string, Schema.Schema.Any, Schema.Struct<{
9044
9232
  id: typeof Schema.String;
9045
9233
  workflowName: typeof Schema.String;
9046
- executionId: typeof Schema.String;
9047
- status: Schema.Literal<["running"]>;
9234
+ executionId: Schema.NullOr<typeof Schema.String>;
9235
+ status: Schema.Literal<["running", "queued", "dropped", "skipped"]>;
9236
+ deferral: Schema.optional<Schema.Struct<{
9237
+ mode: typeof Schema.String;
9238
+ dueAt: Schema.NullOr<typeof Schema.Number>;
9239
+ retryAfterMs: Schema.NullOr<typeof Schema.Number>;
9240
+ intentId: Schema.NullOr<typeof Schema.String>;
9241
+ }>>;
9048
9242
  }>, Schema.Schema.All, never>;
9049
9243
 
9050
9244
  export declare interface WorkflowUpdateOptions {