@voltro/runtime 0.55.0 → 0.57.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
@@ -78,6 +78,7 @@ import { Sampler } from '@opentelemetry/sdk-trace-base';
78
78
  import { Schedule } from 'effect';
79
79
  import { Schema } from 'effect';
80
80
  import { ScopeError } from '@voltro/protocol';
81
+ import { Server } from 'node:http';
81
82
  import { ServerOnlyColumnWrite } from '@voltro/protocol';
82
83
  import { SpanProcessor } from '@opentelemetry/sdk-trace-base';
83
84
  import { spawn } from 'node:child_process';
@@ -1171,6 +1172,9 @@ export declare const applyInverse: (store: UndoApplyStore, op: InverseOp) => Pro
1171
1172
  * `synthesizeInverse`), as the caller's transaction wraps them. */
1172
1173
  export declare const applyInverses: (store: UndoApplyStore, ops: ReadonlyArray<InverseOp>) => Promise<void>;
1173
1174
 
1175
+ /** Apply the timeouts to a node HTTP server. */
1176
+ export declare const applyKeepAlive: (server: Server, t: KeepAliveTimeouts) => void;
1177
+
1174
1178
  /**
1175
1179
  * Apply every rule matching `change`.
1176
1180
  *
@@ -2947,6 +2951,18 @@ export declare const DEFAULT_DELIVERY_CONCURRENCY = 8;
2947
2951
  * framework default. */
2948
2952
  export declare const DEFAULT_HSTS = "max-age=15552000; includeSubDomains";
2949
2953
 
2954
+ /**
2955
+ * 72 seconds.
2956
+ *
2957
+ * The bound that matters is the PROXY's idle timeout, and the defaults cluster
2958
+ * at 60s (ALB, Envoy) and 75s (nginx). 72s clears the 60s group with margin and
2959
+ * sits just under nginx's — a deployment on nginx should raise this, and the
2960
+ * doc says so. Picking 76s instead would leave every ALB user holding a
2961
+ * connection the load balancer has already dropped, which is the same defect
2962
+ * mirrored; the 60s group is the larger one.
2963
+ */
2964
+ export declare const DEFAULT_KEEP_ALIVE_MS = 72000;
2965
+
2950
2966
  /** Ceiling the idle backoff climbs to. Deliberately short enough to be a
2951
2967
  * FLOOR under a missed wake rather than a substitute for one. */
2952
2968
  export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
@@ -2955,6 +2971,15 @@ export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
2955
2971
  * small enough to stop a pathological body being buffered into memory. */
2956
2972
  export declare const DEFAULT_MAX_RPC_BODY_BYTES: number;
2957
2973
 
2974
+ /** The watchdog bound a schedule gets when it declares none. Exported
2975
+ * because the overlap guard dates an un-beating run against it — see
2976
+ * `peerRunIsAlive`; a second copy of the number there would be a second
2977
+ * definition nothing keeps in step. */
2978
+ export declare const DEFAULT_MAX_RUNTIME_MS: number;
2979
+
2980
+ /** See {@link DrainDeps.claimLeaseMs}. */
2981
+ export declare const DEFAULT_OUTBOX_CLAIM_LEASE_MS: number;
2982
+
2958
2983
  /**
2959
2984
  * How many raw-SQL reads one request records for the undeclared-`dependsOn`
2960
2985
  * diagnostic. A bound rather than none: the recording lives for as long as the
@@ -2983,6 +3008,25 @@ export declare const DEFAULT_RESUME_WINDOW_MS = 60000;
2983
3008
  */
2984
3009
  export declare const DEFAULT_ROW_FILTER_RETRY: Schedule.Schedule<unknown, unknown>;
2985
3010
 
