@voltro/plugin-webhooks 0.84.0 → 0.85.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,555 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.85.0] — 2026-10-10
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **Scripted planner responses must use the strict JSON envelope** — `@voltro/plugin-ai-flows`, `@voltro/cli`
47
+
48
+ Since 0.84.0, the final agentic planner model response uses the closed `{ "json": "<serialized deliverable object>" }` envelope. Update scripted model responses, recorded fixtures and custom planner adapters to `JSON.stringify({ json: JSON.stringify(deliverable) })`; direct deliverable responses fail. The engine still returns and stores the original deliverable object. This corrects the earlier classification as a fix; the model response contract changed. `voltro update` reissues the review for applications already on 0.84.0.
49
+ - **CMS: experiments planned on a release, and version B for one section** — `@voltro/cms`
50
+
51
+ `planExperiment` binds an experiment to a draft or scheduled release that publishes its page: it stays `planned` until the release goes live, then starts on the revision the release published (replacing one still running there, `stopReason: 'replaced'`), and a rollback of that release stops it (`stopReason: 'rolledBack'`). The release runs this through `afterPublish` hooks (`ReleaseHook`, `PublishReleaseOptions`), which `publishRelease`, `publishDueReleases` and `rollbackRelease` now take — pass `experimentReleaseHook`. Version B may also replace a text prop of one placed component (`sections[<instanceId>].heading`; `experimentOverrideKeys` lists them). `Experiment.status` gains `'planned'` and `Experiment.startedAt` is `Date | null`.
52
+
53
+ **`voltro update` carries you across this** — codemod `0.85.0/04_cms-experiments-planned-with-a-release`. 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.85.0).
54
+ - **Live row patches omit unchanged ID order and reuse subscription indexes** — `@voltro/protocol`, `@voltro/client`, `@voltro/runtime`, `@voltro/api-client`, `@voltro/cli`, `@voltro/voltro`
55
+
56
+ `RowPatch.order` is now an ID array or `null`: `null` preserves the previous order, while arrays describe changed membership or ordering. Protocol version is 2. Upgrade APIs, browser clients and generated SDK runtimes together; custom consumers must handle `null` or use `applyRowPatch`. `voltro update` supplies the review instructions. Subscription-owned indexes avoid rebuilding the ID map for each delta. Waiting transports apply each delta once, including cell merges.
57
+ - **What Effect 4 changes without renaming anything** — `@voltro/runtime`, `@voltro/protocol`, `@voltro/workflow`, `@voltro/database`, `@voltro/client`, `@voltro/web`, `@voltro/testing`, `@voltro/logger`, `@voltro/data-transfer`, `@voltro/env`, `@voltro/plugin-sentry`, `@voltro/plugin-datadog`, `@voltro/plugin-logship`
58
+
59
+ `Schema.Date` is `DateFromSelf`: a string is no longer decoded, use `Schema.DateFromString` where one arrives. A layer used twice inside one layer graph builds once — per build, not per process; two separate `Effect.provide` calls are two builds (`Layer.fresh` opts out). `Equal.equals` is structural for plain objects and arrays, so `Data.struct` and `Data.array` are gone. An error class without a `message` field reports an empty message: give every `TaggedError` a `message` field or an `override get message()`. `Cause` is flat (`cause.reasons`), `yield* Option.x` is a type error (`Effect.fromOption`), `Effect.fork` binds the child to its parent and a daemon is `Effect.forkDetach`, a process stays alive while fibers are open, `Stream.async` is `Stream.callback`, `Config` is schema-based (`Config.String("NAME")`), `FiberRef` is `Context.Reference`, `FiberId` is a number in log and trace fields, and `Runtime<R>` is gone (`Effect.context<R>()`). A span without a parent reports `root: true`; a detached trace needs an explicit `parent`. `NumberFromString` decodes `"abc"` to `NaN` instead of failing — add `Schema.check(Schema.isFinite())` where you relied on the refusal. A production web image loads `effect` three times (start bundle, precompiled `app.config`, SSR bundle); the shipped `effect` patch keys the default `MetricRegistry` on the process, so the inspect endpoints and the Prometheus exporter see every copy's series. Effect 4 histogram buckets are the declared boundaries only; the catch-all is the sample's `count`. The codemod prints these as written steps against your project.
60
+
61
+ **`voltro update` carries you across this** — codemod `0.85.0/91_effect-4-semantics`. 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.85.0).
62
+ - **The framework runs on Effect 4** — `@voltro/ai`, `@voltro/api-client-effect`, `@voltro/api-client`, `@voltro/cache`, `@voltro/changelog`, `@voltro/client`, `@voltro/cli`, `@voltro/cms`, `@voltro/content`, `@voltro/database`, `@voltro/data-transfer`, `@voltro/datetime`, `@voltro/desktop`, `@voltro/devtools`, `@voltro/devtools-ui`, `@voltro/env`, `@voltro/i18n`, `@voltro/integration-http`, `@voltro/kv`, `@voltro/local-first`, `@voltro/logger`, `@voltro/mcp`, `@voltro/plan`, `@voltro/plugin-ai-flows`, `@voltro/plugin-analytics-postgres`, `@voltro/plugin-atlassian`, `@voltro/plugin-audit`, `@voltro/plugin-auth-auth0`, `@voltro/plugin-auth-clerk`, `@voltro/plugin-auth-kinde`, `@voltro/plugin-auth-oidc`, `@voltro/plugin-auth`, `@voltro/plugin-auth-social`, `@voltro/plugin-auth-supabase`, `@voltro/plugin-auth-workos`, `@voltro/plugin-billing`, `@voltro/plugin-broadcast`, `@voltro/plugin-cdc-out`, `@voltro/plugin-clickhouse`, `@voltro/plugin-comments`, `@voltro/plugin-datadog`, `@voltro/plugin-deactivation`, `@voltro/plugin-duckdb`, `@voltro/plugin-flags`, `@voltro/plugin-governance`, `@voltro/plugin-licensing`, `@voltro/plugin-logship`, `@voltro/plugin-mail`, `@voltro/plugin-moderation`, `@voltro/plugin-multitenancy`, `@voltro/plugin-notifications`, `@voltro/plugin-openapi`, `@voltro/plugin-postgis`, `@voltro/plugin-posthog`, `@voltro/plugin-presence`, `@voltro/plugin-prometheus`, `@voltro/plugin-queue`, `@voltro/plugin-ratelimit`, `@voltro/plugin-rbac`, `@voltro/plugin-releases`, `@voltro/plugin-row-history`, `@voltro/plugin-scim`, `@voltro/plugin-search`, `@voltro/plugin-sentry`, `@voltro/plugin-soft-delete`, `@voltro/plugin-sso-saml`, `@voltro/plugin-storage`, `@voltro/plugin-tinybird`, `@voltro/plugin-webhooks`, `@voltro/protocol`, `@voltro/react-native`, `@voltro/runtime`, `@voltro/serverless`, `@voltro/shared`, `@voltro/sql-mssql`, `@voltro/sql-mysql`, `@voltro/sql-postgres`, `@voltro/sql-sqlite`, `@voltro/sql-turso`, `@voltro/testing`, `@voltro/ui`, `@voltro/ui-shadcn`, `@voltro/voltro`, `@voltro/web`, `@voltro/workflow`
63
+
64
+ Every package depends on `effect` 4.0.1 exactly, and the packages that merged into it (`@effect/platform`, `@effect/sql`, `@effect/rpc`, `@effect/cluster`, `@effect/workflow`, `@effect/experimental`) are modules of `effect` now: `effect/sql`, `effect/rpc`, `effect/cluster`, `effect/workflow`, `effect/http`. Public types render in Effect 4 terms — `Schema.Codec` instead of `Schema.Schema`, `Context.Service` instead of `Context.Tag`, `Result` instead of `Either`, `Schema.optional` instead of `Schema.optionalWith`. Migration: pin `effect` to 4.0.1, remove the merged packages from your dependencies, keep your SQL driver (`@effect/sql-sqlite-node`, `@effect/sql-pg`, `@effect/sql-mysql2` or `@effect/sql-mssql`) at 4.0.1, and add `ESNext.Disposable` to your tsconfig `lib`. The codemod rewrites imports, renamed members and the constructors that take an array now (`Schema.Union([A, B])`, `Schema.Literals(['a', 'b'])`, `Schema.Record(key, value)`, `Context.Service<Self, Shape>()(id)`, `Schema.check(Schema.isMinLength(1))`), and leaves a `// TODO(voltro-migration):` line above every site that needs a decision.
65
+
66
+ **`voltro update` carries you across this** — codemod `0.85.0/90_effect-4`. 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.85.0).
67
+ - **Histogram boundaries are plain arrays and labels are attributes** — `@voltro/runtime`, `@voltro/plugin-prometheus`, `@voltro/plugin-datadog`
68
+
69
+ `histogramMetric` takes a `ReadonlyArray<number>`; `@voltro/runtime` exports `exponentialBoundaries` and `linearBoundaries` instead of re-exporting `MetricBoundaries`. Effect 4's `linearBoundaries` builds `start + i * width` and drops non-positive values, which is not the series `MetricBoundaries.linear` produced — rebuild your boundaries explicitly where the exact buckets matter. Labels are set with `Effect.provideService(Metric.CurrentMetricAttributes, { ... })` instead of `Effect.tagMetrics`; a snapshot is `{ id, type, description, attributes, state }`.
70
+
71
+ **`voltro update` carries you across this** — codemod `0.85.0/91_effect-4-semantics`. 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.85.0).
72
+ - **Postgres workflow runners lock shards in a new key space — no rolling deploy** — `@voltro/workflow`, `@voltro/sql-postgres`, `@voltro/cli`
73
+
74
+ Effect 4's cluster storage takes each postgres shard lock as a two-key advisory lock under its own namespace; Effect 3 took one `bigint` key. A runner on 0.84 and a runner on 0.85 against the same database therefore cannot see each other's locks and could both own a shard. Stop every runner of the old version before the first 0.85 runner starts; a rolling deploy across this version is not safe on postgres. MySQL and MariaDB keep their lock names. The stalled-run diagnostics (`run-stalled`, the cluster panel) read the new key space, so a held shard names its holder again.
75
+
76
+ **`voltro update` carries you across this** — codemod `0.85.0/91_effect-4-semantics`. 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.85.0).
77
+ - **Authorize tenant-private browser upload policies** — `@voltro/protocol`, `@voltro/client`, `@voltro/plugin-storage`, `@voltro/cli`
78
+
79
+ `useUpload` accepts an `access` policy for private uploads. The server authorizes requests and visibility through an Effect `uploadAccess` function before issuing a ticket or accepting the direct base64 upload RPC; an unconfigured or static default rejects requested policies. The approved policy is signed into through-app, presigned, resumable and multipart tickets and kept through finalization. A tenant-scoped read policy lets authorized colleagues use one stored document without publishing it or copying its bytes to another ref.
80
+
81
+ Custom ticket issuers must pass `access` explicitly to share private uploads; receivers no longer apply a mutable server default. Previously issued upload tokens are refused. Coordinate API rollout and restart unfinished upload sessions; existing stored objects and grants remain intact. The upgrade note covers custom issuers and cleanup of abandoned staged uploads.
82
+
83
+ Private tenant-owned objects now refuse a caller in another tenant before owner, policy or explicit grant checks, including an owner who switches tenant. Custom access-check subjects must carry their verified current tenant. Global references retain policy checks; public objects remain publicly readable.
84
+
85
+ **`voltro update` carries you across this** — codemod `0.85.0/03_storage-upload-ticket-access`. 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.85.0).
86
+ - **Bound reaction admission work and batch reference cleanup** — `@voltro/runtime`, `@voltro/cli`
87
+
88
+ Row-set ReBAC filtering prepares relation tuples per invocation while keeping tenant checks and grant revocation fresh. Reaction rate admission uses a transaction-owned bucket cursor and unique slots, preserving fleet caps without restarting slot searches; active windows remain protected from retention cleanup. Custom `ReactionRunDeps` implementations replace `claimRateSlot` with `claimRate(key, limit, expiresAt)`. Custom `PluginRefStore` adapters use predicate-scoped `updateMany`/`deleteMany`; normal reference cleanup batches writes, retains soft-delete and tenant semantics, and drains bounded concurrent work at shutdown. The local fallback avoids copying saturated timestamp windows and reclaims idle partitions using their last declared window.
89
+
90
+ Rate keys use SQL-safe JSON encoding, also supporting PostgreSQL and embedded delimiters. Before changing from the former encoding, stop reaction producers and wait at least the longest configured rate window; upgrade the fleet together after applying the normal cursor-column migration. Mixed encodings must not run in an active window.
91
+
92
+ **`voltro update` carries you across this** — codemod `0.85.0/08_reaction_rate_and_reference_cleanup`. 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.85.0).
93
+ - **Completed memory streams have an explicit replay window** — `@voltro/ai`, `@voltro/cli`
94
+
95
+ Completed memory-stream payloads expire after seven days by default. Set a longer finite `retentionMs` when needed. Active readers defer cleanup; producer-election tombstones remain until the owner disposes the permanently non-reusable store. Replay reads are paged, local persistence wakes readers, remote polling backs off, natural source chunks can persist in bounded bulk writes, and done-aware collection uses bulk deletes. Custom stores may implement `appendMany` and honor the optional read limit.
96
+
97
+ Redis replay verifies list positions and falls back to bounded filtering after sequence gaps, preserving later events and terminal outcomes.
98
+
99
+ **`voltro update` carries you across this** — codemod `0.85.0/07_memory-stream-retention`. 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.85.0).
100
+ - **Fetch bounded SCIM pages and aggregate totals** — `@voltro/plugin-scim`, `@voltro/cli`
101
+
102
+ SCIM lists now fetch a bounded, id-ordered page and a count aggregate instead of loading every matching user or group. HTTP lists default to 100 rows and cap requested counts at 1000; count zero still returns the full matching total. Custom ScimStore adapters must implement pageUsers and pageGroups, returning rows and totalResults. The update codemod provides the required migration steps.
103
+
104
+ **`voltro update` carries you across this** — codemod `0.85.0/06_scim-store-pages`. 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.85.0).
105
+ - **Store transactions share one key across every client and driver** — `@voltro/database`, `@voltro/sql-sqlite`, `@voltro/sql-postgres`, `@voltro/sql-mysql`, `@voltro/sql-mssql`, `@voltro/sql-turso`, `@voltro/cli`
106
+
107
+ Effect 4 keys the transaction connection per `SqlClient` instance, so a statement through a second client (a plugin runtime, a second layer build) would run outside the store's transaction. `@voltro/database` exports `SHARED_TRANSACTION_SERVICE`, every dialect layer passes it to its driver, and the drivers for sqlite-node, mysql2 and mssql accept it through pnpm patches. `voltro create-project` writes the patch files and `patchedDependencies` entries into the workspace root it creates; an existing project runs `voltro add patches` once (the `effect` patch plus the driver patch of every dialect its apps declare) and commits the `patches/` files and entries — a framework package install alone cannot apply them, and `voltro dev` and `voltro serve` warn while they are missing. The sqlite driver opens transactions deferred instead of `BEGIN IMMEDIATE` (two connections in one process no longer deadlock), the postgres client runs unnamed statements (prepared statements went stale after `ALTER TABLE`) and opens its `LISTEN` connection outside the pool as the boot line promises, and `xid`, `xid8` and `pg_snapshot` columns decode. `validatePatch` takes a `Schema.Struct` only; `extractDbCause` and `causeBranches` take an `Exit` or `Cause`; `database/sql` exports `splitStatements` for multi-statement bootstrap SQL and `onTransaction` for work scoped to the surrounding transaction; the postgres type registry moved from `@voltro/database` to `@voltro/sql-postgres`.
108
+
109
+ **`voltro update` carries you across this** — codemod `0.85.0/91_effect-4-semantics`. 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.85.0).
110
+ - **Test runners take no abort signal and the test clock moves** — `@voltro/testing`
111
+
112
+ `runPromise` and `runPromiseExit` from `@voltro/testing/client` follow the Effect 4 `ManagedRuntime` signature and take no `signal` option; interrupt the fiber instead. `TestClock` comes from `effect/testing/TestClock`, `TestContext` is `Layer.mergeAll(TestConsole.layer, TestClock.layer())`, and the test event bus reports a `Result` (`Result.isFailure`, `.failure`, `.success`).
113
+
114
+ **`voltro update` carries you across this** — codemod `0.85.0/91_effect-4-semantics`. 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.85.0).
115
+ - **Schema.Uint8Array no longer decodes a number array** — `@voltro/protocol`
116
+
117
+ Under Effect 4 `Schema.Uint8Array` is the identity codec: it accepts a `Uint8Array` and nothing else, so a procedure field declared with it no longer accepts the number array a JSON client sends. Declare binary fields as `Schema.Uint8ArrayFromBase64` — the form the framework already uses for binary columns — or spell out a number-array transformation with `Schema.decodeTo`. The codemod marks every `Schema.Uint8Array` with a `// TODO(voltro-migration):` line.
118
+
119
+ **`voltro update` carries you across this** — codemod `0.85.0/91_effect-4-semantics`. 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.85.0).
120
+ - **RPC exits carry a reason list and void payloads are null on the wire** — `@voltro/protocol`, `@voltro/runtime`, `@voltro/client`, `@voltro/web`, `@voltro/api-client`, `@voltro/api-client-effect`, `@voltro/cli`, `@voltro/plugin-openapi`
121
+
122
+ An encoded exit's `cause` is a list of reasons (`[{ _tag: 'Fail', error }]`, `Die`, `Interrupt`) instead of a cause tree, an unroutable tag answers as an exit with a `Die` reason, `Schema.Void` input is `null` instead of `{}`, and a stream rpc folds its error into the stream schema (`rpc.errorSchema` is `Never`; read `success`/`error` from the stream schema). Shape digests are computed from the Effect 4 AST, so every client logs the drift warning once after the upgrade; decoding error texts read `Missing key` and `is unexpected`. `formField()` returns `{ voltroField }` instead of `jsonSchema["x-voltro"]`, the `ParseResult` type re-export is `SchemaError`, and search-param defaults are `X.pipe(Schema.withDecodingDefaultType(Effect.succeed(d)))`. A raw `effect/rpc` client keeps a dotted tag as one flat key (`client['todos.list'](...)`). JSON Schema output (OpenAPI, SDK generation, form views) emits plain `number` and closed objects; generated SDK clients expose `Context.Service<ApiClient, ApiClientService>()` with a `Layer.effect`.
123
+
124
+ **`voltro update` carries you across this** — codemod `0.85.0/91_effect-4-semantics`. 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.85.0).
125
+ - **Workflow definitions are callable and journal exits are reason lists** — `@voltro/workflow`, `@voltro/cli`, `@voltro/plugin-webhooks`
126
+
127
+ A `workflow()` definition is a function in Effect 4: a `typeof x === 'object'` guard or an object spread drops it silently (no discovery, the body never runs). Test with `isCarrier` or the exported type id, never by shape. A journal entry's exit is the exported `JournalExit` with a reason list, `failureMessage` renders it, and `poll` answers an `Option`. `DurableRateLimiter` has no Effect 4 counterpart: `rateLimit()` consumes `effect/persistence/RateLimiter`, which the boot layer provides; `RateLimitOptions` is exported. Cluster routing sends a message to the runner that registered the entity type, so a runner booted without the workflow layer no longer fails the dispatch — it never sees it.
128
+
129
+ **`voltro update` carries you across this** — codemod `0.85.0/91_effect-4-semantics`. 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.85.0).
130
+
131
+ ### Added
132
+
133
+ - **CMS: AI candidates for whole documents, adopted unit by unit** — `@voltro/cms`
134
+
135
+ `proposeCandidate` keeps a model's proposal for several fields beside the draft, with the revision it was made against and the AI operation behind it; `candidateUnits` shows it as units (a field, a changed section, an added section); `adoptCandidate` saves any subset as a new draft revision and settles the operation as applied; `discardCandidate` and `candidatesFor` complete it. Nothing publishes by itself. Register `cmsAiCandidatesTable`.
136
+ - **CMS: bulk into a release, and bulk runs that continue where they stopped** — `@voltro/cms`
137
+
138
+ `BulkAction` gains `addToRelease`: each ready item's pinned revision goes into one draft release (`releaseClosed` once it is not a draft). `startBulkRun` records a run (`cmsBulkRunsTable`); `continueBulkRun` does the next items one by one, saving each result before the next, and continues at the first item without one after an interruption; `unfinishedBulkRuns` and `bulkRunState` read runs.
139
+ - **Cache identity compiler for delivery behind shared caches** — `@voltro/cms`
140
+
141
+ `compileCacheIdentity(contract, request, { checkpoint, tags })` computes one canonical cache identity for a delivery answer — typed, sorted, allowlisted query parameters; bounded locale, audience and consent dimensions; the publication checkpoint — and from the same computation the `Vary` header, the purge selector and an explanation. Requests carrying a credential, writes, and paths without a single canonical form are never shared-cacheable; a contract past its `cardinalityBudget` is refused. `edgeHeaders(provider, identity, lifetime)` writes the headers for Cloudflare (`Cache-Tag`), Fastly (`Surrogate-Key`) or any `Cache-Tag` cache; `sharedCacheable` re-checks the origin's answer.
142
+ - **CMS: collections and brand portals with capped external downloads** — `@voltro/cms`
143
+
144
+ `createCollection`, `addToCollection`, `removeFromCollection`, `collectionItems` and `listCollections` keep curated sets of documents. `createPortal` makes a link to one (only the token's hash is stored, expiry ≤ 90 days, optional `maxDownloads`); `resolvePortal` and `recordPortalDownload` serve it to someone without an account — the count is a conditional update, so concurrent downloads never pass the cap. `revokePortal` ends a link at once. Register `cmsCollectionsTable`, `cmsCollectionItemsTable` and `cmsCollectionPortalsTable`.
145
+ - **CMS: components say what they are for; the picker finds them by job** — `@voltro/cms`
146
+
147
+ `defineComponent` accepts `description` and `jobs` (the editorial jobs a component serves). `<ContentForm>`'s component picker groups by job, searches name, description and jobs, says when nothing matches and shows the chosen component's description. New form strings: `findComponent`, `noComponentFound`, `otherComponents`. Neither field is part of the component's `fingerprint`.
148
+ - **CMS: component slots, variants, fixtures and migrations** — `@voltro/cms`
149
+
150
+ A component prop may hold components (a slot): every level gets stable `instanceId`s and fingerprints on save. `defineComponent` accepts `variants` (named presets the picker offers as "Card — compact"), `fixtures` (example props, checked against the component's props when a content type uses it — `componentExampleViolations`) and `migrations` keyed by an earlier definition's fingerprint (`componentFingerprint`), applied on save. `staleComponents(type, values)` lists instances whose definition changed with no migration from them.
151
+ - **CMS: consent vendors and the reader's choice as a browser-enforced policy** — `@voltro/cms`
152
+
153
+ `ConsentPurpose.vendors` names the third parties a purpose lets in — `id`, `origins`, `storage`, `dataUse` — separately from the purpose. `consentContentSecurityPolicy` turns a reader's choices into a Content-Security-Policy whose `frame-src` admits only this origin and the granted vendors (set it from a web middleware); `consentedOrigins` and `checkConsentPurposes` (refuses an origin that is not exactly `https://host`) go with it. Exported from `@voltro/cms` and `@voltro/cms/web`.
154
+ - **CMS: cutover readiness — issues, adoption, criteria, go/fallback ledger** — `@voltro/cms`
155
+
156
+ `evaluateCutover` measures criteria against thresholds; `recordCutoverIssues` / `resolveCutoverIssue` keep a migration source's open issues until each is closed with a reason; `cutoverAdoption` counts who saved revisions of migrated documents since import; `decideCutover` records `go` (refused while a criterion fails) or `fallback` with the criteria as they stood (`cutoverDecisions`). Register `cmsCutoverIssuesTable` and `cmsCutoverDecisionsTable`.
157
+ - **CMS: delivery checks — a publish is done when its public address shows it** — `@voltro/cms`
158
+
159
+ `requestDeliveryCheck` records, per publish or take-down, the public address that must now answer differently; `runDeliveryChecks` reads pending addresses through a reader function the app supplies (through any CDN, without credentials) and marks each `observed` when the address shows the revision (or `gone`) or `stale` after the deadline; `deliveryChecks` lists them. A newer change supersedes a pending check. Register `cmsDeliveryChecksTable`.
160
+ - **CMS: design tokens and actions as component props** — `@voltro/cms`
161
+
162
+ `defineDesignTokens` declares the token groups a design system offers; `Schema.Token(tokens, group)` is a prop that names one of them and `Schema.Action({ id: label })` one that names an action the renderer implements. Both are closed sets checked at save like `Schema.Literal`, and the form shows what each value means (`FieldSchema.literalLabels`). Exported from `@voltro/cms` and `@voltro/cms/web`.
163
+ - **A download block in rich text, its file from the media library** — `@voltro/cms`
164
+
165
+ A rich-text field that names an asset type for files (`Schema.RichText({ files: 'asset' })`) takes a download block: `{ type: 'file', asset, label }` — a file from the media library and the text its link shows (required). It is offered in the editor only where the field allows files and the app can pick one, through the same picker (and selection intent) as an image block; it is an edge to its asset, its link text is a translation unit, and `RichDocumentView` shows it as a download through `fileHrefOf(asset)` — left out when the file is not live, never a dead link. A profile's default blocks now include `file`.
166
+
167
+ A `RichDocumentPicker` learns what its choice is for (`role`: `image`, `file`, `link`, `contact`), so one picker for the asset type can open the media library for images or for files; a `SelectionIntent` carries the wanted `kind` of the picked type (`image`, `file`) for the tool to show only those.
168
+ - **edgePurgeRequests purges pages by address too** — `@voltro/cms`
169
+
170
+ `edgePurgeRequests(provider, config, tags, urls?)` takes the addresses of answers that carry no tags — the HTML pages a reader gets — beside the tags: Cloudflare purges them as `files` (30 a call), Fastly with one URL purge each, and a webhook receives `{ tags, urls }`. An address that is not an absolute http(s) URL is left out. Calls without `urls` are unchanged.
171
+ - **Purge requests for Cloudflare, Fastly or a webhook, by cache tag** — `@voltro/cms`
172
+
173
+ `edgePurgeRequests(provider, { target, token }, tags)` builds the requests a CDN takes to drop every answer carrying the tags — Cloudflare's tag purge (30 tags per call, larger sets split), Fastly's surrogate-key purge, or an `https` webhook taking `{ tags }`. Nothing is sent; tags a header could not carry are left out.
174
+ - **CMS: independent copies with their source and revision** — `@voltro/cms`
175
+
176
+ `copyDocument` makes a new document from another's current draft — every value, rights and references included — and records the source and the revision it was made from (`cms_copies`); `copyOrigin` and `copiesOf` read it back. A copy goes its own way afterwards, unlike a reference. Register `cmsCopiesTable`.
177
+ - **CMS: masked inheritance, effective values and their origin, release pins** — `@voltro/cms`
178
+
179
+ An inherited tree field may name a `mask` — an optional Boolean field that hides the binding on purpose for the page and the pages below (`saveDraft` refuses a page that sets both: `maskedAndSet`; a locked field has no mask). `inheritedValue` now also answers `how` (`own`, `ancestor`, `masked`, `none`) and takes the `mask`; `effectiveInheritance` answers every inherited field of a page with its origin, lock, mask and what a reset brings back. `pinReleaseInheritance` records on a release item what the page inherited as the release went live (new nullable `inherited` column on `cms_release_items`).
180
+ - **CMS: licence terms — from, territories, channels, purposes; rights ending soon** — `@voltro/cms`
181
+
182
+ `availability` accepts `from`, `territories`, `channels` and `purposes`. A right that has not started yet refuses the publish (`notYet`); `unavailableAmong(store, ids, scope, now, { territory, channel, purpose })` withholds a document licensed elsewhere (`territory`, `channel`, `purpose`), checking only what the caller names. `expiringAmong(store, scope, days)` lists rights that end soon. `ReferenceUnavailable`'s `reason` gains the new values.
183
+ - **A rich text's link picker learns its block; visual selection as offsets** — `@voltro/cms`
184
+
185
+ A paragraph's link picker now receives `target: { field, blockId }` like an image block's picker, so a link target can be chosen in another tool through a selection intent. `textSelectionIn(element)` reads the selected text of a block's visual surface as the character offsets a selection intent's `target.selection` takes. Each block's group carries `data-cms-block-id`.
186
+ - **CMS: migration stages with owners and exit gates, record ledger, cutover plan** — `@voltro/cms`
187
+
188
+ A migration runs through `MIGRATION_STAGES` (inventory, mapping, dry run, reconciliation, delta, cutover, fallback, editor acceptance): `setStageOwner` names an owner, `passStage` passes an exit gate — only the owner, with a note and optional evidence link, in order, and while the stage's measured `gate` holds — and `reopenStage` reopens a stage and every later one. `recordMigrationRecords` keeps one ledger row per source record (target, transformation, conflict, loss, result; reruns update, never double) and `migrationLedger` counts and lists them. `decideCutover` now needs a `CutoverPlan` for `go` — freeze or dual-write window, routing, redirects, cache purge and a rollback window of at most `MAX_ROLLBACK_DAYS` — and refuses with `noPlan` (`cutoverPlanProblem` names why). Register `cmsMigrationStagesTable` and `cmsMigrationRecordsTable`; `MIGRATION_STAGES` is also exported from `@voltro/cms/web`.
189
+ - **CMS: quality rules form policy packs per site and language; checks see when** — `@voltro/cms`
190
+
191
+ A quality rule may name its `pack` and the `sites` / `locales` it applies in; elsewhere it neither finds nor blocks, and no exception can be asked for it there. `check(values, { now, scope })` receives when and where it runs, so a claim whose evidence has run out blocks from that day on. `qualityFindings(type, values, { now, scope })` takes that context optionally (without it every rule is checked now, as before), and findings carry their `pack`. `ruleApplies(rule, scope)` is exported.
192
+ - **CMS: typed presence anchors** — `@voltro/cms`
193
+
194
+ `PresenceAnchor` names where someone is — a view, a field, a placed component, one of its props (a slot), a selection or an image region — without any text or time. `presenceAnchorOf` resolves a focused `ContentForm` control to its anchor (the form now marks each placed component with `data-cms-instance`), and `parsePresenceAnchor` accepts another client's anchor only in the declared shapes. Both are also in `@voltro/cms/web`.
195
+ - **CMS: reference graph per perspective and revision; replace references everywhere** — `@voltro/cms`
196
+
197
+ `documentGraph` explains a document both ways in a perspective — published, drafts or one revision as saved: what it references and which components it places (`componentsOf`), what uses it (live edges, or drafts via `draftUses`), its address and former addresses, and the releases carrying it. `revisionsReferencing` lists every saved revision that pointed at a document. `planReferenceReplacement` and `replaceReferences` point every draft that uses a document at another one of the same type — a new draft revision each against the planned revision, in its own transaction (call it from an action or a schedule, not inside a mutation), a result per draft with its detail, nothing published; `withReferenceAt` is the pure path step.
198
+ - **A release's checkpoint across the addresses of its entries** — `@voltro/cms`
199
+
200
+ `releaseCheckpoint(store, releaseId)` answers whether a published release reached readers: for each entry, the delivery check requested when the release went out — a publish answering with the revision the release pinned, a take-down no longer answering — with `reached`, `checked` and `complete`. An entry without a public address is named `unchecked` and is not counted as reached.
201
+ - **CMS: reviewer suggestions on a workflow step, and overdue steps for reminders** — `@voltro/cms`
202
+
203
+ `suggestOnStep` records a reviewer's suggested change on an open workflow step (decision `suggested`, with the `candidateId` of a candidate the author adopts or leaves); candidates carry `suggestedBy` for a person's suggestion. `overdueSteps(store, since, now)` lists open steps that fell due in a window, for reminders a schedule sends.
204
+ - **CMS: review workflows as a versioned decision graph** — `@voltro/cms`
205
+
206
+ `review.workflow` (`defineReviewWorkflow`) turns a type's review into steps with an approver scope, `after` dependencies, a `when` condition, an SLA and an evidence requirement. `startWorkflow` pins an instance to the exact revision and workflow version; `decideStep` (approve, request changes with a note) and `delegateStep` record decisions; `workflowState` / `latestWorkflow` derive each step's status, due time and overdue flag. The publish gate then needs an instance that approved the revision (`approvedByWorkflow`). Register `cmsWorkflowInstancesTable` and `cmsWorkflowDecisionsTable`.
207
+ - **Rich-text field profiles, components in text, track changes, typing together** — `@voltro/cms`
208
+
209
+ `defineRichTextProfile({ name, version, blocks, headings, marks, styles, components, contactPoints, contactLiterals })` narrows what a `Schema.RichText({ profile })` field holds. A save refuses anything outside the profile with the rule `profile`, and the stored document records `profile: '<name>@<version>'`. Without a profile a field behaves as before.
210
+
211
+ - Named styles from one design-token group on paragraphs, headings, lists and runs of text (`style`, markup `{name|text}`), rendered as `data-style`. - Components between the blocks (`type: 'component'`), checked, stamped and migrated like placed ones, translated through XLIFF, and edited with the same props form. - Contacts in text (`type: 'contact'`): a published contact point recorded as a reference edge, or a one-off e-mail address or phone number checked on save (`contactLiteralProblem`, `contactLiteralHref`). `RichDocumentView` takes `contactHrefOf` and `renderComponent`; `richDocumentFromHtml(html, { contacts: true })` keeps `mailto:` and `tel:` links.
212
+
213
+ Track changes in rich text: `trackChanges(base, edited, { id, by, at })` turns an edited copy into suggestions inside the document — deleted and inserted runs, inserted and deleted blocks, by whole words — and names blocks that already hold someone's suggestions as conflicts. `resolveChanges(doc, decide)` accepts or rejects them one by one, `pendingChanges` lists them, and `publishViolations` refuses a document with suggestions left (rule `pendingChanges`). `RichDocumentView` marks them with `<ins>` / `<del>`; the editor shows a block with suggestions read-only with Accept and Reject per change.
214
+
215
+ Typing together in a rich text: every paragraph, heading, list and table of an open draft is a CRDT text of its markup under `<field>#<blockId>` (`richBlockTextKey`, `sharedRichBlocks`). A collaborative `saveDraft` writes each merged text into its block (`withSharedBlocks`), a draft that moved on only in those blocks is no conflict (`changedApartFromShared`), and a block added in the session is shared once saved. `blockMarkup` / `withBlockMarkup` convert a block to and from its markup. The visual text surface keeps the caret in place when someone else's edit arrives.
216
+ - **Selection intents for a component's prop and for a draft not saved yet** — `@voltro/cms`
217
+
218
+ A `SelectionTarget` can name a placed component's prop (`instanceId` + `prop` in a components field), so a hero's image is chosen in the media library like an image block's. Before the first save there is no document to pin: the target names the open form instead (`formId`, exactly one of it or `documentId` + `revisionId`), and `applySelection(values, intent, answer, { formId })` applies the answer only to that form. Answers for saved drafts are checked against document and revision as before.
219
+
220
+ `ContentForm` hands every widget a `target` — the field, and for a prop of a placed component its `instanceId` and `prop` (`WidgetTarget`; a nested props form gets it through `widgetTarget`) — so a picker widget can build that intent. A component placed in the editor gets its instance id at once (`cins_…`, kept by the save), so it can be aimed at before the first save.
221
+ - **Resource selection intents: choose in the tool that owns the resource** — `@voltro/cms`
222
+
223
+ `encodeSelectionIntent` / `decodeSelectionIntent` / `parseSelectionIntent` carry what to pick, where the choice goes (a field, an image block, a selected text for a link), on which revision and where to return, between the editor and the tool that owns the resources (a media library, a page tree). `applySelection` puts the answer there and refuses one for another intent, another document, a revision that moved on, or a block that is gone. A `RichDocumentPicker` receives `target: { field, blockId }`.
224
+ - **CMS: site domains verified by a DNS TXT record; a primary domain** — `@voltro/cms`
225
+
226
+ `addDomain(tx, site, host)` records a host pending, with the TXT record that proves ownership (`_voltro-verify.<host>` → `voltro-verify=<token>`); `verifyDomain(tx, id, resolveTxt)` looks it up through a resolver the caller passes; only a verified domain becomes the site's primary (`setPrimaryDomain`, `primaryDomain`); the primary is not removed. A host belongs to one site anywhere. Register `cmsSiteDomainsTable`.
227
+ - **CMS: templates rule regions, widths and nesting depth** — `@voltro/cms`
228
+
229
+ A `PageTemplate` may now rule other `Schema.Components` fields of its type (`regions`: `allowed`, `required`, `maxItems`), the values a component's width prop may take (`widths`, prop named by `TemplatesConfig.width`, default `width`) and how deep components nest in slots (`maxDepth`). `templateViolations` names each break at its field; `checkTemplates` refuses a region that is not another components field or names a component it does not allow.
230
+ - **A translation export names what it does not carry** — `@voltro/cms`
231
+
232
+ `translationLosses(fields, values)` names what an XLIFF exchange of a document's values leaves out or flattens: a block with suggested changes (its text goes out as written and the translation comes back without suggestion marks) and a component whose definition the field no longer declares (its text is not exported). Show it with the export instead of losing it silently.
233
+ - **CMS: metrics as versioned definitions, drillable to revision and release** — `@voltro/cms`
234
+
235
+ `defineMetric` declares a number with its formula, unit, scope, freshness, quality limits and drill targets under an id and a version; its `fingerprint` follows the meaning, and `metricVersionDrift` names a definition whose meaning changed under an unchanged version. `revisionTimeline` reads when each revision of a document was live (and which release took it live) from the change feed, and `attributeToRevisions` gives per-day values to the revision live at the end of each day. The pure parts are also in `@voltro/cms/web`.
236
+ - **Comments: thread kinds, status policy, visibility, retention and status history** — `@voltro/plugin-comments`
237
+
238
+ A thread now has a `kind` — `discussion` (default), `note` or `review` — set on `comments.create`. Every status change (opening, resolve, reopen, with actor, time and an optional `reason`) is appended to the new `_voltro_comment_thread_events` table and returned as the thread's `history`; resolving tidies the open view and removes nothing. `statusPolicy` decides who may resolve or reopen which thread (`CommentStatusRefused` otherwise), and `visibleTo` hides threads — editorial notes for the team only — from `comments.list`, replies and status changes. `applyCommentRetention` removes the comments of threads resolved longer than their kind's retention, keeping the thread and recording the removal in its history. `useComments().resolve` takes a reason and `create` a kind.
239
+ - **An isr page can render per variant, and send a request past the shared cache** — `@voltro/cli`
240
+
241
+ A page with `renderMode: 'isr'` may export `cacheVary: { cookies, key }`. `cookies` names the cookies its shared render may see (beside `voltro:locale`; nothing else of the jar does); `key(cookies, headers)` maps a request to a variant, which joins the cache key (`|variant=<v>`), or answers `null` — then that request is rendered for itself as `ssr`, with its own credentials, and never cached. A consent-dependent page so stays cached per consent class instead of uncached for everyone, and a signed-in reader never gets nor fills a shared copy. Path revalidation drops every variant of the path. `voltro dev` applies the same rule.
242
+ - **Built-in durable outboxes store subject partitions atomically** — `@voltro/local-first`
243
+
244
+ Persistence adapters can implement the optional paired `loadQueuePartition` and `saveQueuePartition` methods. Built-in memory and IndexedDB adapters replace only the requested subject's queue without reading or rewriting foreign payloads. IndexedDB migrates existing shared queues atomically and resolves saves only after transaction commit. Whole-queue methods retain their order and replacement semantics. Custom adapters without both methods keep the serialized read/merge/write fallback; queue snapshots within the selected partition remain linear.
245
+ - **Static pages read the api at build time when its origin is set** — `@voltro/cli`
246
+
247
+ A prerendered (`renderMode: 'static'`) page's loaders now receive `query` during `voltro build` when the build can reach the api — `VOLTRO_API_ORIGIN[_<NAME>]`, the api's `serverUrl`, or an external api's absolute url — over the same one-shot HTTP rpc a server render uses, without a cookie. Without an origin `query` stays undefined, as before; a build never dials the dev proxy port.
248
+ - **Storage: editImage — creative edits that make a new version** — `@voltro/plugin-storage`
249
+
250
+ `editImage(bytes, contentType, { rotate, flip, flop, grayscale, crop })` returns new image bytes in the input's format — a creative edit to store as a new version, unlike the delivery transforms (`?w=&h=…`). Null when the input is no image, `sharp` is not installed, or the edit is empty.
251
+ - **Storage: bound how long a cached public object outlives turning private** — `@voltro/plugin-storage`
252
+
253
+ `storagePlugin({ publicCacheSeconds })` sets the `cache-control` of public objects the serve route delivers itself, transforms included (`public, max-age=<n>`). Without it they keep the year-long `immutable` cache. An object that turns private later stays reachable from a cached copy until that time runs out; a shorter time bounds the window.
254
+ - **A deferred webhook emit records its outbox row id as the event id** — `@voltro/plugin-webhooks`
255
+
256
+ `emit(event, payload, { eventId })` sets the event's id. An emit deferred to after commit uses its outbox row's id, so every delivery of that intent — a resent one included — records the same `eventId` (`x-voltro-event-id`), and a trace links the request (`traceId`), the outbox row and its deliveries.
257
+
258
+ ### Changed
259
+
260
+ - **Aggregates and memory JOINs prepare their work once per evaluation** — `@voltro/runtime`
261
+
262
+ Materialize aggregate groups directly and prepare memory JOIN qualification, null rows and supported scalar ON membership within each evaluation.
263
+ - **Prepare wide array predicates and migration diagnostics** — `@voltro/database`
264
+
265
+ Prepare wide ordinary in-memory array predicates and drifted migration diagnostics locally while preserving native membership, current files and ledger identity.
266
+ - **Reduce fuzzy matching and repeated feed date parsing** — `@voltro/plugin-search`, `@voltro/content`, `@voltro/database`
267
+
268
+ Memory fuzzy matching computes only the allowed edit-distance band and reuses row buffers, preserving UTF-16 and threshold behavior. Feeds parse dates once before ordering while preserving stable invalid-date behavior and XML output. Charset conversion planning indexes snapshot-local columns and incident foreign keys without changing migration decisions or constraint order.
269
+ - **Bound pending log sends and finish shutdown after a deadline** — `@voltro/logger`, `@voltro/plugin-logship`, `@voltro/plugin-datadog`
270
+
271
+ Buffered sinks allow one active send, share flushes and bound pending records when explicit count/byte caps are configured. Sends receive an AbortSignal with a five-second default; disposal has its own five-second budget and discards unsent work at the deadline. A sender ignoring abort blocks subsequent sends instead of overlapping them. Logship serializes redacted records once; Datadog does not forward its own transport warnings.
272
+ - **Bound plugin reads, image work and broadcast publication** — `@voltro/plugin-storage`, `@voltro/plugin-governance`, `@voltro/plugin-audit`, `@voltro/plugin-broadcast`, `@voltro/integration-http`, `@voltro/ui`
273
+
274
+ Storage pages original references in the database, aggregates counts and usage, coalesces concurrent image transforms, and reuses authorized warm variants without reading the original body. Output caches are limited to 200 variants and 64 MiB; distinct in-flight variants are limited to 32 and saturation serves originals. Governance traverses bounded subject pages without losing descendants of capped parents, uses stable retention cursors and bounded bulk physical deletes or anonymization. Audit verification retains full history coverage while paging bodies and preserving hash-chain state, with per-writer append horizons.
275
+
276
+ Broadcast publication uses an ordered queue bounded by messages and bytes. Loss triggers an explicit peer resync even on quiet tables; shutdown drains admitted work within a deadline and reports unsent changes. Memory providers release owned subscriptions and empty channels. HTTP retry budgets maintain incremental counters, and AutoForm indexes current field descriptors once for named controls.
277
+ - **Reduce build, content and locale-resolution allocations** — `@voltro/cli`, `@voltro/content`, `@voltro/i18n`
278
+
279
+ Reduce temporary collection allocations in environment summaries, mobile binding generation, shell build reports, binary Inspect metadata, content loading and invalidation, and catalog parity. Reuse compiled locale and time-zone cookie patterns while retaining resolution, authorization, diagnostics and output contracts.
280
+ - **Production server bundles drop chunks nothing can load** — `@voltro/cli`
281
+
282
+ `voltro build` removes the `.js` chunks of the web start bundle and the api serve bundle that no file of the bundle references. esbuild emits a chunk for every `import()` target before tree-shaking and keeps it when the code holding the import is dropped; a dependency that stores a lazy loader in a module-level constant shipped a 0.5–0.8 MB chunk nobody loads. Measured on the reference fixtures: start bundle 2.1 MB → 1.0 MB (45 → 13 files), serve bundle 9.3 MB → 8.5 MB.
283
+ - **Inspect nested ICU placeholders with less repeated work** — `@voltro/i18n`
284
+
285
+ Prepare repeated nested ICU brace scans and long catalog messages locally while preserving placeholder order, quoting and live catalog reads.
286
+ - **Release paired CDC expiry timers immediately** — `@voltro/sql-mysql`
287
+
288
+ The MySQL/MariaDB inline/binlog delivery gate cancels a fingerprint's timer as soon as its echo pairs. Expiry callbacks are generation-bound, so an old callback cannot remove a newer identical write. Unpaired events retain their expiry and first-arrival delivery behavior.
289
+ - **Faster schema-derived controls and repeated snapshot lookups** — `@voltro/client`
290
+
291
+ Schema-derived forms collect nested fields directly, retain natural declaration order without sorting and avoid temporary filter and dropped-key lists. Repeated connection lookups prepare an ID index only after multiple reads, while long undo/redo searches reuse selections for immutable subscription snapshots. First-match behavior, pending state, explicit field order and unusual metadata fallbacks remain intact.
292
+ - **Delivery checks on the pages that include a document** — `@voltro/cms`
293
+
294
+ A `ReadRevision` may answer every revision an address shows — the page's own first, then the header and shared blocks it includes — as an array. A check of an included document is observed there once its revision is among them, so a header's publish can be checked on every page that shows it. `releaseCheckpoint` lists each entry's addresses, one per address checked, newest check each. A function answering a single revision as a string works as before.
295
+ - **A link's picker can hand the choosing to the page tree alone** — `@voltro/cms`
296
+
297
+ The rich-text editor's link control is a named group ("Link to"): a `RichDocumentPicker` gets `labelledBy`, the id that names it, to label a list it renders (`aria-labelledby`), or renders just a button that hands the choosing to the page tree with a selection intent. The link's text and "Insert link" appear once a target is chosen in the editor; a target chosen in the tree lands over the selected text by itself, so an editor without a list of pages shows no dead controls.
298
+ - **A draft save that changes nothing makes no revision** — `@voltro/cms`
299
+
300
+ `saveDraft` returns the draft as it is when the saved values equal it field for field: no revision, no status change, no approval to repeat. An autosave of a form that only took up what others typed together and saved used to mint a revision with the same content, which a review workflow then asked to approve again. A save with `basedOn` (a translation catching up) still writes one.
301
+ - **Cold SSR compile queues drain without repeated tail movement** — `@voltro/cli`
302
+
303
+ Cold SSR compilation uses the existing Effect FIFO queue for waiting permits, retaining concurrency limits, deduplication, failure recovery and immediate permit handoff while reducing queue bookkeeping during large route sweeps.
304
+ - **Share resolved subscription reads within committed bulk buffers** — `database`, `runtime`, `sql-postgres`, `sql-mysql`, `sql-sqlite`, `sql-mssql`
305
+
306
+ Committed local buffers share identical resolved subscription reads while preserving every row event, guard and row-filter evaluation. A newer change ends sharing for the older buffer; ordinary and ungrouped CDC changes keep their per-event reads.
307
+ - **Reduce repeated work in core data and observability paths** — `@voltro/protocol`, `@voltro/database`, `@voltro/runtime`, `@voltro/cli`, `@voltro/sql-postgres`, `@voltro/sql-mysql`
308
+
309
+ Row patches avoid full JSON serialization for ordinary scalar and byte rows. Memory queries prepare primitive membership and supported equality joins; range routing uses ordered thresholds. Stateful aggregates update touched groups and persist revision-fenced keyed patches atomically. Aggregate boot batches consistency-checked reads. CDC attribution, event resume counting, trace rings and local timeline replay avoid repeated scans or array shifts while retaining bounds and ordering.
310
+
311
+ CDC echoes also preserve procedure and agent attribution, including identity with no trace or subject.
312
+ - **Large row deliveries share repeated relation implication work** — `@voltro/runtime`
313
+
314
+ Ordinary relationship policies reuse implication decisions within one synchronous row delivery. Tenant checks, cyclic graph handling and immediate tuple or policy revocation remain intact; accessor-backed policies and tenant checks keep live evaluation.
315
+ - **Reduce repeated CSS graph and SPA layout work** — `@voltro/cli`, `@voltro/web`
316
+
317
+ Development SSR visits shared CSS dependencies once and deduplicates URLs without linear list searches. API discovery, closure collection and route metadata avoid intermediate lists. Prerendered SPA shells reuse the layout modules that passed the unchanged loader safety gate; request-dependent shells remain excluded.
318
+ - **Snapshots, boot diagnostics and log sinks build fewer temporary collections** — `@voltro/runtime`, `@voltro/logger`
319
+
320
+ Reduce intermediate collections in graph, fleet, experiment and cost snapshots, boot diagnostics, pretty logs and buffered-sink policies and drains while preserving snapshot ownership, output and sender ordering.
321
+ - **Reduce validation work for broad middleware route lists** — `@voltro/cli`
322
+
323
+ Startup validation prepares ordinary exact route lists once while preserving overlap witnesses, subtree exclusions and asset scope.
324
+ - **Prepare large form error membership and stable accessibility prefixes** — `@voltro/client`
325
+
326
+ Prepare large form error membership and stable accessibility prefixes; bound rich-text cardinality work and avoid fenced-code tail copies.
327
+ - **Reduce frontend and code-generation collections** — `@voltro/client`, `@voltro/web`, `@voltro/cli`
328
+
329
+ Reduce transient frontend and code-generation collections while preserving transport observations, 404 loader behavior, image candidate order and generated declaration contracts.
330
+ - **Client, web and CLI hot paths build fewer temporary collections** — `@voltro/client`, `@voltro/web`, `@voltro/cli`, `@voltro/local-first`
331
+
332
+ Reduce temporary collections in bounded diagnostic restoration, schema field ordering, single-scope UI checks, input restriction, dotted RPC lookup, route ordering, scoped not-found selection, memory prefix reads, scalar conflict inputs and content migration selection while preserving captured input, callback order and current state.
333
+ - **Indexed queues and local queries avoid whole-cache work** — `@voltro/client`, `@voltro/local-first`, `@voltro/web`
334
+
335
+ Reuse prepared Router priorities and target client table/tag/mutation cache memberships directly. Keep unrelated local-row memos and select stable bounded local results without sorting the entire replica.
336
+
337
+ Use persistent reducer pending storage and indexed SyncClient/outbox controller queues while preserving immutable public arrays, FIFO, retries, conflicts and sealed epochs. Join active SyncClient drains in flush and stop queued sends after close. Bulk-read mirror partitions in one IndexedDB transaction, with the original custom-KV fallback and oldest-first budget eviction.
338
+ - **A page that renders per variant tells a cache in front what it may keep** — `@voltro/cli`
339
+
340
+ A `cacheVary` page's answers now name what they vary on (`Vary: Cookie` and the new optional `cacheVary.headers`, the request headers `key` reads), so a reverse proxy or CDN that honours `Vary` keeps one copy per variant. An answer for a request carrying one of `cacheVary.cookies` is `private, max-age=0, must-revalidate`: a CDN that ignores `Vary` would otherwise hand a page rendered with one reader's consent to readers who gave none — the server still keeps it per variant. A request rendered for itself (`key` → `null`) is `private, no-store`. `voltro dev` sends the same headers.
341
+ - **Reduce workflow journal scanning and gateway queue movement** — `@voltro/workflow`, `@voltro/runtime`
342
+
343
+ Workflow run observation selects the latest terminal reply directly and update polling collects terminal update IDs without intermediate arrays. Raw WebSocket gateways drain large outbound bursts using a cursor with occasional compaction, immediately clear consumed payload references and retain FIFO order, active-plus-queued limits, cancellation and transport flush semantics.
344
+ - **Reduce replay retention and eager projection planning work** — `@voltro/runtime`, `@voltro/client`, `@voltro/database`
345
+
346
+ Delta-resume overwrites bounded history slots without shifting retained events and locates the missed suffix by revision. Junction projections resolve column membership once per compilation, preserving requested order and missing keys. Client cache groups reuse ordered membership snapshots until membership changes; destruction and eviction release subscription-owned row-patch indexes.
347
+ - **Local queries build their string sort specification once** — `@voltro/client`
348
+
349
+ Avoid rebuilding string-column sort specifications per local-query comparison, preserving stable and custom ordering.
350
+ - **Share source preparation across localized content views** — `@voltro/content`
351
+
352
+ Concurrent server locale views share parsed, validated and rendered source entries within the collection cache generation. Invalidation, a changed definition, root or asset resolver starts fresh preparation. Locale fallback, default-tree validation, file-relative images and the browser artifact boundary remain intact.
353
+ - **Reduce intermediate allocation in memory reads and runtime diagnostics** — `@voltro/runtime`, `@voltro/cli`
354
+
355
+ Memory reads without predicates avoid a redundant result copy, and small full-text prefix searches count matches directly. Event replay, route snapshots, preview summaries, payload diagnostics, metric projections and level-reaction state reads avoid intermediate lists while retaining their existing ordering, gap, validation and recovery semantics.
356
+ - **Reduce nullable field and pending outbox allocation** — `@voltro/client`
357
+
358
+ Nullable fields with a single non-null schema branch avoid temporary branch lists, and pending outbox counts fold the queue directly. Field metadata, enum options, pending/failed status counting and conflict replay boundaries remain unchanged.
359
+ - **External ID metadata also describes local polymorphic targets** — `@voltro/database`
360
+
361
+ Clarify that `.externalId()` means an identifier has no single database foreign key target, including polymorphic references to rows in the same database. Storage stays unchanged; the application validates targets and handles orphans.
362
+ - **Form length bounds and capability scopes are prepared once per check** — `@voltro/client`
363
+
364
+ Prepare wide form length-bound metadata per validation and large declared capability scope membership per decision, retaining nested/localized errors and unknown resource authority.
365
+ - **Prepare repeated metadata once per operation** — `@voltro/client`, `@voltro/database`, `@voltro/runtime`, `@voltro/api-client`, `@voltro/ai`, `@voltro/mcp`, `@voltro/plugin-notifications`, `@voltro/plugin-presence`, `@voltro/data-transfer`, `@voltro/cache`, `@voltro/kv`, `@voltro/plan`, `@voltro/plugin-flags`, `@voltro/plugin-row-history`, `@voltro/plugin-webhooks`, `@voltro/cli`, `@voltro/devtools-ui`
366
+
367
+ Transient agent hooks batch immutable publication and incrementally build text. Bulk encrypted writes prepare metadata once while retaining per-row authentication binding; generated wire codecs prepare field lookups per call. Admin/form lookups, policy glob compilation, manifest refreshes, subset closure, flag allocation and impact traversal avoid repeated work. Notifications use aggregate unread counts and bulk mark-all updates. Empty presence branches and expired KV entries are reclaimed. Semantic lookup extracts nearest candidates without sorting all of them. Webhook admissions use bounded concurrency; failed admission does not roll back earlier targets. Descriptor mutation and live authorization remain visible.
368
+
369
+ Workflow statistics tally a page once and skip page-duration sorting when server percentiles are available. Memory API-key hash lookup scans live rows without copying the full map; it does not cache authentication. Data transfers retain inspect-create receipts in their source deployment and carry comment-thread status history with the comments.
370
+ - **Prepare membership, authorization and migration matching per operation** — `@voltro/plugin-scim`, `@voltro/protocol`, `@voltro/database`, `@voltro/plan`
371
+
372
+ Large SCIM group patches, repeated global guards, index rename matching and executable plan evaluation avoid repeated scans and growing array copies. Ordering, ambiguity rules, evidence validity and fresh authorization remain unchanged; small lookups retain direct short-circuit paths.
373
+ - **Prepare broad public API routes once** — `@voltro/protocol`
374
+
375
+ Mounted public API routes prepare query and path coercion metadata and index error-response members at construction, preserving validation, response order and plugin error status behavior. Large REST ANY-scope guards prepare membership only for the current call so later revocation remains visible.
376
+ - **Prepare release and database reads without repeated scans** — `@voltro/plugin-releases`, `@voltro/database`, `@voltro/runtime`
377
+
378
+ Batch release artefact summaries and prepare overview memberships per call. Resolve joined result decoder metadata once per result and load nested portable eager relations only for each parent's visible many window.
379
+
380
+ Retain memory transaction snapshots through copy-on-write row maps instead of copying all rows at transaction opening. Reuse immutable JSON row length estimates for blocked subscription accounting; custom/non-JSON values keep native estimates and existing backpressure policies.
381
+ - **Row decoding and flat-row comparison avoid redundant cell work** — `@voltro/database`, `@voltro/protocol`
382
+
383
+ Table-row decoders prepare only columns whose dialect representation needs normalization, while preserving fresh shallow copies and unknown columns. Flat rows with matching field layouts compare guarded property values without per-row Maps; exotic values and differing layouts retain native JSON behavior.
384
+ - **SQLite named composite UNIQUE no longer re-issues unchanged DDL** — `@voltro/database`
385
+
386
+ Recover unambiguous named SQLite composite UNIQUE identity to avoid repeated unchanged-schema DDL; prepare file-migration probe membership, physical decimal source metadata, large mixin index composition without retaining mutable decisions between calls.
387
+
388
+ Preserve same-name composite UNIQUE constraints during verified column renames without redundant drop/add work.
389
+ - **Runtime snapshots copy less; consumed broadcast payloads are released** — `@voltro/runtime`, `@voltro/local-first`, `@voltro/plugin-broadcast`
390
+
391
+ Reduce temporary runtime snapshot collections and release consumed broadcast payload references while preserving ordered delivery, replay, connection teardown and offline lineage behavior.
392
+ - **Reduce reactive form and loader bookkeeping allocations** — `@voltro/client`, `@voltro/runtime`, `@voltro/web`
393
+
394
+ Form errors, numeric native POST values, patch memberships and loader keys avoid temporary lists. Query pickers keep default inputs stable across unchanged renders, and single aggregate-group reads avoid a deduplication Set. Client diagnostics restore large persisted histories without exceeding JavaScript argument limits. Validation, reconciliation, group order, SSR handoff and loader cancellation retain their existing contracts.
395
+ - **Reduce reactive fan-out and workflow inspection overhead** — `@voltro/runtime`, `@voltro/workflow`, `@voltro/local-first`
396
+
397
+ Reactive deliveries share ordinary stripped snapshots within the current column-visibility policy while retaining per-subscriber authorization. Programmatic workflow inspection indexes first-step rows when ordering large histories, and offline mirrors prepare wide encrypted-column membership without retaining metadata across saves.
398
+ - **Subscription routing scales with active index definitions** — `@voltro/runtime`
399
+
400
+ Composite subscription routing looks up the event's tuple per active index definition instead of scanning every tuple bucket. Subscription removal updates only its own index memberships and discards empty buckets. Resume retention uses bounded incremental cleanup while checking each requested ring's expiration directly, preserving replay admission, authorization and row-filter behavior.
401
+ - **Combine replica traces with linear metadata scans** — `@voltro/protocol`
402
+
403
+ Select earliest replica traces and accumulate trace metadata with bounded linear passes while retaining span ordering and partial observation evidence.
404
+ - **Share running request-loader reads** — `@voltro/runtime`
405
+
406
+ Request-scoped loaders share overlapping in-flight reads while preserving batching, positional results, isolation and failure retries.
407
+ - **A cached REST answer can decide who may share it; failures are never shared** — `@voltro/protocol`
408
+
409
+ `cache.audience(output, input, subject)` lets the answer decide instead of the request's cookies: `'public'` — the same for every reader, shared at the CDN even when the request carries cookies such as a consent choice (`Vary: Authorization` only); `'private'` — this reader's alone, `private, no-store`. Answer `'private'` for a signed-in subject whenever the answer depends on who reads it. Without `audience`, any cookie still makes an answer private, and `Authorization` always does.
410
+
411
+ Any answer other than a `200` or `304` of a GET route with `cache` — a `404`, `410`, `401` — is now `private, no-store`, so a reader's "not found" (for example a members page an anonymous reader may not see) never lands in a shared cache. Before, failures carried no cache headers and a CDN rule could keep them.
412
+ - **Reduce captured runtime and form collection overhead** — `@voltro/runtime`, `@voltro/client`
413
+
414
+ Reduce transient collections during memory full-text matching, subscription registration, tenant listings, proxy parsing, connection scope projection, mirror high-water reads, RPC capability publication and asynchronous form submission. Preserve snapshot capture, validation, staged reads and trust boundaries.
415
+ - **Runtime diagnostics, mounts and savepoints build fewer temporary collections** — `@voltro/runtime`
416
+
417
+ Reduce temporary collections in replica, membership and expectation diagnostics, HTTP mount planning, source-gap reports, resume input normalization, ranked memory reads, savepoint capture and commit table bookkeeping while retaining ownership, ordering and transaction semantics.
418
+ - **Reduce routing and burst bookkeeping overhead** — `@voltro/runtime`
419
+
420
+ Replica recovery removes expired failures in one pass. Subscription error queues keep ordered delivery without repeatedly shifting their backlog. Analytics mirrors prepare table routing, HTTP mounts resolve wildcard ancestors by segment boundaries, malformed workflow payload diagnostics prepare provided-name membership once, and memory joins reuse alias/projection metadata.
421
+ - **Raw reads, updates and in-memory full-text prefixes are prepared once** — `@voltro/runtime`
422
+
423
+ Prepare wide immutable raw-read identity, explicit update membership and large in-memory full-text prefixes without retaining mutable decisions or changing authority.
424
+ - **Cheaper cleanup for shared pending scripts** — `@voltro/web`
425
+
426
+ Unmounting a pending Script consumer removes its callback by identity without searching the other listeners. Process-wide script deduplication, callback order, settled remount callbacks and StrictMode cleanup keep their existing behavior.
427
+ - **Reuse Intl formatters and batch comment lists** — `@voltro/i18n`, `@voltro/cache`, `@voltro/plugin-comments`, `@voltro/cli`
428
+
429
+ Reuse Intl instances for equivalent ordinary locale/options combinations with bounded LRU caches, preserving explicit date zones, fallback and render clocks. The memory cache maintains FIFO links atomically with values and tags instead of sorting all entries at capacity. Comment lists batch visible-thread children and reactions within portable parameter limits, retaining access, tenant, unread and ownership checks. Value-import traversal enqueues each source file once without changing browser-safety reach or per-walk invalidation.
430
+ - **Snapshot restore and foreign-key metadata are read once per boundary** — `@voltro/database`
431
+
432
+ Prepare large snapshot restoration and ownership lookups, deduplicate identifier warnings, avoid composite foreign-key prefix copies and repeated parent metadata reads, and reserve exact CTE helper names once per boundary.
433
+ - **Reduce repeated work while scanning source trees** — `@voltro/cli`
434
+
435
+ Avoid recursive source-file result copies and repeated ordinary exclusion-list validation while preserving mutable filters, traversal order and vanished-directory handling.
436
+ - **Reduce subscription metadata and idle event-route selection work** — `@voltro/client`, `@voltro/runtime`
437
+
438
+ Stable object keys and confirmed subscription patch IDs avoid intermediate lists. Event route pressure selects a bounded cold idle batch, retaining equal-time ordering, live subscribers and the fallback for unusual clocks. Key semantics, reconciliation and delivery guarantees remain unchanged.
439
+ - **Bound upload progress and snapshot dependency planning costs** — `@voltro/client`, `@voltro/i18n`, `@voltro/cli`
440
+
441
+ Batch upload progress updates a sum tree instead of rescanning every file, retaining cancellation, retries and current file-list reads. Snapshot cleanup prepares dependency adjacency once and uses an iterative child-first walk, preserving foreign-key ownership checks and ordering without recursive stack exhaustion. Store keys, reconnect cursors, native form paths, deep links, rich-text bodies and locale negotiation avoid unnecessary intermediate collections. Public APIs stay unchanged; upload fractions may differ in their last floating-point bits.
442
+ - **Reduce repeated searches in broad migration plans** — `@voltro/database`
443
+
444
+ Migration planning prepares enum and renamed-column lookups locally. SQLite introspection indexes virtual-table artifacts, and partial unique-index preflight prepares changed table shapes without skipping duplicate probes or convergence checks.
445
+ - **Wide SQL projections prepare repeated compiler lookups** — `@voltro/database`
446
+
447
+ JOIN alias routing, projection ordering, grouped aggregate inputs, exact decimal windows and rows, parallel decoder validation and MSSQL decimal DISTINCT reuse compilation-local lookup metadata. Generated SQL, binding order, exact values, NULL semantics and numeric ordering are preserved; registry column definitions stay live.
448
+ - **Reduce transient wire and route preparation collections** — `@voltro/protocol`
449
+
450
+ Reduce temporary wire collections during event addressing, replica inspection, REST decoding and validators, public route preparation and plugin alias/compatibility resolution. Preserve validation, capture order, error messages and conditional caching.
451
+ - **Reduce repeated schema work in writes, privacy graphs and field visibility** — `@voltro/database`, `@voltro/runtime`
452
+
453
+ Conflict-key validation reads only named column metadata. Larger ordinary bulk writes prepare required column names once per batch, privacy graph derivations prepare reverse-reference indexes per walk, and field visibility indexes larger ordinary scope sets per synchronous decision. Rights and schema metadata remain fresh between calls; custom validators and exotic containers retain live checks. Foreign-key dependency ranking no longer copies growing ancestor sets, preserving parent-first migration order and deferred cycle handling.
454
+
455
+ ### Fixed
456
+
457
+ - **voltro build resolves an unset NODE_ENV to production** — `@voltro/cli`
458
+
459
+ `voltro build` now treats an unset `NODE_ENV` as `production`, as `serve`, `start`, `db` and `migrate` already do. Left unset, the prerender's Vite server declared it `development`, React loaded its development build, and a static page failed to render against the production `react-dom` with `dispatcher.getOwner is not a function`. A declared `NODE_ENV` still wins.
460
+ - **Reuse client lookup metadata and bound URL-prefix checks** — `@voltro/client`
461
+
462
+ Client scheduler callback queues drain without shifting their backlog. Derived values reuse stable source-key ordering and correctly invalidate when a source name changes. Permission providers share scope membership for larger ordinary arrays and rebuild it on every delivery. Rich-text URL checks inspect only the normalized allowlisted prefix while preserving the existing validation rules.
463
+ - **A component in a rich text records its reference props as edges** — `@voltro/cms`
464
+
465
+ `referencesOf` walks the props of a component placed between the blocks of a rich text, as it does for a placed component: a call to action in the body pointing at a page is a reference edge, so `usedBy`, publishing checks and the dependency graph see it.
466
+ - **CMS: a field typed before its shared text arrived is no longer lost on save** — `@voltro/cms`, `@voltro/local-first`
467
+
468
+ A collaborative `saveDraft` took every shared field from the merged text, so a value changed in the form before that text reached the editor was replaced by the revision's old value. Such a value is now saved — while nobody typed into the text or saved the field since the revision the editor saw — and written into the shared text as one edit. `@voltro/local-first` exports `crdtTextEdit` and `rebaseEdit` from its main entry for server code.
469
+ - **CMS: a unique field's duplicate is refused at its field, not by the database** — `@voltro/cms`
470
+
471
+ `saveDraft` and `publish` now refuse a value of a `Schema.unique()` field that another entry of the same tenant and scope already holds, as `ContentValidationFailed` with the rule `unique` at that field, before writing. Before, the write reached the database constraint: callers got an untyped constraint error, and in postgres the failed statement aborted the transaction, so a mutation could not even answer with its own result. The constraint stays as the backstop for a race.
472
+ - **Content collections resolve their lazy server entry in native Node** — `@voltro/content`
473
+
474
+ The lazy content render import includes its emitted `.js` extension. Built collections now resolve in native Node without a TypeScript loader while the render pipeline remains outside the browser chunk graph.
475
+ - **CRDT history uses a short framework-owned index name** — `@voltro/runtime`
476
+
477
+ The CRDT history index uses an explicit short name so SQL apps no longer receive an identifier-length warning for a table they do not own. Schema migration renames the existing index (replaces only the index on SQLite/Turso) and preserves archived history.
478
+ - **db branch: a plain and a partial index on the same columns keep their own names** — `@voltro/database`, `@voltro/cli`
479
+
480
+ `voltro db branch` restores the parent's index names on the branch by matching each copied index by its key columns and uniqueness. Two indexes on the same columns — one plain, one partial (`WHERE "readAt" IS NULL`) — could swap names, and a rehearsal of an already converged schema then reported `infidelity`. The match now includes the partial index's predicate (`BranchIndexName.where`).
481
+ - **Every @effect/* package the framework uses is pinned exactly** — `@voltro/runtime`, `@voltro/cli`
482
+
483
+ `@effect/opentelemetry` was the one `@effect/*` peer declared as `^4.0.1`. A fresh install resolved it to 4.0.2, which requires an `effect` newer than the 4.0.1 the framework pins and imports `effect/Version` that 4.0.1 does not have — `voltro build` failed in the serve bundle, and pnpm split `@voltro/runtime` into several copies. It is pinned to 4.0.1 like `effect` and the SQL drivers. `voltro update` now narrows a range that admits an exactly pinned framework peer and later versions (`^4.0.1` against `4.0.1`) to that version; a range ahead of the pin is left alone.
484
+ - **The Effect 4 codemod leaves your own Left / Right unions and Type lookups alone** — `@voltro/cli`
485
+
486
+ The `0.85.0/90_effect-4` codemod rewrote every `x._tag === 'Left'` and every `{ _tag: 'Left', … }` in a file that imports from `effect`, including unions of your own with those tags. A comparison is now rewritten only in a file that names `Either` or `.either`, and an object literal only when it has Either's `left` / `right` shape. `X['Type']` becomes `X['Service']` unless `X` is a type alias, interface, enum or non-service class the same file declares. `HttpClientRequest.del` becomes `HttpClientRequest.delete`, also where it is passed without a call.
487
+ - **Env values are shared by every copy of @voltro/env in a process** — `@voltro/env`
488
+
489
+ pnpm links one copy of `@voltro/env` for each set of peers its dependents resolve, so a workspace can load two copies in one process — the app's and `@voltro/testing`'s, for example. Each copy kept its own snapshot: values installed by the boot gate or by `makeTestContext({ env })` through one copy were invisible to the other, and `getSecret` failed with "read … before the boot env gate ran". The snapshot, the live-rotation overlay and the clock now belong to the process.
490
+ - **The wildcard event descriptor matches guarded events again** — `@voltro/protocol`
491
+
492
+ A helper that infers an event's payload with `D extends EventDescriptor<string, Schema.Top, infer Payload>` resolved to `never` for every event declared with `guards:` — the guard type was derived from the key's `Type`, which is `unknown` for `Schema.Top`. The wildcard key now reads like `AnyEventDescriptor`, so the helper infers the payload of every event.
493
+ - **A bundled gRPC surface serves grpc.health.v1 and reflection again** — `@voltro/cli`
494
+
495
+ `grpc-health-check` and `@grpc/reflection` read their own `.proto` files at runtime through `__dirname`. Inlined into the ESM serve bundle, the health import threw and was swallowed, so a production gRPC surface built with `voltro build` silently served no `grpc.health.v1`, and reflection had the same problem. Both are now runtime leaves resolved from where `@voltro/cli` is installed, like the other optional dependencies the serve bundle keeps out.
496
+ - **Reduce live table rendering and release subscription buffers** — `@voltro/ui`, `@voltro/client`, `@voltro/runtime`
497
+
498
+ Reuse unchanged live table cells during parent updates and row reordering, while changed rows, columns and custom renderers remain reactive. Aggregate socket buffer bytes across subscriptions sharing a query label and release closed or overrun row state without losing queued patches or errors. Cancel unread capability-probe response bodies when discovery returns unavailable.
499
+ - **A MySQL file migration keeps to two connections** — `@voltro/database`
500
+
501
+ A transactional file migration on MySQL or MariaDB checks, after its body, whether something committed the transaction from inside. That check opened a third connection while the migration lock and the transaction held theirs, so with a pool of two (`DB_MAX_CONNECTIONS=2`) `voltro db apply` waited forever with the lock held — and every later apply waited on that lock. The check now reads through the lock's own connection, which is idle while the migration runs; `HeldMigrationLock` exposes it as `connection`.
502
+ - **Number fields keep their bounds and formField metadata on Effect 4** — `@voltro/protocol`, `@voltro/client`
503
+
504
+ Effect 4 writes a plain `Schema.Number` as a union with the `"Infinity"`, `"-Infinity"` and `"NaN"` spellings and attaches neither its bounds nor any check fragment to it. A number field therefore lost its `minimum` / `maximum` in the OpenAPI document, AI tool and WebMCP schemas, and its `formField` label, section and widget in forms and data tables — the column header fell back to the field name. JSON schemas are now generated from a finite view of every number (`finiteNumberView`, exported from `@voltro/protocol`), which is what the JSON wire carries: `type: number` with its bounds and metadata. Decoding is unchanged.
505
+ - **One vitest peer range across the framework; voltro update aligns it again** — `@voltro/workflow`
506
+
507
+ `@voltro/workflow` declared its optional vitest peer as `^5.0.0` while `@voltro/testing` declared `>=3.0.0`. With two ranges for one peer, `voltro update` could only report a conflict and leave the app's vitest as it was. Both now declare `>=3.0.0`, and a repository check keeps every shared peer on one range.
508
+ - **voltro add patches pins Effect's transitive companion to the effect version** — `@voltro/cli`
509
+
510
+ `@effect/platform-node` asks for `@effect/platform-node-shared` by caret, and each release of it peers on its own `effect` version, so a fresh install of a 0.85 app pulled a newer one than the exact `effect` it pins — a peer conflict, and with the next Effect release possibly an `effect` API the pinned version lacks. `voltro add patches` (and every new project) now writes a pnpm `overrides` entry that pins it to the patched `effect` version, and `voltro dev` / `voltro serve` name it when the installed one drifted.
511
+ - **Preserve optimized work and lifetimes on Effect 4** — `@voltro/ai`, `@voltro/client`, `@voltro/cli`, `@voltro/runtime`, `@voltro/protocol`
512
+
513
+ Use Effect 4 collection and stream APIs in optimized queues and batched replay. Capture streaming readers before waking them so synchronous re-registration cannot loop within one notification; retain optional workflow payload keys and route-specific stylesheets with lazy shell loading.
514
+ - **pgvector columns decode on the Effect 4 postgres driver** — `@voltro/sql-postgres`
515
+
516
+ `vector`, `halfvec` and `sparsevec` columns read as their text form (`[1,2,3]`, `{1:1,3:2}/5`), the shape the row decoder parses, with the same decimals pgvector prints. The driver decoded these extension types' binary values as UTF-8 and failed the query. Extension types are now resolved again after a `CREATE EXTENSION` through the client, so a migration that installs pgvector or PostGIS on a running pool does not leave those columns undecodable until a restart.
517
+ - **Plugin references retain their orphan policy through column modifiers** — `@voltro/database`, `@voltro/runtime`
518
+
519
+ `pluginRef` keeps its target and orphan rules through immutable column modifiers, including `.nullable()` and `.renamedFrom()`. A null orphan policy now refuses a non-nullable column during table declaration, as documented. References contributed by schema mixins are collected and validated too.
520
+
521
+ Orphan cleanup uses the same predicate constructor as native SQL stores, so registered delete/null policies also execute against real databases.
522
+ - **Concurrent first callback upserts of one key all land on postgres** — `@voltro/sql-postgres`
523
+
524
+ A function-form `upsert` (`update: (existing) => …`, the counter pattern) locks the existing row, so concurrent upserts of a key that exists never lose an update. When the row did not exist yet, every concurrent transaction inserted it and all but one failed with a unique violation. The INSERT now yields (`ON CONFLICT DO NOTHING`) to the one that won, locks that row and applies the patch to it: many first upserts of one key all count. MySQL/MariaDB and SQL Server still look up and write separately; the documentation says so.
525
+ - **A workflow's rateLimit() counts the run once, not once per replay** — `@voltro/workflow`
526
+
527
+ `rateLimit()` consults the `RateLimiter` inside a journaled activity again, as it did before 0.85.0. A workflow body runs from the top after every suspension; consulting the limiter directly in the body took another token on each replay and could choose a different wait than the journal had recorded. The admission now reserves its slot once (`onExceeded: 'delay'`), records the answer, and waits for the slot with a durable sleep. The `name` identifies the admission within the run: two calls with one name share one decision, so a loop gives each call its own (`send/${i}`).
528
+ - **voltro serverless build . bundles the functions in that directory** — `@voltro/cli`
529
+
530
+ `voltro serverless list|build|serve|deploy` took every path argument as a function file, so the documented `voltro serverless build .` (the `build` script of the edge-functions template) tried to bundle the directory and failed, and `serve .` refused to start. A directory argument now means the functions discovered in it; a file argument still names one function.
531
+ - **SQLite decimal review lists executable declarations for relevant stores** — `@voltro/cli`
532
+
533
+ SQLite decimal upgrade guidance identifies imported schema calls, including aliases, and table SQL instead of test titles, editor labels or prose. Explicit PostgreSQL, MySQL, MariaDB and MSSQL app stores have no SQLite migration hit list. The review is reissued for upgrades from 0.84.0; unscanned dependency schemas and unresolved dynamic store configurations still require a qualified operational review.
534
+ - **Retry SQLite transactions after a concurrent WAL snapshot change** — `@voltro/database`
535
+
536
+ SQLite `SQLITE_BUSY_SNAPSHOT` contention now retries the complete transaction with a fresh snapshot under the existing bounded retry policy. Concurrent read-then-write operations keep their atomicity without treating stale snapshots as permanent store failures.
537
+ - **A page render stops presenting a session the api rejected** — `@voltro/cli`
538
+
539
+ A session cookie the api refuses (revoked or expired server-side, signed with a retired secret) turned every page whose loader reads into a 500 for the reader still holding it — after a secret rotation, for every signed-in reader. `voltro dev` and `voltro start` now make a refused server-side loader read once more without the framework session cookie and clear that cookie on the response; later reads in the same render go without it. The api still never reads a rejected credential as anonymous. A no-JS form POST is not retried, and a refused credential other than the session cookie is answered as before.
540
+ - **Startup reuses source inventories and clears completed teardown deadlines** — `@voltro/cli`, `@voltro/runtime`
541
+
542
+ Web generation reuses its validated page tree, indexes layout lookup, and prepares large middleware matchers once. Import walks share contextual package resolution and metadata, while browser fingerprints reuse already-read source bytes; later walks remain fresh. Shared schema edits also invalidate the development API client. API discovery excludes reserved `.framework` output. Production reads islands shells only for on-demand renders, preserving stylesheet links and the full-shell fallback. Repeated missing-table diagnostics share suggestion work without dropping procedure warnings. Asynchronous startup teardowns cancel completed timeout timers while retaining LIFO cleanup and the five-second ceiling.
543
+
544
+ Compiled API production shutdown now calls the shared runner's actual shutdown handle, so registered startup cleanup also runs on graceful termination.
545
+ - **A derivative is never more open than its original** — `@voltro/plugin-storage`
546
+
547
+ A rendition or poster (`derivedFrom`) is checked against its original: `checkAccess`, `mintUrl` and the serve route decide by the original's visibility, policy and grants, so a public derivative of a private original is no longer served to anyone, a grant or token for the original opens its derivatives, and a derivative whose original is gone answers 404. `setVisibility` on an original now takes its derivatives along — an image made private no longer leaves public renditions behind.
548
+ - **Encrypted transfers release input streams and avoid repeated buffer copies** — `@voltro/data-transfer`, `@voltro/plugin-openapi`
549
+
550
+ Encrypted transfers process fragmented input without repeatedly copying a growing frame buffer; decryption closes its input iterator on cancellation or failure and rejects oversized frames and invalid final markers. Schema-drift comparisons index target tables once per comparison. OpenAPI prepares version groups, specs and the Swagger viewer together at surface updates, retaining the last successful surface if preparation fails and checking the Bearer gate on every request.
551
+ - **An undeclared input key is refused inside class and transformed inputs too** — `@voltro/protocol`
552
+
553
+ Procedure inputs refuse a key the schema does not declare instead of dropping it. Under Effect 4 that rule stopped at a node carrying its own encoding: a `Schema.Class` input, a class nested in a struct, or a struct piped through `Schema.decodeTo` decoded an extra key silently. Such a node is now closed on its wire form and decoded through its own transformation, so the call fails with the key's name like every other input, and the decoded value is still the class instance.
554
+ - **Summary metric snapshots use fewer temporary arrays** — `@voltro/runtime`
555
+
556
+ Summary metric snapshots collect populated quantiles directly without allocating a temporary array for every quantile. Zero values and quantile order are preserved, absent values are excluded, and earlier snapshots remain unchanged.
557
+ - **Timeline preparation follows the recording mode** — `@voltro/cli`
558
+
559
+ Schema maintenance with recording disabled no longer provisions Timeline capture or its workflow-storage bootstrap. Existing capture triggers and generation helpers are retired during explicit disabled maintenance while application rows and private capture tables remain. Disabled replica startup preserves another replica's active capture contract. Explicitly enable recording on both schema maintenance and serving to prepare production history.
560
+ - **Optional Timeline failures preserve successful schema maintenance** — `@voltro/cli`
561
+
562
+ MySQL/MariaDB and SQL Server optional capture preparation includes its workflow-storage bootstrap: an insufficient connection pool now warns instead of failing a schema command after its migrations committed. Warnings retain the underlying cause, including database driver codes, error numbers and missing privileges such as error 1419 and SUPER. Cancellation still interrupts preparation.
563
+ - **Turso stores share the transaction key with every other SQL client** — `@voltro/sql-turso`
564
+
565
+ A statement issued through a second Turso client — a plugin's own runtime, a second layer build — inside a store transaction now runs on that transaction's connection, like every other dialect since 0.85.0. Before, it took a fresh connection and waited on the transaction's write lock until it timed out.
566
+ - **voltro update aligns catalogs and writes ranges YAML can read** — `@voltro/cli`
567
+
568
+ `voltro update` now also moves a peer held below the framework's floor in a pnpm catalog — `catalog:` and `catalogs:` in `pnpm-workspace.yaml` — because a manifest that declares `catalog:` resolves there, not in the manifest. The YAML rewrite quotes a range YAML would otherwise read as syntax (`>=4.0.1`), keeps CRLF line endings, ends a section at a quoted top-level key, and leaves a range it cannot read as one value (`^3.0.0 || ^4.0.0`) as written.
569
+ - **voltro update aligns peers installed under a workspace member and in overrides** — `@voltro/cli`
570
+
571
+ `voltro update` aligns the peer ranges the framework requires — `effect` among them — with what the workspace declares. It read the installed `@voltro/*` packages only from the workspace root, where pnpm links nothing unless the root itself depends on the framework, so in a pnpm workspace the alignment was skipped without a word. It now reads each member's packages, and it also moves a peer held below the framework's floor in `pnpm-workspace.yaml` `overrides`, the root `pnpm.overrides`, npm `overrides` or yarn `resolutions` — an override wins over every manifest, so moving only the manifests left the install where it was.
572
+
573
+ An update run by an older CLI cannot benefit from that fix, because the older CLI performs the bump. The new CLI therefore checks the installed `effect` before it loads anything else: at the codemod step of an update it aligns the workspace's peers, re-installs and continues; any other command names the version it needs and the command that repairs it, instead of failing with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
574
+ - **voltro update --wait asks the registry the project installs from** — `@voltro/cli`
575
+
576
+ `voltro update`'s registry probe — `--wait`, and the diagnosis after a failed install — asked `registry.npmjs.org` whatever the project was configured for, so a mirror or a local registry was polled at the wrong place and reported versions missing that the install would have found. It now resolves the `@voltro` scope's registry, else the default registry, through `npm config get`, and falls back to npmjs only when neither is set. The printed endpoints name the same registry.
577
+ - **voltro update runs the source transforms in every framework workspace member** — `@voltro/cli`
578
+
579
+ The version bump moves every workspace member that declares `@voltro/*`, but the codemods that rewrite source ran only in the app `voltro update` was started from — a web app or a shared package beside it kept the old API while its dependencies moved. The transforms now run in each of those members too, once per file; a member that does not use the framework is not touched, and the written notes still search the whole workspace once.
580
+
581
+ ### Internal (no consumer-facing effect)
582
+
583
+ - **Prepare file-safe UI installation and update internals** — `@voltro/cli`
584
+
585
+ Prepare internal registry resolution, installed-file state and atomic file updates. The internals preserve customized and deliberately deleted files, reject stale plans and symlinked targets, and retain concurrent edits during rollback. Add additive Base UI configuration planning and AST-based alias adaptation that preserves complete source/license bytes outside actual module specifiers. This change does not register a new CLI command.
586
+
587
+ Prepare CSS compilation from delivered theme/style data, with light/dark token validation, Tailwind v4 utility bridges and rejection of invalid rules, references and unsafe values.
588
+
589
+ ---
590
+
42
591
  ## [0.84.0] — 2026-10-07
43
592
 
44
593
  ### ⚠ BREAKING