@voltro/data-transfer 0.75.0 → 0.77.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,465 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.77.0] — 2026-09-22
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **Flow run rows record a cancellation request as `cancelling` until confirmed** — `@voltro/plugin-ai-flows`, `@voltro/devtools-ui`, `@voltro/cli`
47
+
48
+ `cancelFlow` used to write the flow row to `cancelled` before asking the engine, so a receipt reporting `engine: 'unreachable'` or `'unknown'` sat next to a row that read as a stopped run, and a reader of the row alone could not tell the difference. `RunStatus` gains `cancelling`: a live row now records the request as `cancelling` with `cancelledAt` as the request time and reads `cancelled` only once the engine confirmed — through the receipt's `engine: 'cancelled'`, the engine honouring the request at its next step boundary, its own run row turning cancelled, or a repeated confirmed request. Terminal rows are never resubmitted. The plugin documentation now states what a UI renders per `engine` outcome. Migration: extend exhaustive `RunStatus` switches and any filter that treated `cancelled` as the request marker; render `cancelling` as an open request and offer a retry when `engine` was `unreachable` or `unknown`. Only `succeeded`, `failed` and `cancelled` are terminal; `useFlowRun` reports `done` for those alone.
49
+
50
+ **`voltro update` carries you across this** — codemod `0.77.0/01_flow-cancelling-row-status`. 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.77.0).
51
+
52
+ ### Added
53
+
54
+ - **An `@source` glob that matches no file is now reported** — `@voltro/cli`
55
+
56
+ Tailwind generates a utility only when it finds the class in a scanned file, and a `@source` pointing at a directory that does not exist is not an error — it is a set with no members. The build stays green and the page renders with classes that name rules which were never generated. `voltro build` now warns for every `@source` glob that matches nothing, and `voltro doctor` reports the same. The usual cause is scanning a dependency at a path it does not publish: a package that ships only its bundle has no `src/` once installed, so `@source "../node_modules/<pkg>/src/**"` matches everything in a workspace (where the dependency is a symlink to its source tree) and nothing in CI. Scan the one path the package publishes — `@source "../node_modules/<pkg>/dist/*.js"` — and rebuild that dependency after editing it, since its bundle is what you scan.
57
+
58
+ ### Fixed
59
+
60
+ - **A failing admission placement no longer holds the drain at one tick per second** — `@voltro/cli`
61
+
62
+ Physical workflow admission drains every placement on a coordinated tick and backs off to a five-second reconciliation bound when a tick finds nothing to admit. A placement whose recovery failed on every tick — a release census page that threw, a placement that could not be opened — kept the tick at its base interval instead: the failure counted as "not idle", the interval never left one second, and the process wrote one schedule claim per second for as long as the condition lasted, with `scanned: 0` on every drain and every run terminal. Such a tick now reports `idle: true` beside its `placementFailures`, `discoveryProblems` and `releaseFailures`, backs off like a healthy idle tick, and retries the failing placement at the reconciliation bound; it is never disarmed, and the `physical recovery incomplete` warning still names the placement, the phase and the database's own reason. Incomplete discovery is unchanged: a further page is known work.
63
+
64
+ Two causes of a permanently failing release census are closed with it. The census pager compared row identities in code-unit order to detect a stalled cursor, while the store had ordered and cursor-filtered the page under its own collation; a correctly ordered page could be refused wholesale, and with it every global pool permit release behind it. The pager now walks a page as the store returns it and refuses only a page that repeats its cursor row or an identity within itself, naming both identities. And a census page that throws no longer pins its cursor: the next tick restarts that placement's census from the beginning, so an owner released behind the cursor since is visited again rather than waiting behind a page that fails on every read.
65
+ - **A file migration body runs under test without a database** — `@voltro/database`
66
+
67
+ `makeFileMigrationContext` accepts an in-memory store without a `sql` client, so a migration's `each`, `rewrite` (temporal guards included), `expect`, `skipUnless`, `store`, `log` and a Promise body under `transactional` run in a unit test against the framework's own in-memory store. The surfaces that genuinely need a connection — `raw()`, every `ctx.schema` operation, an Effect body under `transactional` — fail with the new typed `MigrationNeedsSqlClient`, whose message names the operation and what is missing. A placeholder handed in as `sql`, or a SQL-backed store arriving without its client, is refused at construction with the same error. Previously `rewrite` introspected the live schema unconditionally and a database-free test died with `TypeError: onDialectOrElse is not a function` from inside the bundle. `FileMigrationContextInput` is now the union of `FileMigrationContextLiveInput` (client + store) and `FileMigrationContextInMemoryInput` (a store without `run`); the runner's call shape is unchanged.
68
+ - **`voltro data restore` accepts an empty target and names a takeable remedy** — `@voltro/cli`, `@voltro/data-transfer`, `@voltro/database`
69
+
70
+ A native restore into a freshly created database was refused because the in-progress marker's table did not exist there, and the refusal said to run `voltro db apply` first — which on that target installs the current declared schema, the very state the artefact is about to replace. The restore now creates that one table itself, index- and trigger-free like the migration ledgers, and writes the marker into it before the first destructive statement; a refusal that remains carries the driver's own reason. `--no-marker` now means no marker on every target, warns every time, and no longer reports afterwards that "the restore artefact removed the in-progress marker and it could NOT be written back" over a run that never wrote one. On sqlite and turso the restore closes its bookkeeping connection before the file copy, removes the replaced file's `-wal` / `-shm` sidecars with it, and reopens bookkeeping afterwards: writes through the old connection landed the old database's WAL frames beside the new file, which SQLite replayed over it on the next open.
71
+ - **Rolling-deploy advisory skips tables the same plan creates** — `@voltro/database`, `@voltro/cli`
72
+
73
+ `voltro db plan` and `db apply` no longer flag an operation as unsafe under a rolling deploy when the table it targets is created by the same plan. A fresh deployment against an empty database used to print one "old instances may write values … the new CHECK rejects" line per enum-membership CHECK, because the classifier judged each operation alone and never asked whether the plan's own `create-table` list held the table. No instance on a previous release can touch a table that did not exist a moment earlier, so the rule covers every kind — CHECK, foreign key, unique constraint, `NOT NULL`, type change, JSON validation — not only the one reported. A mixed plan still flags the same operations on pre-existing tables. Every remaining verdict now names its premise (`instances on the previous release …`), and the JSON warning carries a `premise` field, so a deployment with no previous release can tell from the message alone that the advisory does not apply. `classifyRollingDeploySafety` accepts an optional plan context (`{ createdTables }`); `createdTablesOf` builds it from a plan.
74
+ - **The precompiled serve bundle boots with a trace exporter on** — `@voltro/cli`
75
+
76
+ `voltro serve` from the bundle `voltro build` writes died at boot whenever a trace exporter was active — `FRAMEWORK_TRACING=console` with `ConsoleSpanExporter is not a constructor`, an `OTEL_EXPORTER_OTLP_ENDPOINT` with `OTLPTraceExporter is not a constructor` — while the ring-only serve, dev and every test path booted fine. The OpenTelemetry SDK packages are CommonJS and are reached through a dynamic `import()`; the bundler splits such an import into a chunk that exposes only `default`, so every named member read off that namespace was `undefined`. The production bundler now routes a dynamic import of an inlined CommonJS package through an ESM facade that carries the same named exports Node gives it, so the console and OTLP exporters, the metrics readers and any other lazily imported CommonJS package construct as they do under development. Both exporters are now booted from a relocated production bundle in the bundle test matrix, and each has to deliver a span.
77
+
78
+ The facade has a measured cost, split without remainder against the previous release: the web start bundle grows 14 901 B (0.87 %) and the api serve bundle 28 552 B (0.34 %), both from the re-exported CommonJS surfaces; no file count moves. The remaining growth of the same release (22 B and 35 893 B) is ordinary source change. The artefact budgets are re-pinned to the measured sizes with the same 15 % headroom.
79
+ - **A store failure carries the sentence that explains it** — `@voltro/protocol`, `@voltro/database`, `@voltro/runtime`
80
+
81
+ `StoreOperationFailed.cause` reduced every error to its class name, so an eager load on a table with no relations registered arrived as `cause: "Error"` while the refusal's own sentence — naming the table, the missing `relations(...)` declaration and the likely fix — was only recoverable by unwrapping the value by hand. A sentence the framework authored now travels whole in `cause`; anything a driver raised is still reduced to its class and code, because the driver's text carries row data and socket addresses. The error's `message` is now one readable sentence (`query on 'x' failed: …`) instead of a JSON rendering of its fields, so a test reporter, `Cause.pretty` and the undeclared-error envelope all show the diagnosis. And when a statement fails and takes the connection with it, `EffectStore.transactional` now reports the statement's failure (with its code) instead of the rollback's uncoded error that used to replace it. `@voltro/database` exports `authoredError` / `authoredMessageOf` for a hand-written store that wants the same treatment.
82
+ - **A boot missing `@effect/opentelemetry` refuses once and names its cause** — `@voltro/runtime`, `@voltro/cli`
83
+
84
+ `@effect/opentelemetry` has been an optional peer of `@voltro/runtime` since 0.33.0, and the note that made it one said to skip the install unless traces or metrics are exported. That scope was too narrow: the tracer layer loads the OpenTelemetry SDK whenever a trace sink is installed, and `voltro dev` arms its in-process trace ring by default — so every `voltro dev` needs the package with no tracing variable set, and `voltro serve` needs it when its inspect trace ring or an exporter is on. Measured against the published 0.75.0 and 0.76.0 with the package absent: identical refusals, unrelated to `VOLTRO_TRACING_PERSIST`. Three things change. The refusal is now a marked boot refusal: printed once without a stack on every boot path, listing each cause that kept the layer active with the switch that removes it (the previous message named `FRAMEWORK_TRACING=off` for a ring that variable does not touch). `voltro doctor` reports `@effect/opentelemetry is not installed` when the installed runtime cannot see it, using the runtime's own lookup path, and fails the run unless `VOLTRO_TRACING_BUFFER=off` is set. And the corrected scope is re-issued as `0.77.0/03_tracing-peer`, printed for every project because tracing is configured outside the repository; the codemod text scan now also reads tracked `.env*` files, where those switches live. The golden churn is `otelImportFailure` gaining an optional second parameter, which no existing call can fail to compile against.
85
+ - **TypeScript 7 upgrade note: typed ESLint alias pair and removed tsconfig options** — `@voltro/cli`
86
+
87
+ The 0.76.0 dependency note recommended TypeScript `^7.0.2` and read as though a plain bump were sufficient. It is not for a workspace that lints with typescript-eslint: the current release line declares the peer `"typescript": ">=4.8.4 <6.1.0"` and exits 2 on load under TypeScript 7, before linting anything. TypeScript 7 also removed `"moduleResolution": "node"` (TS5108) and `baseUrl` (TS5102), which the note did not mention. A note under an already-published version reaches nobody who acted on it, so the corrected guidance is re-issued under `0.77.0/02_typescript7-side-by-side`: the side-by-side alias pair (`"@typescript/native": "npm:typescript@^7.0.2"`, `"typescript": "npm:@typescript/typescript6@^6.0.2"`) that keeps `tsc` on 7.0 while typed ESLint resolves the 6.0 API, the two removals with their replacements, and when a plain bump is fine. Gated on any `typescript` declaration or removed option, so it reaches workspaces that bumped, reverted, or have not bumped yet.
88
+
89
+ ### Internal (no consumer-facing effect)
90
+
91
+ - **A release moves `latest` only once every tarball is downloadable** — `@voltro/cli`
92
+
93
+ npm writes a new version into a package's metadata before the tarball behind it is downloadable from the CDN, and a consumer installing on `latest` inside that window sees a resolvable version whose download answers 404. The release now holds the promotion of `latest` until every published tarball answers 200 from the URL its own metadata publishes, and refuses to promote — leaving the previous release in place — if one does not within a bounded wait.
94
+
95
+ ---
96
+
97
+ ## [0.76.0] — 2026-09-21
98
+
99
+ ### ⚠ BREAKING
100
+
101
+ - **Align i18n peers and generated projects with current dependency majors** — `@voltro/cli`, `@voltro/i18n`, `@voltro/plugin-mail`, `@voltro/ai`, `@voltro/web`
102
+
103
+ `@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.
104
+
105
+ 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.
106
+
107
+ **`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).
108
+ - **Persist explicit workflow retry intents and return durable observations** — `@voltro/runtime`, `@voltro/cli`, `@voltro/devtools-ui`, `@voltro/voltro`
109
+
110
+ `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`.
111
+
112
+ 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.
113
+
114
+ 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.
115
+
116
+ 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.
117
+
118
+ 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.
119
+
120
+ Delayed retry preparation and errors preserve the currently inspected app’s journal when navigating between apps.
121
+ - **Flow cancellation preserves engine acknowledgements and completed results** — `@voltro/plugin-ai-flows`, `@voltro/cli`
122
+
123
+ `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.
124
+
125
+ **`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).
126
+ - **Signed private-file links bind to their storage mount** — `@voltro/plugin-storage`
127
+
128
+ 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.
129
+
130
+ **`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).
131
+ - **Budget refusals stay typed and zero checks require headroom** — `@voltro/ai`, `@voltro/workflow`, `@voltro/cli`, `@voltro/voltro`
132
+
133
+ `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.
134
+
135
+ **`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).
136
+ - **WebSocket gateways cancel pending setup and bound outbound writes** — `@voltro/protocol`, `@voltro/runtime`, `@voltro/cli`, `@voltro/voltro`
137
+
138
+ 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.
139
+ - **Bulk workflow CLI commits require a saved preview plan** — `@voltro/cli`, `@voltro/devtools-ui`
140
+
141
+ `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.
142
+
143
+ **`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).
144
+
145
+ ### Added
146
+
147
+ - **Explain workflow admission polling with live diagnostics** — `@voltro/runtime`, `@voltro/cli`
148
+
149
+ 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`.
150
+ - **Compose durable admission bindings atomically** — `@voltro/workflow`, `@voltro/cli`
151
+
152
+ 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.
153
+
154
+ Declare the internal retry-intent table through the shared workflow schema assembly so development and deployment migrations agree.
155
+
156
+ The shared admission worker recovers pending internal retry preparations in each physical workflow store, retaining deferred deadlines and terminal admission outcomes.
157
+ - **Storage upload input carries an optional operation identity** — `@voltro/devtools-ui`, `@voltro/cli`
158
+
159
+ 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.
160
+ - **Dispatch binary Inspect uploads before text decoding** — `@voltro/runtime`, `@voltro/cli`
161
+
162
+ 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.
163
+ - **Add single-replica targeting for Inspect actions** — `@voltro/cli`
164
+
165
+ 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.
166
+
167
+ Older dispatchers reject the dedicated path rather than ignoring a target query and executing an untargeted mutation.
168
+
169
+ 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.
170
+ - **Plugin inspection lists data-change, observability and surface hooks** — `@voltro/cli`
171
+
172
+ Include data-change, observability, REST-surface and procedure-surface hooks in plugin inspection metadata without invoking them.
173
+ - **Authenticated server-owned realtime voice and text sessions** — `@voltro/ai`
174
+
175
+ `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.
176
+ - **Inspect upload tickets accept a durable operation identity** — `@voltro/plugin-storage`
177
+
178
+ 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.
179
+ - **Streaming transcription with explicit raw-audio and partial-result contracts** — `@voltro/ai`
180
+
181
+ `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.
182
+ - **Restore database read shapes in JSON date fixtures** — `@voltro/testing`
183
+
184
+ `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.
185
+ - **Describe durable retry observations without conflating acceptance and completion** — `@voltro/protocol`, `@voltro/cli`, `@voltro/runtime`
186
+
187
+ 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.
188
+
189
+ Expose `ctx.workflows.readRetry(runId, requestId)` in development and production runtimes. Reading validates the requested identity and never dispatches another attempt.
190
+
191
+ 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.
192
+
193
+ 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.
194
+
195
+ ### Changed
196
+
197
+ - **Dashboard analytics activity measures real sink operations** — `@voltro/cli`
198
+
199
+ 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.
200
+ - **Dashboard uploads resume under a persisted operation identity** — `@voltro/devtools-ui`, `@voltro/cli`
201
+
202
+ 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.
203
+
204
+ Damaged recovery records can be explicitly discarded after confirmation. Changed or valid records are protected from this recovery action; storage removal failures remain visible.
205
+
206
+ 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).
207
+ - **Expectation observations carry the full declarative rule** — `@voltro/runtime`, `@voltro/devtools-ui`
208
+
209
+ 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.
210
+ - **Plugin badges report activation, not live health** — `@voltro/devtools-ui`
211
+
212
+ Clarify that plugin activation badges report successful startup activation, not live health or shutdown status.
213
+
214
+ Omit unavailable plugin counts during initial loading and failed reads rather than showing zero or a stale count.
215
+ - **Clarify terminal stream events versus the persisted done flag** — `@voltro/ai`
216
+
217
+ 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.
218
+ - **Distinguish stale workflow observations from dead runners** — `@voltro/workflow`
219
+
220
+ 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.
221
+
222
+ ### Fixed
223
+
224
+ - **Agent tool panels hide stale snapshots and explain disabled surfaces** — `@voltro/devtools-ui`
225
+
226
+ Hide stale agent-tool snapshot timestamps and provenance during loading and failed reads, including falsy error values.
227
+
228
+ Give disabled agent surfaces a dedicated explanation instead of unusable search and misleading empty inventories.
229
+
230
+ Use a tool-specific search label that describes the fields actually searched.
231
+ - **Handle aggregate dashboard refresh failures and duplicate submissions** — `@voltro/devtools-ui`
232
+
233
+ 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.
234
+ - **Report manual aggregate refresh failures and skipped runs** — `@voltro/cli`
235
+
236
+ 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.
237
+ - **Aggregate inspection and refresh reach serve through the shared router** — `@voltro/cli`
238
+
239
+ 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.
240
+ - **AI flow inspection reads carry a process observation scope** — `@voltro/plugin-ai-flows`
241
+
242
+ 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.
243
+ - **AI run details stay closable while their request is pending** — `@voltro/devtools-ui`
244
+
245
+ 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.
246
+ - **Analytics inspection behaves the same in development and serve** — `@voltro/cli`
247
+
248
+ 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.
249
+ - **Await asynchronous schedule Inspect snapshots before serialization** — `@voltro/cli`
250
+
251
+ 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.
252
+ - **Cache hit percentages follow the selected dashboard locale** — `@voltro/devtools-ui`
253
+
254
+ 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.
255
+ - **Reverse-ETL health and dead-letter reads keep their replica identity** — `@voltro/plugin-cdc-out`
256
+
257
+ 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.
258
+ - **Reverse-ETL retries refresh sink health before another attempt** — `@voltro/devtools-ui`
259
+
260
+ 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.
261
+ - **Comment threads offer read recovery without losing the search** — `@voltro/devtools-ui`
262
+
263
+ 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.
264
+ - **Aggregate refresh indicators stay hidden during pending or failed reads** — `@voltro/devtools-ui`
265
+
266
+ Hide aggregate refresh indicators during pending or failed reads and preserve text error messages instead of replacing them with an unknown error.
267
+ - **Analytics panels separate loading, failed reads and missing data** — `@voltro/devtools-ui`
268
+
269
+ 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.
270
+ - **Small compute-budget amounts survive dashboard formatting** — `@voltro/devtools-ui`
271
+
272
+ 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.
273
+ - **Experiment rates read as percentages and differences as percentage points** — `@voltro/devtools-ui`
274
+
275
+ 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.
276
+ - **Inspect credential badges follow current access observations** — `@voltro/devtools-ui`
277
+
278
+ 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.
279
+ - **The inspect credential form admits one save at a time** — `@voltro/devtools-ui`
280
+
281
+ 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.
282
+ - **Plugin hooks count towards the cumulative invocation metrics** — `@voltro/devtools-ui`
283
+
284
+ 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.
285
+ - **Route boundaries resolve in root-to-page inheritance order** — `@voltro/devtools-ui`
286
+
287
+ 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.
288
+ - **The route inventory distinguishes loading, failure and an empty result** — `@voltro/devtools-ui`
289
+
290
+ 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.
291
+ - **Missing invocation latency reads as unknown instead of zero** — `@voltro/devtools-ui`
292
+
293
+ 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.
294
+ - **Keep streamed loader handoff scripts out of client recovery renders** — `@voltro/web`
295
+
296
+ `<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.
297
+ - **Identify tasks in development claim-change logs** — `@voltro/cli`
298
+
299
+ 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.
300
+ - **Actions get their own Devtools scope and pending calls stay first** — `@voltro/client`, `@voltro/devtools`, `@voltro/web`
301
+
302
+ 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`.
303
+ - **Devtools panels keep consistent spacing in narrow docks** — `@voltro/devtools`
304
+
305
+ 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.
306
+ - **Trace inspection keeps selected records and exposes recorded span details** — `@voltro/devtools`
307
+
308
+ 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.
309
+ - **Doctor distinguishes query results from per-row reads** — `@voltro/cli`
310
+
311
+ 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.
312
+ - **Environment inspection stops returning declared defaults and examples** — `@voltro/cli`
313
+
314
+ Exclude declared defaults and examples from environment inspection responses. Preserve default-satisfaction and missing-required counts while projecting only dashboard metadata and boolean states.
315
+ - **The environment panel separates loading, errors and empty declarations** — `@voltro/devtools-ui`
316
+
317
+ Distinguish environment inspection loading, errors, and confirmed empty declarations. Hide stale variable states and counts after read failures and restore them on recovery.
318
+
319
+ Improve environment status contrast in light themes and wrap long names and descriptions on narrow screens.
320
+ - **Expectations re-read current rows when a change omits a row image** — `@voltro/runtime`
321
+
322
+ 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.
323
+ - **Expectation null rates and freshness carry their units** — `@voltro/devtools-ui`
324
+
325
+ 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.
326
+ - **Running experiments refresh from current rows on incomplete changes** — `@voltro/runtime`
327
+
328
+ 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.
329
+ - **Apply flag audit filters before the result limit** — `@voltro/plugin-flags`
330
+
331
+ 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.
332
+ - **Reconcile unconfirmed flag toggles before allowing another change** — `@voltro/devtools-ui`
333
+
334
+ 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.
335
+ - **Prevent duplicate flag toggles before the next dashboard render** — `@voltro/devtools-ui`
336
+
337
+ 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.
338
+ - **Commit durable flag toggles and audit entries atomically** — `@voltro/plugin-flags`
339
+
340
+ 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.
341
+
342
+ Concurrent durable toggles lock the override row before reading its previous enabled state, including the first override. Updates preserve targeting and rollout fields.
343
+
344
+ 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.
345
+
346
+ Overlapping durable reloads discard superseded responses instead of allowing a delayed earlier snapshot to overwrite newer flag state.
347
+ - **Preserve explicitly supplied gateway media dimensions** — `@voltro/ai`
348
+
349
+ 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.
350
+ - **Custom consent-store reads accept explicit replica requests** — `@voltro/plugin-governance`
351
+
352
+ 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.
353
+ - **Governance exports, erasures and sweeps run one at a time** — `@voltro/devtools-ui`
354
+
355
+ 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.
356
+ - **Subject export and erasure bodies are validated before data access** — `@voltro/plugin-governance`
357
+
358
+ 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.
359
+ - **Offloaded inference recovery retains paid tasks and pending signals** — `@voltro/ai`
360
+
361
+ 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.
362
+ - **Retain a single replica target in dashboard download tickets** — `@voltro/cli`
363
+
364
+ 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.
365
+ - **Any defined inspect error counts as a failed section read** — `@voltro/devtools-ui`
366
+
367
+ 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.
368
+ - **Bind Inspect upload tickets to the issuing replica** — `@voltro/plugin-storage`
369
+
370
+ 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.
371
+ - **Inspection headers hide stale timestamps while a read is pending** — `@voltro/devtools-ui`
372
+
373
+ Hide stale timestamps and provenance in shared inspection page headers during loading or failed reads, including falsy error values.
374
+ - **Migration rewrite guards retain full timestamp precision** — `@voltro/database`
375
+
376
+ `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.
377
+ - **Migration inspection plans against the active database dialect** — `@voltro/cli`
378
+
379
+ 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.
380
+ - **Migration inspection and db drift share the planner live-table scope** — `@voltro/database`, `@voltro/cli`
381
+
382
+ 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.
383
+ - **Refuse known undersized migration pools before taking the lock** — `@voltro/database`, `@voltro/sql-postgres`
384
+
385
+ 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.
386
+ - **Moderation decisions reconcile against a fresh queue read** — `@voltro/devtools-ui`
387
+
388
+ 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.
389
+ - **Malformed moderation decision bodies are rejected before lookup** — `@voltro/plugin-moderation`
390
+
391
+ Reject malformed moderation decision bodies and non-string review IDs before looking up or changing a review.
392
+ - **Finite durable waits suspend inside activities** — `@voltro/workflow`, `@voltro/ai`
393
+
394
+ `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.
395
+ - **Notification, comment and billing panels declare process scopes** — `@voltro/plugin-notifications`, `@voltro/plugin-comments`, `@voltro/plugin-billing`
396
+
397
+ 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.
398
+ - **Queue search covers policies, activity and produced counts** — `@voltro/devtools-ui`
399
+
400
+ 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.
401
+ - **Queue topics count under their literal names** — `@voltro/plugin-queue`
402
+
403
+ 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.
404
+ - **Replica panels survive a temporary fleet inventory outage** — `@voltro/devtools-ui`
405
+
406
+ 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.
407
+ - **Resumable uploads enforce size limits before buffering chunk bodies** — `@voltro/plugin-storage`
408
+
409
+ 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.
410
+ - **Workflow retry intents are classified as environment-local** — `@voltro/data-transfer`
411
+
412
+ `_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.
413
+ - **Keep schedule inspection usable when snapshots are malformed** — `@voltro/devtools-ui`
414
+
415
+ 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.
416
+ - **Schema convergence reports unavailable without a live SQL connection** — `@voltro/cli`
417
+
418
+ 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.
419
+ - **Serialize dashboard search maintenance actions and reload their status** — `@voltro/devtools-ui`
420
+
421
+ 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.
422
+ - **Search maintenance requests are validated before any data access** — `@voltro/plugin-search`
423
+
424
+ Validate search maintenance requests before accessing data: reject malformed bodies and repair limits, and only reindex explicitly configured source tables.
425
+ - **Search index status and drift reads declare a process scope** — `@voltro/plugin-search`
426
+
427
+ 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.
428
+ - **Production logs delivered row changes at debug level, like development** — `@voltro/cli`
429
+
430
+ `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.
431
+ - **Migration history reads use SQL Server pagination syntax** — `@voltro/cli`
432
+
433
+ 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.
434
+ - **An uncertain storage upload can be checked without resending bytes** — `@voltro/devtools-ui`
435
+
436
+ 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.
437
+ - **Preserve storage reads in replica selection** — `@voltro/plugin-storage`, `@voltro/devtools-ui`
438
+
439
+ 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.
440
+
441
+ The shared Storage panel presents multiple selected replicas as labelled observations instead of passing non-composable inventories to the generic fleet projection.
442
+ - **Preserve gateway charges when structured output is rejected** — `@voltro/ai`
443
+
444
+ 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.
445
+ - **Honor both turn and SDK cancellation in tool calls** — `@voltro/ai`
446
+
447
+ 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.
448
+ - **Recent traces prioritize application activity over routine housekeeping** — `@voltro/cli`, `@voltro/devtools`
449
+
450
+ 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.
451
+ - **Database spans retain their request context across runtime boundaries** — `@voltro/runtime`
452
+
453
+ 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.
454
+ - **Native bundle dependencies resolve through transitive driver packages** — `@voltro/cli`
455
+
456
+ 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.
457
+ - **Expired credentials retain their typed RPC failure** — `@voltro/protocol`, `@voltro/runtime`
458
+
459
+ 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.
460
+ - **Webhook tables and counts hide after a failed snapshot read** — `@voltro/devtools-ui`
461
+
462
+ 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.
463
+ - **Invalid webhook commands differ from missing deliveries and failures** — `@voltro/cli`
464
+
465
+ 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.
466
+ - **Webhook snapshots project explicit fields instead of database rows** — `@voltro/cli`
467
+
468
+ 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.
469
+ - **Failed webhook reads surface instead of reporting empty activity** — `@voltro/cli`
470
+
471
+ 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.
472
+ - **Undeclared webhook storage is distinct from a failed database read** — `@voltro/cli`, `@voltro/devtools-ui`
473
+
474
+ 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.
475
+ - **Refuse webhook management actions for missing target IDs** — `@voltro/plugin-webhooks`, `@voltro/cli`
476
+
477
+ 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.
478
+ - **Mint compatible webhook signing secrets for subscriptions and rotation** — `@voltro/plugin-webhooks`
479
+
480
+ 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.
481
+
482
+ 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.
483
+ - **Preserve webhook subscription drafts during connection failures** — `@voltro/devtools-ui`
484
+
485
+ 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.
486
+ - **Bind durable admission requests to their original payload** — `@voltro/workflow`
487
+
488
+ 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.
489
+
490
+ ### Internal (no consumer-facing effect)
491
+
492
+ - **Resolve standard library declarations through the JavaScript compiler** — `@voltro/build`
493
+
494
+ 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.
495
+ - **Verify durable workflow retry recovery across process crashes** — `@voltro/cli`
496
+
497
+ 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.
498
+
499
+ ---
500
+
42
501
  ## [0.75.0] — 2026-09-19
43
502
 
44
503
  ### ⚠ BREAKING
@@ -9,7 +9,7 @@ Generated from the resolved runtime dependency closure (119 packages).
9
9
 
10
10
  ---
11
11
 
12
- ## @aws-sdk/checksums@3.1000.29
12
+ ## @aws-sdk/checksums@3.1001.0
13
13
 
14
14
  License: Apache-2.0
15
15
 
@@ -217,7 +217,7 @@ Apache License
217
217
  limitations under the License.
218
218
  ```