3011
+ /**
3012
+ * How often a run in flight bumps `heartbeatAt`.
3013
+ *
3014
+ * The default lives HERE, next to the only code that reads it, and
3015
+ * `cli/src/schedulingConfig.ts` imports it as the default of the
3016
+ * `scheduling.scheduleHeartbeatMs` field. One definition — a second one in the
3017
+ * resolver is how a config default and a library default come to disagree
3018
+ * about a number neither of them prints.
3019
+ *
3020
+ * 30 s rather than the couple of seconds `_voltro_replace_in_progress` beats
3021
+ * at, because the two are bounding different things. A bulk load's beat decides
3022
+ * whether a boot may DROP staging tables, so its cost is worth paying to keep
3023
+ * the window tight. This one decides how long a dead run holds its schedule
3024
+ * shut, measured against a cron period — a minute and a half of latency on a
3025
+ * half-hourly job is one slot, and a run shorter than one interval writes no
3026
+ * beat at all and costs nothing.
3027
+ */
3028
+ export declare const DEFAULT_SCHEDULE_HEARTBEAT_MS = 30000;
3029
+
2986
3030
  /** Defaults, overridable via `app.config.ts` `reactive.socket.*` (+ env) —
2987
3031
  * resolved by the boot paths' tunables resolver. */
2988
3032
  export declare const DEFAULT_SOCKET_MAX_BUFFERED_BYTES = 1048576;
@@ -2991,6 +3035,15 @@ export declare const DEFAULT_SOCKET_OVERRUN_AFTER_MS = 10000;
2991
3035
 
2992
3036
  export declare const DEFAULT_SOCKET_OVERSIZED_EVENT_BYTES = 262144;
2993
3037
 
3038
+ /**
3039
+ * How old a row may be before a reader should stop trusting it as current.
3040
+ *
3041
+ * Tunable rather than a constant because the write cadence is: a fleet that
3042
+ * writes every 30s and one that writes every 5m disagree about what "stale"
3043
+ * means, and a single number would be wrong for one of them.
3044
+ */
3045
+ export declare const DEFAULT_STALE_AFTER_MS = 120000;
3046
+
2994
3047
  /**
2995
3048
  * The message of an UNDECLARED throw, bounded, with nothing else attached.
2996
3049
  *
@@ -3436,13 +3489,39 @@ export declare interface DispatcherDependencies {
3436
3489
  export declare const DORMANCY_WAKEUP_TENANT = "default";
3437
3490
 
3438
3491
  export declare interface DrainDeps {
3439
- readonly store: Pick<DataStore, 'query' | 'update' | 'insert' | 'delete'>;
3492
+ readonly store: Pick<DataStore, 'query' | 'update' | 'updateMany' | 'insert' | 'delete'>;
3440
3493
  readonly handlers: ReadonlyMap<string, OutboxHandlerDefinition>;
3441
3494
  readonly now?: () => Date;
3442
3495
  /** Max rows per drain pass. */
3443
3496
  readonly batchSize?: number;
3444
3497
  /** Per-entry cap on retained attempt rows. See {@link OUTBOX_ATTEMPT_LOG_CAP}. */
3445
3498
  readonly attemptLogCap?: number;
3499
+ /**
3500
+ * Who this process is, stamped on every row it claims.
3501
+ *
3502
+ * REQUIRED, and deliberately so. The first version made it optional "so an
3503
+ * embedder driving a single process is not forced to invent an identity",
3504
+ * which meant a caller who omitted it silently got the old behaviour: every
3505
+ * replica delivers every row. An exactly-once gate that can be switched off by
3506
+ * leaving a field out is not a gate — it is a default, and the wrong one.
3507
+ *
3508
+ * The INSTANCE rather than the replica name: a pod that restarts under a
3509
+ * stable name must not look like it still holds the claim it died inside, or
3510
+ * the lease is reading its own ghost.
3511
+ */
3512
+ readonly claimedBy: string;
3513
+ /**
3514
+ * How long a claim stands before another replica may take the row.
3515
+ *
3516
+ * It is a LEASE, not a lock: the holder can die, and nothing it left behind
3517
+ * may block delivery forever. It must therefore exceed the slowest handler
3518
+ * you are willing to wait for — a lease shorter than a real delivery hands
3519
+ * the same row to a second replica while the first is still working, which is
3520
+ * the duplicate this whole mechanism removes, reintroduced by a number.
3521
+ *
3522
+ * Default 5 minutes.
3523
+ */
3524
+ readonly claimLeaseMs?: number;
3446
3525
  }
3447
3526
 
