@voltro/ui-shadcn 0.74.0 → 0.76.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,729 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.76.0] — 2026-09-21
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **Align i18n peers and generated projects with current dependency majors** — `@voltro/cli`, `@voltro/i18n`, `@voltro/plugin-mail`, `@voltro/ai`, `@voltro/web`
47
+
48
+ `@voltro/i18n` now requires `react-intl ^12.0.0`. Align direct declarations before reinstalling. New workspaces, packages and generated TypeScript clients use TypeScript 7, Vite 8 and Vitest 5; JavaScript compiler-API consumers use the official `@typescript/typescript6` compatibility package. Email examples use `react-email` instead of deprecated `@react-email/components`; update both dependencies and imports. Run typechecks, tests, declaration builds and email rendering after upgrading. Gateway catalogs preserve the `evaluation` modality, and the realtime relay rejects models without credential-minting or WebSocket support before provider I/O.
49
+
50
+ The browser bundle grows with React 19.3: the `react` group is +8.4 KB raw (react-dom and scheduler), `hooks` +251 B and `router` +184 B, while `index` shrinks 2.7 KB — a net +6.2 KB first load and +8.4 KB islands first load. The React group is the price; the rest is churn.
51
+
52
+ **`voltro update` carries you across this** — codemod `0.76.0/04_dependency-toolchain`. 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.76.0).
53
+ - **Persist explicit workflow retry intents and return durable observations** — `@voltro/runtime`, `@voltro/cli`, `@voltro/devtools-ui`, `@voltro/voltro`
54
+
55
+ `ctx.workflows.retry` requires `{ requestId, payloadOverride? }` and returns `WorkflowRetryObservation`. Dev and Serve persist the retry before admission and dispatch a distinct execution; resubmitting the same intent reuses its execution. Inspect retry requires the same request ID and returns the observation. Bulk retries require preview-bound request identities and preserve outcomes in `retries` instead of marking engine acceptance as execution success. The single-run CLI requires `--request-id`.
56
+
57
+ Migration: persist one request ID per deliberate retry, reuse it after lost replies, and branch on the observation kind. Read the existing intent instead of creating another to check status. `voltro update` includes `0.76.0/06_durable-workflow-retry` with the caller migration steps.
58
+
59
+ Shared workflow pages persist retry references before dispatch and restore them after reload. `useRetryRun` returns `WorkflowRetryObservation` and accepts `WorkflowRetryCommand`; supply `controlScope` and `useRetryReceipt` for recovery. Payloads are not stored in the browser journal.
60
+
61
+ Bulk dashboard retries persist the complete preview before submission, share identities with individual retries, and retain every observation in `BulkRunResult.retries`. Custom bulk panels provide `prepareRetries` and `observeRetry`; queued or accepted retries are not counted as completed workflows.
62
+
63
+ CLI bulk retry requires a saved `--plan-file` preview before `--commit`. The private, exclusively created file binds process URLs and original run identities to request IDs, including shared identities across replicas. Repeated commits reuse the plan; `--observe` only reads receipts. Changed selectors or process sets refuse dispatch.
64
+
65
+ Delayed retry preparation and errors preserve the currently inspected app’s journal when navigating between apps.
66
+ - **Flow cancellation preserves engine acknowledgements and completed results** — `@voltro/plugin-ai-flows`, `@voltro/cli`
67
+
68
+ `cancelFlow` returns `FlowCancelReceipt`, retaining the full workflow control receipt for later observation and separating row status from engine acknowledgement. Unknown or accepted interruptions no longer report confirmed cancellation; completed flow rows survive competing cancellation requests. Migration: use `FlowCancelReceipt` from `@voltro/plugin-ai-flows/ir` as the action output schema, handle all engine outcomes, and preserve `control` for read-only request observation. Do not show a stopped-success message for accepted or unknown outcomes.
69
+
70
+ **`voltro update` carries you across this** — codemod `0.76.0/01_flow-cancellation-receipts`. 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.76.0).
71
+ - **Signed private-file links bind to their storage mount** — `@voltro/plugin-storage`
72
+
73
+ Bind signed private-file grants to their storage HTTP mount as well as the reference ID. Moving a link to another storage instance is refused even when independent stores contain matching reference IDs. Migration: `GrantTokenPayload` now carries a required `audience`, and `signGrant` takes the mount as a fourth optional argument defaulting to `/_voltro/storage`. Links minted before this release carry no audience and are refused, so regenerate them through the authorized URL-minting flow after updating.
74
+
75
+ **`voltro update` carries you across this** — codemod `0.76.0/07_storage-grant-audience`. 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.76.0).
76
+ - **Budget refusals stay typed and zero checks require headroom** — `@voltro/ai`, `@voltro/workflow`, `@voltro/cli`, `@voltro/voltro`
77
+
78
+ `requireAiBudget` exposes `AiBudgetError`: capacity, invalid input and reservation identity conflicts are typed failures; `releaseAiBudget` exposes invalid input too. Zero or omitted `addUsd` checks strictly positive headroom, refusing an exactly exhausted budget without reserving anything. Positive reservations can fill the limit and replay without double spending. Durable budget holds recheck the completed generation after waking instead of immediately parking again, and preserve typed recheck failures. Migration: handle `AiBudgetInvalidInput` and `AiBudgetReservationConflict` beside `AiBudgetExceeded`, widen explicit error-channel annotations to `AiBudgetError`, and do not use a zero preflight as authorization for a free call at an exhausted limit.
79
+
80
+ **`voltro update` carries you across this** — codemod `0.76.0/02_budget-refusals`. 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.76.0).
81
+ - **WebSocket gateways cancel pending setup and bound outbound writes** — `@voltro/protocol`, `@voltro/runtime`, `@voltro/cli`, `@voltro/voltro`
82
+
83
+ Gateway connections expose `signal` and `sendAsync`, keep message listeners live during asynchronous setup, reclaim teardown returned after disconnect, and bound active plus queued writes. Defaults are 1 MiB per message, 4 MiB pending bytes and 64 pending messages; declare `limits` on the gateway for larger foreign-protocol frames. Await `sendAsync` for local transport backpressure, not peer delivery confirmation; `send` remains a convenience that closes on overflow. Inbound limits apply before application dispatch, after platform message assembly. Migration: custom `WebSocketGatewayConnection` adapters and test doubles must implement the abort signal and awaitable bounded writer; ordinary gateway callbacks remain source-compatible. `voltro update` explains this contract.
84
+ - **Bulk workflow CLI commits require a saved preview plan** — `@voltro/cli`, `@voltro/devtools-ui`
85
+
86
+ `cancel-many` and `replay-many` require `--plan-file PATH` before `--commit`. Save a preview first, then reuse the same options and file to commit exactly those original runs. Existing plans are never overwritten. Cancel plans retain their request IDs and support read-only `--observe`; cancellation acceptance is not reported as confirmation. Redrive plans and the dashboard bulk panel bind the previewed execution identity and refuse a changed execution, without claiming a new retry receipt or workflow completion.
87
+
88
+ **`voltro update` carries you across this** — codemod `0.76.0/06_durable-workflow-retry`. 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.76.0).
89
+
90
+ ### Added
91
+
92
+ - **Explain workflow admission polling with live diagnostics** — `@voltro/runtime`, `@voltro/cli`
93
+
94
+ Expose process-local workflow admission timing, wake and failure diagnostics through flow-control inspection and `voltro workflows flow --format json`. Each responding process keeps its own snapshot; reads do not wake or claim work. Explain physical-discovery recovery polling instead of promising that primary-store notifications eliminate every idle claim. The generic scheduler also exposes bounded, payload-free snapshots through `inspectCoordinatedSchedule`.
95
+ - **Compose durable admission bindings atomically** — `@voltro/workflow`, `@voltro/cli`
96
+
97
+ Expose the database-only `reserveAdmissionSubmissionInTransaction` primitive for hosts that must bind admission and start receipts in one existing admission transaction. Internal retry preparation now rolls back capacity, receipt, and execution binding together when any write fails; this does not yet activate a new dashboard retry path.
98
+
99
+ Declare the internal retry-intent table through the shared workflow schema assembly so development and deployment migrations agree.
100
+
101
+ The shared admission worker recovers pending internal retry preparations in each physical workflow store, retaining deferred deadlines and terminal admission outcomes.
102
+ - **Storage upload input carries an optional operation identity** — `@voltro/devtools-ui`, `@voltro/cli`
103
+
104
+ Expose an optional operationId in shared storage upload input and preserve it through the dashboard upload proxy. Validate it before issuing a storage ticket so an explicitly identified upload is never silently converted into an ordinary write.
105
+ - **Dispatch binary Inspect uploads before text decoding** — `@voltro/runtime`, `@voltro/cli`
106
+
107
+ Inspect routes can declare exact binary handlers that receive the original byte stream and request cancellation signal. Development and production use this entrance for the replica-bound storage upload relay, preserving the Inspect disable gate and development host guard. Early rejection responses are preserved without prematurely destroying the request socket. Both dashboard hosts use the relay for replica-bound upload transfers.
108
+ - **Add single-replica targeting for Inspect actions** — `@voltro/cli`
109
+
110
+ The shared development and production Inspect entrances accept `targetReplica` on the dedicated `/_voltro/inspect/target/` path for a single addressed request, independently of observation collection. Authorization uses the actual method, the receiving process verifies its identity, and forwarding never retries or falls back to another replica. Binary responses preserve bytes and attachment headers without JSON projection. Client adapter and upload-ticket affinity must explicitly carry this target; read selection does not automatically target writes.
111
+
112
+ Older dispatchers reject the dedicated path rather than ignoring a target query and executing an untargeted mutation.
113
+
114
+ Addressed writes retain the separate write credential for the receiving process to verify. Read-only requests never forward it. A two-process regression covers separate in-memory object stores with colliding reference IDs: downloading and deleting on one replica leaves the other untouched.
115
+ - **Plugin inspection lists data-change, observability and surface hooks** — `@voltro/cli`
116
+
117
+ Include data-change, observability, REST-surface and procedure-surface hooks in plugin inspection metadata without invoking them.
118
+ - **Authenticated server-owned realtime voice and text sessions** — `@voltro/ai`
119
+
120
+ `defineRealtimeSession` relays normalized voice/text events through an authenticated WebSocket gateway while keeping all provider credentials on the server. Subject/model authorization precedes provider setup; bounded queues, setup/session deadlines and scoped cancellation prevent abandoned sessions. `resolveRealtimeModel` supports Gateway and OpenAI SDK realtime models, and browser-safe event schemas ship at `@voltro/ai/events`. Realtime joins the executable modality set. This uses a dedicated gateway socket, not RPC subscriptions, and provides no tool execution, budget reservation or automatic retry/replay of paid input.
121
+ - **Inspect upload tickets accept a durable operation identity** — `@voltro/plugin-storage`
122
+
123
+ Accept an optional operationId for inspect upload tickets and carry it through the binary upload into the existing durable object-write boundary. Renewed tickets reuse the same reference and quota reservation; changed content under the same identity is refused. Identities are scoped by storage instance. Durable admission requires a bound data store with default reference and grant stores.
124
+ - **Streaming transcription with explicit raw-audio and partial-result contracts** — `@voltro/ai`
125
+
126
+ `streamTranscribe` exposes the provider's live partial, delta and finalized-segment events followed by one complete result. Its explicit raw-audio format refuses container files, preserves provider send backpressure, and cancels audio/provider work on abort or stream disposal. Browser-safe event and typed error schemas are exported through `@voltro/ai/events`; the existing one-shot file transcription API remains unchanged.
127
+ - **Restore database read shapes in JSON date fixtures** — `@voltro/testing`
128
+
129
+ `hydrateFixtureRows(table, rows)` restores strings in declared timestamp and date columns to `Date` before seeding a test store. Text and JSON payloads stay untouched, and invalid dates report their row and column. The helper does not manufacture defaults or claim to emulate SQL constraints and driver behavior.
130
+ - **Describe durable retry observations without conflating acceptance and completion** — `@voltro/protocol`, `@voltro/cli`, `@voltro/runtime`
131
+
132
+ Add a shared retry observation schema for queued, accepted, unconfirmed, not-started and rejected intents. Internal resident observations preserve stopped automatic-retry state and reject acceptance of the original execution ID. The dashboard retry action is being migrated to this contract.
133
+
134
+ Expose `ctx.workflows.readRetry(runId, requestId)` in development and production runtimes. Reading validates the requested identity and never dispatches another attempt.
135
+
136
+ Share validated retry target and command schemas across resident preparation and SDK observations. Reject blank identities before ownership resolution and verify that resident observations match the requested intent.
137
+
138
+ Expose a read-authenticated Inspect retry observation endpoint through the shared Dev/Serve router. Reads validate the target and returned evidence without dispatching another attempt.
139
+
140
+ ### Changed
141
+
142
+ - **Dashboard analytics activity measures real sink operations** — `@voltro/cli`
143
+
144
+ Measure actual AnalyticsSink operation executions for dashboard activity instead of unrelated plugin hooks. Track invocation counts, failures and duration for track, aggregate, timeseries and topN in every boot path without recording event names, identities or payloads. Preserve provider initialization, mirroring and disposal semantics.
145
+ - **Dashboard uploads resume under a persisted operation identity** — `@voltro/devtools-ui`, `@voltro/cli`
146
+
147
+ Persist unfinished dashboard uploads before sending bytes and reuse their operation identity after reload. Reselect the original file to recover an uncertain result; changing attribution requires explicitly starting another upload. StoragePage now requires uploadScope to isolate attempts by destination. Browser storage failures prevent sending an untracked upload.
148
+
149
+ Damaged recovery records can be explicitly discarded after confirmation. Changed or valid records are protected from this recovery action; storage removal failures remain visible.
150
+
151
+ Coordinate upload and recovery-discard operations across tabs on the same origin with Web Locks. Refuse concurrent actions and stale attempts. Uploads require Web Locks in a secure browser context (HTTPS or localhost).
152
+ - **Expectation observations carry the full declarative rule** — `@voltro/runtime`, `@voltro/devtools-ui`
153
+
154
+ Include the full declarative rule in expectation observations and inspection responses. The dashboard displays both row-count bounds, the checked column and allowed value range, and distinguishes tolerated violation rates from absolute violation counts. Cloud inspection preserves the same rule metadata.
155
+ - **Plugin badges report activation, not live health** — `@voltro/devtools-ui`
156
+
157
+ Clarify that plugin activation badges report successful startup activation, not live health or shutdown status.
158
+
159
+ Omit unavailable plugin counts during initial loading and failed reads rather than showing zero or a stale count.
160
+ - **Clarify terminal stream events versus the persisted done flag** — `@voltro/ai`
161
+
162
+ Document that a resumable consumer can receive its terminal event before `isDone` becomes true, even without a failure. Event persistence precedes `markDone`; consumers finish on the event, while callers requiring the done write await `startResumableProducer`. Reversing this order could let readers miss the final event. This clarifies existing behavior without changing it.
163
+ - **Distinguish stale workflow observations from dead runners** — `@voltro/workflow`
164
+
165
+ Workflow debugging guidance no longer recommends terminating a database session from stale heartbeat timestamps, missing runner entries or pooler addresses. These observations show missing progress, not process death. Establish runner lifecycle and exact session ownership before a disruptive recovery action; row-based leases do not prove that an old worker has stopped either.
166
+
167
+ ### Fixed
168
+
169
+ - **Agent tool panels hide stale snapshots and explain disabled surfaces** — `@voltro/devtools-ui`
170
+
171
+ Hide stale agent-tool snapshot timestamps and provenance during loading and failed reads, including falsy error values.
172
+
173
+ Give disabled agent surfaces a dedicated explanation instead of unusable search and misleading empty inventories.
174
+
175
+ Use a tool-specific search label that describes the fields actually searched.
176
+ - **Handle aggregate dashboard refresh failures and duplicate submissions** — `@voltro/devtools-ui`
177
+
178
+ Manual refresh catches request failures, identifies the affected aggregate, and unlocks another attempt. Concurrent submissions are prevented and local hosts reload the snapshot after a processed request. Read failures no longer appear as an empty inventory. Refresh metadata wraps on narrow screens.
179
+ - **Report manual aggregate refresh failures and skipped runs** — `@voltro/cli`
180
+
181
+ Manual Inspect refresh returns 409 for an overlapping refresh or an unclaimed coordination slot, and 500 for a failed computation. These outcomes no longer produce a successful receipt. Failed builds preserve the last-good rows and record their error. Scheduled refresh and runtime handle coordination retain their existing skip semantics.
182
+ - **Aggregate inspection and refresh reach serve through the shared router** — `@voltro/cli`
183
+
184
+ Expose aggregate inspection and manual refresh through the shared authenticated router in both development and serve deployments. Serve now supplies its actual aggregate registry; write operations retain method-aware authorization.
185
+ - **AI flow inspection reads carry a process observation scope** — `@voltro/plugin-ai-flows`
186
+
187
+ Declare AI flow definitions and run inspection as process-scoped observations. Replica-selected reads now carry the required observation envelope instead of failing with HTTP 502. The dashboard keeps run summaries and selected detail payloads associated with the answering replica, including when bound stores are process-local.
188
+ - **AI run details stay closable while their request is pending** — `@voltro/devtools-ui`
189
+
190
+ Keep the selected AI run identity and Close details action visible while its detail request is loading or has failed. Closing the detail returns to the available run list without waiting for the request to succeed.
191
+ - **Analytics inspection behaves the same in development and serve** — `@voltro/cli`
192
+
193
+ Serve analytics inspection through the common authenticated API inspect router. Development and serve boot paths now expose the configured analytics sink and recent plugin metrics consistently, including explicit unavailable responses when no inspection provider is attached.
194
+ - **Await asynchronous schedule Inspect snapshots before serialization** — `@voltro/cli`
195
+
196
+ The schedule-list Inspect endpoint awaits the schedule reader before returning its observation. Asynchronous liveness queries no longer serialize as an empty object and crash the dashboard schedule list. Reader failures return an explicit error response. The shared snapshot builder and Dev/Serve wiring now use the concrete schedule response type, including external-schedule liveness fields.
197
+ - **Cache hit percentages follow the selected dashboard locale** — `@voltro/devtools-ui`
198
+
199
+ Format cache hit percentages in the selected dashboard locale, consistently with the hit and miss counters. Hide old observation metadata for every defined read error, including falsy error values, just as old measurements are hidden.
200
+ - **Reverse-ETL health and dead-letter reads keep their replica identity** — `@voltro/plugin-cdc-out`
201
+
202
+ Declare process observation scopes for sink health and dead-letter reads so the dashboard can inspect selected replicas without losing responder identity or double-counting shared outbox counters.
203
+ - **Reverse-ETL retries refresh sink health before another attempt** — `@voltro/devtools-ui`
204
+
205
+ Refresh sink health and failed deliveries after every Reverse-ETL retry attempt. Keep further retries disabled until a fresh successful failure-list read, including after a lost write response or a loading-state remount. Provide an explicit read retry after connection failures.
206
+ - **Comment threads offer read recovery without losing the search** — `@voltro/devtools-ui`
207
+
208
+ Keep thread dismissal available while comment messages load or fail. Add explicit read recovery for failed thread and inventory reads without clearing the current search or page. Inspection section actions remain available independently of their read state.
209
+ - **Aggregate refresh indicators stay hidden during pending or failed reads** — `@voltro/devtools-ui`
210
+
211
+ Hide aggregate refresh indicators during pending or failed reads and preserve text error messages instead of replacing them with an unknown error.
212
+ - **Analytics panels separate loading, failed reads and missing data** — `@voltro/devtools-ui`
213
+
214
+ Separate loading, failed reads and missing analytics data. Failed or pending reads no longer leave stale provider capabilities and activity visible, and the error state uses accessible feedback and theme-aware colors.
215
+ - **Small compute-budget amounts survive dashboard formatting** — `@voltro/devtools-ui`
216
+
217
+ Preserve small nonzero compute-budget amounts in the dashboard. Usage, limits, warning thresholds and tenant attribution use locale-aware significant-digit formatting instead of rounding to four decimal places and incorrectly showing zero.
218
+ - **Experiment rates read as percentages and differences as percentage points** — `@voltro/devtools-ui`
219
+
220
+ Display experiment conversion rates as percentages and their absolute differences as percentage points, distinct from relative percentage lift. Preserve small nonzero measurements and negative differences with locale-aware significant-digit formatting. Unknown measurements remain unknown.
221
+ - **Inspect credential badges follow current access observations** — `@voltro/devtools-ui`
222
+
223
+ Keep inspect credential configuration badges aligned with current access observations after saving. A saved result no longer masks later credential changes, and fresh observations preserve unfinished input edits.
224
+ - **The inspect credential form admits one save at a time** — `@voltro/devtools-ui`
225
+
226
+ Prevent duplicate in-flight saves in the shared inspect credential form even when a host does not publish mutation pending state. Lock token/removal controls until completion, clear successful edits, and retain failed edits for retry.
227
+ - **Plugin hooks count towards the cumulative invocation metrics** — `@voltro/devtools-ui`
228
+
229
+ Include recorded plugin hooks in the cumulative invocation metrics overview. Derive invocation counts and latency percentiles from hook-duration histograms and failures from the dedicated hook-error counter, so these measurements are no longer omitted from the shared dashboard.
230
+ - **Route boundaries resolve in root-to-page inheritance order** — `@voltro/devtools-ui`
231
+
232
+ Resolve route boundaries from filesystem segments in root-to-page inheritance order. The shared route explorer now includes root and deeply nested layouts, errors, loading and not-found boundaries, including route groups omitted from the URL.
233
+ - **The route inventory distinguishes loading, failure and an empty result** — `@voltro/devtools-ui`
234
+
235
+ Distinguish loading, failed reads and confirmed empty results in the shared route inventory. Hide retained route snapshots during pending or failed reads and expose loading/error feedback to assistive technology.
236
+ - **Missing invocation latency reads as unknown instead of zero** — `@voltro/devtools-ui`
237
+
238
+ Show unavailable invocation latency as unknown instead of zero when duration histograms are absent, empty, or have no finite boundaries. Keep request counts visible and sort measured p95 values ahead of unknown values in the shared dashboard.
239
+ - **Keep streamed loader handoff scripts out of client recovery renders** — `@voltro/web`
240
+
241
+ `<Await>` retains its registry script for server rendering and hydration, then removes it after hydration. Client recovery and hot updates reuse the settled registry value without recreating an executable script, avoiding React 19.3's script warning while preserving streamed layout and page data.
242
+ - **Identify tasks in development claim-change logs** — `@voltro/cli`
243
+
244
+ Development store debug records for `_voltro_schedule_claims` include `task` and `bucket` when valid metadata is available. Only these bounded identity fields are projected; full row payloads remain excluded. Records describe delivered row changes, not SQL statement counts: bulk deletes can produce multiple records, and remote change delivery can also appear.
245
+ - **Actions get their own Devtools scope and pending calls stay first** — `@voltro/client`, `@voltro/devtools`, `@voltro/web`
246
+
247
+ The Devtools console separates Actions from Mutations and shows their input, result, errors and timing. Pending calls sort above subscriptions and completed events, newest start first. A pending counter remains visible in every scope and opens a filter for running calls. Client invocation events carry an optional `kind` classification; `startMutation` defaults it to `mutation` and `useAction` publishes `action`.
248
+ - **Devtools panels keep consistent spacing in narrow docks** — `@voltro/devtools`
249
+
250
+ Webhooks and Traces use full-width section headings and inset content, while the Indexes offline state uses the same content spacing. Long webhook names and descriptions wrap within narrow panels instead of crowding the card edge or truncating their descriptions.
251
+ - **Trace inspection keeps selected records and exposes recorded span details** — `@voltro/devtools`
252
+
253
+ Selecting a trace keeps its latest captured version even when polling evicts it from the recent list. The saved selection remains visible until another entry is selected. Trace details show recorded attributes, SQL operations, errors and timing, and identify missing caller context. Subscription delivery latency stays separate from connection lifetime.
254
+ - **Doctor distinguishes query results from per-row reads** — `@voltro/cli`
255
+
256
+ The `n-plus-one` check no longer mistakes a single query followed by `map`, `flatMap`, `forEach`, or `reduce` for a query executed per row. Reads inside those callbacks or a real enclosing loop remain findings. Guarded sibling reads and conditional filter-building queries retain their existing sequential-read exemptions.
257
+ - **Environment inspection stops returning declared defaults and examples** — `@voltro/cli`
258
+
259
+ Exclude declared defaults and examples from environment inspection responses. Preserve default-satisfaction and missing-required counts while projecting only dashboard metadata and boolean states.
260
+ - **The environment panel separates loading, errors and empty declarations** — `@voltro/devtools-ui`
261
+
262
+ Distinguish environment inspection loading, errors, and confirmed empty declarations. Hide stale variable states and counts after read failures and restore them on recovery.
263
+
264
+ Improve environment status contrast in light themes and wrap long names and descriptions on narrow screens.
265
+ - **Expectations re-read current rows when a change omits a row image** — `@voltro/runtime`
266
+
267
+ Re-evaluate expectations from current rows when a store change omits a required row image. SQL updates no longer inflate row counts or retain stale value-bound violations, and scoped rules correctly detect rows leaving their population. Violation transitions retain the triggering write's provenance.
268
+ - **Expectation null rates and freshness carry their units** — `@voltro/devtools-ui`
269
+
270
+ Display expectation null rates as locale-aware percentages without rounding small nonzero rates to zero. Freshness measurements and thresholds both include milliseconds; unknown measurements remain distinct from zero.
271
+ - **Running experiments refresh from current rows on incomplete changes** — `@voltro/runtime`
272
+
273
+ Refresh running experiment measurements from current rows when a store change omits a required row image. SQL updates no longer double-count exposure or retain an old variant contribution; counts, sums, averages and conversion rates stay correct when rows move between variants. Update notifications retain the triggering change's provenance.
274
+ - **Apply flag audit filters before the result limit** — `@voltro/plugin-flags`
275
+
276
+ Filtered flag history now applies the flag predicate in the database before limiting results. Newer changes to unrelated flags no longer hide the selected flag’s history.
277
+ - **Reconcile unconfirmed flag toggles before allowing another change** — `@voltro/devtools-ui`
278
+
279
+ After an unconfirmed flag toggle, the shared dashboard requests fresh state and blocks further writes until a successful new snapshot arrives. Failed reads keep controls blocked with a read-retry action when supported by the host. Audit history refreshes after completed write attempts. A committed change with a lost response is reflected before the next toggle.
280
+ - **Prevent duplicate flag toggles before the next dashboard render** — `@voltro/devtools-ui`
281
+
282
+ Flag controls synchronously prevent concurrent submissions across the panel. Rejected requests retain the displayed state and unlock an explicit retry. Successful local toggles request a fresh snapshot after displaying the response.
283
+ - **Commit durable flag toggles and audit entries atomically** — `@voltro/plugin-flags`
284
+
285
+ Durable flag toggles now persist the override and audit entry in one transaction before reloading the local registry. Failed or unconfirmed writes return 503 and reconcile from durable state instead of leaving an optimistic local toggle or blindly undoing a potentially committed change. A missing durable store refuses the write. Invalid toggle bodies return 400.
286
+
287
+ Concurrent durable toggles lock the override row before reading its previous enabled state, including the first override. Updates preserve targeting and rollout fields.
288
+
289
+ First-override contenders now use distinct candidate row IDs so the flag-key conflict handler can arbitrate concurrent creation without a separate primary-key collision.
290
+
291
+ Overlapping durable reloads discard superseded responses instead of allowing a delayed earlier snapshot to overwrite newer flag state.
292
+ - **Preserve explicitly supplied gateway media dimensions** — `@voltro/ai`
293
+
294
+ Gateway image and video calls no longer silently omit explicitly supplied empty dimension strings. Image size/aspect ratio and video aspect ratio/resolution reach the provider unchanged, including asynchronous video starts. Numeric zeros remain preserved and absent parameters remain absent. This prevents silently choosing provider defaults; it does not make empty dimensions valid or replace application input validation.
295
+ - **Custom consent-store reads accept explicit replica requests** — `@voltro/plugin-governance`
296
+
297
+ Label custom consent-store reads with process observations so explicit replica requests work, while retaining shared-store scope for the built-in durable consent adapter.
298
+ - **Governance exports, erasures and sweeps run one at a time** — `@voltro/devtools-ui`
299
+
300
+ Serialize subject exports, erasures and retention sweeps within each Governance panel, prevent duplicate submissions before React renders pending state, and refresh status/history after every operation outcome.
301
+ - **Subject export and erasure bodies are validated before data access** — `@voltro/plugin-governance`
302
+
303
+ Validate subject export and erasure request bodies before accessing subject data. Reject malformed subject IDs and unknown erasure modes instead of coercing inputs or falling through to deletion.
304
+ - **Offloaded inference recovery retains paid tasks and pending signals** — `@voltro/ai`
305
+
306
+ Offloaded inference journals successful results and terminal errors before resuming the workflow, and retries failed signal delivery without another provider call. Polling and artifact-persistence failures retain the confirmed media task, submission attempt and artifact operation IDs. Only an explicit retryable terminal provider status starts a new paid attempt; missing-handle ambiguity remains `unknown` unless the provider guarantees idempotent submission.
307
+ - **Retain a single replica target in dashboard download tickets** — `@voltro/cli`
308
+
309
+ Storage download handoffs accept one validated `targetReplica` only on the dedicated Inspect target path and retain it during preparation and streaming. Ambiguous targets, read-selection parameters and unrelated query parameters remain rejected. The local dashboard uses this target for downloads and storage grant, revoke and delete actions instead of dropping the selected replica.
310
+ - **Any defined inspect error counts as a failed section read** — `@voltro/devtools-ui`
311
+
312
+ Treat every defined inspect error as a failed read, including falsy error values, so failed sections hide stale contents and show an accessible error state.
313
+ - **Bind Inspect upload tickets to the issuing replica** — `@voltro/plugin-storage`
314
+
315
+ Inspect-issued upload tickets carry a signed replica identity. The binary upload handler rejects a ticket presented to a different process before storing bytes, including deployments whose replicas share a signing secret. Ordinary application tickets without the optional replica binding retain their existing behavior. Dashboard transports must route bound uploads to the issuing process; the ticket does not itself perform routing.
316
+ - **Inspection headers hide stale timestamps while a read is pending** — `@voltro/devtools-ui`
317
+
318
+ Hide stale timestamps and provenance in shared inspection page headers during loading or failed reads, including falsy error values.
319
+ - **Migration rewrite guards retain full timestamp precision** — `@voltro/database`
320
+
321
+ `ctx.rewrite` compares timestamp and date guards at their original database precision while continuing to give callbacks normal decoded rows. A timestamp with microseconds no longer creates permanent false contention, and a concurrent change within one JavaScript millisecond is still rejected. This also covers implicit guards on patched temporal columns. Exhaustion diagnostics no longer assert that another writer must be responsible.
322
+ - **Migration inspection plans against the active database dialect** — `@voltro/cli`
323
+
324
+ Use the active database dialect when planning migration inspection snapshots in dev and serve. SQLite timestamp and JSON storage no longer cause phantom type changes in the migrations panel or a false schema-convergence failure after a successful migration.
325
+ - **Migration inspection and db drift share the planner live-table scope** — `@voltro/database`, `@voltro/cli`
326
+
327
+ Share the boot planner's live-table selection with migration inspection and `voltro db drift`. Runtime tables created after boot no longer cause false drift against a planner-scoped baseline. Unchanged full-catalog baselines remain accepted, while undeclared application tables and changes to declared tables still count as drift.
328
+ - **Refuse known undersized migration pools before taking the lock** — `@voltro/database`, `@voltro/sql-postgres`
329
+
330
+ Direct migration calls now reject a known one-connection pool before reserving the migration lock on PostgreSQL, MySQL, MariaDB and MSSQL. Configure at least two connections for the migrating process. PostgreSQL retains capacity information for prebuilt pools as well as ordinary clients; SQLite stays exempt. Unknown custom-layer capacity and remote pooler exhaustion remain subject to connection-acquisition failures.
331
+ - **Moderation decisions reconcile against a fresh queue read** — `@voltro/devtools-ui`
332
+
333
+ Serialize moderation confirmations and refresh after every decision. Unconfirmed writes block further decisions until a fresh successful queue read resolves the current review state, including when reading fails during the write.
334
+ - **Malformed moderation decision bodies are rejected before lookup** — `@voltro/plugin-moderation`
335
+
336
+ Reject malformed moderation decision bodies and non-string review IDs before looking up or changing a review.
337
+ - **Finite durable waits suspend inside activities** — `@voltro/workflow`, `@voltro/ai`
338
+
339
+ `awaitSignalSuspending` propagates the signal/clock race's suspension to its calling activity. A `mediaStep` nested inside `step` now parks with a finite timeout instead of retrying the suspension as an interrupted operation. Resume reuses the queued inference; elapsed deadlines remain failures and explicit workflow interruption remains cancellation. Suspension may run ordinary Effect finalizers and must not alone be treated as business failure.
340
+ - **Notification, comment and billing panels declare process scopes** — `@voltro/plugin-notifications`, `@voltro/plugin-comments`, `@voltro/plugin-billing`
341
+
342
+ Declare process observation scopes for notification delivery/inbox, comment thread, and billing usage dashboard reads so explicit and combined replica selections can load their data without 502 errors.
343
+ - **Queue search covers policies, activity and produced counts** — `@voltro/devtools-ui`
344
+
345
+ Apply queue dashboard search consistently to consumer policies, topic activity and produced-message counts. Searching a consumer group or dead-letter topic includes production for its matching source topics. Producer-only topics remain searchable. Explain empty search results and provide a clear-search action.
346
+ - **Queue topics count under their literal names** — `@voltro/plugin-queue`
347
+
348
+ Count queue topics by their literal names in process-local inspect metrics. Topics matching JavaScript object properties, including constructor and __proto__, now retain numeric consumed, retry, dead-letter and produced counts and appear correctly in the dashboard's serialized producer statistics.
349
+ - **Replica panels survive a temporary fleet inventory outage** — `@voltro/devtools-ui`
350
+
351
+ Keep the last known replica panels mounted when fleet inventory is temporarily unavailable, preserving filters and open-panel state. Mark retained membership explicitly while each panel reports its own read status. Fresh inventory replaces the retained list, including an empty result; app-scoped host boundaries prevent carrying replica state into another app.
352
+ - **Resumable uploads enforce size limits before buffering chunk bodies** — `@voltro/plugin-storage`
353
+
354
+ Resumable finalization rejects known oversized uploads using chunk metadata before fetching bodies and checks actual bytes incrementally while assembling. Streaming backends stop reading on overflow; buffered backends stop before subsequent chunks. Database metadata checks and cleanup no longer fetch blob payloads. The existing scan and storage pipeline remains in place; the configured upload limit is not a process-memory cap.
355
+ - **Workflow retry intents are classified as environment-local** — `@voltro/data-transfer`
356
+
357
+ `_voltro_workflow_retry_requests` no longer travels in a cross-environment transfer. A retry intent names a run and an execution in the deployment that raised it, so a foreign row binds the target to an operator action nobody asked it to perform and lets a later observation read another deployment's execution. An unclassified framework table travels by default, which is why this is a fix rather than a preference.
358
+ - **Keep schedule inspection usable when snapshots are malformed** — `@voltro/devtools-ui`
359
+
360
+ Schedule lists and firing histories validate incoming snapshots before rendering. Malformed responses show a localized error instead of crashing or appearing empty. The local dashboard offers an immediate retry, cancels the previous read, and ignores its late response while retaining automatic polling.
361
+ - **Schema convergence reports unavailable without a live SQL connection** — `@voltro/cli`
362
+
363
+ Report schema convergence as unavailable when the migration snapshot has no SQL inspection connection, rather than treating an empty placeholder as a passing database check.
364
+ - **Serialize dashboard search maintenance actions and reload their status** — `@voltro/devtools-ui`
365
+
366
+ The search dashboard synchronously prevents duplicate Reindex and Resync submissions, respects pending host operations, and requests fresh index and drift data after completed attempts. Failed attempts retain their confirmation for an explicit retry.
367
+ - **Search maintenance requests are validated before any data access** — `@voltro/plugin-search`
368
+
369
+ Validate search maintenance requests before accessing data: reject malformed bodies and repair limits, and only reindex explicitly configured source tables.
370
+ - **Search index status and drift reads declare a process scope** — `@voltro/plugin-search`
371
+
372
+ Declare process observation scopes for search index status and drift reads, fixing failed explicit-replica requests in the dashboard while keeping already-aggregated counters separate between replicas.
373
+ - **Production logs delivered row changes at debug level, like development** — `@voltro/cli`
374
+
375
+ `voltro serve` emits the same `<op> on <table>` debug line on the store change channel that `voltro dev` emits, through the same bounded projection: the operation, the table, and for the schedule-claim ledger only, its allowlisted task name and bucket. No row payload is ever logged. A change-stream question that was answerable while developing and unanswerable in production is the asymmetry this removes; the line stays off unless the log level asks for it.
376
+ - **Migration history reads use SQL Server pagination syntax** — `@voltro/cli`
377
+
378
+ Use SQL Server pagination syntax when reading migration history. The migrations panel and schema-convergence check no longer fail on SQL Server because of an unsupported LIMIT clause.
379
+ - **An uncertain storage upload can be checked without resending bytes** — `@voltro/devtools-ui`
380
+
381
+ Add a read-only check after an uncertain storage upload: search the attempted object key and refresh files and statistics without resubmitting bytes. Preserve the selected file and ownership fields for inspection and an explicit later retry.
382
+ - **Preserve storage reads in replica selection** — `@voltro/plugin-storage`, `@voltro/devtools-ui`
383
+
384
+ Storage inventory, statistics and grants now return process-scoped Inspect observations. Individual and combined replica reads no longer discard successful storage responses as missing observations. Custom process-local metadata stores are not incorrectly presented as fleet-wide shared state.
385
+
386
+ The shared Storage panel presents multiple selected replicas as labelled observations instead of passing non-composable inventories to the generic fleet projection.
387
+ - **Preserve gateway charges when structured output is rejected** — `@voltro/ai`
388
+
389
+ Structured generation captures allowlisted consumption before SDK validation, so `AiError.evidence` retains reported gateway charges when JSON or schema validation rejects a paid response. Tool loops sum costs only when every step reports a valid charge; missing prices remain unknown, and a reported zero stays actual zero. Evidence stays isolated per call and excludes response content, headers and private provider metadata.
390
+ - **Honor both turn and SDK cancellation in tool calls** — `@voltro/ai`
391
+
392
+ Tool calls combine the caller's turn signal with the SDK invocation signal. An already cancelled turn cannot be overridden by a fresh SDK signal; either source interrupts running Effects and reaches cooperative Promise handlers through `ctx.abortSignal`. The first abort reason is preserved without aborting either source controller.
393
+ - **Recent traces prioritize application activity over routine housekeeping** — `@voltro/cli`, `@voltro/devtools`
394
+
395
+ The grouped trace overview excludes successful traces made entirely of recognized framework housekeeping spans before applying the result limit. Errors and application traces remain visible. Show background traces in the embedded console, or use `/_voltro/inspect/traces?includeBackground=1`, to include housekeeping again. The bounded raw span buffer and direct trace-ID lookup remain unchanged.
396
+ - **Database spans retain their request context across runtime boundaries** — `@voltro/runtime`
397
+
398
+ Awaited work dispatched through another Effect runtime sharing a tracing layer retains its active parent span, so database calls stay in the request's trace. Concurrent requests and independent tracing layers remain isolated, and explicit parent or root choices take precedence.
399
+ - **Native bundle dependencies resolve through transitive driver packages** — `@voltro/cli`
400
+
401
+ Resolve native bundle dependencies through installed transitive driver and plugin dependencies. SQLite serve bundles now find better-sqlite3 under the Effect driver in strict pnpm layouts, including after deployment relocation. Failures while loading a resolved native module remain visible rather than falling back to another copy.
402
+ - **Expired credentials retain their typed RPC failure** — `@voltro/protocol`, `@voltro/runtime`
403
+
404
+ RPC authentication rejections now reach HTTP and WebSocket clients as `Unauthenticated`, including rejections from the authentication strategy chain. Auth-refresh handlers can match `_tag` instead of inspecting `InternalError` messages. Unexpected resolver exceptions remain defects. The same middleware handles development and production servers; no configuration change is needed.
405
+ - **Webhook tables and counts hide after a failed snapshot read** — `@voltro/devtools-ui`
406
+
407
+ Show initial Webhook loading and hide stale tables and counts after snapshot read failures. Successful reads restore the selected view while subscription drafts and revealed signing secrets remain available during interruptions.
408
+ - **Invalid webhook commands differ from missing deliveries and failures** — `@voltro/cli`
409
+
410
+ Webhook Inspect writes reject non-object JSON before dispatch. Subscription policy errors return HTTP 400 with an actionable error and field; missing replay deliveries return HTTP 404. Unexpected internal errors remain HTTP 500 without exposing raw driver details.
411
+ - **Webhook snapshots project explicit fields instead of database rows** — `@voltro/cli`
412
+
413
+ Project explicit dashboard fields in Webhook Inspect snapshots instead of spreading database rows. Signing secrets, custom headers, signing configuration, delivery payloads, response bodies and unknown database fields no longer appear in target and delivery responses.
414
+ - **Failed webhook reads surface instead of reporting empty activity** — `@voltro/cli`
415
+
416
+ Propagate failed Webhook target, delivery and delivery-history reads instead of reporting empty lists or never-delivered activity. Use bounded error messages without database-driver details so dashboards can show a failed read and recover on their next successful snapshot.
417
+ - **Undeclared webhook storage is distinct from a failed database read** — `@voltro/cli`, `@voltro/devtools-ui`
418
+
419
+ Distinguish undeclared Webhook storage from failed database reads. Development and production inspection derive availability from the declared schema; the shared dashboard explains unavailable storage and hides management actions. Existing declared storage remains visible even without current event declarations.
420
+ - **Refuse webhook management actions for missing target IDs** — `@voltro/plugin-webhooks`, `@voltro/cli`
421
+
422
+ Webhook target resolution checks explicit IDs before management writes. Pause, resume, deletion, secret rotation, target edits and payload-version changes refuse a missing target with the browser-safe `WebhookTargetNotFound` error instead of acknowledging a no-op or returning an unused secret. Inspect maps this error to HTTP 404. Test deliveries use the same typed error.
423
+ - **Mint compatible webhook signing secrets for subscriptions and rotation** — `@voltro/plugin-webhooks`
424
+
425
+ The webhook service now mints Standard Webhooks-compatible secrets when creating or rotating endpoint subscriptions. Grouped subscriptions retain one shared secret. This fixes default outgoing deliveries failing before HTTP dispatch because the service supplied a hex key to the Standard Webhooks signer. Custom and inherited secrets remain unchanged; existing incompatible keys can be replaced through secret rotation and synchronized with the receiver.
426
+
427
+ Validate supplied and inherited keys against the effective scheme after event defaults are resolved. Validate the entire subscription group before inserting its first target; incompatible Standard Webhooks keys fail before any target is stored.
428
+ - **Preserve webhook subscription drafts during connection failures** — `@voltro/devtools-ui`
429
+
430
+ Keep open webhook subscription forms and their inputs when the configuration source disconnects or reloads. Disable submission until the configuration is available and the selected event still exists, while keeping cancellation available. The same shared behavior applies to local and Cloud dashboard hosts.
431
+ - **Bind durable admission requests to their original payload** — `@voltro/workflow`
432
+
433
+ Reusing an admission submission identity with a different encoded payload now fails instead of silently selecting the original payload. Identical JSON content still reuses the reservation regardless of object property order, including after persistence.
434
+
435
+ ### Internal (no consumer-facing effect)
436
+
437
+ - **Resolve standard library declarations through the JavaScript compiler** — `@voltro/build`
438
+
439
+ Declaration rollups use the standard library from the official JavaScript compiler compatibility package alongside the native TypeScript compiler. A real build regression verifies generic standard-library types and a complete self-contained declaration bundle.
440
+ - **Verify durable workflow retry recovery across process crashes** — `@voltro/cli`
441
+
442
+ Exercise fresh retry recovery through real Serve processes and SQLite resident and engine stores. Crash before engine submission or after submission without acknowledgement, then verify recovery without another command and repeated submission of the same intent without a duplicate execution. Preserve the original failed run, payload, caller context and resident-store ownership.
443
+
444
+ ---
445
+
446
+ ## [0.75.0] — 2026-09-19
447
+
448
+ ### ⚠ BREAKING
449
+
450
+ - **A storage object shared through an `operationId` is held by per-caller claims** — `@voltro/plugin-storage`
451
+
452
+ Two callers completing the same `operationId` share one ref, and a caller that failed later and removed what it put deleted the file the other caller had already kept — while skipping cleanup left a file behind for every job that finally failed. `put({ operationId, claim })` now registers the caller's own claim when it receives the ref, `delete(id, { claim })` releases only that claim, and the object goes with the last one. A delete that names no claim is still the whole object. A claim without an operation is refused (`'claim-without-operation'`), and a claim released on a ref that holds none is a 409 that deletes nothing. Claims live in `_voltro_storage_object_claims`, migrated like the other framework tables.
453
+
454
+ `put` also reports what it did: it returns `StoredRef`, a `StorageRef` plus an `outcome` of `'created'`, `'resumed'` or `'joined'` — a fact about the call, never stored. Migration: code that reads the result compiles unchanged; a hand-written `StorageServiceShape` implementation answers `{ ...ref, outcome: 'created' }`, and a direct reader of `makeStorageObjectMetadata(...).beginDelete` narrows its result with `'operation' in result`.
455
+
456
+ **`voltro update` carries you across this** — codemod `0.75.0/02_storage-put-reports-its-outcome`. 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.75.0).
457
+ - **A workflow declares who may drive a run — `controls:`** — `@voltro/protocol`, `@voltro/cli`
458
+
459
+ A `workflow()` said who may START it (`guards:` / `openAccess:` / `internal: true`) and nothing about who may DRIVE one of its runs. The built-ins behind `useWorkflow(...)` — `cancel`, `signal`, `update`, `resume` — admitted any caller in the run's tenant. So an app that guarded its approval surface
460
+
461
+ ```ts
462
+ defineAction({ name: 'orders.approve', guards: [{ scope: 'orders:approve' }], … })
463
+ ```
464
+
465
+ had a second way in that it never wrote: a colleague the mutation refused could send the same `approval` signal through `__voltro.workflow.signal` and release the order. Measured through a real server, with both callers on real sockets.
466
+
467
+ Driving a run is now its own declaration, in the same vocabulary:
468
+
469
+ ```ts
470
+ export default workflow({
471
+ name: 'orders.fulfil',
472
+ internal: true,
473
+ controls: { guards: [{ scope: 'orders:approve' }] },
474
+ payload: { orderId: Schema.String },
475
+ success: Schema.String,
476
+ })
477
+ ```
478
+
479
+ `controls: { openAccess: '<why>' }` says anyone who may see the run may drive it. A resource-scoped guard reads its id from the run's start payload, so one `resource:` function serves the start and the controls.
480
+
481
+ **A workflow that declares no `controls:` cannot be driven over the wire** — a system subject and a caller holding `admin:full` aside — and the refusal names the missing declaration. Starting and driving are different authorities: the customer who starts an order must not signal its own approval, so inheriting the start decision would have been wrong in both directions.
482
+
483
+ Unchanged: reading. The run feeds stay scoped by tenant, so a caller may watch a run it may not drive. Server-side control (`ctx.workflows.signal`, a schedule, a parent workflow) is unaffected. Cancelling a start that has not run yet (`cancelQueued`) is still authorised by the receipt id the start returned plus the tenant — there is no run and no payload for a resource guard to read.
484
+
485
+ **`voltro update` carries you across this** — codemod `0.75.0/03_workflow-controls-decision`. 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.75.0).
486
+ - **A usage receipt recorded twice with different values is a typed conflict** — `@voltro/ai`, `@voltro/voltro`
487
+
488
+ `recordAiUsage` answered `replayed` for any second recording of a receipt, even with different values, and returned the values it was handed — so the caller saw a charge that was never booked and no sign of the disagreement. It now fails with `AiUsageReceiptConflict` (the `recorded` and the `attempted` values), and a genuine replay returns the booked row's breakdown. A failed ledger write is `AiUsageLedgerError` in the error channel instead of a defect.
489
+
490
+ For a call that is already paid for, `recordAiUsageBestEffort` never fails and logs both errors with what is needed to reconcile them. The framework's own recording paths use it; they used to discard a failed write without a trace.
491
+
492
+ **`voltro update` carries you across this** — codemod `0.75.0/01_ai-usage-receipt-conflict`. 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.75.0).
493
+ - **Preserve workflow bulk cancellation receipts through the dashboard** — `@voltro/cli`, `@voltro/devtools-ui`
494
+
495
+ Bulk cancellation returns resident control receipts instead of reporting accepted requests as failures. Both dashboard bulk surfaces persist per-run identities before dispatch and restore receipt observation after reload or a lost response. Cancellation reasons are retained; producer sweeps still require confirmed termination.
496
+
497
+ Custom bulk providers must forward request `controls` and response `targets`/`controls`. Standalone `BulkRunActionsPanel` and `SelectionBar` integrations supply `prepareControls` and `observeControl`; `WorkflowsPage` connects its existing journal. Count only confirmed cancellation as success and use the control reader for pending or uncertain outcomes.
498
+
499
+ **`voltro update` carries you across this** — codemod `0.75.0/01_workflow-bulk-control-receipts`. 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.75.0).
500
+
501
+ ### Added
502
+
503
+ - **A paid media step keeps its usage and its charge, even when it fails after** — `@voltro/plugin-ai-flows`
504
+
505
+ A media step's result carries `usage` — the quantity it consumed, in the provider's unit (tokens, characters or seconds), with `status: 'reported'` or `'unreported'` — and the run step records it. A step priced `unpriced` used to read the same whether the provider had reported a tally or nothing at all.
506
+
507
+ `FlowCapabilityMissing` can carry a `receipt` (cost, cost source, request id, usage) for a failure that happened AFTER a billed generation, typically when the artifact could not be stored. The engine writes it onto the failed step and adds the charge to the run, and `costCompleteness` counts a failed step that carries one — a run that paid for a generation and then failed to store it reads `incomplete`, not `unknown`. The built-in media adapter fills both. A failed sequential step also updates the run's `costCompleteness` now; it used to keep the verdict of the last step that succeeded.
508
+ - **Inspect individual outcomes of stored schedule backfills** — `@voltro/devtools-ui`
509
+
510
+ Schedule backfills expose paginated recorded outcomes with status, scheduled time and complete run identity, including read-only access and results older than the recent firing history.
511
+
512
+ Each outcome opens its original firing details on demand, including error, timing and replica information. Both dashboard hosts query the exact upstream run independently of recent-history limits. Shared hosts provide useScheduleRun for the detail read.
513
+ - **Generated media is stored with the owner, visibility and tags you give it** — `@voltro/ai`, `@voltro/protocol`, `@voltro/plugin-storage`, `@voltro/voltro`
514
+
515
+ `mediaStep({ persist: { ownerId, visibility, tags } })` hands all three through the queue and the dispatcher to the storage plugin's `mediaPersist`, which stores the artifact with them. Every generated artifact used to land owner-less, private and untagged, although the store takes all three — so deletion, restore and listing by owner or tag did not reach generated media.
516
+
517
+ The capability's `put` and `ingestUrl` gain three OPTIONAL fields. A `mediaPersist` implementation that ignores them stays assignable, and a caller that does not pass them compiles unchanged — which is why the surface change is marked compatible.
518
+ - **Read individual schedule firings by identity** — `@voltro/cli`
519
+
520
+ The schedule firing Inspect endpoint accepts a runId filter combined with the schedule name, so older run details remain queryable outside the recent-history page. Missing or mismatched identities return an empty result.
521
+ - **Stream storage uploads through the local dashboard** — `@voltro/cli`
522
+
523
+ Local dashboard servers can prepare session-bound, one-use storage upload handoffs and stream exact file bytes to the issuing app. Read/write credentials authorize preparation; binary transfers carry only the signed storage capability, enforce the admitted size, reject cross-site requests and redirects, and support named storage instances.
524
+ - **`registerMediaTaskProvider` — media jobs through a provider with a task handle** — `@voltro/ai`
525
+
526
+ A hosted media queue answers a submission with a task handle and is asked for the result later; `mediaStep` had no way to reach one. `registerMediaTaskProvider(name, { submit, poll })` registers the contract, and `mediaStep({ provider: { name } })` routes through it. The dispatcher writes the handle onto the queue row before anything else, polls once per claim without holding the lease across the job, persists the artifacts under the same `<row>#<attempt>#<index>` identity and records usage in the provider's unit.
527
+
528
+ A job whose worker was lost after the handle was written is polled by the next claim — never submitted again and never reported `unknown`. The remaining window is a worker lost between the provider accepting the job and the handle reaching the row: such a row still resumes its run with `outcome: 'unknown'`. `submit` receives an idempotency key derived from the row and the attempt; a registration that declares `idempotentSubmit: true` closes that window too. A `failed` status is terminal unless the provider marks it `retryable`; a task still pending after `taskTimeoutMs` is failed with the reason.
529
+
530
+ `_voltro_ai_inferences` gains the nullable `taskHandle` and `taskSubmittedAt` columns; the declarative differ adds them on the next boot or `db apply`.
531
+ - **`tx.savepoint()` works on every dialect and on the store a handler holds** — `@voltro/database`, `@voltro/runtime`, `@voltro/sql-postgres`, `@voltro/sql-mysql`, `@voltro/sql-mssql`, `@voltro/sql-sqlite`
532
+
533
+ Inside a transaction, `tx.savepoint(fn)` runs `fn` so that a failure undoes all of `fn` — and only `fn` — while the transaction goes on. It existed on Postgres alone and was not passed through to `ctx.store`, so in a handler it was `undefined`; the in-memory store every app test runs on had none either. It is now on Postgres, MySQL, MariaDB, SQL Server, SQLite and Turso, on the in-memory store, and on the store a handler holds.
534
+
535
+ This is the way to skip a failing row and keep the rest: catching the failure without a savepoint ends the transaction on Postgres, and `transactional()` then fails with `TransactionAbortedError` rather than committing nothing.
536
+
537
+ ```ts
538
+ await ctx.store.transactional(async (tx) => {
539
+ for (const row of rows) {
540
+ await tx.savepoint!(() => tx.insert('imports', row)).catch(() => skipped.push(row.id))
541
+ }
542
+ })
543
+ ```
544
+ - **Upload files from the shared storage inspector** — `@voltro/devtools-ui`
545
+
546
+ The shared storage inspector supports file uploads through a host-provided mutation. It requires explicit tenant and owner attribution, defaults to private visibility, prevents duplicate submissions, preserves inputs on failure, and refreshes inventory after success. Read-only hosts do not show upload controls.
547
+
548
+ ### Fixed
549
+
550
+ - **A payload the client refuses to encode is printed with the field it names** — `@voltro/client`
551
+
552
+ A payload with a field the procedure does not declare fails its schema before it is sent, so the server never sees the request and no server log has it. The error the caller receives names the field, but an app that logs the object rather than its message saw only an opaque `ParseError`. The client now prints the full message to the console once when a value fails its schema on the client — on every path that reduces a failed call, reads and writes alike. A declared failure from a handler is not printed; that one is the app's to handle.
553
+ - **VOLTRO_DESTRUCTIVE_OK no longer takes credit for drops it never covered** — `@voltro/cli`
554
+
555
+ A `db apply` whose lossy operations were all `dropped()` columns printed, with `VOLTRO_DESTRUCTIVE_OK` set,
556
+
557
+ ```text
558
+ db apply --plan: proceeding with acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)
559
+ ```
560
+
561
+ and then refused for want of `--yes`. Neither was true of those operations. A `dropped()` marker is the acknowledgement of its drop — the planner never blocks it — so the variable had nothing to relax, and the same plan applies with `--yes` alone. Read together, the two lines taught the opposite: that `--yes` acknowledges data loss, and that a variable kept "for this release" stops mattering once a pipeline passes `--yes`.
562
+
563
+ - The acknowledgement line appears only when the variable actually unblocked something. When it is set and the plan has nothing blocked, the apply says that instead — `VOLTRO_DESTRUCTIVE_OK is set but this plan has no blocked operation — it changed nothing in this run` — so a variable left in a deploy's configuration is noticed before it covers the next release's undeclared drop. - The `--yes` question on MySQL, MariaDB, SQLite and Turso now says what `--yes` does not answer: the lossy operations were authorised where they are declared, and a drop the schema does not declare stays blocked with or without it. - A refused first run ended with `nothing of this run remains … there was nothing to undo` while the deploy journal that run had created stood in the database. The journal is the runner's own and stays, hidden from every plan; the line now says what it is about and names the table it keeps.
564
+ - **A table whose key is spelled `(id)` can be written by id again** — `@voltro/runtime`
565
+
566
+ `update(table, '<id>', …)` and `delete(table, '<id>')` were refused on any table with an explicit `.primaryKey([...])`, including `.primaryKey(['id'])` — whose key IS the `id` column. The refusal's own message read "has no `id` column — its primary key is (id)". It exists for keys a string cannot name, `(userId, orgId)` or a single column shared with a parent, and those are still refused.
567
+
568
+ Framework tables declare their key this way, so the store a handler holds could not write them by id: `storage.put({ operationId })` failed at its reservation, and clearing a tenant selection threw, turning a revoked membership into `Unauthenticated` instead of a return to the home tenant. The raw store wrote all of them fine, which is why suites built on it stayed green.
569
+ - **`voltro doctor` no longer calls a retry loop an N+1** — `@voltro/cli`
570
+
571
+ `n-plus-one` already ignored reads inside `while` and `do` loops — they advance a cursor or retry, which proves no read per row. `for (;;)` is `while (true)` spelled differently and was reported: a handler that rolls a random code until one is free, one read per attempt over no collection, came back as an N+1. A `for` with no condition is now treated like the `while` it is; a counted `for` over input is still reported.
572
+ - **A failed AI call says why — the driver code joins the message** — `@voltro/ai`
573
+
574
+ A provider failure became an `AiError` carrying only the top-level message, so a dead upstream read `fetch failed` with nothing under it. The error now keeps the raw failure as `cause`, and the codes of its chain (`ECONNREFUSED`, `CERT_HAS_EXPIRED`, …) join the message — including through the SDK's retry wrapper, where the chain continues under `lastError`. Structured calls that used to report only `structured provider call failed` now carry the provider's message as well.
575
+
576
+ `cause` is not enumerable and not a schema field, so it stays on the server: the request body, headers and socket address it can hold never reach an encoded error.
577
+ - **Prepare a new backfill without submitting it** — `@voltro/devtools-ui`
578
+
579
+ Preparing a new schedule backfill now only clears the previous request. The click cannot also submit the newly rendered start control; operators explicitly start and, when required, confirm the new range.
580
+ - **Preserve timestamp precision in the data editor** — `@voltro/devtools-ui`
581
+
582
+ Cell and record timestamp editors preserve milliseconds. Saving an unchanged inline draft issues no write, including during repeated daylight-saving hours. Date validation rejects impossible calendar days instead of silently normalizing them into another month.
583
+ - **Keep record edits isolated from incoming live snapshots** — `@voltro/devtools-ui`
584
+
585
+ Record editors retain their opening snapshot when live rows change. Untouched fields no longer become stale patches that overwrite external changes. Open drafts remain available when their row leaves the current page.
586
+ - **A first production deploy no longer refuses over a table the apply created** — `@voltro/database`, `@voltro/cli`
587
+
588
+ `voltro db apply --plan` refused on its FIRST run against any database that predates the deploy journal, with a drift nobody caused:
589
+
590
+ ```text
591
+ db apply --plan: refusing — the live schema has drifted since this plan was generated.
592
+ planFrom=bfff4758e04fee2f live=d3d95a02dc514886
593
+ ```
594
+
595
+ Nothing had touched the database between the two commands. The drift came from the refusing run itself: `db apply` creates its own deploy journal (`_voltro_deploys`) before it holds the live fingerprint against the saved plan's, so it introspected a schema that now contained a table its own plan had listed as work still to do.
596
+
597
+ **It hit the only route production allows.** A bare `voltro db apply` is refused there, so `db plan --json` → `db apply --plan` is the prescribed path — and it could not complete on its first run. For a pre-upgrade job without retries that is a stopped rollout. The second run succeeds (the table exists by then), which is why this read like a fluke on a concurrency path rather than like something every first deploy meets.
598
+
599
+ **The fix is two halves, and either one alone is wrong.** The journal is no longer DECLARED in the framework table set, and it is hidden from the introspection snapshot — exactly how `_voltro_migration_ops`, the applier's other bookkeeping table created in the same run for the same reason, has always been treated.
600
+
601
+ - Hiding it while still declaring it is **worse than the original defect**: the planner then proposes `create-table _voltro_deploys` on every run, and `applyPlan`'s convergence proof refuses to record a fingerprint — every apply dies, not just the first. Measured, not reasoned: an integration test on a virgin database caught it. - Undeclaring it without hiding it leaves the fingerprint moving between plan and apply, which is the reported bug unchanged.
602
+
603
+ The cost is the one the bootstrap path already states for the other ledger: the journal's shape cannot evolve through the declarative differ. Affordable for the same reason — it records in-flight deploys, not history anyone rebuilds a schema from. An existing database keeps its journal: an undeclared `_voltro_*` table is never planned for a drop.
604
+
605
+ There was no workaround. No read-only command creates the table, so nothing could be run ahead of the plan; `|| (db plan; db apply)` would swallow a REAL drift just as thoroughly; and creating a framework-internal table by hand in a production database is not something a user should be asked to do.
606
+ - **The bulk-write rule sees the typed write helpers, which are now documented** — `@voltro/cli`
607
+
608
+ `voltro doctor`'s `bulk-write-in-loop` matched `store.insert(` and nothing else. Converting a loop to the typed helper made the finding disappear — and with it the reviewed decision attached to that finding, which the next run then reported as a `Stale review`.
609
+
610
+ A deployment that moved 107 write sites to the typed helpers hit exactly that on one loop and rolled the conversion BACK, with the right reasoning: trading a working rule for a typed check on four scalar fields is the wrong way round. Nobody should have to choose between our type safety and our linting.
611
+
612
+ The rule now matches the typed forms as well — `insertRow`, `insertRowEffect`, `upsertRowEffect`, `upsertRowOutcomeEffect`, `insertIgnoreRowOutcomeEffect` — and maps each onto the store verb it wraps, so the advice stays the one that fits (`insertMany` for a plain insert; one transaction around a conflict verb, which has no set-based form).
613
+
614
+ **The Effect helpers were documented nowhere, and that is fixed in the same change.** The two families live in different packages: the Promise forms `insertRow` / `upsertRow` are `@voltro/database`'s and were documented; the five Effect variants are `@voltro/runtime`'s and appeared on no page in either language. Knowing one family therefore sent readers looking for the other in the wrong package. The mutations page now carries both, with the split stated plainly, the typed error channel that a `Effect.promise` thunk throws away, and the `outcome` value — reading only the row and then recording a `created` event writes an audit entry for a write a conflict skipped.
615
+
616
+ That page already warned that adopting the typed form can blind tooling matching the old spelling. The warning was right and this rule was one of its victims.
617
+ - **A project with a file migration can be applied to an empty database** — `@voltro/database`
618
+
619
+ `voltro db apply` could not create the schema of a project carrying even ONE file under `migrations/`, against a database that did not have `_voltro_migration_plans` yet — every new environment, every reset local database, every hermetic test stack. On postgres it ended like this, and nothing was applied:
620
+
621
+ ```text
622
+ INFO file-based migration applying id=20260918_040000_example
623
+ ERROR db apply: a file-based migration FAILED — the schema diff was NOT run
624
+ ERROR db apply: rolled back — postgres applies a deploy as one transaction, so
625
+ nothing changed
626
+ ```
627
+
628
+ Two halves of one cause, and the file pass runs BEFORE the planner that would create the table both of them want.
629
+
630
+ **Reading it.** The discovery step asks the ledger which migrations already ran, as a plain `SELECT` with a catch beside it that read an absent table as "nothing applied". That is correct outside a transaction and wrong inside one: postgres and SQL Server apply a whole deploy as a single transaction, and a failed statement aborts it, so every later statement of the deploy died — starting with the migration's own savepoint. The catch turned an error the database does not forget into one only the runner had forgotten. The question is asked of the catalog now, which answers on an empty database instead of failing.
631
+
632
+ **Writing it.** Recording a migration inserts into that same table, so on an empty database `up()` succeeded and the row could not be written — the one outcome whose message tells you not to re-run. The runner creates the ledger itself when something is pending, the way the resume ledger has always been created: a run needs it before any schema work of its own has happened. Nothing is created for a project with no pending file migration.
633
+
634
+ This was not confined to the dialects that hold a deploy in one transaction. On MySQL, MariaDB, Turso and SQLite the same recording insert failed, without a poisoned transaction to make it loud. Measured on all six dialects, red before the fix and green after it.
635
+
636
+ **A `voltro dev` boot was never affected, and that is measured rather than reasoned:** it creates the bookkeeping tables BEFORE the file pass, so the ledger is already there, and it holds no deploy transaction for a failed statement to poison. A real boot against a virgin schema with a file migration present, on the unfixed code, applies the migration and comes up. So the command to re-run after upgrading is `voltro db apply` — a development boot will not have told you anything was wrong.
637
+
638
+ **The two ledger readers now declare the failure they always had.** `appliedFileMigrationIds` and `appliedFileMigrations` (from `@voltro/database/sql`) were typed `Effect<…, never>` while the read could fail — the catch beside them was exactly the bug above. Their error channel is `SqlError` now. This is filed `apiSurface: compatible` rather than breaking: the old `never` was unsound, no documented path, guide or example reaches either symbol, and inside the framework only `deployCompensation` calls them. A consumer who nonetheless annotated one of them `never` widens that annotation to `SqlError`; nothing else changes, and the runtime behaviour is the one the type now admits.
639
+ - **Two processes no longer run the same file migration** — `@voltro/database`
640
+
641
+ The runner read the pending file migrations before taking the migration lock and never again. A second replica that booted beside the first waited for the lock and then ran the list it had read while the first was still applying it: the body ran twice, and the second replica's boot died on the ledger row the first had written. It now re-reads the ledger under the lock, the same way schema migrations re-plan under theirs. A rollback of one migration from two places no longer runs its `down` twice either.
642
+
643
+ When a ledger row cannot be written because the row already exists, the message now says so — the migration IS recorded, by another process, and the body has run twice. It used to say the row was missing and that the next run would apply the migration again, which invited deleting a correct row.
644
+ - **Delete inspected rows atomically against their selected snapshots** — `@voltro/cli`, `@voltro/devtools-ui`
645
+
646
+ Single and bulk deletion now use one conditional batch request with complete selected-row snapshots. A changed or missing row refuses the entire transaction without partial deletion. Live updates preserve the selection baseline, errors retain the selection or dialog, and duplicate confirmation clicks cannot dispatch another request. Both dashboard hosts use the same batch contract; the sequential single-delete fallback is removed.
647
+ - **Reject stale data editor updates atomically** — `@voltro/cli`, `@voltro/devtools-ui`
648
+
649
+ Data editor updates carry the opening values of edited fields as required `expected` preconditions. Inspection compares them inside the write transaction, returning HTTP 409 for changed or deleted rows instead of overwriting concurrent changes. Cell editors retain their opening value during live refresh, and rejected writes retain the draft. Verified with Memory, Postgres, MySQL, MariaDB, SQL Server, SQLite and Turso. Deletion preconditions are not included.
650
+
651
+ Conditional reads reuse the framework row-lock query so namespace-bound stores lock the same physical table they update or delete.
652
+ - **The migration pool refusal names the variable it actually reads** — `@voltro/cli`, `@voltro/database`
653
+
654
+ A migration needs two connections — one holds the lock, the other runs the statements — and a pool of one is refused up front. The refusal then told you to fix it with a variable nothing reads:
655
+
656
+ ```text
657
+ migrate needs ≥ 2 connections, the pool has 1 (DB_POOL_MAX=1) …
658
+ Set DB_POOL_MAX to at least 2 for the migrating process.
659
+ ```
660
+
661
+ `DB_POOL_MAX` is read nowhere in the framework. The pool size comes from `DB_MAX_CONNECTIONS` (or `PG_MAX_CONNECTIONS`). Following the instruction literally set a variable with no effect, produced the identical refusal, and left no next step — the worst shape a fix line can take, because it looks like the answer while it moves nothing. Both messages name the real variable now, and the one at the failure adds the driver's own `maxConnections` for a pool sized in code rather than in the environment.
662
+
663
+ **The check that exists to catch exactly this could not see it.** The message-API guard verifies that a variable a message tells you to SET is read somewhere — but it matched `VOLTRO_*` only, so every `DB_*` and `PG_*` instruction was outside its reach. The pattern now covers the framework's database variables too, which is what makes this class of defect detectable rather than merely fixed once.
664
+ - **Offloaded calls carry their provider name to the dispatcher** — `@voltro/ai`
665
+
666
+ Every queued call on `_voltro_ai_inferences` stored `provider: 'unknown'`: the step read a key no provider config has. The dispatcher rebuilt the config from that column, so a real dispatcher resolved no model for any offloaded text, object, transcription or media call and gave up after its retries. The row and the step input now carry the config's `name`, and the dispatcher resolves the provider that was declared.
667
+ - **Keep Radix primitives on the web dev server React instance** — `@voltro/cli`
668
+
669
+ The web dev server processes Radix packages through Vite so workspace dependencies share its deduplicated React instance during SSR, preventing invalid hook calls in dashboard navigation.
670
+ - **Storage object claims are not carried between environments** — `@voltro/data-transfer`
671
+
672
+ The per-caller claims that hold a shared storage object were not classified for cross-environment transfer, so `voltro data` carried them by default. A claim names a physical key in one deployment's provider and a caller of that deployment; imported elsewhere, it kept the target retaining a key for a claimant it never had. The table is environment-local now, beside the storage operation and object-head tables it belongs with.
673
+ - **No production warning about anonymous tenants when one is configured** — `@voltro/cli`
674
+
675
+ With `auth.anonymousTenantId` set, every production boot warned that anonymous callers resolve to the tenant their own `x-tenant` header names. A configured anonymous tenant is the server's choice and ignores that header, so the warning stated the opposite of what the server does. It now appears only when the header really decides.
676
+ - **The junction-FK check no longer asks a dropped column for a reference** — `@voltro/cli`
677
+
678
+ `voltro doctor`'s junction-FK audit reported id-shaped columns that are plain scalars and asked for a `reference()`. It reported `dropped()` columns too:
679
+
680
+ ```text
681
+ ✗ junction FKs: 12 junction tables with an id column that is a plain text()
682
+ 'apiKeys': 'keyId' (sits beside a wired reference() on this table)
683
+ ```
684
+
685
+ where the declaration is `keyId: dropped().safe()`. A tombstone cannot carry a `reference()` — the marker exists so the column disappears — so the finding was unresolvable by construction. Two of twelve entries in that list could never be acted on, and a list with unresolvable entries gets read less as a whole. That argument came from the deployment that reported it and it is the reason this is worth fixing rather than documenting.
686
+
687
+ The information was already there and thrown away: `dropped()` builds a `text` column carrying `dropped: true`, and the mapping into the audit lifted only the name, the type and the external-id marker. It carries the tombstone flag now, and the audit skips those columns.
688
+ - **The pre-apply question no longer promises a rollback the dialect cannot do** — `@voltro/cli`
689
+
690
+ On the dialects that apply a plan one statement at a time, the confirmation you answer before a deploy said:
691
+
692
+ ```text
693
+ … if one fails, this run undoes what it applied (the schema back to its
694
+ pre-deploy shape, file migrations by their down) and stops.
695
+ ```
696
+
697
+ and the failure message in the SAME run said the opposite:
698
+
699
+ ```text
700
+ This dialect does NOT apply a plan atomically: those 20 are COMMITTED and
701
+ there is no rollback.
702
+ ```
703
+
704
+ The second one is right. The first is not occasionally wrong — the question is asked only where a plan is applied statement by statement, so the promise was wrong at every site it could ever be printed. Someone reading it went into the deploy believing a failure was free.
705
+
706
+ It now says what actually happens: the statements before the failure are COMMITTED, and the runner then ATTEMPTS a compensation. The attempt and its limits are stated together rather than one before the deploy and the other after the failure.
707
+
708
+ The `down` clause needed the same treatment. A migration declared `irreversible()` has no `down`, so for a project whose file migrations are all irreversible the sentence promised something that could not happen in any case. What no undo can reach — the rows a lossy operation removed, and every irreversible migration's effect — is named in the same breath.
709
+ - **`transactional()` no longer resolves over a transaction the database ended** — `@voltro/database`, `@voltro/sql-postgres`, `@voltro/sql-mysql`, `@voltro/sql-mssql`, `@voltro/sql-sqlite`
710
+
711
+ A work callback that caught a failed statement and carried on made `transactional()` resolve — and on Postgres nothing was committed: the failed statement aborts the transaction, every later one is refused, and COMMIT runs as ROLLBACK without raising. The rows that had succeeded were lost too, and the caller was told they were written.
712
+
713
+ It now fails with `TransactionAbortedError`, which names the caught failure and the working form. To skip a failing statement and keep the rest, run it inside `tx.savepoint(() => …)`: only that statement is rolled back. Code that catches store failures inside a transaction without a savepoint will now see this error where it used to see a silent loss.
714
+
715
+ The same guard runs on MySQL, MariaDB, SQL Server and SQLite, where it closes a different hole: a deadlock rolls the WHOLE transaction back and the session leaves it, so a caller that caught the deadlock and kept writing wrote in autocommit — durable at once, and out of reach of any later rollback. After such a failure no further statement is sent, and because a deadlock is retryable the work runs again from the start in a fresh transaction.
716
+ - **An app whose workflows are all internal no longer reports a false mismatch** — `@voltro/cli`
717
+
718
+ An app that declares at least one workflow and marks every one of them `internal: true` logged this on every page load:
719
+
720
+ ```text
721
+ [voltro] the api client this page loaded does not match what the 'app' server
722
+ binds: __voltro.workflow.cancel, __voltro.workflow.control,
723
+ __voltro.workflow.domainEvents and 10 more. Responses for those procedures may
724
+ decode against the wrong schema …
725
+ ```
726
+
727
+ Nothing was wrong with the schemas, and the advice the message gives — reload, delete `.framework/.vite` — could not help, because the two sides were answering different questions. Both boot paths bind the 13 workflow built-ins for **any** discovered workflow; the codegen emitted them only when one was reachable over the wire. An internal-only app therefore ran a server binding 13 procedures its generated client had never been given.
728
+
729
+ `internal: true` answers *who may start this workflow over the wire* — it is a start decision. The built-ins start nothing: they read and steer runs that already exist, which is why an internal workflow's runs stay inspectable. So the server was right, and the codegen now follows it.
730
+
731
+ The two questions are separate in the generator now rather than one flag doing both, and the narrower one still decides what it should: an app with no wire-exposed workflow gets no start lift and no unused import for one. An app with no workflow at all is unchanged — emitting the built-ins there would be the same mismatch in the mirror image.
732
+ - **Bind dashboard bulk actions to previewed workflow runs** — `@voltro/devtools-ui`
733
+
734
+ Bulk workflow actions use the eligible run IDs from their preview instead of selecting newly matching runs. Pending requests lock filters and block duplicate submissions; failures discard the preview while retaining the operator's inputs.
735
+ - **Report declined bulk workflow recovery actions accurately** — `@voltro/cli`
736
+
737
+ Bulk replay reports declined redrives with their original reason instead of counting them as successful. Retry requires an execution identity; a missing workflow runtime cannot produce a successful no-op. Accepted recovery actions do not imply completed workflow executions.
738
+ - **Refuse false recovery success for an unchanged retry identity** — `@voltro/cli`, `@voltro/devtools-ui`
739
+
740
+ Bulk retry reports an unchanged execution identity instead of claiming successful recovery of the original failed run. Dashboard retry guidance explains payload resubmission and idempotency rather than promising a fresh execution or empty journal.
741
+ - **Retain operator cancellation causes in control receipts** — `@voltro/cli`
742
+
743
+ Shared cancellation helpers persist the supplied reason and details even when generating a request ID. Repeating an explicit intent preserves its original cause; producer sweeps still require confirmed cancellation rather than mere acceptance.
744
+ - **Workflow controls ask the same access question as the workflow read feeds** — `@voltro/runtime`, `@voltro/cli`
745
+
746
+ The workflow built-ins decided access twice, in two places. The read feeds behind `useWorkflowRuns`, `useWorkflowRun`, `useWorkflowRunSteps` and `useWorkflowRunEvents` let a subject holding `admin:full` see every run; the control actions — `cancel`, `signal`, `update`, `resume` — compared tenants only, so the same subject could see a schedule's run in the list and was refused when it tried to cancel it.
747
+
748
+ Both halves now ask one function. What a caller may drive is what it may read:
749
+
750
+ - a system subject or one holding `admin:full` reads and drives every run; - a run the system started — a schedule's, say, or one of an `internal: true` workflow started from server code — belongs to the operator, and no user reaches it; - a run with a tenant is shared by that tenant's users.
751
+
752
+ Every signed-in subject carries a tenant, so an app that does not use tenants is one whose users share one: they see and drive each other's runs, and none of them reaches a system-started run.
753
+ - **Show complete bulk workflow recovery failures** — `@voltro/devtools-ui`
754
+
755
+ Both workflow bulk-action surfaces expose every per-run failure with its full identity and reason in paginated details. Recovery acceptance is labelled separately from successful workflow completion.
756
+ - **Resolve workflow retry through the original run owner** — `@voltro/cli`
757
+
758
+ Development and production workflow retry resolve the original run’s physical store before reading its payload, matching the Inspect path. Missing or ambiguous ownership refuses dispatch instead of searching the primary store.
759
+ - **Keep workflow selection confirmation bound to reviewed runs** — `@voltro/devtools-ui`
760
+
761
+ Changing checked workflow runs resets their bulk-action preview, including delayed responses for a previous selection. Apply targets only previewed eligible IDs. Pending actions lock controls and reject duplicate submissions; changed reasons and failed requests require a new preview.
762
+
763
+ ---
764
+
42
765
  ## [0.74.0] — 2026-09-18
