@voltro/runtime 0.65.0 → 0.66.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,100 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.66.0] — 2026-09-06
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/client, @voltro/cli** — Socket reconnects retry immediately, then after 500 ms with factor 1.5 and a 5 s cap; configure `web.api.recovery.retry: { initialMs, thenMs, factor, maxMs }` beside `graceMs` and rebuild the web app. Web and React Native share the policy, reset it on protocol readiness, preserve subscription bases/revisions and report every scheduled retry in client diagnostics. One transport loop owns retries, preventing competing supervisor rebuilds. Migration for custom `startApiSupervisor`/`BuildClient` adapters: own socket retries inside the transport (or use `buildApiRuntime`); `onSocketIssue` now reports health/backstop signals only, and `retryDelayMs` overrides construction failures only. Header resolvers are evaluated per runtime build, so do not rely on socket cuts to refresh a rotated token: the web host retains bounded auth-error refresh, and custom hosts can call `SupervisorHandle.refreshAuth()`. Explicit `refreshAuth()`/`reconnect()` still rebuild. Ordinary web/RN mounts require no adapter changes.
47
+
48
+ **`voltro update` carries you across this** — codemod `0.66.0/02_socket-retry-owner`. 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.66.0).
49
+ - **@voltro/database, @voltro/runtime, @voltro/voltro** — `id({ scheme: 'numeric' })` declares the numeric row key it always emitted. The builder was typed `ColumnBuilder<string, 'id'>` for every scheme and the row decoder used `Schema.String` for every id, so a BIGSERIAL / AUTO_INCREMENT / IDENTITY column arrived in application code as a decimal string. It is now `number`, decoded as `Schema.Number`, and `InMemoryDataStore` allocates numbers so the in-memory store and a SQL store agree.
50
+
51
+ Migration — the codemod prints these, and they are ordered by how the failures surface. String operations on such an id stop compiling and the compiler names every one; wrap with `String(id)` at whatever boundary wants text. Equality against a quoted literal (`row.id === '4'`) is the half that keeps compiling and is now permanently false — drop the quotes. Seeds and fixtures spelling the id as a string fail to decode; write them as numbers. Template interpolation needs no change.
52
+
53
+ `reference(target, { storage: 'numeric' })` carries the same numeric type.
54
+
55
+ **`voltro update` carries you across this** — codemod `0.66.0/01_numeric-id-is-a-number`. 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.66.0).
56
+
57
+ ### Added
58
+
59
+ - **@voltro/client, @voltro/protocol** — Add typed `useConnectionSetup(tag)` and `useMutation(tag).withConnectionSetup(handle)` for acknowledged connection-local setup before initial writes and replay. The Effect-scoped barrier follows physical sockets, restores acknowledged intent, shares concurrent setup attempts and clears on leave, unmount or subject/tenant change. Setup descriptors must explicitly declare connection-scoped idempotency; normal writes keep their keys and replay limits. This is not a durable outbox or an authorization grant. Existing mutation identity helpers retain their signatures.
60
+ - **@voltro/cli** — Generated production Compose stacks run declared boot-lifecycle seeds in the migration job, after the reviewed schema plan has applied. Seeding is therefore one pre-deploy operation rather than something every serving replica repeats on boot.
61
+ - **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/voltro** — Mutations can declare `idempotencyScope: 'connection'` for native WebSocket side effects: repeat keys deduplicate within that server-owned connection but execute again on a replacement connection. HTTP RPCs fail with typed `MutationConnectionRequired`, including when dedup is disabled; internal, public REST and agent-tool projections are refused. The default subject-scoped replay behavior of ordinary business mutations is unchanged. This does not replace application authorization, reconnect ordering, durable pending writes or disconnect cleanup.
62
+ - **@voltro/runtime, @voltro/cli, @voltro/testing** — `ctx.connectionState` keeps server-owned, structured-cloneable values for one native RPC connection: mutations defer writes/deletes to the existing commit queue, rollback/retry discards them, and transport close clears the state without allowing late commits to resurrect it. Dev, serve and `makeTestContext({ connection })` share the implementation; `withRpcConnection` selects another connection against the same test store and services. This is neither authentication nor durable membership storage.
63
+ - **@voltro/database, @voltro/runtime, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql, @voltro/testing, @voltro/voltro** — `store.update(table, key, patch)`, `delete`, `patchJson` and `hardDelete` take EITHER an id string or the columns that identify the row:
64
+
65
+ await store.update('memberships', { userId: u, orgId: o }, { role: 'member' }) await store.delete('memberships', { userId: u, orgId: o })
66
+
67
+ A table declared `.primaryKey(['userId','orgId'])` — or with one explicit key column, which is how a one-to-one shares its parent's key — has no `id`, so it could not be reached by the keyed forms at all. Measured before this, against the in-memory store: `update('memberships', 'u1', …)` returned `null`, `delete('memberships', 'u1')` returned `false`, and the row was untouched. Those are the SAME values a missing row returns.
68
+
69
+ The five store implementations did not agree about it either. The SQL stores emitted `WHERE id = ?` literally — 3 sites in postgres, 8 in mysql, 3 in sqlite, 3 in mssql — so a dialect answered `column "id" does not exist`, while the in-memory store keys rows internally and simply missed. A suite on `makeTestApp` runs the quiet one, so it went green over a call that fails in production.
70
+
71
+ A key is resolved ONCE, by `rowKeyColumns` in `@voltro/database`, so the five implementations cannot drift about what a key is. A bare string still means the `id` column, unchanged; on a table that has none it is refused with the key columns named, rather than answering `null`.
72
+
73
+ Two further defects surfaced when a composite-key table joined the dialect parity harness, both of them older than this change and invisible while the feature had no users:
74
+
75
+ - **MySQL could not INSERT such a row.** A row arriving without an `id` was assumed to be an AUTO_INCREMENT table, so the store recovered the key via `LAST_INSERT_ID()` and failed with "is the primary key AUTO_INCREMENT?" — a question about a column the table never declared. It now reads the post-image back by the values just written when nothing was generated. - **MSSQL could not CREATE one over `text()` columns.** `text()` renders as `NVARCHAR(MAX)` there, which SQL Server refuses as a key column. The emitter already bounds indexed text to `NVARCHAR(450)` for exactly this reason; the primary key's own columns were missing from the set that drives it.
76
+
77
+ `runDialectParity` gained three composite-key scenarios, so all five dialects assert it — the harness that found both of the above.
78
+
79
+ `apiSurface: compatible` — the golden lines that are REPLACED rather than added are the three keyed signatures widening `primaryKey: string` to `primaryKey: RowKey`. A widened parameter accepts everything it did before, so no call site can fail; `@voltro/voltro` is named because the two aggregate goldens carry the same surface and a source package's own report does not pull them along.
80
+ - **@voltro/cli** — `voltro create-project` can add multiple independently cloud-bound projects to one workspace. `add-app --to` and `cloud project link --project` select a target explicitly when needed, and default project port ranges never overlap. API apps now receive manifest/config ports alongside web apps. While the `compose` baseline is active, project/app changes and `voltro baseline sync` regenerate every service plus per-project PostgreSQL databases from those manifests. The generated stack pins PostgreSQL 18.6 and lets containerized web dev resolve its API proxy through `VOLTRO_API_ORIGIN` service DNS.
81
+ - **@voltro/database** — `.primaryKey(['memberId'])` accepts a single explicit natural or reference column as well as a composite set. It previously refused one field and pointed the caller at `id()`, which made a one-to-one shared primary-key/foreign-key impossible to declare without inventing a second key column.
82
+ - **@voltro/database** — `reference(target, { storage: 'numeric' })` declares a foreign key to a table whose primary key is numeric, without disguising it as a plain integer column: row schemas decode it as a number, every SQL renderer emits the dialect's 64-bit integer form, snapshots and introspection round-trip the storage kind, and the migration planner no longer churns it back to text.
83
+
84
+ `reference(target, { column: 'key' })` targets a unique alternate key instead of assuming `id`. Deferred cyclic constraints, subject graphs and test factories carry that target column through. The default remains text-and-`id`.
85
+ - **@voltro/protocol, @voltro/cli, @voltro/testing** — Custom REST handlers receive the per-request application context as `ctx.app`. Development, production `serve` and `makeTestApp` bind the same surface, so a REST webhook can publish a typed event or reach cache, KV, workflows and webhooks exactly as an RPC handler does — without a protocol-to-runtime dependency and without stepping outside the framework to do it.
86
+ - **@voltro/protocol** — Typed REST handlers can shape their own response. `response(body, { status, headers })` selects a status and headers, including repeated values such as the several `Set-Cookie` lines a login emits; `restError(status, body, headers)` preserves an application-defined JSON error contract instead of flattening it to the framework's shape; binary responses can pick status and headers the same way. `RestRouteContext` exposes the byte-exact request body alongside method, path and query, so a signed webhook or a multipart protocol no longer has to drop down to a raw plugin route to see the bytes it must verify.
87
+
88
+ `requireAuthenticated()` gives a protected REST route one consistent 401 path, distinct from the 403 a scope check produces.
89
+
90
+ `apiSurface: compatible` — the golden's one replaced line is the handler's return type widening from `Promise<O> | O` to `Promise<O | RestResponse<O>> | O | RestResponse<O>`. A handler that returns `O` is still assignable, so no declaration site breaks. The one place it is observable is a consumer who reads the descriptor's `handler` type and calls it directly, whose result is now the wider union; the supported way to exercise a route is `makeTestApp`, which is unaffected.
91
+ - **@voltro/database, @voltro/runtime, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-mssql, @voltro/sql-sqlite, @voltro/sql-turso, @voltro/testing, @voltro/voltro** — Upserts accept `updateValues` for additional conflict-only assignments without modifying the INSERT row or changing native column-list upserts into read/compute callbacks. Defined values override the selected update projection; undefined is omitted and identity rewrites are refused. The typed, Effect, replication, transaction and codec surfaces preserve the option, including row-bound encryption. Covered by the shared live-dialect parity suite, including both Turso backends. Existing option fields and calls remain compatible; this is the persistence foundation, not activation of column `.onUpdate()` callbacks.
92
+
93
+ ### Fixed
94
+
95
+ - **@voltro/client** — Keep a transport-interrupted mutation pending through repeated native `MutationInFlight` replies while its original replay window still permits it. Preserve one key, pending slot and optimistic patch; recheck identity, capability, transport and connection setup before each attempt. Do not renew the window, silently accept a failed write, retry other handler failures or busy-loop on malformed non-positive server delays.
96
+ - **@voltro/client** — `useOutbox` drains online enqueues and restored writes, joins concurrent replay calls, includes writes appended during an acknowledgement, and stops at every failed predecessor without a render-driven retry loop. A subsequent explicit retry or reconnection can retry transient failures; conflicts still require resolution. This scheduling fix does not add a durable-save acknowledgement or cross-tab ownership.
97
+ - **@voltro/client** — Keep late failures from ended physical mutation attempts from marking a healed replacement socket disconnected. Native mutation error events record `affectsTransport: false` when their lifetime evidence proves this distinction; the original failure, commit uncertainty and raw error reporting remain intact. Emitters without native lifetime evidence retain the conservative fallback.
98
+ - **@voltro/database, @voltro/runtime, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-mssql, @voltro/sql-sqlite, @voltro/testing, voltro** — Request-scoped stores evaluate column `.onUpdate()` before validation and encryption for keyed/bulk updates, soft deletes, upsert conflicts and JSON patches. `patchJson` accepts optional companion assignments in the same database UPDATE; object merges are shallow and retain JSON nulls. SQLite-family escaped object keys are compared and preserved after exactly one decoding step, based on a constant in-statement engine probe. Raw stores do not run application callbacks. Scalar JSON roots on SQL Server remain a separate schema-validation limitation.
99
+ - **@voltro/cli** — A generated multi-project Compose stack probes each API's declared port. The generator added one but probed the neighbouring web app's port instead, so an API could be marked healthy without ever being tested — and a web service that correctly waited for API readiness deadlocked behind a check that was never going to describe it.
100
+ - **@voltro/cli** — Production serve/start and SSR bundles now resolve dependencies with the bundler's actual import/require conditions. Installed ESM-only transitive packages no longer become bare external imports that fail after strict-pnpm deployment or pruning. Missing required dependencies and broken installed exports fail the build instead of being reported as optional peers; only actually absent packages declared optional by their importer use that fallback. Native runtime shims remain intact. If this exposes a previously hidden missing dependency, install/fix it in its importing package rather than labelling a required import optional. Real esbuild and Vite tests cover conditional exports, strict transitive resolution, optional and required failures, and execution after relocation without node_modules.
101
+ - **@voltro/cli** — `voltro doctor`'s `cron/bucket-on-wall-clock` follows value references into relative app helpers, including renamed imports, namespace members and re-exports. A `new Date()` in a called `midnightUtcForToday()` is now attributed to its cron in text and `--json` output. Unused helper exports, comments, strings and types do not supply either the wall clock or a clearing `scheduledAt` read. This remains a static advisory, not bucket-key data-flow proof; package imports, tsconfig aliases and dynamic imports are not followed.
102
+ - **@voltro/i18n** — `placeholdersOf` is now exported from `@voltro/i18n`'s public entry, including the built JavaScript and declarations. The 0.64.0 changelog promised that export, but it existed only in the internal catalog module; its tests imported that module and missed the absent package export. Those tests now exercise the public entry. No application change is needed unless you want to use the helper.
103
+ - **@voltro/runtime** — `InMemoryDataStore` keeps seeded rows from tables that deliberately have no `id` column — natural and composite primary keys — and inserts into those tables no longer fabricate one. The store assigns an internal map identity instead, which is what a SQL store does, rather than leaking an undeclared field into application rows.
104
+
105
+ Its numeric sequence now starts above the highest seeded id, so the first generated insert can no longer land on top of a seed row.
106
+ - **@voltro/sql-turso** — File-backed libsql connections enable WAL at startup so readers from completed interactive transactions do not block subsequent writers' commits. Remote transports retain server-owned journal configuration. Local-file concurrent-transaction tests require no Turso Cloud account; network and replica synchronization still need separate live validation.
107
+ - **@voltro/runtime** — The in-memory store now applies registered schema literal defaults (including timestamp `now`, JSON and arrays) and implicit nullable values on insert, including transactional, bulk, upsert-insert and insert-ignore-insert paths. Returned rows and change events contain the populated values; explicit values, seeded rows and conflict-update fields are preserved. Mutable defaults are copied per row. App-side factories/computed values remain owned by the shared mutation wrapper; this does not emulate generated SQL expressions or certify database-dialect parity.
108
+ - **@voltro/sql-mssql** — Bind Date parameters as `DateTime2` and strings as `NVarChar`, matching the schema before values reach SQL Server. This preserves milliseconds, dates before 1753, Unicode text and JSON keys/values across row writes, predicates and raw SQL. Explicit NULL remains `NVarChar`. The direct `tedious` dependency uses the version already locked for the SQL client.
109
+ - **@voltro/runtime, @voltro/cli, @voltro/protocol, @voltro/plugin-audit** — Every trace id now comes from one function, `newTraceId()` in `@voltro/runtime` — 128 random bits as 32 hex, the shape a `traceparent` parser, a collector and `voltro traces` all accept.
110
+
111
+ Seventeen call sites minted their own, and three of the shapes were not ids. `incoming:<webhookId>`, `reaction:<workflow>`, `schedule:<name>-<ms>` and the literal `'webhooks:trigger'` were CONSTANTS — five of them wrote the identical string into `traceId` and `source`, so the trace id was the caller's label wearing a second hat and every delivery a process handled shared one trace, with the waterfall merging unrelated work. Six more carried `public-`/`grpc-`/`rest-` plus eight base36 characters, about 41 bits, which collides well inside a busy day. One passed `randomUUID()` unstripped, 36 characters with dashes, which no `traceparent` parser accepts — including this framework's own.
112
+
113
+ The entry-point kind is not lost: it lives in the `source` field beside the id, which is where it was already being written.
114
+
115
+ Plugin HTTP routes now carry a real one too. `PluginHttpRouteEvent` gains `traceId` — the caller's `traceparent` where one arrived, freshly minted otherwise — and the audit trail records it instead of the empty string it wrote before, so an incident review can join a login to what it caused. `apiSurface: compatible`: an observer that ignores the new field is unaffected, and the framework is the only producer of the event.
116
+
117
+ A guard in `@voltro/runtime` now asks the question over the whole tree rather than per call site, because the previous test pinned exactly one of them and the same defect went on living at sixteen others.
118
+ - **@voltro/database, @voltro/runtime** — Declarative migrations now retain partial-index predicates and partial uniqueness on new and existing tables, introspect native filters on PostgreSQL/SQLite/SQL Server, detect same-name predicate changes and invalid PostgreSQL online indexes, and include predicates in drift fingerprints. A partial unique index is never emitted as a full UNIQUE constraint. Existing qualifying duplicates refuse the apply without automatic data repair; a pre-DDL conflict check protects replacements when the current column shape supports it. MySQL/MariaDB uniqueActive lowering remains; unlowered partial indexes are refused by the declarative path. SQL normalization is conservative, not a general expression-equivalence prover.
119
+
120
+ Memory upsert now addresses matched rows through their actual conflict-column key instead of assuming an id column, both directly and transactionally. Natural/composite primary-key rows preserve independent fields, rollback and post-commit events. The schema/API spelling is unchanged and needs no source codemod; projects upgrading must review and apply the newly visible missing constraints, resolving conflicting historical data explicitly rather than deleting it automatically.
121
+ - **@voltro/cli** — Production workspace WebSocket proxies preserve the browser's Host header, so the native same-origin guard accepts legitimate requests behind internal Docker or cluster hostnames while continuing to reject foreign and opaque origins.
122
+ - **@voltro/protocol** — REST descriptors reject undeclared descendant paths before decoding, resolving identity or executing their handler, instead of accidentally serving parent data or performing parent writes. Named parameters, trailing slashes, explicit splats and raw plugin HTTP prefix routes keep their declared behavior.
123
+ - **@voltro/runtime** — Isolate connection credentials across HTTP/WebSocket protocols and listeners, and deliver every disconnect to both runtime cleanup and the RPC core. Executors receive the server-owned `ctx.request.connection` identity, transport and close signal; numeric client IDs remain opaque process-local handles. Connection state is shared across bundled and external module copies.
124
+ - **@voltro/cli** — `voltro prune-runtime` now preserves package-resolution metadata for rooted native dependencies, so packages such as `argon2` remain importable in pruned production images.
125
+ - **@voltro/sql-sqlite, @voltro/sql-turso, @voltro/testing** — SQLite-family raw stores now encode registered JSON and array columns on every row-write path, including insertMany, updateMany, callback upserts and conflict-only updateValues. Previously objects could fail binding or reach Turso as invalid JSON when a request codec had not already serialized them. Shared live-dialect tests cover direct and transactional writes over SQLite and both Turso clients as well as the other SQL stores.
126
+ - **@voltro/testing** — Declared events published by a mutation under `invoke` now use the transaction's commit queue, including subject/tenant re-scopes; rollback and deadlock replay discard failed-attempt notifications, while untransacted actions still publish immediately.
127
+ - **@voltro/cli** — Apply the shared page-safe security-header policy to both web listeners, including streaming, assets and errors, with `security.headers` configuration and route-owned overrides. Do not derive HSTS from untrusted forwarded headers. Proxy native runtime capability discovery in production as well as dev: one public GET per configured workspace API, not the development inspect surface. Named and custom socket paths reach the actual server's replay capability; ambiguous custom socket parents fail explicitly instead of advertising the wrong API. No replay support or successful writes are synthesized by the proxy.
128
+
129
+ ### Internal (no consumer-facing effect)
130
+
131
+ - **@voltro/cli** — Building the CLI package now gives its declaration-rollup process a bounded 6144 MiB heap, and packing delegates to the same build script. The seven-entry rollup exhausted the default roughly 4 GiB heap even in an isolated Node 24 build; the measured build succeeds with the scoped allowance. This does not change the memory settings of application builds or serving processes, and does not disable declaration validation or increase build concurrency.
132
+ - The local release gate now executes the CI test planner with all packages and both CLI projects, rather than skipping its GitHub-specific preparation and reading missing shard files. Missing filters or project selectors fail before tests start; the coverage verifier also refuses a missing expected-package plan. Selftests execute the workflow's actual producer/consumer handoff. MySQL test fixtures check authenticated TCP readiness and distinguish cold initialization from failed steady-state healthchecks. The Postgres partial-index contract creates and cleans up its own database on the regular fixture server, so all seven assertions run without an additional opt-in flag in both local and CI gates. Row-history test files run serially because their live fixtures install and write the same fixed-name Postgres table; parallel setup could deadlock before the history assertions ran when the schema was cold.
133
+
134
+ ---
135
+
42
136
  ## [0.65.0] — 2026-09-04