219
219
 
220
- ## @aws-sdk/client-s3@3.1126.0
220
+ ## @aws-sdk/client-s3@3.1134.0
221
221
 
222
222
  License: Apache-2.0
223
223
 
@@ -425,7 +425,7 @@ Apache License
425
425
  limitations under the License.
426
426
  ```
427
427
 
428
- ## @aws-sdk/core@3.977.9
428
+ ## @aws-sdk/core@3.978.0
429
429
 
430
430
  License: Apache-2.0
431
431
 
@@ -633,7 +633,7 @@ Apache License
633
633
  limitations under the License.
634
634
  ```
635
635
 
636
- ## @aws-sdk/credential-provider-env@3.972.70
636
+ ## @aws-sdk/credential-provider-env@3.972.71
637
637
 
638
638
  License: Apache-2.0
639
639
 
@@ -841,13 +841,13 @@ Apache License
841
841
  limitations under the License.
842
842
  ```
843
843
 
844
- ## @aws-sdk/credential-provider-http@3.972.72
844
+ ## @aws-sdk/credential-provider-http@3.972.73
845
845
 
846
846
  License: Apache-2.0
847
847
 
848
848
  _(no bundled license text found in the package; see its npm page.)_
849
849
 
850
- ## @aws-sdk/credential-provider-ini@3.973.15
850
+ ## @aws-sdk/credential-provider-ini@3.973.16
851
851
 
852
852
  License: Apache-2.0
853
853
 
@@ -1055,13 +1055,13 @@ Apache License
1055
1055
  limitations under the License.
1056
1056
  ```
