@voltro/runtime 0.56.0 → 0.58.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
@@ -2971,6 +2971,12 @@ export declare const DEFAULT_MAX_IDLE_INTERVAL_MS = 30000;
2971
2971
  * small enough to stop a pathological body being buffered into memory. */
2972
2972
  export declare const DEFAULT_MAX_RPC_BODY_BYTES: number;
2973
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
+
2974
2980
  /** See {@link DrainDeps.claimLeaseMs}. */
2975
2981
  export declare const DEFAULT_OUTBOX_CLAIM_LEASE_MS: number;
2976
2982
 
@@ -3002,6 +3008,25 @@ export declare const DEFAULT_RESUME_WINDOW_MS = 60000;
3002
3008
  */
3003
3009
  export declare const DEFAULT_ROW_FILTER_RETRY: Schedule.Schedule<unknown, unknown>;
3004
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
+
3005
3030
  /** Defaults, overridable via `app.config.ts` `reactive.socket.*` (+ env) —
3006
3031
  * resolved by the boot paths' tunables resolver. */
3007
3032
  export declare const DEFAULT_SOCKET_MAX_BUFFERED_BYTES = 1048576;
@@ -4616,6 +4641,28 @@ export declare interface FleetMergeResult<T> {
4616
4641
  readonly replicaId: string;
4617
4642
  readonly ageMs: number;
4618
4643
  }>;
4644
+ /**
4645
+ * Rows left behind by replicas the roster no longer lists.
4646
+ *
4647
+ * Reported rather than deleted, and separately from `stale`, because the two
4648
+ * are different facts with different next steps: a stale row belongs to a
4649
+ * process that is RUNNING and has stopped refreshing — a fault — while a
4650
+ * departed one belongs to a process that is gone, which is what a scale-down
4651
+ * looks like and needs no action.
4652
+ *
4653
+ * Conflating them cost both readings. Every corpse landed in `stale`, so
4654
+ * `complete` could never be true again after the first scale-down; and they
4655
+ * were counted as responders, so a row from the release before last reported
4656
+ * a version SPLIT across a fleet that had none.
4657
+ *
4658
+ * Empty when no membership is wired: with no roster there is no evidence of
4659
+ * departure, and assuming it would empty a single-process answer.
4660
+ */
4661
+ readonly departed: ReadonlyArray<{
4662
+ readonly replicaId: string;
4663
+ readonly ageMs: number;
4664
+ readonly version: string | undefined;
4665
+ }>;
4619
4666
  /** Framework versions among the responders, counted. Present only when they
4620
4667
  * DIFFER — a single-version fleet needs no such note, and a note nobody
4621
4668
  * needs is one everybody learns to skip. */
@@ -6456,6 +6503,15 @@ export declare const noteRead: (descriptor: QueryDescriptor) => void;
6456
6503
  */
6457
6504
  export declare const noteResumeVerdict: (rawLabel: string, verdict: true | ResumeExclusion) => void;
6458
6505
 