43
137
 
44
138
  ### ⚠ BREAKING
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ import { ConnectionCredential } from '@voltro/protocol';
11
11
  import { ConnectionInfo } from '@voltro/protocol';
12
12
  import { ConnectionInfoMiddleware } from '@voltro/protocol';
13
13
  import { ConnectionKind } from '@voltro/protocol';
14
- import { ConnectionState } from '@voltro/protocol';
14
+ import { ConnectionState as ConnectionState_2 } from '@voltro/protocol';
15
15
  import { ConstraintFacts } from '@voltro/database';
16
16
  import { ConstraintKind } from '@voltro/database';
17
17
  import { ConstraintViolation } from '@voltro/protocol';
@@ -57,6 +57,7 @@ import { listRetentions } from '@voltro/database';
57
57
  import { Metric } from 'effect';
58
58
  import { MetricBoundaries } from 'effect';
59
59
  import { MetricReader } from '@opentelemetry/sdk-metrics';
60
+ import { MutationIdempotencyScope } from '@voltro/protocol';
60
61
  import { NoRowFound } from '@voltro/protocol';
61
62
  import { PendingApproval } from '@voltro/protocol';
62
63
  import { PluginHttpRoute } from '@voltro/protocol';
@@ -72,6 +73,7 @@ import { RetentionSource } from '@voltro/database';
72
73
  import { RetentionSpec } from '@voltro/database';