1057
1057
 
1058
- ## @aws-sdk/credential-provider-login@3.972.77
1058
+ ## @aws-sdk/credential-provider-login@3.972.78
1059
1059
 
1060
1060
  License: Apache-2.0
1061
1061
 
1062
1062
  _(no bundled license text found in the package; see its npm page.)_
1063
1063
 
1064
- ## @aws-sdk/credential-provider-node@3.972.82
1064
+ ## @aws-sdk/credential-provider-node@3.972.83
1065
1065
 
1066
1066
  License: Apache-2.0
1067
1067
 
@@ -1269,7 +1269,7 @@ Apache License
1269
1269
  limitations under the License.
1270
1270
  ```
1271
1271
 
1272
- ## @aws-sdk/credential-provider-process@3.972.70
1272
+ ## @aws-sdk/credential-provider-process@3.972.71
1273
1273
 
1274
1274
  License: Apache-2.0
1275
1275
 
@@ -1477,7 +1477,7 @@ Apache License
1477
1477
  limitations under the License.
1478
1478
  ```
1479
1479
 
1480
- ## @aws-sdk/credential-provider-sso@3.973.14
1480
+ ## @aws-sdk/credential-provider-sso@3.973.15
1481
1481
 
1482
1482
  License: Apache-2.0
1483
1483
 