43
766
 
44
767
  ### Added
@@ -1991,7 +1991,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1991
1991
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1992
1992
  ```
1993
1993
 
1994
- ## jose@6.2.10
1994
+ ## jose@6.2.12
1995
1995
 
1996
1996
  License: MIT
1997
1997
 
@@ -2539,7 +2539,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
2539
2539
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2540
2540
  ```
2541
2541
 
2542
- ## tailwind-merge@3.6.0
2542
+ ## tailwind-merge@3.7.0
2543
2543
 
2544
2544
  License: MIT
2545
2545
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/ui-shadcn",
3
- "version": "0.74.0",
3
+ "version": "0.76.0",
4
4
  "description": "Voltro's first-party shadcn/ui kit: Tailwind v4 design tokens (light + dark), 30+ primitives, layout compositions, styled widgets for the @voltro/ui seam, and the canonical theme/language preference-cookie helpers.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -52,13 +52,13 @@
52
52
  "@radix-ui/react-toggle-group": "^1.1.19",
53
53
  "@shikijs/langs": "^4.4.3",
54
54
  "@shikijs/themes": "^4.4.3",
55
- "@voltro/ui": "0.74.0",
55
+ "@voltro/ui": "0.76.0",
56
56
  "class-variance-authority": "^0.7.1",
57
57
  "clsx": "^2.1.1",
58
58
  "shiki": "^4.4.3",
59
- "tailwind-merge": "^3.6.0",
59
+ "tailwind-merge": "^3.7.0",
60
60
  "tailwindcss": "^4.3.3",
61
- "tw-animate-css": "^1.2.5"
61
+ "tw-animate-css": "^1.4.0"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "react": "^19.0.0"