73
74
  import { retentionTtlMsFromEnv } from '@voltro/database';
74
75
  import { Row } from '@voltro/database';
76
+ import { RowKey } from '@voltro/database';
75
77
  import { RowPatch } from '@voltro/protocol';
76
78
  import { Rpc } from '@effect/rpc';
77
79
  import { RpcGroup } from '@effect/rpc';
@@ -103,6 +105,7 @@ import { TenantRowNotFound } from '@voltro/protocol';
103
105
  import { TenantScopeViolation } from '@voltro/protocol';
104
106
  import { Tracer } from 'effect';
105
107
  import { Unauthenticated } from '@voltro/protocol';
108
+ import { UpsertOptions } from '@voltro/database';
106
109
  import { ValidationError } from '@voltro/protocol';
107
110
  import { ValidationErrors } from '@voltro/protocol';
108
111
  import { VersionConflict } from '@voltro/database';
@@ -1026,6 +1029,10 @@ export declare interface AppContext {
1026
1029
  */
1027
1030
  readonly storeForTenant: (tenantId: string) => FluentStore;
1028
1031
  readonly request: RuntimeContext;
1032
+ /** Ephemeral server-owned state of this RPC connection. Absent for internal
1033
+ * calls; mutation writes commit with the operation and disconnect clears it.
1034
+ * It grants no authority until the app explicitly records its own decision. */
1035
+ readonly connectionState?: ConnectionState;
1029
1036
  /** Typed authorization slice — the caller's effective scopes. Use
1030
1037
  * `ctx.access.has(scope)` / `yield* ctx.access.require(scope)` instead of
1031
1038
  * reaching into `ctx.request.subject.scopes`. Always present. */
@@ -1650,7 +1657,7 @@ export declare const bindMutation: <Input, Output, E = never>(execute: (input: I
1650
1657
  * schema keeps exactly the old behaviour rather than collapsing errors it
1651
1658
  * cannot classify.
1652
1659
  */
1653
- declaredError?: Schema.Schema.All) => Effect.Effect<Output, E, SubjectService | ConnectionInfo>;
1660
+ declaredError?: Schema.Schema.All, idempotencyScope?: MutationIdempotencyScope) => Effect.Effect<Output, E, SubjectService | ConnectionInfo>;
1654
1661
 
1655
1662
  /**
1656
1663
  * Bind a non-reactive server→client stream (A3). The executor builds a
@@ -2296,7 +2303,7 @@ export declare interface ConnectionsFacade {
2296
2303
  tryGet(connectionId: string): Promise<ResolvedConnection | null>;
2297
2304
  /** Every declared connection with the caller's status — the same projection
2298
2305
  * `__voltro.connections.list` serves, for server-side rendering. */
2299
- list(): Promise<ReadonlyArray<ConnectionState>>;
2306
+ list(): Promise<ReadonlyArray<ConnectionState_2>>;
2300
2307
  }