@@ -1685,7 +1685,7 @@ Apache License
1685
1685
  limitations under the License.
1686
1686
  ```
1687
1687
 
1688
- ## @aws-sdk/credential-provider-web-identity@3.972.76
1688
+ ## @aws-sdk/credential-provider-web-identity@3.972.77
1689
1689
 
1690
1690
  License: Apache-2.0
1691
1691
 
@@ -1893,7 +1893,7 @@ Apache License
1893
1893
  limitations under the License.
1894
1894
  ```
1895
1895
 
1896
- ## @aws-sdk/lib-storage@3.1126.0
1896
+ ## @aws-sdk/lib-storage@3.1134.0
1897
1897
 
1898
1898
  License: Apache-2.0
1899
1899
 
@@ -2101,7 +2101,7 @@ Apache License
2101
2101
  limitations under the License.
2102
2102
  ```
2103
2103
 
2104
- ## @aws-sdk/middleware-sdk-s3@3.972.75
2104
+ ## @aws-sdk/middleware-sdk-s3@3.972.76
2105
2105
 
2106
2106
  License: Apache-2.0
2107
2107
 
@@ -2309,13 +2309,13 @@ Apache License
2309
2309
  limitations under the License.
2310
2310
  ```
2311
2311
 
2312
- ## @aws-sdk/nested-clients@3.997.44
2312
+ ## @aws-sdk/nested-clients@3.997.45
2313
2313
 