3448
3527
  /**
@@ -3474,14 +3553,6 @@ export declare const drainForShutdown: (options: {
3474
3553
  }) => void;
3475
3554
  }) => void;
3476
3555
 
3477
- /**
3478
- * One drain pass: claim due rows, run their handler, record the outcome.
3479
- *
3480
- * A row whose handler is unknown is left PENDING rather than dead-lettered —
3481
- * the usual cause is a deploy where the enqueuing code shipped before the
3482
- * handler, and discarding those would turn a rollout ordering detail into
3483
- * permanent data loss.
3484
- */
3485
3556
  export declare const drainOutbox: (deps: DrainDeps) => Promise<DrainResult>;
3486
3557
 
3487
3558
  export declare interface DrainResult {
@@ -4544,6 +4615,38 @@ export declare interface FinopsRunnerHandle {
4544
4615
  readonly runTick: () => void;
4545
4616
  }
4546
4617
 
4618
+ /** A member of the fleet, as membership sees it. The shape this module needs
4619
+ * from `instanceMembership`, named so the merge can be tested without one. */
4620
+ export declare interface FleetMember {
4621
+ readonly instanceId: string;
4622
+ readonly startedAt: number;
4623
+ }
4624
+
4625
+ export declare interface FleetMergeResult<T> {
4626
+ /** One entry per replica that has written, newest generation only. */
4627
+ readonly byReplica: ReadonlyArray<{
4628
+ readonly replicaId: string;
4629
+ readonly version: string | undefined;
4630
+ readonly ageMs: number;
4631
+ /** Where a peer can reach it, or `undefined`. */
4632
+ readonly reachableAt: string | undefined;
4633
+ readonly payload: T;
4634
+ }>;
4635
+ /** Replicas membership knows about that have written nothing readable. */
4636
+ readonly missing: ReadonlyArray<string>;
4637
+ readonly responded: number;
4638
+ readonly expected: number;
4639
+ /** Rows old enough that a reader should not treat them as current. */
4640
+ readonly stale: ReadonlyArray<{
4641
+ readonly replicaId: string;
4642
+ readonly ageMs: number;
4643
+ }>;
4644
+ /** Framework versions among the responders, counted. Present only when they
4645
+ * DIFFER — a single-version fleet needs no such note, and a note nobody
4646
+ * needs is one everybody learns to skip. */
4647
+ readonly versions: Readonly<Record<string, number>> | undefined;
4648
+ }
4649
+
4547
4650
  export declare interface FluentStore extends Omit<MutationStore, 'update' | 'delete' | 'query'> {
4548
4651
  /**
4549
4652
  * Diff-based many-to-many link writer for a junction table. `anchor` names the
@@ -4665,6 +4768,17 @@ export declare const forbiddenColumnsForSubject: (tableName: string, subject: Su
4665
4768
  /** Forget the calling subject's credential. Returns whether a row was removed. */
4666
4769
  export declare const forgetCredential: (store: VaultStore, connectionId: string, subjectId: string) => Promise<boolean>;
4667
4770
 
4771
+ /**
4772
+ * Drop rows for a replica GENERATION that is gone.
4773
+ *
4774
+ * Not a retention sweep — the table has no history to age out. This removes
4775
+ * the row of a process that restarted, because the replica's NEW row is
4776
+ * written under the same `(replicaId, kind)` key and the old generation is
4777
+ * simply overwritten. What this handles is a replica that went away for good
4778
+ * (a scale-down), whose row would otherwise be counted forever.
4779
+ */
4780
+ export declare const forgetReplicaObservations: (store: DataStore, replicaId: string) => Promise<void>;
4781
+
4668
4782
  /** A boot-ready message for a set of cache-scope leaks (empty → `undefined`). */
4669
4783
  export declare const formatCacheScopeLeaks: (leaks: ReadonlyArray<CacheScopeLeak>) => string | undefined;
4670
4784
 
@@ -4776,6 +4890,9 @@ export declare type HandlerBody = void | Promise<unknown> | Effect.Effect<unknow
4776
4890
  */
4777
4891
  export declare const hashUnitInterval: (s: string) => number;
4778
4892
 
4893
+ /** How much longer than `keepAliveTimeout` the headers deadline runs. */
4894
+ export declare const HEADERS_TIMEOUT_MARGIN_MS = 5000;
4895
+
4779
4896
  /** Define a histogram. `boundaries` default to the framework duration buckets. */
4780
4897
  export declare const histogramMetric: (name: string, boundaries?: MetricBoundaries.MetricBoundaries, description?: string) => Metric.Metric.Histogram<number>;
4781
4898
 
@@ -5294,6 +5411,23 @@ export declare interface JunctionLinks {
5294
5411
  }>;
5295
5412
  }
5296
5413
 
5414
+ /** The `http:` block's keep-alive knobs. */
5415
+ export declare interface KeepAliveConfig {
5416
+ readonly http?: {
5417
+ /** Idle keep-alive lifetime, ms. Default 72000. Raise it above YOUR
5418
+ * proxy's idle timeout — see `DEFAULT_KEEP_ALIVE_MS` for why 72s. */
5419
+ readonly keepAliveTimeoutMs?: number;
5420
+ /** Header-completion deadline, ms. Must exceed `keepAliveTimeoutMs`.
5421
+ * Default `keepAliveTimeoutMs + 5000`. */
5422
+ readonly headersTimeoutMs?: number;
5423
+ };
5424
+ }
5425
+
5426
+ export declare interface KeepAliveTimeouts {
5427
+ readonly keepAliveTimeoutMs: number;
5428
+ readonly headersTimeoutMs: number;
5429
+ }
5430
+
5297
5431
  export declare interface KvFacade {
5298
5432
  readonly kv: AsyncKv;
5299
5433
  /** The resolved Effect-native `Kv` service instance. Its methods close over
@@ -6029,6 +6163,25 @@ export declare const memoryRywPositionStore: (options?: MemoryRywOptions) => Ryw
6029
6163
  */
6030
6164
  export declare const mergeCookieHeader: (existing: string | undefined, patch: Readonly<Record<string, string>>) => string;
6031
6165
 
6166
+ /**
6167
+ * Merge rows into a fleet view, and say what is missing from it.
6168
+ *
6169
+ * The three things that make this honest, each of which a naive `rows.map()`
6170
+ * gets wrong:
6171
+ *
6172
+ * 1. **A replica membership knows about but that has no row is MISSING**, not
6173
+ * absent. Dropping it silently is the unmarked sample this whole feature
6174
+ * exists to end — one level up, and more expensive, because the reader now
6175
+ * believes they asked everybody.
6176
+ * 2. **A stale row is reported WITH its age** rather than filtered out.
6177
+ * Removing it would make a partial answer look complete; keeping it
6178
+ * unmarked would make fiction look current. Neither is available.
6179
+ * 3. **Mixed versions are named.** During a rolling deploy the fleet genuinely
6180
+ * spans two shapes, and an aggregate that averages them silently is wrong
6181
+ * in a way nothing downstream can detect.
6182
+ */
6183
+ export declare const mergeFleetObservations: <T>(rows: ReadonlyArray<ReplicaObservationRow>, members: ReadonlyArray<FleetMember>, staleAfterMs?: number) => FleetMergeResult<T>;
6184
+
6032
6185
  export { MetricBoundaries }
6033
6186
 
6034
6187
  export declare interface MetricBucketPoint {
@@ -6328,6 +6481,15 @@ export declare const noteRead: (descriptor: QueryDescriptor) => void;
6328
6481
  */
6329
6482
  export declare const noteResumeVerdict: (rawLabel: string, verdict: true | ResumeExclusion) => void;
6330
6483
 
6484
+ /**
6485
+ * Note that a row filter was resolved on `arm`, and say so the FIRST time.
6486
+ *
6487
+ * Exported so the arms that are not in this module (the serve pipeline) report
6488
+ * through the same counter — four arms reporting four ways is how a reader comes
6489
+ * to believe three of them are covered.
6490
+ */
6491
+ export declare const noteRowFilterResolved: (arm: string, tables: number | undefined) => void;
6492
+
6331
6493
  export declare interface Notification {
6332
6494
  readonly subscriberId: string;
6333
6495
  readonly fingerprint: string;
@@ -6665,6 +6827,42 @@ export declare const payloadPropertyNames: (schema: Schema.Schema.Any) => {
6665
6827
  readonly optional: ReadonlyArray<string>;
6666
6828
  };
6667
6829
 
6830
+ /**
6831
+ * Is a `running` row on ANOTHER replica a live run, or a corpse?
6832
+ *
6833
+ * Pure, and separated from the query for the same reason
6834
+ * `stagingLeftovers` is: the rule is four lines and each of them has a way to
6835
+ * be quietly wrong in a direction nobody sees. Wrong towards "alive" and a
6836
+ * schedule stops firing with `overlap skip` in the log; wrong towards "dead"
6837
+ * and two replicas run the same occurrence.
6838
+ *
6839
+ * ── The two cases, and why there are two ───────────────────────────────────
6840
+ *
6841
+ * **A beat is present.** Then the answer is the beat and nothing else: a live
6842
+ * handler bumps it every `heartbeatMs`, so silence longer than `STALE_BEATS`
6843
+ * of them is a process that is not coming back.
6844
+ *
6845
+ * **No beat.** A row written before this column existed — i.e. by a process
6846
+ * still running the previous version during a rolling deploy. `firedAt` is all
6847
+ * there is, and the honest bound on it is the schedule's OWN `maxRuntimeMs`:
6848
+ * the watchdog records a run that exceeds it as `failed`, so a row older than
6849
+ * that plus the stale window cannot belong to a process that is still going to
6850
+ * write a terminal status.
6851
+ *
6852
+ * **Neither is a usable instant** → NOT alive. Fail-open, deliberately, and it
6853
+ * is the pre-existing reading (`firedAt != null &&`). A row nobody can date
6854
+ * must not hold a schedule shut forever, and the counterweight is the claim
6855
+ * coordinator, which is what actually guarantees once-only for a clock firing —
6856
+ * this guard is the second line, not the first.
6857
+ */
6858
+ export declare const peerRunIsAlive: (row: {
6859
+ readonly firedAt: Date | string | null;
6860
+ readonly heartbeatAt?: Date | string | null;
6861
+ }, now: number, bounds: {
6862
+ readonly heartbeatMs: number;
6863
+ readonly maxRuntimeMs: number;
6864
+ }) => boolean;
6865
+
6668
6866
  /**
6669
6867
  * The calling subject's approval work: intents they may DECIDE, plus intents
6670
6868
  * they REQUESTED that are still live.
@@ -7044,10 +7242,21 @@ export declare class ReactionRateLimiter {
7044
7242
  }
7045
7243
 
7046
7244
  export declare interface ReactionRunDeps {
7047
- /** Idempotency store — `has(key)` true ⇒ already acted. */
7245
+ /**
7246
+ * Atomically claim ONE logical change. `true` = ours, act. `false` = someone
7247
+ * already has it (or the claim could not be taken — fail-closed, like the
7248
+ * rate slot and the budget beside it).
7249
+ *
7250
+ * ONE operation, not `has` then `add`, and the difference is the whole
7251
+ * guarantee. A read-then-write cannot be won by exactly one caller: two
7252
+ * replicas both read "not seen", both act, and `dedupeKey` — the field
7253
+ * `defineReaction` REQUIRES precisely so an act cannot repeat — delivers N
7254
+ * acts on N replicas. It was worse than a missing gate, because it read as a
7255
+ * present one: the in-memory set behind it was per-process, so every test and
7256
+ * every single-instance run confirmed it worked.
7257
+ */
7048
7258
  readonly dedupe: {
7049
- readonly has: (key: string) => boolean | Promise<boolean>;
7050
- readonly add: (key: string) => void | Promise<void>;
7259
+ readonly claim: (key: string) => boolean | Promise<boolean>;
7051
7260
  };
7052
7261
  /** Resolve + run the reaction's act AS the agent actor. Injected by the serve
7053
7262
  * layer (it owns agent/workflow execution). */
@@ -7166,6 +7375,16 @@ export declare const readPollCeilingMs: (env?: {
7166
7375
  /** Is anything listening? Call sites skip their work when nothing is. */
7167
7376
  export declare const readRecordingActive: () => boolean;
7168
7377
 
7378
+ /**
7379
+ * Every replica's rows for one kind.
7380
+ *
7381
+ * `now` is injected so a caller can compute ages against one instant rather
7382
+ * than a moving clock — two rows compared against two `Date.now()` calls are
7383
+ * two measurements, and the difference between them is noise a reader would
7384
+ * read as signal.
7385
+ */
7386
+ export declare const readReplicaObservations: (store: DataStore, kind: ReplicaObservationKind, now?: number) => Promise<ReadonlyArray<ReplicaObservationRow>>;
7387
+
7169
7388
  /** `admin:full` (mirrors `@voltro/protocol`'s ADMIN_SCOPE) bypasses ReBAC. */
7170
7389
  export declare const REBAC_ADMIN_SCOPE = "admin:full";
7171
7390
 
@@ -7591,6 +7810,8 @@ export declare interface RelationTuple {
7591
7810
  */
7592
7811
  export declare const releaseWakeup: (store: DataStore, ref: WakeupRef) => Promise<void>;
7593
7812
 
7813
+ export declare const REPLICA_OBSERVATIONS_TABLE = "_voltro_replica_observations";
7814
+
7594
7815
  export declare interface ReplicaHealth {
7595
7816
  /** Whether this replica is currently eligible for reads. */
7596
7817
  isHealthy(replica: DataStore): boolean;
@@ -7620,6 +7841,27 @@ export declare interface ReplicaHealthOptions {
7620
7841
  readonly now?: () => number;
7621
7842
  }
7622
7843
 
7844
+ /** Which observation a row carries. A closed set — a new kind is a deliberate
7845
+ * addition with a reader, not a free-form label. */
7846
+ export declare type ReplicaObservationKind = 'resume' | 'subscriptions';
7847
+
7848
+ /** One replica's row, as a reader gets it back. */
7849
+ export declare interface ReplicaObservationRow {
7850
+ readonly replicaId: string;
7851
+ readonly startedAt: number;
7852
+ readonly version: string | undefined;
7853
+ readonly kind: ReplicaObservationKind;
7854
+ readonly payload: unknown;
7855
+ /** Where a peer can reach it, or `undefined` — never a guess. */
7856
+ readonly reachableAt: string | undefined;
7857
+ readonly writtenAt: number;
7858
+ /** How old this row is, on the READER's clock. Approximate by construction —
7859
+ * it compares two machines' clocks, which is exactly what
7860
+ * `instanceMembership` refuses to do for liveness. It is good enough to
7861
+ * say "this is minutes old", and must never be used to order two rows. */
7862
+ readonly ageMs: number;
7863
+ }
7864
+
7623
7865
  /** Captured per-read for observability. Pushed into trace buffers
7624
7866
  * by the runtime so `voltro traces` shows the routing decision +
7625
7867
  * any RYW wait latency. */
@@ -7773,6 +8015,11 @@ export declare const resetResumeCensus: () => void;
7773
8015
  /** Test seam. */
7774
8016
  export declare const resetResumeRingsForTest: () => void;
7775
8017
 
8018
+ /** Test helper: forget which arms have reported. Not for production — the set
8019
+ * is a record of what this PROCESS has proven, and forgetting it would make
8020
+ * the line repeat and the list lie. */
8021
+ export declare const resetRowFilterResolvedArms: () => void;
8022
+
7776
8023
  /** Reset to the env default (tests). */
7777
8024
  export declare const resetSecretsBackend: () => void;
7778
8025
 
@@ -7919,6 +8166,8 @@ export declare interface ResolvedWorkflowCallerContext extends WorkflowCallerCon
7919
8166
  readonly traceId: string;
7920
8167
  }
7921
8168
 
8169
+ export declare const resolveKeepAlive: (config: KeepAliveConfig | null | undefined) => KeepAliveTimeouts;
8170
+
7922
8171
  /**
7923
8172
  * Resolve the guard's configuration.
7924
8173
  *
@@ -7957,31 +8206,6 @@ export declare const resolveRequestIsHttps: (input: {
7957
8206
  * current table (or at least every row in the rescan groups). */
7958
8207
  export declare const resolveRescan: (shape: AggregateShape, state: IvmState, rescanGroups: ReadonlyArray<string>, baseRows: ReadonlyArray<Row_3>) => IvmState;
7959
8208
 
7960
- /**
7961
- * Resolve the request-scoped filter for `subject`.
7962
- *
7963
- * Three stages, in order, and the order is the design:
7964
- *
7965
- * 1. **Bypass** — no registered filter, or a `system` subject: `NO_ROW_FILTER`,
7966
- * zero cost, no `load` call.
7967
- * 2. **Retry** — `load` runs under the filter's `retry` schedule
7968
- * (`DEFAULT_ROW_FILTER_RETRY` unless overridden). A transient failure must
7969
- * never reach stage 3, because at stage 3 it is indistinguishable from an
7970
- * authorization answer.
7971
- * 3. **Refuse** — a failure that survived retry is reported through `onError`
7972
- * and then, per `onLoadError`, either FAILS with `RowFilterUnavailable`
7973
- * (default) or degrades to `DENY_ALL`.
7974
- *
7975
- * Fails CLOSED, never open: a `load` failure never yields an unfiltered read.
7976
- * A row filter that degrades to "no filter" under load failure is worse than
7977
- * none, because the system keeps serving and nothing looks wrong.
7978
- *
7979
- * And refusal is an ERROR by default, not an empty result. An empty result for
7980
- * an infrastructure failure is the most misleading outcome available: it is
7981
- * byte-identical to legitimate emptiness, so the user reads "you have no
7982
- * tickets" and the operator reads a healthy 200. Every constrained page IS
7983
- * broken when this happens; the honest thing is to say so.
7984
- */
7985
8209
  export declare const resolveRowFilterScope: (subject: Subject, onError?: (error: unknown) => void) => Effect.Effect<RowFilterScope, RowFilterUnavailable>;
7986
8210
 
7987
8211
  /**
@@ -8440,6 +8664,10 @@ export declare const rowFilterMayNarrow: (tables: ReadonlyArray<string>) => bool
8440
8664
  */
8441
8665
  export declare const rowFilterRegistered: () => boolean;
8442
8666
 
8667
+ /** Which arms have been proven to resolve the filter in this process. Read by
8668
+ * the inspect surface; a test reads it to assert an arm was exercised. */
8669
+ export declare const rowFilterResolvedArms: () => ReadonlyArray<string>;
8670
+
8443
8671
  /** What a scoped store needs: a sync `table → Predicate?` lookup. */
8444
8672
  export declare type RowFilterScope = (table: string) => Predicate | undefined;
8445
8673
 
@@ -9020,6 +9248,16 @@ export declare interface SchedulerDeps {
9020
9248
  /** Stable id of this process/replica (pod name, pid, …) recorded on
9021
9249
  * every run row for debugging "which instance fired this." */
9022
9250
  readonly replicaId: string;
9251
+ /**
9252
+ * How often a run in flight bumps `heartbeatAt` — the signal the
9253
+ * cross-instance overlap guard reads to tell a live run from a corpse.
9254
+ *
9255
+ * Omitted → {@link DEFAULT_SCHEDULE_HEARTBEAT_MS}. Both boot paths pass the
9256
+ * resolved `scheduling.scheduleHeartbeatMs`, so a deployment changes it in
9257
+ * one place; a non-positive value falls back to the default rather than
9258
+ * turning the beat into a spin.
9259
+ */
9260
+ readonly runHeartbeatMs?: number;
9023
9261
  readonly log: SchedulerLogger;
9024
9262
  /** Notify on every run-row insert/update so the SSE bridge can
9025
9263
  * mirror schedule runs to the cloud dashboard in real time. */
@@ -9900,6 +10138,61 @@ export declare interface SubscriberDefinition {
9900
10138
  readonly table: string;
9901
10139
  /** Default `'any'`. */
9902
10140
  readonly on?: SubscribeOp | ReadonlyArray<Exclude<SubscribeOp, 'any'>>;
10141
+ /**
10142
+ * Run the handler ONCE PER CHANGE across the whole fleet, instead of once on
10143
+ * every replica that sees it.
10144
+ *
10145
+ * **Without it, the handler runs on EVERY api instance.** That is the correct
10146
+ * default and not a gap: a subscriber that refreshes a search index, drops a
10147
+ * process-local cache entry or updates in-memory state has to run everywhere,
10148
+ * and a fleet-wide gate would leave every other replica stale. The default
10149
+ * assumes the handler is IDEMPOTENT.
10150
+ *
10151
+ * It is the wrong default for an EFFECT — a notification, a mail, a webhook, a
10152
+ * payment — because there is nothing to make idempotent: the effect IS the
10153
+ * write, so each run produces another one. Two replicas send two mails.
10154
+ *
10155
+ * `once` returns a deterministic key for the change, and exactly one replica
10156
+ * wins the claim on it (`_voltro_change_claims`, INSERT-wins on a UNIQUE — the
10157
+ * same arbiter the cron scheduler uses for a firing). The key is namespaced by
10158
+ * this subscriber's file id, so two subscribers on one table never lock each
10159
+ * other out.
10160
+ *
10161
+ * ```ts
10162
+ * export default defineSubscriber({
10163
+ * table: 'absence_requests',
10164
+ * on: ['insert'],
10165
+ * once: true,
10166
+ * handler: notifyApprovers,
10167
+ * })
10168
+ * ```
10169
+ *
10170
+ * **`once: true` derives the key for you**, and is the right default choice.
10171
+ * It names the change by its CONTENT plus its position among content-identical
10172
+ * repeats (`changeDigest` + `OccurrenceCounter`, `@voltro/database`) — which is
10173
+ * the only thing two replicas provably agree on, since a fleet change carries
10174
+ * no LSN, no commit id and no `traceId`. `A→B`, then `B→A`, then `A→B` again is
10175
+ * three changes, not two.
10176
+ *
10177
+ * **A function is for when you want to be COARSER than one-per-change.** Two
10178
+ * updates that differ only in a field you do not care about are two changes to
10179
+ * `once: true` and can be one to a key function that ignores that field.
10180
+ *
10181
+ * **If you write one, it must tell two genuine changes apart.** A row id is
10182
+ * enough for `insert` and `delete`, where a row changes state once. It is NOT
10183
+ * enough for `update`: two edits to the same row produce the same id, and the
10184
+ * second would be dropped as a duplicate of the first. Put something that moves
10185
+ * in the key — `` `${row.id}:${row.updatedAt}` `` — or use `once: true`.
10186
+ *
10187
+ * **AT MOST once, not exactly once.** The claim is taken BEFORE the handler
10188
+ * runs, so a replica that wins and then dies takes the event with it, and a
10189
+ * claim that cannot be written (database unreachable) is not taken by anyone.
10190
+ * Both are loud in the log and neither is retried — a subscriber is
10191
+ * best-effort by construction. When the effect must not be lost, the change
10192
+ * stream is the wrong seam for it: run it in the mutation, or start a workflow
10193
+ * from a `*.reaction.tsx`, where the durability is the primitive's job.
10194
+ */
10195
+ readonly once?: true | ((event: SubscribeEvent) => string);
9903
10196
  readonly handler: SubscribeHandler;
9904
10197
  }
9905
10198
 
@@ -10736,6 +11029,8 @@ export declare const VOLTRO_TENANT_MIXIN_ID: "voltro/tenant";
10736
11029
  */
10737
11030
  export declare const _voltroApprovalsTable: TableLike;
10738
11031
 
11032
+ export declare const _voltroChangeClaimsTable: TableLike;
11033
+
10739
11034
  /**
10740
11035
  * `_voltro_connection_grants` — an IN-FLIGHT oauth2 handshake. Separate from
10741
11036
  * the credential table on purpose: a handshake is a short-lived, single-use
@@ -11489,4 +11784,13 @@ export declare const wrapStoreWithBootCodec: (underlying: DataStore, dialectId:
11489
11784
 
11490
11785
  export declare const wrapStoreWithMixinBehaviour: (rawUnderlying: DataStore, ctx: StoreMiddlewareContext) => FluentStore;
11491
11786
 
11787
+ /**
11788
+ * Write (upsert) this replica's counters for one kind.
11789
+ *
11790
+ * Best-effort throughout: a diagnostic that can fail a request, or a boot, is a
11791
+ * new way to be down. A failure logs at debug and leaves the previous row —
11792
+ * which then reads as stale, which is the truth.
11793
+ */
11794
+ export declare const writeReplicaObservation: (store: DataStore, kind: ReplicaObservationKind, payload: unknown) => Promise<void>;
11795
+
11492
11796
  export { }