2301
2308
 
2302
2309
  export declare interface ConnectionsFacadeDeps {
@@ -2305,6 +2312,19 @@ export declare interface ConnectionsFacadeDeps {
2305
2312
  readonly subject: Subject;
2306
2313
  }
2307
2314
 
2315
+ /** Server-local application state for ONE RPC connection. Not authentication,
2316
+ * a presence roster or durable storage. Mutation writes become visible only
2317
+ * after commit; reads always see committed state, including in the writer.
2318
+ * Values must be structured-cloneable and are copied on write and read so a
2319
+ * mutable alias cannot bypass the transaction boundary.
2320
+ * Disconnect clears everything and a reconnect starts empty. */
2321
+ export declare interface ConnectionState {
2322
+ readonly get: (key: string) => unknown;
2323
+ readonly has: (key: string) => boolean;
2324
+ readonly set: (key: string, value: unknown) => void;
2325
+ readonly delete: (key: string) => void;
2326
+ }
2327
+
2308
2328
  /** Test-only — number of streams currently registered for a client. */
2309
2329
  export declare const _connectionStreamCount: (clientId: number) => number;
2310
2330
 
@@ -3645,7 +3665,7 @@ export declare interface EffectStoreOps {
3645
3665
  readonly query: <R = Row>(descriptor: QueryDescriptor<R>) => Effect.Effect<ReadonlyArray<R>, StoreError>;
3646
3666
  readonly insert: (table: string, row: Row) => Effect.Effect<Row, StoreError>;
3647
3667
  readonly insertMany: (table: string, rows: ReadonlyArray<Row>) => Effect.Effect<ReadonlyArray<Row>, StoreError>;
3648
- readonly patchJson: (table: string, primaryKey: string, path: string, value: unknown) => Effect.Effect<Row | null, StoreError>;
3668
+ readonly patchJson: (table: string, primaryKey: string, path: string, value: unknown, additionalPatch?: Readonly<Record<string, unknown>>) => Effect.Effect<Row | null, StoreError>;
3649
3669
  readonly update: (table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>) => Effect.Effect<Row | null, StoreError>;
3650
3670
  readonly updateMany: (table: string, patch: Readonly<Record<string, unknown>>, options: {
3651
3671
  readonly where: Predicate;
@@ -3654,10 +3674,7 @@ export declare interface EffectStoreOps {
3654
3674
  readonly deleteMany: (table: string, options: {
3655
3675
  readonly where: Predicate;
3656
3676
  }) => Effect.Effect<number, StoreError>;
3657
- readonly upsert: (table: string, row: Row, options: {
3658
- readonly conflictColumns: ReadonlyArray<string>;
3659
- readonly update?: ReadonlyArray<string> | ((existing: Row) => Readonly<Record<string, unknown>>);
3660
- }) => Effect.Effect<Row, StoreError>;
3677
+ readonly upsert: (table: string, row: Row, options: UpsertOptions) => Effect.Effect<Row, StoreError>;
3661
3678
  readonly insertIgnore: (table: string, row: Row, options: {
3662
3679
  readonly conflictColumns: ReadonlyArray<string>;
3663
3680
  }) => Effect.Effect<Row, StoreError>;
@@ -4826,11 +4843,11 @@ export declare interface FluentStore extends Omit<MutationStore, 'update' | 'del
4826
4843
  /** Fluent predicate update: `update('notes').where('id', id).set({...})`. */
4827
4844
  update(table: string): UpdateBuilder;
4828
4845
  /** Keyed update by primary key — returns the post-image. */
4829
- update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row_7 | null>;
4846
+ update(table: string, primaryKey: RowKey, patch: Readonly<Record<string, unknown>>): Promise<Row_7 | null>;
4830
4847
  /** Fluent predicate delete: `delete('notes').where('id', id).soft()`. */
4831
4848
  delete(table: string): DeleteBuilder;
4832
4849
  /** Keyed delete by primary key. */
4833
- delete(table: string, primaryKey: string): Promise<boolean>;
4850
+ delete(table: string, primaryKey: RowKey): Promise<boolean>;
4834
4851
  /**
4835
4852
  * Build an update patch from `input`, keeping ONLY the listed `keys` whose
4836
4853
  * value the caller actually PROVIDED (`!== undefined`). Collapses the
@@ -4955,6 +4972,10 @@ export declare const getResourcePolicy: (resourceType: string) => ResourcePolicy
4955
4972
  /** The registered row filter, or `undefined`. */
4956
4973
  export declare const getRowFilter: () => RowFilter<never> | undefined;
4957
4974
 
4975
+ /** Read the live server-owned connection for an opaque runtime clientId.
4976
+ * Missing for internal invocations and after transport teardown. */
4977
+ export declare const getRpcConnection: (clientId: number) => RpcConnection | undefined;
4978
+
4958
4979
  /**
4959
4980
  * Read the currently-registered process-wide handle (`undefined` when none
4960
4981
  * is registered — the state in which `runAsSystem` refuses).
@@ -5139,19 +5160,16 @@ export declare class InMemoryDataStore implements DataStore {
5139
5160
  query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Row>>;
5140
5161
  insert(table: string, row: Row): Promise<Row>;
5141
5162
  insertMany(table: string, rows: ReadonlyArray<Row>): Promise<ReadonlyArray<Row>>;
5142
- patchJson(table: string, primaryKey: string, path: string, value: unknown): Promise<Row | null>;
5143
- update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row | null>;
5144
- delete(table: string, primaryKey: string): Promise<boolean>;
5163
+ patchJson(table: string, primaryKey: RowKey, path: string, value: unknown, additionalPatch?: Readonly<Record<string, unknown>>): Promise<Row | null>;
5164
+ update(table: string, primaryKey: RowKey, patch: Readonly<Record<string, unknown>>): Promise<Row | null>;
5165
+ delete(table: string, primaryKey: RowKey): Promise<boolean>;
5145
5166
  updateMany(table: string, patch: Readonly<Record<string, unknown>>, options: {
5146
5167
  where: Predicate;
5147
5168
  }): Promise<number>;
5148
5169
  deleteMany(table: string, options: {
5149
5170
  where: Predicate;
5150
5171
  }): Promise<number>;
5151
- upsert(table: string, row: Row, options: {
5152
- conflictColumns: ReadonlyArray<string>;
5153
- update?: ReadonlyArray<string> | ((existing: Row) => Readonly<Record<string, unknown>>);
5154
- }): Promise<Row>;
5172
+ upsert(table: string, row: Row, options: UpsertOptions): Promise<Row>;
5155
5173
  insertIgnore(table: string, row: Row, options: {
5156
5174
  conflictColumns: ReadonlyArray<string>;
5157
5175
  }): Promise<Row>;
@@ -5583,7 +5601,7 @@ export declare class LinearScanMatcher implements Matcher {
5583
5601
  }
5584
5602
 
5585
5603
  /** Every connection the app declares, projected for ONE subject. */
5586
- export declare const listConnectionStates: (store: VaultStore, registry: ConnectionRegistry, subjectId: string) => Promise<ReadonlyArray<ConnectionState>>;
5604
+ export declare const listConnectionStates: (store: VaultStore, registry: ConnectionRegistry, subjectId: string) => Promise<ReadonlyArray<ConnectionState_2>>;
5587
5605
 
5588
5606
  export declare const listResourcePolicies: () => ReadonlyArray<ResourcePolicy>;
5589
5607
 
@@ -5817,6 +5835,12 @@ export declare const makeConnectionResolver: (deps: {
5817
5835
 
5818
5836
  export declare const makeConnectionsFacade: (deps: ConnectionsFacadeDeps) => ConnectionsFacade;
5819
5837
 
5838
+ /** Host seam shared by dev/serve and testing. Pass the mutation lifecycle's
5839
+ * existing afterCommit collector; rolled-back/retried bodies must discard it.
5840
+ * Outside a transaction writes are immediate. Values are server-owned; do not
5841
+ * put a client payload here as an authority claim. */
5842
+ export declare const makeConnectionState: (connection: RpcConnection, afterCommit?: (work: () => Promise<unknown>) => void) => ConnectionState;
5843
+
5820
5844
  /**
5821
5845
  * Factory the CLI binds into the plugin bind-ctx: closes over the
5822
5846
  * process-wide coordinator + replicaId so a plugin calls the ergonomic
@@ -5891,7 +5915,7 @@ export declare const makeIdleGate: (graceMs: number, now?: () => number) => Idle
5891
5915
  export declare const makeLazyWorkflowFacade: (resolve: () => WorkflowsAppContext | undefined) => WorkflowsAppContext;
5892
5916
 
5893
5917
  /** `__voltro.connections.list` — every declared connection, for the caller. */
5894
- export declare const makeListConnectionsExecutor: (deps: ConnectionBuiltinDeps) => (_input: Record<string, never>, ctx: ConnectionExecutorCtx) => Promise<ReadonlyArray<ConnectionState>>;
5918
+ export declare const makeListConnectionsExecutor: (deps: ConnectionBuiltinDeps) => (_input: Record<string, never>, ctx: ConnectionExecutorCtx) => Promise<ReadonlyArray<ConnectionState_2>>;
5895
5919
 
5896
5920
  /**
5897
5921
  * Build the replay codec from a mutation's `descriptor.output` Schema (call it at
@@ -6489,7 +6513,7 @@ export declare interface MutationRunnerDeps {
6489
6513
  export declare interface MutationStore extends DataStore {
6490
6514
  /** Bypass auto-soft-delete. Hard-removes the row regardless of
6491
6515
  * whether the table has `softDelete()` applied. */
6492
- hardDelete(table: string, primaryKey: string): Promise<boolean>;
6516
+ hardDelete(table: string, primaryKey: RowKey): Promise<boolean>;
6493
6517
  }
6494
6518
 
6495
6519
  export declare type NegotiatedEncoding = 'br' | 'gzip';
@@ -6499,6 +6523,25 @@ export declare type NegotiatedEncoding = 'br' | 'gzip';
6499
6523
  * (a client sending both accepts both). */
6500
6524
  export declare const negotiateEncoding: (acceptEncoding: string | undefined) => NegotiatedEncoding | undefined;
6501
6525
 
6526
+ /**
6527
+ * A fresh trace id for work that has NO incoming span to continue — a webhook
6528
+ * delivery, a schedule firing, a devtools invoke, a REST or gRPC entry point.
6529
+ *
6530
+ * 128 random bits as 32 lowercase hex, the shape `TRACEPARENT_RE` above accepts
6531
+ * and the shape a collector, a log pipeline or `voltro traces` can join on.
6532
+ *
6533
+ * The reason this exists as ONE function: every entry point that minted its own
6534
+ * had drifted somewhere different, and three of the shapes were not ids at all.
6535
+ * `incoming:<webhookId>`, `reaction:<workflow>` and the literal
6536
+ * `'webhooks:trigger'` are CONSTANTS — they were the caller's `source` label
6537
+ * copied into the id field, so every delivery a process ever handled shared one
6538
+ * trace and the waterfall merged unrelated work. `public-`/`grpc-`/`rest-` plus
6539
+ * eight base36 characters carried ~41 bits, which collides long before a busy
6540
+ * day is over. The entry-point kind is not lost by fixing this: it lives in the
6541
+ * `source` field beside the id, which is where it was already being written.
6542
+ */
6543
+ export declare const newTraceId: () => string;
6544
+
6502
6545
  /**
6503
6546
  * The backoff curve, extracted so it can be asserted directly — a schedule
6504
6547
  * that backs off wrongly is otherwise only visible as a latency an integration
@@ -7089,7 +7132,7 @@ export declare interface PreviewStore {
7089
7132
  /** Project a stored row into the wire shape. Deliberately total over the
7090
7133
  * columns the descriptor declares — and deliberately silent about the token
7091
7134
  * columns, which have no wire representation at all. */
7092
- export declare const projectConnectionState: (definition: ConnectionDefinition, row: AnyRow_2 | undefined) => ConnectionState;
7135
+ export declare const projectConnectionState: (definition: ConnectionDefinition, row: AnyRow_2 | undefined) => ConnectionState_2;
7093
7136
 
7094
7137
  /** Exact-match property filter. Values are AND'd. */
7095
7138
  export declare type PropertyFilter = Readonly<Record<string, string | number | boolean | null>>;
@@ -7646,6 +7689,17 @@ export declare interface RebacSubject {
7646
7689
 
7647
7690
  export declare const recordCoalesced: (label: string, collapsed: number) => void;
7648
7691
 
7692
+ /**
7693
+ * The verified expiry of the credential a connection authenticated with, in
7694
+ * unix SECONDS — recorded by the auth chain, read by `ConnectionInfo`.
7695
+ *
7696
+ * It lives here rather than being re-derived from headers because those are two
7697
+ * different facts once more than one auth shape exists. The header derivation
7698
+ * read the `voltro:session` cookie and nothing else, so an app on Bearer JWTs
7699
+ * had no bound at all while the docs said every realtime primitive had one.
7700
+ * The strategy that verified the token is the only place the expiry is known,
7701
+ * and this is how it reaches the consumer without a second derivation.
7702
+ */
7649
7703
  export declare const recordCredentialExpiry: (clientId: number, exp: number | undefined) => void;
7650
7704
 
7651
7705
  /** What the framework emit-seams (servePipeline / rpcServer / plugin wrappers)
@@ -7935,6 +7989,8 @@ export declare interface RegistryTableLike {
7935
7989
  * Distinct from `defaultValue` (literal landed in DDL).
7936
7990
  */
7937
7991
  readonly defaultFactory?: () => unknown;
7992
+ /** Application-side value used when an UPDATE omits this column. */
7993
+ readonly onUpdate?: 'now' | (() => unknown);
7938
7994
  /**
7939
7995
  * Computed field — re-evaluated on INSERT (AFTER defaults) and on
7940
7996
  * UPDATE (against the merged post-update row); the return value
@@ -7969,6 +8025,13 @@ export declare interface RegistryTableLike {
7969
8025
  readonly name: string;
7970
8026
  readonly fields: ReadonlyArray<string>;
7971
8027
  }>;
8028
+ /**
8029
+ * The columns of an EXPLICIT primary key (`.primaryKey([...])`). Absent or
8030
+ * empty for the ordinary table whose `id()` column carries the key. The
8031
+ * registry surfaces it so the keyed store forms can refuse a table they have
8032
+ * no single string to address — see `SchemaInfo.declaredPrimaryKey`.
8033
+ */
8034
+ readonly appliedPrimaryKey?: ReadonlyArray<string>;
7972
8035
  /**
7973
8036
  * Optional `effect/Schema` the framework decodes a row against
7974
8037
  * BEFORE every INSERT. Set via `table().validate(schema)`. The
@@ -8118,7 +8181,7 @@ export declare class ReplicatedDataStore implements DataStore {
8118
8181
  private routeWaitMode;
8119
8182
  insert(table: string, row: Row): Promise<Row>;
8120
8183
  insertMany(table: string, rows: ReadonlyArray<Row>): Promise<ReadonlyArray<Row>>;
8121
- patchJson(table: string, primaryKey: string, path: string, value: unknown): Promise<Row | null>;
8184
+ patchJson(table: string, primaryKey: string, path: string, value: unknown, additionalPatch?: Readonly<Record<string, unknown>>): Promise<Row | null>;
8122
8185
  update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Row | null>;
8123
8186
  delete(table: string, primaryKey: string): Promise<boolean>;
8124
8187
  updateMany(table: string, patch: Readonly<Record<string, unknown>>, options: {
@@ -8127,10 +8190,7 @@ export declare class ReplicatedDataStore implements DataStore {
8127
8190
  deleteMany(table: string, options: {
8128
8191
  where: Predicate;
8129
8192
  }): Promise<number>;
8130
- upsert(table: string, row: Row, options: {
8131
- conflictColumns: ReadonlyArray<string>;
8132
- update?: ReadonlyArray<string> | ((existing: Row) => Readonly<Record<string, unknown>>);
8133
- }): Promise<Row>;
8193
+ upsert(table: string, row: Row, options: UpsertOptions): Promise<Row>;
8134
8194
  insertIgnore(table: string, row: Row, options: {
8135
8195
  conflictColumns: ReadonlyArray<string>;
8136
8196
  }): Promise<Row>;
@@ -8933,6 +8993,15 @@ export declare interface RowHistoryEntry {
8933
8993
  readonly traceId?: string | null;
8934
8994
  }
8935
8995
 
8996
+ /** Server-owned lifetime, not a user identity or a client-supplied claim.
8997
+ * WebSockets retain it across calls; HTTP retains it only within one request.
8998
+ * Reconnects and server restarts receive a new id. */
8999
+ export declare interface RpcConnection {
9000
+ readonly id: string;
9001
+ readonly transport: 'websocket' | 'http';
9002
+ readonly signal: AbortSignal;
9003
+ }
9004
+
8936
9005
  export declare interface RpcServerOptions<Rpcs extends Rpc.Any> {
8937
9006
  readonly group: RpcGroup.RpcGroup<Rpcs>;
8938
9007
  readonly handlersLayer: Layer.Layer<Rpc.ToHandler<Rpcs>>;
@@ -9210,14 +9279,15 @@ export declare interface RuntimeContext {
9210
9279
  * build the outbound `traceparent` header for cross-api hops. */
9211
9280
  readonly spanId?: string;
9212
9281
  /**
9213
- * Per-connection identifier the rpc transport assigns to a
9214
- * WebSocket connection. Stable for the lifetime of that connection.
9215
- * Used by re-auth handlers (`auth.signin`) to write into the
9216
- * per-connection subject override map so subsequent calls on the
9217
- * same connection see the new subject. `undefined` for non-rpc
9218
- * call paths (workflows, smoke tests, internal invocations).
9282
+ * Opaque process-wide identifier, distinct across RPC transports/listeners.
9283
+ * Stable within a WebSocket or one HTTP request; never persist this number.
9284
+ * Re-auth handlers use it to bind a credential, not a resolved subject.
9285
+ * Undefined for non-RPC call paths (workflows, internal invocations).
9219
9286
  */
9220
9287
  readonly clientId?: number;
9288
+ /** Server-owned transport identity and close signal. Not authentication;
9289
+ * session membership remains an application's explicit access decision. */
9290
+ readonly connection?: RpcConnection;
9221
9291
  }
9222
9292
 
9223
9293
  /**
@@ -9635,6 +9705,8 @@ export declare interface SchemaInfo {
9635
9705
  * on every UPDATE against the merged post-update row.
9636
9706
  */
9637
9707
  readonly computedFields: ReadonlyMap<string, (row: Readonly<Record<string, unknown>>) => unknown>;
9708
+ /** Optional for custom registries; absent means no automatic update columns. */
9709
+ readonly onUpdateFields?: ReadonlyMap<string, 'now' | (() => unknown)>;
9638
9710
  /** The column marked `.version()`, when the table declares one. */
9639
9711
  readonly versionColumn?: string;
9640
9712
  /**
@@ -9671,11 +9743,24 @@ export declare interface SchemaInfo {
9671
9743
  readonly validatePatchSchema?: Schema.Schema.Any;
9672
9744
  /**
9673
9745
  * Resolved id-scheme for the table's primary-key column. `undefined`
9674
- * for tables that have no `id`-typed column (rare; framework tables
9675
- * always have one). MutationStore reads this to decide whether to
9676
- * auto-generate a string id or omit the column for the DB to allocate.
9746
+ * for tables that have no `id`-typed column — a natural or composite
9747
+ * primary key, see `declaredPrimaryKey`. MutationStore reads this to decide
9748
+ * whether to auto-generate a string id or omit the column for the DB to
9749
+ * allocate.
9677
9750
  */
9678
9751
  readonly idScheme?: IdScheme;
9752
+ /**
9753
+ * The columns of an EXPLICIT primary key (`.primaryKey(['userId','orgId'])`,
9754
+ * or one column for a shared key). Empty for the ordinary table whose `id()`
9755
+ * column carries the key.
9756
+ *
9757
+ * Read by the keyed store forms — `update(table, pk, patch)` and
9758
+ * `delete(table, pk)` address a row by a single string, which such a table
9759
+ * has nothing to match against. Before this was carried here they answered
9760
+ * `null` / `false`: the SAME values that mean "no such row", so a caller
9761
+ * could not tell a missing row from a call that can never find one.
9762
+ */
9763
+ readonly declaredPrimaryKey: ReadonlyArray<string>;
9679
9764
  }
9680
9765
 
9681
9766
  /**
@@ -9714,6 +9799,11 @@ export declare interface SchemaRegistry {
9714
9799
  * hook in `./storeMiddleware.ts`.
9715
9800
  */
9716
9801
  idScheme(table: string): IdScheme | undefined;
9802
+ /**
9803
+ * The columns of the table's EXPLICIT primary key, or an empty array when
9804
+ * its `id()` column carries the key. See {@link SchemaInfo.declaredPrimaryKey}.
9805
+ */
9806
+ declaredPrimaryKey(table: string): ReadonlyArray<string>;
9717
9807
  /**
9718
9808
  * Per-column app-side default factories for a table. Empty map if
9719
9809
  * none. Read by MutationStore.stampedForInsert to fill missing
@@ -11071,7 +11161,7 @@ export declare type TupleSource = (req: {
11071
11161
  export declare const TX_READ_VERSIONS_KEY: unique symbol;
11072
11162
 
11073
11163
  /**
11074
- * Drop everything this connection accumulated. Called by the WS-close finalizer
11164
+ * Drop everything this connection accumulated. Called by the transport finalizer
11075
11165
  * and by explicit sign-out flows. Idempotent.
11076
11166
  */
11077
11167
  export declare const unbindConnection: (clientId: number) => void;