2314
2314
  License: Apache-2.0
2315
2315
 
2316
2316
  _(no bundled license text found in the package; see its npm page.)_
2317
2317
 
2318
- ## @aws-sdk/s3-request-presigner@3.1126.0
2318
+ ## @aws-sdk/s3-request-presigner@3.1134.0
2319
2319
 
2320
2320
  License: Apache-2.0
2321
2321
 
@@ -2731,7 +2731,7 @@ Apache License
2731
2731
  limitations under the License.
2732
2732
  ```
2733
2733
 
2734
- ## @aws-sdk/token-providers@3.1116.0
2734
+ ## @aws-sdk/token-providers@3.1129.0
2735
2735
 
2736
2736
  License: Apache-2.0
2737
2737
 
@@ -3956,7 +3956,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3956
3956
  SOFTWARE.
3957
3957
  ```
3958
3958
 
3959
- ## @effect/platform-node@0.108.1
3959
+ ## @effect/platform-node@0.108.2
3960
3960
 
3961
3961
  License: MIT
3962
3962
 
@@ -8848,7 +8848,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
8848
8848
  SOFTWARE.
8849
8849
  ```
8850
8850
 
8851
- ## @upstash/redis@1.38.3
8851
+ ## @upstash/redis@1.38.4
8852
8852
 
8853
8853
  License: MIT
8854
8854
 
@@ -9764,7 +9764,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9764
9764
  SOFTWARE.
9765
9765
  ```
9766
9766
 
9767
- ## jose@6.2.10
9767
+ ## jose@6.2.12
9768
9768
 
9769
9769
  License: MIT
9770
9770
 
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Cause as e, Data as t, Effect as n, Exit as r, Metric as i, MetricBoundaries as a, Runtime as o, Schedule as s, Schema as c, Stream as l } from "effect";
2
2
  import { DATA_TRANSFERS_TABLE as u, REPLACE_IN_PROGRESS_TABLE as d, STAGING_TABLE_PREFIX as f, _voltroDataTransfersTable as p, classifyConstraintViolation as m, derivedRowId as h, ensureTableRegistered as g, eq as _, extractDbCause as v, getTable as y, inSet as b, isNotNull as x, or as S, recordsTable as C, registerTable as ee, streamTable as w, suspendWriteRecorders as te, suspendWriteRecordersEffect as ne, unregisterTable as re } from "@voltro/database";
3
3
  import { fingerprintSchema as T } from "@voltro/database/sql";
4
- import { createCipheriv as E, createDecipheriv as ie, createHash as ae, createHmac as oe, randomBytes as se, randomUUID as ce, scrypt as le } from "node:crypto";
5
- import { copyFile as D, createReadStream as ue, createWriteStream as O, existsSync as de, rename as fe, statSync as k, unlinkSync as pe } from "node:fs";
4
+ import { createCipheriv as ie, createDecipheriv as ae, createHash as oe, createHmac as se, randomBytes as ce, randomUUID as le, scrypt as ue } from "node:crypto";
5
+ import { copyFile as E, createReadStream as de, createWriteStream as D, existsSync as fe, rename as pe, statSync as O, unlinkSync as k } from "node:fs";
6
6
  import { STORAGE_REFS_TABLE as A, fromNodeReadable as me, getObjectStream as j, putObjectStream as he, toNodeReadable as M } from "@voltro/plugin-storage";
7
7
  import * as N from "node:zlib";
8
8
  import { PassThrough as P, Readable as ge } from "node:stream";
@@ -57,7 +57,7 @@ var Ce = (e) => {
57
57
  return r.classes?.[e] ?? we[e] ?? "redact";
58
58
  }
59
59
  return n?.safe ? r.onSafe ?? "keep" : n?.columnType === "id" || n?.columnType === "reference" ? "keep" : r.onUnclassified ?? "error";