6506
+ /**
6507
+ * Note that a row filter was resolved on `arm`, and say so the FIRST time.
6508
+ *
6509
+ * Exported so the arms that are not in this module (the serve pipeline) report
6510
+ * through the same counter — four arms reporting four ways is how a reader comes
6511
+ * to believe three of them are covered.
6512
+ */
6513
+ export declare const noteRowFilterResolved: (arm: string, tables: number | undefined) => void;
6514
+
6459
6515
  export declare interface Notification {
6460
6516
  readonly subscriberId: string;
6461
6517
  readonly fingerprint: string;
@@ -6793,6 +6849,42 @@ export declare const payloadPropertyNames: (schema: Schema.Schema.Any) => {
6793
6849
  readonly optional: ReadonlyArray<string>;
6794
6850
  };
6795
6851
 
6852
+ /**
6853
+ * Is a `running` row on ANOTHER replica a live run, or a corpse?
6854
+ *
6855
+ * Pure, and separated from the query for the same reason
6856
+ * `stagingLeftovers` is: the rule is four lines and each of them has a way to
6857
+ * be quietly wrong in a direction nobody sees. Wrong towards "alive" and a
6858
+ * schedule stops firing with `overlap skip` in the log; wrong towards "dead"
6859
+ * and two replicas run the same occurrence.
6860
+ *
6861
+ * ── The two cases, and why there are two ───────────────────────────────────
6862
+ *
6863
+ * **A beat is present.** Then the answer is the beat and nothing else: a live
6864
+ * handler bumps it every `heartbeatMs`, so silence longer than `STALE_BEATS`
6865
+ * of them is a process that is not coming back.
6866
+ *
6867
+ * **No beat.** A row written before this column existed — i.e. by a process
6868
+ * still running the previous version during a rolling deploy. `firedAt` is all
6869
+ * there is, and the honest bound on it is the schedule's OWN `maxRuntimeMs`:
6870
+ * the watchdog records a run that exceeds it as `failed`, so a row older than
6871
+ * that plus the stale window cannot belong to a process that is still going to
6872
+ * write a terminal status.
6873
+ *
6874
+ * **Neither is a usable instant** → NOT alive. Fail-open, deliberately, and it
6875
+ * is the pre-existing reading (`firedAt != null &&`). A row nobody can date
6876
+ * must not hold a schedule shut forever, and the counterweight is the claim
6877
+ * coordinator, which is what actually guarantees once-only for a clock firing —
6878
+ * this guard is the second line, not the first.
6879
+ */
6880
+ export declare const peerRunIsAlive: (row: {
6881
+ readonly firedAt: Date | string | null;
6882
+ readonly heartbeatAt?: Date | string | null;
6883
+ }, now: number, bounds: {
6884
+ readonly heartbeatMs: number;
6885
+ readonly maxRuntimeMs: number;
6886
+ }) => boolean;
6887
+
6796
6888
  /**
6797
6889
  * The calling subject's approval work: intents they may DECIDE, plus intents
6798
6890
  * they REQUESTED that are still live.
@@ -7939,12 +8031,21 @@ export declare const resetEventMetricTagCacheForTests: () => void;
7939
8031
  /** Drop everything recorded. For tests; not called by the runtime. */
7940
8032
  export declare const resetObservedGraph: () => void;
7941
8033
 
8034
+ /** Test seam — a process only ever warns once, which would make the second
8035
+ * assertion in a suite vacuous. */
8036
+ export declare const resetReplicaObservationWarnings: () => void;
8037
+
7942
8038
  /** Clear the census. For tests; a running app has no reason to forget. */
7943
8039
  export declare const resetResumeCensus: () => void;
7944
8040
 
7945
8041
  /** Test seam. */
7946
8042
  export declare const resetResumeRingsForTest: () => void;
7947
8043
 
8044
+ /** Test helper: forget which arms have reported. Not for production — the set
8045
+ * is a record of what this PROCESS has proven, and forgetting it would make
8046
+ * the line repeat and the list lie. */
8047
+ export declare const resetRowFilterResolvedArms: () => void;
8048
+
7948
8049
  /** Reset to the env default (tests). */
7949
8050
  export declare const resetSecretsBackend: () => void;
7950
8051
 
@@ -8089,6 +8190,25 @@ export declare interface ResolvedWorkflowCallerContext extends WorkflowCallerCon
8089
8190
  readonly subject: unknown;
8090
8191
  /** Never absent — the caller's trace id, or `'workflow-bootstrap'`. */
8091
8192
  readonly traceId: string;
8193
+ /**
8194
+ * Never absent — the row-filter scope this run's steps read through,
8195
+ * resolved for the subject above at EXECUTION time.
8196
+ *
8197
+ * It is here for the same reason `resolveAuthority` exists, and the wording
8198
+ * carries over: authority re-read from the app's live source is what a
8199
+ * three-day-old run may DO, and this is what it may SEE. Restoring either
8200
+ * from the start-context row would be a frozen fact made durable.
8201
+ *
8202
+ * It is also what makes such a run possible at all. The store refuses a
8203
+ * scope-less context whose subject is not the system, so a workflow started
8204
+ * BY A USER — the common case — threw before its executor ran in any app that
8205
+ * calls `setRowFilter`. A bootstrap run passed, because its subject IS the
8206
+ * system, which is why the gap was invisible from the framework's own tests.
8207
+ *
8208
+ * `NO_ROW_FILTER` when the app registered none: the resolution short-circuits
8209
+ * without touching a store, so a filter-less app pays nothing.
8210
+ */
8211
+ readonly rowFilter: RowFilterScope;
8092
8212
  }
8093
8213
 
8094
8214
  export declare const resolveKeepAlive: (config: KeepAliveConfig | null | undefined) => KeepAliveTimeouts;