60
- }, De = (e, t) => oe("sha256", e).update(t == null ? "∅" : String(t)).digest(), V = (e, t) => e.readUInt32BE(t * 4 % (e.length - 4)), Oe = [
60
+ }, De = (e, t) => se("sha256", e).update(t == null ? "∅" : String(t)).digest(), V = (e, t) => e.readUInt32BE(t * 4 % (e.length - 4)), Oe = [
61
61
  "Ada",
62
62
  "Alan",
63
63
  "Grace",
@@ -319,6 +319,7 @@ var Ce = (e) => {
319
319
  ["_voltro_workflow_control_requests", "cancel/resume traffic issued against executions HERE — a foreign row claims an operation nobody asked the target to perform"],
320
320
  ["_voltro_workflow_queued_cancellations", "cancellations waiting on a run that is queued HERE — a foreign row cancels something the target has not queued"],
321
321
  ["_voltro_workflow_restart_requests", "restarts pending against this deployment's generations — a foreign row restarts a generation the target never entered"],
322
+ ["_voltro_workflow_retry_requests", "retry intents an operator raised against runs HERE, keyed by their own request id — a foreign row binds the target to an intent nobody raised against it, and lets a later observation read somebody else's execution"],
322
323
  ["_voltro_workflow_parent_closures", "which parents are still waiting on children in flight HERE — a foreign row holds a parent open against a child the target is not running"],
323
324
  ["_voltro_workflow_failure_reports", "failures being handed off to a runner HERE — a foreign row makes the target take delivery of somebody else's failure"],
324
325
  ["_voltro_cdc_log", "the change rows this deployment captured from its own database — a foreign row replays a change that never happened here"],
@@ -565,7 +566,7 @@ var Ce = (e) => {
565
566
  objectBytes: o,
566
567
  missing: u
567
568
  };
568
- }), ht = (e) => me("bundle", () => ue(e)).pipe(l.decodeText(), l.splitLines, l.filter((e) => e.trim().length > 0), l.map((e) => JSON.parse(e)), l.mapError((t) => t instanceof G ? t : new G({
569
+ }), ht = (e) => me("bundle", () => de(e)).pipe(l.decodeText(), l.splitLines, l.filter((e) => e.trim().length > 0), l.map((e) => JSON.parse(e)), l.mapError((t) => t instanceof G ? t : new G({
569
570
  reason: `asset index unreadable: ${String(t)}`,
570
571
  path: e
571
572
  }))), gt = (e) => n.gen(function* () {
@@ -1441,20 +1442,20 @@ voltro data import <this-directory> --target api --api-url <url> --token <secret
1441
1442
  for (let e of n) e in t && (r ??= { ...t }, delete r[e]);
1442
1443
  return r ?? t;
1443
1444
  }, T = (e, t, n, r = !1) => {
1444
- let i = k ? X(t.name) : t.name, a = t.primaryKey, o = w(t.name, n);
1445
+ let i = O ? X(t.name) : t.name, a = t.primaryKey, o = w(t.name, n);
1445
1446
  return f === "append" ? _ === "fail" ? e.insert(i, o) : e.insertIgnore(i, o, { conflictColumns: [a] }) : f === "replace" ? r ? e.upsert(i, o, { conflictColumns: [a] }) : e.insert(i, o) : e.upsert(i, o, { conflictColumns: [a] });
1446
- }, E = (e, t) => e.savepoint ? e.savepoint(t) : t(), ie = 0, ae = (t, n, r) => {
1447
- r.unclassified && (ie++, !(ie > 3) && e.onWarn?.(`row refused with no driver detail — table '${t.name}', row '${String(n.id ?? "?")}'. The full rendering follows so the LAYER that refused is on the record; it is logged, never returned. ${r.raw}` + (ie === 3 ? " (further unclassified refusals are counted, not logged)" : "")));
1448
- }, oe = (e, t, r = !0) => n.tryPromise({
1447
+ }, ie = (e, t) => e.savepoint ? e.savepoint(t) : t(), ae = 0, oe = (t, n, r) => {
1448
+ r.unclassified && (ae++, !(ae > 3) && e.onWarn?.(`row refused with no driver detail — table '${t.name}', row '${String(n.id ?? "?")}'. The full rendering follows so the LAYER that refused is on the record; it is logged, never returned. ${r.raw}` + (ae === 3 ? " (further unclassified refusals are counted, not logged)" : "")));
1449
+ }, se = (e, t, r = !0) => n.tryPromise({
1449
1450
  try: e,
1450
1451
  catch: (e) => On(e)
1451
- }).pipe(n.map(() => !0), n.catchAll((e) => n.sync(() => (r && (t.reason = e.reason, ae(t.entry, t.row, e)), !1)))), se = (e) => {
1452
+ }).pipe(n.map(() => !0), n.catchAll((e) => n.sync(() => (r && (t.reason = e.reason, oe(t.entry, t.row, e)), !1)))), ce = (e) => {
1452
1453
  let t = Object.entries(e.entry.columnTypes).filter(([t, n]) => n === "reference" && e.row[t] !== null && e.row[t] !== void 0).map(([e]) => e);
1453
1454
  if (t.length !== 0) return {
1454
1455
  ...e.row,
1455
1456
  ...Object.fromEntries(t.map((e) => [e, null]))
1456
1457
  };
1457
- }, ce = (e) => {
1458
+ }, le = (e) => {
1458
1459
  let t = new Set(e.map((e) => String(e.row[e.entry.primaryKey]))), n = (e) => Object.entries(e.entry.columnTypes).some(([n, r]) => {
1459
1460
  if (r !== "reference") return !1;
1460
1461
  let i = e.row[n];
@@ -1482,32 +1483,32 @@ voltro data import <this-directory> --target api --api-url <url> --token <secret
1482
1483
  primary: t.primary
1483
1484
  })).sort((e, t) => t.refused - e.refused || e.table.localeCompare(t.table)).slice(0, 100)
1484
1485
  });
1485
- }, le = (e, t) => n.gen(function* () {
1486
+ }, ue = (e, t) => n.gen(function* () {
1486
1487
  let r = t, i = [], a = !0;
1487
1488
  for (; r.length > 0 && a;) {
1488
1489
  a = !1;
1489
1490
  let t = [];
1490
- for (let n of r) (yield* oe(() => E(e, () => T(e, n.entry, n.row, !0)), n)) ? a = !0 : t.push(n);
1491
+ for (let n of r) (yield* se(() => ie(e, () => T(e, n.entry, n.row, !0)), n)) ? a = !0 : t.push(n);
1491
1492
  if (!a && t.length > 0) for (let n = t.length - 1; n >= 0; n--) {
1492
- let r = t[n], o = se(r);
1493
- o !== void 0 && (yield* oe(() => E(e, () => T(e, r.entry, o, !0)), r, !1)) && (i.push(r), t.splice(n, 1), a = !0);
1493
+ let r = t[n], o = ce(r);
1494
+ o !== void 0 && (yield* se(() => ie(e, () => T(e, r.entry, o, !0)), r, !1)) && (i.push(r), t.splice(n, 1), a = !0);
1494
1495
  }
1495
1496
  r = t;
1496
1497
  }
1497
- for (let t of i) (yield* oe(() => E(e, () => e.upsert(t.entry.name, t.row, { conflictColumns: [t.entry.primaryKey] })), t)) || (r = [...r, t]);
1498
- if (r.length > 0) return yield* n.fail(ce(r));
1499
- }), D = (() => {
1498
+ for (let t of i) (yield* se(() => ie(e, () => e.upsert(t.entry.name, t.row, { conflictColumns: [t.entry.primaryKey] })), t)) || (r = [...r, t]);
1499
+ if (r.length > 0) return yield* n.fail(le(r));
1500
+ }), E = (() => {
1500
1501
  let t = e.targetDialect;
1501
1502
  return t === "postgres" || t === "mysql" || t === "mariadb" || t === "sqlite" || t === "mssql" ? t : void 0;
1502
- })(), O = qn({
1503
+ })(), D = qn({
1503
1504
  mode: f,
1504
1505
  ...e.targetDialect === void 0 ? {} : { targetDialect: e.targetDialect },
1505
1506
  ...e.targetSnapshot === void 0 ? {} : { targetSnapshot: e.targetSnapshot },
1506
1507
  bundleTables: s.tables.map((e) => e.name),
1507
1508
  isSkipped: W,
1508
1509
  canRunSql: Mn(e.store)
1509
- }), de = O.tables, fe = s.tables.filter((e) => !W(e.name) && C(e.name)).map((e) => e.name), k = O.active, pe = !1, A = 0, j = 0, he, M, N = 0, P = 0, ge = (t, r) => n.gen(function* () {
1510
- if (f === "replace" && !k && !(r && b.truncated)) {
1510
+ }), fe = D.tables, pe = s.tables.filter((e) => !W(e.name) && C(e.name)).map((e) => e.name), O = D.active, k = !1, A = 0, j = 0, he, M, N = 0, P = 0, ge = (t, r) => n.gen(function* () {
1511
+ if (f === "replace" && !O && !(r && b.truncated)) {
1511
1512
  e.recordInterruption === !0 && (yield* n.tryPromise({
1512
1513
  try: () => t.insert(d, {
1513
1514
  id: h("repl", F),
@@ -1559,22 +1560,22 @@ voltro data import <this-directory> --target api --api-url <url> --token <secret
1559
1560
  total: h.rowCount
1560
1561
  });
1561
1562
  let y = a.length, x = n.gen(function* () {
1562
- let { createHash: r } = yield* n.promise(() => import("node:crypto")), i = r("sha256"), o = 0, c = me("bundle", () => ue(_).pipe(xt(s.compression))).pipe(l.tap((e) => n.sync(() => i.update(e)))), u = async (e) => {
1563
+ let { createHash: r } = yield* n.promise(() => import("node:crypto")), i = r("sha256"), o = 0, c = me("bundle", () => de(_).pipe(xt(s.compression))).pipe(l.tap((e) => n.sync(() => i.update(e)))), u = async (e) => {
1563
1564
  let n = {
1564
1565
  entry: h,
1565
1566
  row: e,
1566
1567
  reason: ""
1567
1568
  };
1568
1569
  try {
1569
- await E(t, () => T(t, h, e));
1570
+ await ie(t, () => T(t, h, e));
1570
1571
  } catch (t) {
1571
1572
  let r = On(t);
1572
- n.reason = r.reason, ae(h, e, r), a.push(n);
1573
+ n.reason = r.reason, oe(h, e, r), a.push(n);
1573
1574
  }
1574
1575
  }, d = yield* n.promise(async () => {
1575
1576
  let n = e.copyLoader;
1576
1577
  if (!n || n.dialect !== "postgres") return !1;
1577
- if (m) return pe || (pe = !0, e.onWarn?.("bulk COPY is off for this run: it needs its own connection and this import runs in ONE transaction (the default for --mode replace, so a failure mid-load cannot leave the target emptied and unfilled). Pass --no-atomic to trade that guarantee back for COPY.")), !1;
1578
+ if (m) return k || (k = !0, e.onWarn?.("bulk COPY is off for this run: it needs its own connection and this import runs in ONE transaction (the default for --mode replace, so a failure mid-load cannot leave the target emptied and unfilled). Pass --no-atomic to trade that guarantee back for COPY.")), !1;
1578
1579
  if (f === "replace") return !0;
1579
1580
  if (f !== "upsert") return !1;
1580
1581
  try {
@@ -1659,7 +1660,7 @@ voltro data import <this-directory> --target api --api-url <url> --token <secret
1659
1660
  checksum: h.checksum
1660
1661
  }, yield* J(v, b)));
1661
1662
  }
1662
- if (a.length > 0 && (yield* le(t, a)), f === "replace" && (yield* n.promise(() => t.deleteMany(d, { where: {
1663
+ if (a.length > 0 && (yield* ue(t, a)), f === "replace" && (yield* n.promise(() => t.deleteMany(d, { where: {
1663
1664
  column: "startedAt",
1664
1665
  op: "lte",
1665
1666
  value: I
@@ -1707,14 +1708,14 @@ voltro data import <this-directory> --target api --api-url <url> --token <secret
1707
1708
  sourceFingerprint: s.source.schemaFingerprint ?? null,
1708
1709
  tablesWritten: 0,
1709
1710
  rowsWritten: 0,
1710
- staged: f === "replace" ? k : null
1711
+ staged: f === "replace" ? O : null
1711
1712
  }).then(() => {
1712
1713
  ve = !0;
1713
1714
  }, (t) => {
1714
1715
  e.onWarn?.(`the import trace could not be written (${String(t?.message ?? t)}).\n The import continues — this is a record of what happened, not a safety check. Run
1715
1716
  \`voltro db apply\` so the next one is recorded.`);
1716
1717
  }))), e.traceId !== void 0 && B && (yield* n.promise(() => e.store.update(u, z, {
1717
- staged: f === "replace" ? k : null,
1718
+ staged: f === "replace" ? O : null,
1718
1719
  sourceFingerprint: s.source.schemaFingerprint ?? null
1719
1720
  }).then(() => void 0, () => void 0)));
1720
1721
  let ye = 0, be = !1;
@@ -1734,17 +1735,17 @@ voltro data import <this-directory> --target api --api-url <url> --token <secret
1734
1735
  rowsWritten: A,
1735
1736
  failure: t
1736
1737
  }).then(() => void 0, () => void 0) : Promise.resolve();
1737
- if (t = (e) => xe(e), f === "replace" && !k && O.blocker !== void 0 && e.onWarn?.(Nn(O.blocker)), f === "replace" && fe.length > 0 && e.onWarn?.(`write recorders are SUSPENDED for this replace (${fe.length} table(s) have one).\n A replace sets a state rather than changing rows, so a per-row history entry would describe
1738
+ if (t = (e) => xe(e), f === "replace" && !O && D.blocker !== void 0 && e.onWarn?.(Nn(D.blocker)), f === "replace" && pe.length > 0 && e.onWarn?.(`write recorders are SUSPENDED for this replace (${pe.length} table(s) have one).\n A replace sets a state rather than changing rows, so a per-row history entry would describe
1738
1739
  something that did not happen — and this path already writes through the raw store, without
1739
- tenant scoping or \`audit()\` stamping, for the same reason.`), k) {
1740
- e.onWarn?.(`staging ${de.length} table(s) before the swap — the target keeps its rows until the load stands.\n The destructive step is one server-side transaction at the end, not the whole load.`);
1741
- let t = e.store, r = de.map((e) => e.name), i = h("repl", `staging-${F}`), a = !1;
1740
+ tenant scoping or \`audit()\` stamping, for the same reason.`), O) {
1741
+ e.onWarn?.(`staging ${fe.length} table(s) before the swap — the target keeps its rows until the load stands.\n The destructive step is one server-side transaction at the end, not the whole load.`);
1742
+ let t = e.store, r = fe.map((e) => e.name), i = h("repl", `staging-${F}`), a = !1;
1742
1743
  if (e.recordInterruption === !0) {
1743
1744
  yield* n.promise(() => e.store.insert(d, {
1744
1745
  id: i,
1745
1746
  startedAt: I,
1746
1747
  kind: "staging",
1747
- tables: String(de.length),
1748
+ tables: String(fe.length),
1748
1749
  capture: L?.dir ?? null,
1749
1750
  via: e.via ?? "direct",
1750
1751
  staging: r.map(X).join(","),
@@ -1775,20 +1776,20 @@ voltro data import <this-directory> --target api --api-url <url> --token <secret
1775
1776
  let i = m ? r : r.filter((e) => !Ut(b, e));
1776
1777
  yield* n.tryPromise({
1777
1778
  try: async () => {
1778
- await Hn(t, i.map((e) => In(e, D))), await Hn(t, i.map((e) => Fn(e, D)));
1779
+ await Hn(t, i.map((e) => In(e, E))), await Hn(t, i.map((e) => Fn(e, E)));
1779
1780
  },
1780
1781
  catch: (e) => new G({ reason: `could not prepare the staging tables, so the import did not start and the target is untouched: ${String(e?.message ?? e)}` })
1781
1782
  }), yield* R(ge(e.store, !m)), !m && b.truncated === !0 ? c = !0 : yield* n.tryPromise({
1782
- try: () => Un(t, de, D).then(() => {
1783
+ try: () => Un(t, fe, E).then(() => {
1783
1784
  c = !0;
1784
1785
  }),
1785
1786
  catch: (e) => new G({ reason: String(e?.message ?? e) })
1786
1787
  }).pipe(n.catchAll((i) => n.gen(function* () {
1787
- let a = Vn(yield* n.promise(() => Wn(t, zn(e.targetSnapshot, r), D).then((e) => e, () => [])));
1788
+ let a = Vn(yield* n.promise(() => Wn(t, zn(e.targetSnapshot, r), E).then((e) => e, () => [])));
1788
1789
  return yield* n.fail(new G({ reason: a ?? `the swap failed and the target is UNCHANGED: ${i.reason}` }));
1789
1790
  }))), !m && c && b.truncated !== !0 && (b.truncated = !0, yield* J(v, b));
1790
1791
  } finally {
1791
- s.undo(), c || m ? (M = void 0, yield* n.promise(() => Hn(t, r.map((e) => In(e, D))).then(() => void 0, () => void 0)), yield* n.promise(o)) : e.onWarn?.("the staged rows are KEPT so a re-run can continue from them rather than reload.\n If you are not going to re-run this bundle, drop them: voltro data clear-staging --yes");
1792
+ s.undo(), c || m ? (M = void 0, yield* n.promise(() => Hn(t, r.map((e) => In(e, E))).then(() => void 0, () => void 0)), yield* n.promise(o)) : e.onWarn?.("the staged rows are KEPT so a re-run can continue from them rather than reload.\n If you are not going to re-run this bundle, drop them: voltro data clear-staging --yes");
1792
1793
  }
1793
1794
  } else m ? yield* n.tryPromise({
1794
1795
  try: () => e.store.transactional(async (e) => {
@@ -1851,11 +1852,11 @@ async function* vr(e) {
1851
1852
  yield new Uint8Array(Q);
1852
1853
  for (let t of await hr(e.dir)) {
1853
1854
  let n = B(e.dir, t), r = await _e(n);
1854
- if (yield new Uint8Array(gr(t, r.size)), r.size > 0) for await (let e of ue(n)) yield new Uint8Array(e);
1855
+ if (yield new Uint8Array(gr(t, r.size)), r.size > 0) for await (let e of de(n)) yield new Uint8Array(e);
1855
1856
  }
1856
1857
  if (e.blobs) for await (let t of e.blobs.list()) {
1857
1858
  yield new Uint8Array(_r(t.key, t.contentType, t.size));
1858
- let n = ae("sha256"), r = 0;
1859
+ let n = oe("sha256"), r = 0;
1859
1860
  for await (let i of e.blobs.open(t.key)) {
1860
1861
  let e = Buffer.from(i);
1861
1862
  n.update(e), r += e.length, yield new Uint8Array(e);
@@ -1892,7 +1893,7 @@ var yr = (e, t) => new Promise((n, r) => e.write(t, (e) => e ? r(e) : n())), br
1892
1893
  let i = c(8).readBigUInt64BE(0), a = ye(n, t);
1893
1894
  if (a !== n && !a.startsWith(n + be)) throw Error(`archive path '${t}' escapes the destination`);
1894
1895
  await I(ve(a), { recursive: !0 });
1895
- let o = O(a);
1896
+ let o = D(a);
1896
1897
  for (; i > 0n;) {
1897
1898
  r.length === 0 && await s(1);
1898
1899
  let e = i < BigInt(r.length) ? Number(i) : r.length;
@@ -1909,7 +1910,7 @@ var yr = (e, t) => new Promise((n, r) => e.write(t, (e) => e ? r(e) : n())), br
1909
1910
  await s(i);
1910
1911
  let a = c(i).toString("utf8");
1911
1912
  await s(8);
1912
- let o = c(8).readBigUInt64BE(0), l = await t.blobs.has(n), u = ae("sha256"), d = new P(), f = l ? Promise.resolve() : t.blobs.write(n, d, {
1913
+ let o = c(8).readBigUInt64BE(0), l = await t.blobs.has(n), u = oe("sha256"), d = new P(), f = l ? Promise.resolve() : t.blobs.write(n, d, {
1913
1914
  contentType: a,
1914
1915
  size: Number(o)
1915
1916
  }), p = o;
@@ -1969,10 +1970,10 @@ var yr = (e, t) => new Promise((n, r) => e.write(t, (e) => e ? r(e) : n())), br
1969
1970
  write: async (n, r, i) => {
1970
1971
  let a = await import("node:fs/promises"), { pipeline: o } = await import("node:stream/promises"), { Transform: s } = await import("node:stream"), c = B(e, "assets");
1971
1972
  await I(c, { recursive: !0 });
1972
- let l = ae("sha256"), u = 0, d = new s({ transform(e, t, n) {
1973
+ let l = oe("sha256"), u = 0, d = new s({ transform(e, t, n) {
1973
1974
  l.update(e), u += e.length, n(null, e);
1974
1975
  } }), f = B(c, `.tmp-${Date.now()}-${t.size}`);
1975
- await o(r, d, O(f));
1976
+ await o(r, d, D(f));
1976
1977
  let p = l.digest("hex");
1977
1978
  await a.rename(f, B(c, p)), await a.appendFile(B(c, "index.ndjson"), `${JSON.stringify({
1978
1979
  key: n,
@@ -1982,7 +1983,7 @@ var yr = (e, t) => new Promise((n, r) => e.write(t, (e) => e ? r(e) : n())), br
1982
1983
  })}\n`), t.add(n);
1983
1984
  }
1984
1985
  };
1985
- }, Tr = xe(le), Er = Buffer.from("VENC1\0\0\0"), Dr = 32, Or = 16, kr = 7, Ar = 16, jr = 262144, Mr = {
1986
+ }, Tr = xe(ue), Er = Buffer.from("VENC1\0\0\0"), Dr = 32, Or = 16, kr = 7, Ar = 16, jr = 262144, Mr = {
1986
1987
  N: 32768,
1987
1988
  r: 8,
1988
1989
  p: 1
@@ -1992,13 +1993,13 @@ var yr = (e, t) => new Promise((n, r) => e.write(t, (e) => e ? r(e) : n())), br
1992
1993
  };
1993
1994
  async function* Ir(e, t) {
1994
1995
  if (!t) throw Error("encryptStream: a non-empty passphrase is required");
1995
- let n = se(Or), r = se(kr), i = await Tr(t, n, Dr, {
1996
+ let n = ce(Or), r = ce(kr), i = await Tr(t, n, Dr, {
1996
1997
  ...Mr,
1997
1998
  maxmem: Nr
1998
1999
  }), a = Buffer.alloc(Pr);
1999
2000
  Er.copy(a, 0), a.writeUInt32BE(Mr.N, 8), a.writeUInt32BE(Mr.r, 12), a.writeUInt32BE(Mr.p, 16), n.copy(a, 20), r.copy(a, 36), yield new Uint8Array(a);
2000
2001
  let o = 0, s = (e, t) => {
2001
- let n = E("aes-256-gcm", i, Fr(r, o, t));
2002
+ let n = ie("aes-256-gcm", i, Fr(r, o, t));
2002
2003
  o === 0 && n.setAAD(a);
2003
2004
  let s = Buffer.concat([n.update(e), n.final()]), c = n.getAuthTag();
2004
2005
  o++;
@@ -2039,7 +2040,7 @@ async function* Lr(e, t) {
2039
2040
  if (!await o(5 + t)) throw Error("encrypted bundle truncated (frame body)");
2040
2041
  let n = r.subarray(5, 5 + t - Ar), i = r.subarray(5 + t - Ar, 5 + t);
2041
2042
  r = r.subarray(5 + t);
2042
- let a = ie("aes-256-gcm", p, Fr(f, m, e));
2043
+ let a = ae("aes-256-gcm", p, Fr(f, m, e));
2043
2044
  m === 0 && a.setAAD(s), a.setAuthTag(i);
2044
2045
  let c = Buffer.concat([a.update(n), a.final()]);
2045
2046
  m++, e && (h = !0), c.length > 0 && (yield new Uint8Array(c));
@@ -2085,7 +2086,7 @@ var Rr = (e, t) => ({
2085
2086
  tool: r,
2086
2087
  hint: `'${n}' is not on PATH, so this ran '${r}' instead. If that binary came from Oracle's MySQL client it queries information_schema.COLUMN_STATISTICS, which MariaDB does not have, and the run dies with error 1109 after the first table. Install the MariaDB client package (which ships '${n}') and re-run. Do NOT reach for --column-statistics=0: that flag does not exist on '${n}', so it fixes the wrong client and breaks the right one. Note: a MariaDB 12.x client requires TLS by default, so running '${n}' BY HAND against a server without it fails with "TLS/SSL error: SSL is required" (2026) and needs --skip-ssl. This command is not affected.`
2087
2088
  } : { tool: r };
2088
- }, Hr = (e) => (process.env.PATH ?? "").split(z).filter((e) => e !== "").some((t) => de(B(t, e))), Ur = (e) => {
2089
+ }, Hr = (e) => (process.env.PATH ?? "").split(z).filter((e) => e !== "").some((t) => fe(B(t, e))), Ur = (e) => {
2089
2090
  switch (e) {
2090
2091
  case "postgres": return "db.dump";
2091
2092
  case "mysql":
@@ -2289,21 +2290,28 @@ var Rr = (e, t) => ({
2289
2290
  });
2290
2291
  }
2291
2292
  }, Zr = (e) => e.kind === "copy" ? n.async((t) => {
2292
- let r = `${e.to}.restoring-${ce()}`, i = (e) => {
2293
+ let r = `${e.to}.restoring-${le()}`, i = (e) => {
2293
2294
  try {
2294
- pe(r);
2295
+ k(r);
2295
2296
  } catch {}
2296
2297
  t(n.fail(new K({
2297
2298
  tool: "copy",
2298
2299
  reason: e
2299
2300
  })));
2300
2301
  };
2301
- D(e.from, r, (a) => {
2302
+ E(e.from, r, (a) => {
2302
2303
  if (a) {
2303
2304
  i(`copy ${e.from} → ${e.to}: ${a.message}`);
2304
2305
  return;
2305
2306
  }
2306
- fe(r, e.to, (r) => {
2307
+ for (let t of [
2308
+ `${e.to}-wal`,
2309
+ `${e.to}-shm`,
2310
+ `${e.to}-journal`
2311
+ ]) try {
2312
+ k(t);
2313
+ } catch {}
2314
+ pe(r, e.to, (r) => {
2307
2315
  if (r) {
2308
2316
  i(`copy ${e.from} → ${e.to}: ${r.message}`);
2309
2317
  return;
@@ -2332,12 +2340,12 @@ var Rr = (e, t) => ({
2332
2340
  s ??= e;
2333
2341
  };
2334
2342
  if (e.stdoutFile && r.stdout) {
2335
- let t = O(e.stdoutFile);
2343
+ let t = D(e.stdoutFile);
2336
2344
  t.on("error", (t) => c(`writing ${R(e.stdoutFile)}: ${t.message}`)), r.stdout.on("error", (e) => c(`reading the tool's output: ${e.message}`)), r.stdout.pipe(t);
2337
2345
  }
2338
2346
  let l = 0;
2339
2347
  if (e.stdinFile && r.stdin) {
2340
- let t = ue(e.stdinFile);
2348
+ let t = de(e.stdinFile);
2341
2349
  t.on("data", (e) => {
2342
2350
  l += e.length;
2343
2351
  }), t.on("error", (t) => {
@@ -2348,7 +2356,7 @@ var Rr = (e, t) => ({
2348
2356
  }
2349
2357
  let u = () => {
2350
2358
  if (e.stdoutFile !== void 0) try {
2351
- pe(e.stdoutFile);
2359
+ k(e.stdoutFile);
2352
2360
  } catch {}
2353
2361
  };
2354
2362
  r.on("error", (t) => {
@@ -2373,7 +2381,7 @@ var Rr = (e, t) => ({
2373
2381
  });
2374
2382
  }), Qr = (e) => {
2375
2383
  try {
2376
- return String(k(e).size);
2384
+ return String(O(e).size);
2377
2385
  } catch {
2378
2386
  return "?";
2379
2387
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/data-transfer",
3
- "version": "0.75.0",
3
+ "version": "0.77.0",
4
4
  "description": "Portable export/import for a Voltro app's data + assets — typed-NDJSON row codec, content-addressed assets, framed compression, a resumable chunk ledger, and FK-ordered idempotent import. Dialect-agnostic; the resilient backbone under `voltro data export|import` and native backups.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -33,11 +33,11 @@
33
33
  "node": ">=24.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@voltro/database": "0.75.0",
37
- "@voltro/plugin-storage": "0.75.0"
36
+ "@voltro/database": "0.77.0",
37
+ "@voltro/plugin-storage": "0.77.0"
38
38
  },
39
39
  "peerDependencies": {
40
- "effect": "^3.22.1"
40
+ "effect": "^3.22.2"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public"