@@ -8131,31 +8251,6 @@ export declare const resolveRequestIsHttps: (input: {
8131
8251
  * current table (or at least every row in the rescan groups). */
8132
8252
  export declare const resolveRescan: (shape: AggregateShape, state: IvmState, rescanGroups: ReadonlyArray<string>, baseRows: ReadonlyArray<Row_3>) => IvmState;
8133
8253
 
8134
- /**
8135
- * Resolve the request-scoped filter for `subject`.
8136
- *
8137
- * Three stages, in order, and the order is the design:
8138
- *
8139
- * 1. **Bypass** — no registered filter, or a `system` subject: `NO_ROW_FILTER`,
8140
- * zero cost, no `load` call.
8141
- * 2. **Retry** — `load` runs under the filter's `retry` schedule
8142
- * (`DEFAULT_ROW_FILTER_RETRY` unless overridden). A transient failure must
8143
- * never reach stage 3, because at stage 3 it is indistinguishable from an
8144
- * authorization answer.
8145
- * 3. **Refuse** — a failure that survived retry is reported through `onError`
8146
- * and then, per `onLoadError`, either FAILS with `RowFilterUnavailable`
8147
- * (default) or degrades to `DENY_ALL`.
8148
- *
8149
- * Fails CLOSED, never open: a `load` failure never yields an unfiltered read.
8150
- * A row filter that degrades to "no filter" under load failure is worse than
8151
- * none, because the system keeps serving and nothing looks wrong.
8152
- *
8153
- * And refusal is an ERROR by default, not an empty result. An empty result for
8154
- * an infrastructure failure is the most misleading outcome available: it is
8155
- * byte-identical to legitimate emptiness, so the user reads "you have no
8156
- * tickets" and the operator reads a healthy 200. Every constrained page IS
8157
- * broken when this happens; the honest thing is to say so.
8158
- */
8159
8254
  export declare const resolveRowFilterScope: (subject: Subject, onError?: (error: unknown) => void) => Effect.Effect<RowFilterScope, RowFilterUnavailable>;
8160
8255
 
8161
8256
  /**
@@ -8614,6 +8709,10 @@ export declare const rowFilterMayNarrow: (tables: ReadonlyArray<string>) => bool
8614
8709
  */
8615
8710
  export declare const rowFilterRegistered: () => boolean;
8616
8711
 
8712
+ /** Which arms have been proven to resolve the filter in this process. Read by
8713
+ * the inspect surface; a test reads it to assert an arm was exercised. */
8714
+ export declare const rowFilterResolvedArms: () => ReadonlyArray<string>;
8715
+
8617
8716
  /** What a scoped store needs: a sync `table → Predicate?` lookup. */
8618
8717
  export declare type RowFilterScope = (table: string) => Predicate | undefined;
8619
8718
 
@@ -9194,6 +9293,16 @@ export declare interface SchedulerDeps {
9194
9293
  /** Stable id of this process/replica (pod name, pid, …) recorded on
9195
9294
  * every run row for debugging "which instance fired this." */
9196
9295
  readonly replicaId: string;
9296
+ /**
9297
+ * How often a run in flight bumps `heartbeatAt` — the signal the
9298
+ * cross-instance overlap guard reads to tell a live run from a corpse.
9299
+ *
9300
+ * Omitted → {@link DEFAULT_SCHEDULE_HEARTBEAT_MS}. Both boot paths pass the
9301
+ * resolved `scheduling.scheduleHeartbeatMs`, so a deployment changes it in
9302
+ * one place; a non-positive value falls back to the default rather than
9303
+ * turning the beat into a spin.
9304
+ */
9305
+ readonly runHeartbeatMs?: number;
9197
9306
  readonly log: SchedulerLogger;
9198
9307
  /** Notify on every run-row insert/update so the SSE bridge can
9199
9308
  * mirror schedule runs to the cloud dashboard in real time. */
@@ -11720,13 +11829,6 @@ export declare const wrapStoreWithBootCodec: (underlying: DataStore, dialectId:
11720
11829
 
11721
11830
  export declare const wrapStoreWithMixinBehaviour: (rawUnderlying: DataStore, ctx: StoreMiddlewareContext) => FluentStore;
11722
11831
 
11723
- /**
11724
- * Write (upsert) this replica's counters for one kind.
11725
- *
11726
- * Best-effort throughout: a diagnostic that can fail a request, or a boot, is a
11727
- * new way to be down. A failure logs at debug and leaves the previous row —
11728
- * which then reads as stale, which is the truth.
11729
- */
11730
11832
  export declare const writeReplicaObservation: (store: DataStore, kind: ReplicaObservationKind, payload: unknown) => Promise<void>;
11731
11833
 
11732
11834
  export { }