@voltro/cli 0.25.0 → 0.26.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 +244 -0
- package/dist/{apiBuild-BqhCSytw.js → apiBuild-BtrTyKnF.js} +2 -2
- package/dist/apiBuild-DDJ0It4j.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/{commands-7EmYJ9Xg.js → commands-D-9iYF2D.js} +185 -84
- package/dist/dbCommand-DrzXimKf.js +2 -0
- package/dist/{dbCommand-FUU12FvD.js → dbCommand-uuNCrFAb.js} +238 -238
- package/dist/{dev-BvHT7WZa.js → dev-BnWq4jeA.js} +1 -1
- package/dist/{dev-MacSQ1Ll.js → dev-Dbm6SWtn.js} +1665 -1463
- package/dist/{frameworkTableAssembly-Cw5zJz6n.js → frameworkTableAssembly-BwHU9Euq.js} +10 -6
- package/dist/frameworkTableAssembly-lrjZtk0G.js +2 -0
- package/dist/index.js +1 -1
- package/dist/{inspect-DuLUrZp9.js → inspect-CUCCzw2I.js} +6 -3
- package/dist/inspect-gt8bq-Tz.js +2 -0
- package/dist/{inspectMetrics-EQwH7BI4.js → inspectMetrics-BU90mvJN.js} +1 -1
- package/dist/{manifestBuild-Dneq4_Jx.js → manifestBuild-BnzAxp2O.js} +1 -1
- package/dist/manifestBuild-ifczArzr.js +2 -0
- package/dist/serveCommand-XBXuwJty.js +1294 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-C-ZWSDpg.js → start-BGXIf6zT.js} +2 -2
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/agent-docs/data.md +35 -2
- package/templates/agent-docs/database/schema.md +47 -0
- package/templates/agent-docs/deployment.md +19 -0
- package/templates/agent-docs/plugins.md +25 -3
- package/templates/agent-docs/whats-new.md +19 -3
- package/templates/agent-docs/workflows.md +116 -22
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/dist/apiBuild-N1R4V792.js +0 -2
- package/dist/dbCommand-CIrdFLp9.js +0 -2
- package/dist/frameworkTableAssembly-BsnCKzQ6.js +0 -2
- package/dist/inspect-C9gjHwBk.js +0 -2
- package/dist/manifestBuild-BVwS1Z_6.js +0 -2
- package/dist/serveCommand-5ZFiNO1R.js +0 -1241
package/dist/serveEntry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Z as e } from "./inspectMetrics-
|
|
1
|
+
import { Z as e } from "./inspectMetrics-BU90mvJN.js";
|
|
2
2
|
import { c as t } from "./seedRunner-D6eu-u5U.js";
|
|
3
3
|
import { r as n } from "./appModuleLoader-C9r9mxZt.js";
|
|
4
|
-
import { t as r } from "./serveCommand-
|
|
4
|
+
import { t as r } from "./serveCommand-XBXuwJty.js";
|
|
5
5
|
export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as e, D as t, F as n, G as r, H as i, J as a, K as o, M as s, T as c, U as l, W as u, Y as d, _ as ee, a as te, b as f, c as p, dt as m, et as h, f as g, ft as _, g as ne, h as v, i as re, lt as y, m as b, mt as x, nt as ie, o as S, ot as C, p as w, pt as ae, r as T, rt as E, s as D, t as O, tt as k, v as A, w as j, x as M, y as oe, z as se } from "./inspectMetrics-
|
|
2
|
-
import { D as ce, E as le, T as ue, a as de, p as N, w as P } from "./inspect-
|
|
1
|
+
import { $ as e, D as t, F as n, G as r, H as i, J as a, K as o, M as s, T as c, U as l, W as u, Y as d, _ as ee, a as te, b as f, c as p, dt as m, et as h, f as g, ft as _, g as ne, h as v, i as re, lt as y, m as b, mt as x, nt as ie, o as S, ot as C, p as w, pt as ae, r as T, rt as E, s as D, t as O, tt as k, v as A, w as j, x as M, y as oe, z as se } from "./inspectMetrics-BU90mvJN.js";
|
|
2
|
+
import { D as ce, E as le, T as ue, a as de, p as N, w as P } from "./inspect-CUCCzw2I.js";
|
|
3
3
|
import { t as fe } from "./bootTiming-BdyP9nYw.js";
|
|
4
4
|
import { dirname as F, extname as I, join as L, resolve as R } from "node:path";
|
|
5
5
|
import { fileURLToPath as z, pathToFileURL as B } from "node:url";
|
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { Z as e } from "./inspectMetrics-
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { Z as e } from "./inspectMetrics-BU90mvJN.js";
|
|
2
|
+
import { t } from "./start-BGXIf6zT.js";
|
|
3
3
|
export { e as loadDotEnv, t as runStartCommand };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -62,22 +62,22 @@
|
|
|
62
62
|
"@effect/platform-node": "^0.108.0",
|
|
63
63
|
"@effect/sql": "^0.52.0",
|
|
64
64
|
"@effect/workflow": "^0.19.0",
|
|
65
|
-
"@voltro/ai": "0.
|
|
66
|
-
"@voltro/cache": "0.
|
|
67
|
-
"@voltro/data-transfer": "0.
|
|
68
|
-
"@voltro/database": "0.
|
|
69
|
-
"@voltro/env": "0.
|
|
70
|
-
"@voltro/kv": "0.
|
|
71
|
-
"@voltro/logger": "0.
|
|
72
|
-
"@voltro/plugin-auth": "0.
|
|
73
|
-
"@voltro/plugin-broadcast": "0.
|
|
74
|
-
"@voltro/plugin-mail": "0.
|
|
75
|
-
"@voltro/plugin-storage": "0.
|
|
76
|
-
"@voltro/plugin-webhooks": "0.
|
|
77
|
-
"@voltro/protocol": "0.
|
|
78
|
-
"@voltro/runtime": "0.
|
|
79
|
-
"@voltro/serverless": "0.
|
|
80
|
-
"@voltro/workflow": "0.
|
|
65
|
+
"@voltro/ai": "0.26.0",
|
|
66
|
+
"@voltro/cache": "0.26.0",
|
|
67
|
+
"@voltro/data-transfer": "0.26.0",
|
|
68
|
+
"@voltro/database": "0.26.0",
|
|
69
|
+
"@voltro/env": "0.26.0",
|
|
70
|
+
"@voltro/kv": "0.26.0",
|
|
71
|
+
"@voltro/logger": "0.26.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.26.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.26.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.26.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.26.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.26.0",
|
|
77
|
+
"@voltro/protocol": "0.26.0",
|
|
78
|
+
"@voltro/runtime": "0.26.0",
|
|
79
|
+
"@voltro/serverless": "0.26.0",
|
|
80
|
+
"@voltro/workflow": "0.26.0",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
|
@@ -1469,6 +1469,38 @@ Tables with the `tenant()` mixin are scoped by the runtime using `ctx.request.su
|
|
|
1469
1469
|
|
|
1470
1470
|
The devtools subscription surfaces show active subscribers, recent deltas, and cache state. Use them when a query updates too often or not at all.
|
|
1471
1471
|
|
|
1472
|
+
## Cost — how large may a live query be?
|
|
1473
|
+
|
|
1474
|
+
Every change re-runs the query and diffs the WHOLE result against the previous
|
|
1475
|
+
one, so the cost is linear in the RESULT SIZE, not in the size of the change.
|
|
1476
|
+
Measured on `diffRows`:
|
|
1477
|
+
|
|
1478
|
+
| result rows | one column changed | every row replaced |
|
|
1479
|
+
| --- | --- | --- |
|
|
1480
|
+
| 50 | 45 µs | — |
|
|
1481
|
+
| 500 | 480 µs | — |
|
|
1482
|
+
| 2 000 | 1.23 ms | 1.29 ms |
|
|
1483
|
+
| 5 000 | 3.1 ms | — |
|
|
1484
|
+
|
|
1485
|
+
Two things follow, and the second is the one that surprises people:
|
|
1486
|
+
|
|
1487
|
+
- **The curve is linear, not quadratic.** Per-row cost is flat across a 100×
|
|
1488
|
+
growth (910 ns → 625 ns), so a large result gets slower in proportion and
|
|
1489
|
+
never falls off a cliff.
|
|
1490
|
+
- **A one-column edit costs the same as replacing everything.** 2 000 rows with
|
|
1491
|
+
a single change is 1.23 ms; the same 2 000 rows entirely replaced is 1.29 ms —
|
|
1492
|
+
5 % more. The cost is the WALK, not the delta. Making your mutation smaller
|
|
1493
|
+
does not make the subscription cheaper.
|
|
1494
|
+
|
|
1495
|
+
So the number to design against is the RESULT SIZE. A few hundred rows is free.
|
|
1496
|
+
A 5 000-row live query costs 3.1 ms of CPU per change, per replica — fine for a
|
|
1497
|
+
dashboard that changes a few times a minute, wrong for one fed by a high-rate
|
|
1498
|
+
writer. Page the query, or narrow it with a predicate, rather than reaching for
|
|
1499
|
+
a bigger machine.
|
|
1500
|
+
|
|
1501
|
+
These numbers are asserted by `rowPatch.perf.test.ts`, so they are current
|
|
1502
|
+
rather than a note somebody wrote down once.
|
|
1503
|
+
|
|
1472
1504
|
## See also
|
|
1473
1505
|
|
|
1474
1506
|
- [Subscribers (`*.subscribe.ts`)](/docs/data/subscribers) — server-side, best-effort post-commit reactivity to a table (NOT the client hook on this page).
|
|
@@ -1693,7 +1725,8 @@ Everything you would otherwise hand-roll is gone, and each of these was a real b
|
|
|
1693
1725
|
|
|
1694
1726
|
- **At-most-once, best-effort, live.** No persistence, no retry, no redelivery. For guarantees use the [outbox](/docs/data/outbox); this is the other axis.
|
|
1695
1727
|
- **Ordered per publishing instance per key.** *Not* globally per key — two instances publishing the same key have no shared counter, and we do not promise an order we cannot keep.
|
|
1696
|
-
- **Guards are re-checked
|
|
1728
|
+
- **Guards are re-checked on EVERY delivery**, exactly as a live query's are. A resource-scoped guard (`{ scope: 'arena:read', from: 'arenaId' }`) runs its resolver each time, so un-sharing a resource or ending a membership stops the stream at the next delivery — the client is told, not silently skipped. What this does *not* catch is a ROLE revoked on the subject itself: those scopes were captured when the subscription opened. That half is covered by the credential bound below.
|
|
1729
|
+
- **A subscription cannot outlive the credential that authorized it.** When the session carries an expiry, the stream ends at it — and `useEvent` reconnects immediately, which is a NEW request, so the subject is resolved afresh and the guards run again for real. Still entitled: it continues and your app sees nothing. No longer entitled: the reconnect is refused, loudly. You write no reconnect handling for this; it is the existing retry doing its job. Note the limit precisely — this bounds EXPIRY, not revocation.
|
|
1697
1730
|
- **Payloads are capped at 7,500 bytes** of encoded envelope, on **every** dialect. An event says something happened, so carry a photo *reference*, not a photo.
|
|
1698
1731
|
|
|
1699
1732
|
### `missed` is a number, not a feeling
|
|
@@ -1742,7 +1775,7 @@ export const orderPaid = defineEvent({
|
|
|
1742
1775
|
|
|
1743
1776
|
One `publish` now reaches connected clients, every matching workflow trigger, and every subscribed HTTP target. Without this an app that does both declares the thing twice, in two shapes, and the two drift.
|
|
1744
1777
|
|
|
1745
|
-
The `webhook:` block is namespaced because its settings mean nothing to the other audiences — a top-level `
|
|
1778
|
+
The `webhook:` block is namespaced because its settings mean nothing to the other audiences — a top-level `rateLimit` would read as if it throttled client delivery, which it does not — it is a ceiling on webhook deliveries only. Requires [`@voltro/plugin-webhooks`](/docs/plugins/webhooks); absent, it costs nothing.
|
|
1746
1779
|
|
|
1747
1780
|
## Across instances
|
|
1748
1781
|
|
|
@@ -423,6 +423,53 @@ the framework ships richer types for specific use cases:
|
|
|
423
423
|
- **[JSON](/docs/database/json)** — `json<T>()` for arbitrary nested
|
|
424
424
|
data; native JSONB on postgres.
|
|
425
425
|
|
|
426
|
+
## Pointing at a plugin-owned row — `pluginRef`
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { pluginRef } from '@voltro/database'
|
|
430
|
+
import { aiFlowsTable } from '@voltro/plugin-ai-flows'
|
|
431
|
+
|
|
432
|
+
export const favourites = table('favourites', {
|
|
433
|
+
id: id(),
|
|
434
|
+
flowId: pluginRef(aiFlowsTable, { orphanPolicy: 'delete' }),
|
|
435
|
+
sharedFlow: pluginRef(aiFlowsTable, { orphanPolicy: 'null' }).nullable(),
|
|
436
|
+
})
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
A plain typed id column with **no foreign key**, plus a declared orphan rule the
|
|
440
|
+
framework runs on the post-commit change channel.
|
|
441
|
+
|
|
442
|
+
**Reach for `reference()` first.** A real foreign key across a plugin boundary
|
|
443
|
+
works and survives the plugin renaming its table, because `reference()` takes
|
|
444
|
+
the table as a VALUE — see
|
|
445
|
+
[plugins/overview](/docs/plugins/overview#pointing-your-table-at-a-plugins-row).
|
|
446
|
+
`pluginRef` is for the case where you have deliberately chosen NOT to have a
|
|
447
|
+
key: it enforces nothing at the database level.
|
|
448
|
+
|
|
449
|
+
What it restores is the piece that goes missing with that choice — not the
|
|
450
|
+
constraint but the RULE, which otherwise becomes a hand-written subscriber per
|
|
451
|
+
app that nobody notices the absence of.
|
|
452
|
+
|
|
453
|
+
| policy | on the target's delete |
|
|
454
|
+
| --- | --- |
|
|
455
|
+
| `'delete'` | delete the referencing row — for one that only exists to point (a favourite, a pin, a share) |
|
|
456
|
+
| `'null'` | clear the column, keep the row. Requires `.nullable()` |
|
|
457
|
+
| `'keep'` (default) | nothing — the explicit "I handle it myself" |
|
|
458
|
+
|
|
459
|
+
Four things worth knowing before you rely on it:
|
|
460
|
+
|
|
461
|
+
- **The tenant boundary fails closed.** A referencing row whose tenant differs
|
|
462
|
+
from the deleted row's — or which has none — is left alone. Deleting across a
|
|
463
|
+
tenant boundary because a scope was missing is worse than leaving an orphan.
|
|
464
|
+
- **Soft deletes are opt-in** (`onSoftDelete: true`). A soft delete is a state
|
|
465
|
+
the target can undo, so cascading on it destroys rows a restore cannot bring
|
|
466
|
+
back — and plugin tables are inconsistent about carrying `deletedAt` at all.
|
|
467
|
+
- **The target is a table VALUE.** A rename carries the rule with it, which is
|
|
468
|
+
the whole reason the FK was refused; a string would put the coupling back.
|
|
469
|
+
- **A missing plugin refuses at boot**, naming both sides. A declared rule
|
|
470
|
+
against a table nothing registers would sit there looking enforced.
|
|
471
|
+
|
|
472
|
+
|
|
426
473
|
## Anti-patterns
|
|
427
474
|
|
|
428
475
|
- **`serial`/`bigserial` integer ids.** Leaks row counts via `/users/12345`. Use `id()` (TypeID).
|
|
@@ -1150,6 +1150,24 @@ KV_BACKEND=redis
|
|
|
1150
1150
|
|
|
1151
1151
|
Schedules and aggregates auto-coordinate via an advisory lock on SQL stores — no extra config to keep them from double-firing across replicas.
|
|
1152
1152
|
|
|
1153
|
+
### Workflow failover across replicas
|
|
1154
|
+
|
|
1155
|
+
On a **SQL store** (postgres / mysql / mariadb / mssql), durable workflows survive a replica crash: completed `step({...})` activities are checkpointed in the cluster journal, so when a replica dies mid-run, a **surviving replica takes over the run and continues it from the last completed step** — it replays the completed steps rather than re-running them. (On sqlite the engine is single-process — durable within one replica, no cross-replica failover.) Two requirements:
|
|
1156
|
+
|
|
1157
|
+
- **Inject `POD_IP`** (K8s downward API, `fieldRef: status.podIP`) or set `VOLTRO_WORKFLOW_RUNNER_HOST`. This is each replica's cluster **identity** — without a distinct value, every replica registers as the *same* runner and they stop distributing shards (and cross-pod resume degrades). The boot logs a warning if it sees `localhost` with SQL storage.
|
|
1158
|
+
- **Make step side effects idempotent.** Failover is *at-least-once at the step boundary*: a crash between a side effect and its journal write re-runs that step. A step's own [`retry:`](/docs/workflows/retries) does not change this — it's about the step you're inside, not the replica handoff.
|
|
1159
|
+
|
|
1160
|
+
**Is polling the bottleneck? No — reclaim is a lease, not a poll.** A crashed replica keeps its shards until its heartbeat goes stale; only then can a survivor claim them. So takeover latency is bounded by the **lease TTL (~35s by default)**, not by any message-poll interval, and a push mechanism (LISTEN/NOTIFY) does **not** move it. Two knobs tune it:
|
|
1161
|
+
|
|
1162
|
+
```sh
|
|
1163
|
+
VOLTRO_WORKFLOW_FAILOVER_LEASE=15 # seconds a dead replica's work stays locked (default 35)
|
|
1164
|
+
VOLTRO_WORKFLOW_FAILOVER_HEARTBEAT=5 # lease-refresh cadence (default 10; keep ≈ lease/3)
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
Lower the lease for **faster failover**, at the cost of **false-positive reclaims**: if a *healthy* replica is paused longer than the lease by a GC pause or a DB-latency spike, another replica may briefly also claim its shards. Keep the heartbeat around a third of the lease so one slow refresh doesn't trip a reclaim. For crash detection that doesn't depend on the timeout at all, pair it with a K8s **liveness probe** so a dead pod is removed promptly.
|
|
1168
|
+
|
|
1169
|
+
(A separate concern is *new*-message pickup: a workflow triggered on the replica that owns its shard starts immediately, but one owned by ANOTHER replica is otherwise picked up on that replica's next storage poll — up to 10s. **If you run a broadcast broker (Redis/NATS — which a multi-replica deployment already does for cross-replica reactivity), this is automatic and near-instant**: a trigger pushes a "wake" over the bus and the shard owner re-polls at once, on any SQL dialect. Without a broker, tune `VOLTRO_WORKFLOW_POLL_INTERVAL=2` instead. Unrelated to the failover path above.)
|
|
1170
|
+
|
|
1153
1171
|
## Checklist
|
|
1154
1172
|
|
|
1155
1173
|
- [ ] `VOLTRO_SESSION_SECRET` set from `voltro secret generate session`, in a secrets manager
|
|
@@ -1163,6 +1181,7 @@ Schedules and aggregates auto-coordinate via an advisory lock on SQL stores —
|
|
|
1163
1181
|
- [ ] `VOLTRO_LOG_FORMAT=json`; `sentryPlugin()` + `SENTRY_DSN` for errors
|
|
1164
1182
|
- [ ] `terminationGracePeriodSeconds` generous for graceful drain; `VOLTRO_SHUTDOWN_GRACE_MS` set just under it (minus the preStop sleep)
|
|
1165
1183
|
- [ ] `CACHE_BACKEND` / `KV_BACKEND` + a cross-replica change bus when running >1 replica
|
|
1184
|
+
- [ ] For durable workflows on >1 replica: SQL store + `POD_IP` injected; tune `VOLTRO_WORKFLOW_FAILOVER_LEASE` if a 35s takeover is too slow; step side effects idempotent
|
|
1166
1185
|
|
|
1167
1186
|
|
|
1168
1187
|
|
|
@@ -166,9 +166,31 @@ the `_voltro_` namespace in 0.22.0, a `reference(() => table)` followed the rena
|
|
|
166
166
|
**`fk: false`-style decoupling is still available** — declare a plain `text()`
|
|
167
167
|
column instead. Choose it when you deliberately want the app schema independent
|
|
168
168
|
of the plugin's, and accept that nothing then enforces the link. What you should
|
|
169
|
-
NOT do is reach for it by default
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
NOT do is reach for it by default.
|
|
170
|
+
|
|
171
|
+
**When you DO want the decoupling, `pluginRef` gives you the rule without the
|
|
172
|
+
key.** It is a plain typed id column — no constraint, no cross-schema
|
|
173
|
+
dependency — plus a declared orphan policy the framework runs on the post-commit
|
|
174
|
+
change channel:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { pluginRef } from '@voltro/database'
|
|
178
|
+
|
|
179
|
+
flowId: pluginRef(aiFlowsTable, { orphanPolicy: 'delete' })
|
|
180
|
+
sharedFlow: pluginRef(aiFlowsTable, { orphanPolicy: 'null' }).nullable()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
That closes the gap named above: an unenforced id column plus a hand-written
|
|
184
|
+
`defineSubscriber` that cleans up on delete is referential integrity
|
|
185
|
+
re-implemented per app, and it is silently wrong the first time somebody forgets
|
|
186
|
+
it. The declaration is the same one line either way — the difference is that the
|
|
187
|
+
framework performs it.
|
|
188
|
+
|
|
189
|
+
**Prefer `reference()` when you want a real key.** `pluginRef` is for the case
|
|
190
|
+
where you have deliberately chosen not to have one; it does not make the
|
|
191
|
+
database enforce anything. The tenant boundary fails closed, soft deletes are
|
|
192
|
+
opt-in (`onSoftDelete`), and a `pluginRef` at a table no installed plugin
|
|
193
|
+
registers refuses at boot rather than sitting there looking enforced.
|
|
172
194
|
|
|
173
195
|
**`orphanPolicy` is not part of this.** It is migration metadata — how existing
|
|
174
196
|
orphan rows are cleaned up *before* the FK constraint is added — and has no
|
|
@@ -266,6 +266,13 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
|
|
|
266
266
|
|
|
267
267
|
### Fixed
|
|
268
268
|
|
|
269
|
+
- **@voltro/runtime** — **A change watched by N identical live subscribers cost N reads and N diffs. It now costs one of each.** Fifty screens open on the same list re-ran the same query fifty times per change and recomputed the same delta fifty times — in memory that is wasted CPU; against SQL it is fifty round trips.
|
|
270
|
+
|
|
271
|
+
`diffRows` costs the WALK, not the delta: ~31µs at 50 rows, ~289µs at 500, ~3.1ms at 5000, and one changed row costs what zero does. So on a large list the diff share is worth as much as the read share.
|
|
272
|
+
|
|
273
|
+
Both are keyed by the resolved read descriptor; the diff share additionally keys on the previous rows' OBJECT IDENTITY. That second half is a safety property rather than an optimisation: a patch computed against another subscriber's base silently corrupts its rows, and it is the one failure on this path that neither a test nor a log would catch. Reference identity cannot be wrong about it — two subscribers share only when they hold the literally same array, which is exactly when the read share already served them together. A late joiner holds a different object and gets its own diff.
|
|
274
|
+
|
|
275
|
+
Recorded because the route was not straight, and the wrong turns are the instructive part. The read memo was removed mid-release as dead code on a measurement that was broken: `handleChange` is dispatched with `void`, so the counter was sampled before the reads had landed, and the "1.00 reads" that condemned it was an artefact of the sampling. The diff share was separately shipped once with a test that could not prove it — counting deliveries, which happen either way, stays green with the share disabled — and was removed for that reason before being rebuilt. The proof needs no mocks: a shared diff is the SAME OBJECT in every delivery, so counting distinct patch identities is exact, and disabling reuse turns one shared patch into fifty.
|
|
269
276
|
- **@voltro/cli** — **The domain-event audit tables are bounded now.** `ctx.events.emit(...)` writes one row to `_voltro_workflow_events` plus **one per matching trigger** to `_voltro_workflow_event_deliveries`, and nothing in the framework ever deleted from either. Both are registered with the boot retention GC on a 30-day default, env-tunable via `VOLTRO_WORKFLOW_EVENTS_TTL_HOURS` and `VOLTRO_WORKFLOW_EVENT_DELIVERIES_TTL_HOURS`.
|
|
270
277
|
|
|
271
278
|
Same family as `_voltro_schedule_claims`, whose sweep landed one release ago after a consumer measured 35,128 rows in 14 days. The comment there already named the pattern — *"the one table of this family with no sweep"* — and two more members of the family were sitting next to it. The delivery log is the faster half: three triggers on one event write four rows per emit.
|
|
@@ -359,6 +366,13 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
|
|
|
359
366
|
It needs no cluster coordination, and that is a consequence of the design rather than a shortcut: the table version had shared rows, so one replica had to evict them or they would fight. Owner-partitioned presence has no shared state, so every replica sweeps its own and there is nothing to coordinate.
|
|
360
367
|
|
|
361
368
|
Removals are ANNOUNCED — a local removal nobody broadcasts is a member every other replica keeps showing. The sweep runs at a third of the timeout, so a vanished member is gone within roughly 1.3× the window rather than up to 2×.
|
|
369
|
+
- **@voltro/cli** — **`voltro build` could delete output it had just written.** The post-build orphan prune compared each file's mtime against `Date.now()` taken at build start — two different clocks. Linux stamps inode times from a COARSE clock (`ktime_get_coarse_real_ts64`) that advances once per timer tick, so a file written microseconds AFTER the cutoff can carry an mtime a tick BEFORE it, and the strict comparison then removed it.
|
|
370
|
+
|
|
371
|
+
The consequence is the exact failure the prune was designed to avoid: a bundle that is missing pieces mid-run. The wipe-before-build version had the same effect for a different reason, and this reintroduced it in a narrower window.
|
|
372
|
+
|
|
373
|
+
The comparison now carries a one-second tolerance. The two directions are not symmetric — too small deletes a fresh artefact, too large lets an orphan survive until the next prune — so the margin sits on the side of keeping. Real orphans are minutes or builds old.
|
|
374
|
+
|
|
375
|
+
Found by the release gate on Linux, where the suite's own concurrency case failed while asserting a precondition that held: the file it checked was fine, a different one was pruned. It had never failed on macOS, whose timestamp granularity differs. The suite now pins the tolerance directly — a file stamped just before the cutoff must survive, and one past the tolerance must still go, so the margin cannot quietly widen into a no-op.
|
|
362
376
|
- **@voltro/cli** — **A `source:` that names no table is now reported at boot.** It was silent, and the silence is the defect: `source` is matched BY NAME against change events, so one naming a table that does not exist matches nothing — the query returns its first result and never updates again. Not a broken subscription, a permanently silent one, which from the outside is indistinguishable from "nothing has changed".
|
|
363
377
|
|
|
364
378
|
```text
|
|
@@ -407,8 +421,10 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
|
|
|
407
421
|
An `armed` step now sits between `step1` and the nap, and the teardown waits for it. `activityExecute` returns only once a step's result is durably recorded — that is what lets replay skip it, and what the idempotency-key scenario in the same suite already depends on — so `armed` starting IS the journal write having landed. No duration is left in that path.
|
|
408
422
|
|
|
409
423
|
Distinct from the ceiling raises around it, which address a resumed run needing longer than the timeout under load. This one is not a timeout: no amount of waiting turns a re-executed step back into a skipped one.
|
|
410
|
-
- **@voltro/
|
|
424
|
+
- **@voltro/cli** — The dev-SSR streaming tests defined "the shell" as *every chunk that arrived before 0.6 × the deferral delay* — an assertion about the machine wearing the shape of an assertion about the renderer. Under a loaded CI runner the shell lands after that deadline, the derived `shell` string comes out EMPTY, and the failure reads `Expected SHELL_LAYOUT_EAGER_OK`, as though the renderer had dropped a field. Green on every developer machine.
|
|
425
|
+
|
|
426
|
+
The shell is now everything BEFORE the chunk carrying the deferred value, and the claim the first-byte deadline was reaching for is stated as what it actually is: the eager field's chunk index is strictly lower than the deferred value's. No duration remains in that path. The one clock that stays is the lower bound on WHEN the deferred value arrived — a slower machine only makes that more true.
|
|
411
427
|
|
|
412
|
-
|
|
428
|
+
It still fails a buffered implementation, which is the point of the suite: one chunk means the deferred index is 0, the shell is empty, and the eager-field assertion fails.
|
|
413
429
|
|
|
414
|
-
|
|
430
|
+
**Found because a red suite had been reporting green.** CI's test step ends in `| tee`, and GitHub's default `run` shell is `bash -e` — *without* `pipefail` — so the step's exit status was tee's. `@voltro/cli#test` failed, turbo exited 1, and the step reported SUCCESS; the comment above it asserted pipefail was on. It surfaced only because the failing package died before printing its summary, which tripped the undeclared-skip check further down. One line later and the gate would have gone green on a failing test. The workflow now sets `defaults.run.shell: bash` so no future piped step can reintroduce it.
|
|
@@ -525,34 +525,55 @@ Workflow durability and retry are related, but not identical. The engine checkpo
|
|
|
525
525
|
|
|
526
526
|
## Per-step retries
|
|
527
527
|
|
|
528
|
-
|
|
528
|
+
Declare a `retry:` policy on `step({...})` and the framework **enforces** it — it compiles the policy to an Effect `Schedule` and retries `execute` for you. No hand-written retry needed:
|
|
529
529
|
|
|
530
530
|
```tsx
|
|
531
|
-
import { workflow, step
|
|
532
|
-
import {
|
|
531
|
+
import { workflow, step } from '@voltro/workflow'
|
|
532
|
+
import { Schema } from 'effect'
|
|
533
533
|
|
|
534
534
|
class ProviderDown extends Schema.TaggedError<ProviderDown>()('ProviderDown', {
|
|
535
535
|
message: Schema.String,
|
|
536
536
|
}) {}
|
|
537
537
|
|
|
538
|
-
const summary = yield*
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}),
|
|
551
|
-
Schedule.exponential('500 millis').pipe(Schedule.recurs(4)),
|
|
552
|
-
)
|
|
538
|
+
const summary = yield* step({
|
|
539
|
+
name: 'summarise-with-llm',
|
|
540
|
+
input: { noteId },
|
|
541
|
+
success: Schema.String,
|
|
542
|
+
error: ProviderDown,
|
|
543
|
+
retry: {
|
|
544
|
+
maxAttempts: 5,
|
|
545
|
+
strategy: 'exponential',
|
|
546
|
+
baseDelay: '500 millis',
|
|
547
|
+
},
|
|
548
|
+
execute: callLlm(note.body),
|
|
549
|
+
})
|
|
553
550
|
```
|
|
554
551
|
|
|
555
|
-
|
|
552
|
+
Retries run inside the one step and are transparent to the durable engine — completed steps still checkpoint; the step's final outcome is recorded. `retry: { maxAttempts: 5 }` is already a good policy (exponential backoff, jittered).
|
|
553
|
+
|
|
554
|
+
### The conditions you actually want
|
|
555
|
+
|
|
556
|
+
The useful retry question is rarely "how many times" — it's *which* failures, for *how long*, and *how spread out*. The policy covers all of it:
|
|
557
|
+
|
|
558
|
+
```tsx
|
|
559
|
+
retry: {
|
|
560
|
+
maxAttempts: 5, // total attempts including the first (default 3)
|
|
561
|
+
strategy: 'exponential', // 'exponential' | 'fixed' | 'linear'
|
|
562
|
+
baseDelay: '500 millis',
|
|
563
|
+
maxDelay: '30 seconds', // ceiling so exponential growth can't run away
|
|
564
|
+
factor: 2, // exponential growth factor
|
|
565
|
+
jitter: true, // full jitter (default true) — anti-thundering-herd
|
|
566
|
+
maxElapsed: '5 minutes', // a total time BUDGET — stop retrying after this
|
|
567
|
+
retryableErrors: ['ProviderDown'], // retry ONLY these typed errors; others fail fast
|
|
568
|
+
respectRetryAfter: true, // honor a 429: retryAfter REPLACES this attempt's backoff
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
- **`retryableErrors`** (or a `retryable: (error) => boolean` predicate) is the important one: retry the *transient* failures, fail *fast* on the permanent ones. A `ValidationError` should never be retried; a `ProviderDown` should.
|
|
573
|
+
- **`maxElapsed`** is a deadline across all attempts, not another count — the right bound when "keep trying for up to 5 minutes" matters more than "try 8 times".
|
|
574
|
+
- **`respectRetryAfter`** uses exactly the delay a provider asked for (a `retryAfterMillis` number, or `retryAfter` in seconds, on the thrown error) as the next delay, replacing the computed backoff for that attempt; falls back to backoff when there is no hint.
|
|
575
|
+
|
|
576
|
+
Retries here run *inside* the one step attempt, so the step is recorded as a SINGLE row in `_voltro_workflow_run_steps` with its final outcome — the individual in-step retries are not separate rows. `respectRetryAfter` uses the provider's delay AS the next delay (replacing the computed backoff for that attempt), falling back to backoff when the error carries no hint. For per-attempt rows in the dashboard, or a bespoke `Schedule`, use `stepModule.retry(step({...}), schedule)` instead. Do NOT set `retry:` on a step you ALSO wrap in `stepModule.retry` / `Effect.retry`: it would retry twice.
|
|
556
577
|
|
|
557
578
|
## What to retry
|
|
558
579
|
|
|
@@ -604,6 +625,64 @@ For ad-hoc ops, the same operation is available manually — the CLI (`voltro wo
|
|
|
604
625
|
voltro workflows retry wfrun_01H...
|
|
605
626
|
```
|
|
606
627
|
|
|
628
|
+
`retry` starts a **fresh execution** — a new execution id, an empty journal, every step runs again. That is the right tool for a short, idempotent job, or when the input itself was wrong (`payloadOverride`). For a long multi-step pipeline where re-doing steps 1…N‑1 is expensive or unsafe, you want the opposite: resume from where it failed.
|
|
629
|
+
|
|
630
|
+
## Resume from where it failed — `suspendOnFailure`
|
|
631
|
+
|
|
632
|
+
Declare `suspendOnFailure: true` on a workflow and a failure no longer becomes a terminal `failed` run — it **suspends** with the durable journal intact:
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
export default workflow({
|
|
636
|
+
name: 'billing.close-month',
|
|
637
|
+
payload: { orgId: Schema.String },
|
|
638
|
+
success: Schema.Void,
|
|
639
|
+
error: Schema.Unknown,
|
|
640
|
+
idempotencyKey: ({ orgId }) => `billing.close-month:${orgId}`,
|
|
641
|
+
suspendOnFailure: true, // a failure suspends (recoverable), not fails (terminal)
|
|
642
|
+
execute: ({ orgId }) => Effect.gen(function* () {
|
|
643
|
+
yield* step('snapshot-ledger', /* … */) // completed steps are journaled
|
|
644
|
+
yield* step('call-tax-provider', /* … */) // ← a transient 503 here …
|
|
645
|
+
yield* step('finalise-invoices', /* … */)
|
|
646
|
+
}),
|
|
647
|
+
})
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
When `call-tax-provider` fails, the run goes to `suspended` (not `failed`), carrying the failure reason. Fix the cause, then **resume** — the engine replays `snapshot-ledger` from the journal (it does **not** re-run) and continues from the failed step:
|
|
651
|
+
|
|
652
|
+
```sh
|
|
653
|
+
voltro workflows resume wfrun_01H... # re-drives from the failure point
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
await ctx.workflows.resume(run.id) // the same, from a handler
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Because a suspended-on-failure run is **recoverable, not dead**, it shows up under `--status suspended`, NOT in the [dead-letter view](/docs/workflows/debugging) (`voltro workflows list --dead-letter`, which is failed-and-unhandled). Choose per workflow: `suspendOnFailure: true` for a long pipeline where prior work must not be redone; the default (`retry` from scratch) for short idempotent jobs.
|
|
661
|
+
|
|
662
|
+
## Re-drive a failed run — `redrive`
|
|
663
|
+
|
|
664
|
+
`suspendOnFailure` is a decision you make **before** the run. What if a run already **failed** — it's sitting in the dead-letter view — and you still want to continue it from where it died, not re-run it from scratch? That is `redrive`:
|
|
665
|
+
|
|
666
|
+
```sh
|
|
667
|
+
voltro workflows redrive wf_01H... # re-drive a FAILED run from the step it died on
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
await ctx.workflows.redrive(run.id) // the same, from a handler; addressed by run id
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
`redrive` re-drives the run from its durable journal: every **completed** step replays from the journal (it does **not** re-run), and only the **failed** step(s) re-execute. It is the after-the-fact counterpart to `suspendOnFailure` + `resume` — same "continue from the failure point" outcome, but for a run that already went terminal without being marked suspend-on-failure. Fix the downstream cause first, then redrive.
|
|
675
|
+
|
|
676
|
+
The three recovery tools, and when each applies:
|
|
677
|
+
|
|
678
|
+
| Tool | Use when | Journal |
|
|
679
|
+
|---|---|---|
|
|
680
|
+
| `retry` | The input was wrong, or the job is short + idempotent | Fresh execution, empty journal — every step runs again |
|
|
681
|
+
| `resume` | The run is `suspended` (you set `suspendOnFailure: true`, or it awaits a signal) | Continues from the failure/suspend point — completed steps replay |
|
|
682
|
+
| `redrive` | The run is `failed` (dead-letter) and re-running earlier steps is expensive/unsafe | Continues from the failure point — completed steps replay, failed steps re-run |
|
|
683
|
+
|
|
684
|
+
`redrive` refuses a run that isn't a not-yet-discarded failure (use `resume` for a suspended run, `retry` for a fresh execution), and it needs a durable journal — on the memory store there is nothing to re-drive, so it declines cleanly (`redriven: false` with a reason) rather than pretending. It works under `voltro serve` as well as `voltro dev`, because dead-letter recovery happens in production. A re-drive records a `run-redriven` event on the run's timeline.
|
|
685
|
+
|
|
607
686
|
## Compensation
|
|
608
687
|
|
|
609
688
|
For saga-style workflows, model compensation explicitly with `Effect.catchAll` around the step that can fail after an earlier side effect:
|
|
@@ -664,7 +743,7 @@ The dashboard and `voltro workflows show <runId>` read these same tables through
|
|
|
664
743
|
|
|
665
744
|
- **Catching errors and returning success.** Operators lose the failed run.
|
|
666
745
|
- **Retrying non-idempotent side effects.** Double charges and duplicate emails are workflow bugs, not retry bugs.
|
|
667
|
-
- **
|
|
746
|
+
- **Declaring `retry:` on a step you ALSO wrap in `Effect.retry` / `stepModule.retry`.** `retry:` is enforced now — the step would retry twice. Keep one.
|
|
668
747
|
- **Expecting a DLQ table.** The current triage surface is failed runs plus retry controls.
|
|
669
748
|
|
|
670
749
|
|
|
@@ -1181,12 +1260,22 @@ Voltro records workflow runs, step attempts, and lifecycle events into framework
|
|
|
1181
1260
|
| `_voltro_workflow_start_contexts` | One row per started execution id: starter subject, trace id, source, parent execution id, parent-close policy, and creation time. Used for cross-runner context handoff. |
|
|
1182
1261
|
| `_voltro_workflow_runs` | One row per run: `id`, `tag`, `executionId`, `status`, `payload`, `workflowVersion`, `workflowPatches`, `output`, error fields, subject, start source, timing, trace id, parent execution id, parent-close policy. |
|
|
1183
1262
|
| `_voltro_workflow_run_steps` | One row per step attempt: step name, attempt number, recorded input, retry metadata, output or error, duration. |
|
|
1184
|
-
| `_voltro_workflow_run_events` | Lifecycle events: `run-started`, `run-succeeded`, `run-failed`, `run-suspended`, `run-resumed`, `run-cancelled`, `timer-set`, `timer-fired`, `signal-awaited`, `signal-sent`, `signal-received`, `update-requested`, `update-received`, `update-completed`, `update-failed`. |
|
|
1263
|
+
| `_voltro_workflow_run_events` | Lifecycle events: `run-started`, `run-succeeded`, `run-failed`, `run-suspended`, `run-resumed`, `run-redriven`, `run-cancelled`, `timer-set`, `timer-fired`, `signal-awaited`, `signal-sent`, `signal-received`, `update-requested`, `update-received`, `update-completed`, `update-failed`. |
|
|
1185
1264
|
| `_voltro_workflow_events` | Domain events emitted through `ctx.events.publish(...)`: event id, name, payload, source, subject, trace id, occurred time. |
|
|
1186
1265
|
| `_voltro_workflow_event_deliveries` | One row per workflow trigger delivery: event id, trigger id, workflow name, execution id, idempotency key, status, error. |
|
|
1187
1266
|
|
|
1188
1267
|
Run status is `running`, `succeeded`, `failed`, `cancelled`, or `suspended`.
|
|
1189
1268
|
|
|
1269
|
+
## Dead-letter: triaging failed runs
|
|
1270
|
+
|
|
1271
|
+
The framework does not retry a workflow on its own (see [Retries](/docs/workflows/retries) — you compose retries inside `execute:` with `Effect.retry`). So a run that reaches `failed` is **terminal**: it is the dead-letter. `voltro workflows list --dead-letter` (or the Prometheus series `voltro_workflow_runs_total{status="failed"}` and the stale `voltro_workflow_last_success_timestamp_seconds` gauge — see [Prometheus](/docs/plugins/prometheus)) is your queue of unhandled failures.
|
|
1272
|
+
|
|
1273
|
+
Triage a dead-letter run one of three ways:
|
|
1274
|
+
|
|
1275
|
+
- **Retry** it — `voltro workflows retry <id>` starts a fresh execution against the original (or an overridden) payload once you've fixed the cause. Every step runs again from scratch.
|
|
1276
|
+
- **Re-drive** it — `voltro workflows redrive <id>` re-drives the run from its durable journal: completed steps replay, only the failed step(s) re-execute. Use this instead of `retry` for a long pipeline where redoing steps 1…N‑1 is expensive or unsafe. It is the after-the-fact counterpart to [`suspendOnFailure` + `resume`](/docs/workflows/retries) and works under `voltro serve` too. Refuses a non-failed / already-discarded run; declines cleanly when there is no durable journal (the memory store). Records a `run-redriven` event.
|
|
1277
|
+
- **Discard** it — `voltro workflows discard <id>` acknowledges the failure so it drops off the `--dead-letter` view. It is an **ack, not a re-classification**: the run stays `status: 'failed'` (the outcome + audit trail survive) and gains a `discardedAt` timestamp. `--status failed` still lists it, marked `discarded`; only `--dead-letter` hides it. Discarding a non-failed run is refused, and discarding is idempotent.
|
|
1278
|
+
|
|
1190
1279
|
## Dashboard
|
|
1191
1280
|
|
|
1192
1281
|
When `voltro dev` is running, the Workflows panel lists recent runs, their status, start source, timing, payload, output/error, step attempts, and events. The run detail view is the fastest way to answer:
|
|
@@ -1199,7 +1288,7 @@ When `voltro dev` is running, the Workflows panel lists recent runs, their statu
|
|
|
1199
1288
|
- What output or typed error did it produce?
|
|
1200
1289
|
- Did a timer or signal fire?
|
|
1201
1290
|
|
|
1202
|
-
The panel also exposes run controls for users with the right capability: cancel, retry, suspend, resume, send signal, and send a tracked update.
|
|
1291
|
+
The panel also exposes run controls for users with the right capability: cancel, retry, suspend, resume, redrive (re-drive a failed run from its journal), discard (acknowledge a failed run), send signal, and send a tracked update.
|
|
1203
1292
|
|
|
1204
1293
|
Run filters can be saved as named views. The selected view and ad-hoc
|
|
1205
1294
|
filters are mirrored into the URL query string, so a teammate can open the
|
|
@@ -1213,12 +1302,15 @@ queued/running/waiting work by workflow lane and start source.
|
|
|
1213
1302
|
```sh
|
|
1214
1303
|
voltro workflows list --status running --tail 50
|
|
1215
1304
|
voltro workflows list --tag notes.summarise --format json
|
|
1305
|
+
voltro workflows list --dead-letter # failed runs not yet discarded
|
|
1216
1306
|
voltro workflows start notes.summarise --payload '{"noteId":"note_123"}'
|
|
1217
1307
|
voltro workflows show wfrun_01H...
|
|
1218
1308
|
voltro workflows retry wfrun_01H...
|
|
1219
1309
|
voltro workflows cancel wfrun_01H...
|
|
1220
1310
|
voltro workflows suspend wfrun_01H...
|
|
1221
1311
|
voltro workflows resume wfrun_01H...
|
|
1312
|
+
voltro workflows redrive wfrun_01H... # re-drive a failed run from where it died
|
|
1313
|
+
voltro workflows discard wfrun_01H... # acknowledge a failed run
|
|
1222
1314
|
voltro workflows signal wfrun_01H... --name approval --payload '{"approved":true}'
|
|
1223
1315
|
voltro workflows update wfrun_01H... --name approve --payload '{"decision":true}'
|
|
1224
1316
|
voltro workflows children exec_01H...
|
|
@@ -1241,6 +1333,8 @@ POST /_voltro/inspect/workflows/runs/:id/cancel
|
|
|
1241
1333
|
POST /_voltro/inspect/workflows/runs/:id/retry
|
|
1242
1334
|
POST /_voltro/inspect/workflows/runs/:id/suspend
|
|
1243
1335
|
POST /_voltro/inspect/workflows/runs/:id/resume
|
|
1336
|
+
POST /_voltro/inspect/workflows/runs/:id/redrive
|
|
1337
|
+
POST /_voltro/inspect/workflows/runs/:id/discard
|
|
1244
1338
|
POST /_voltro/inspect/workflows/runs/:id/signal
|
|
1245
1339
|
POST /_voltro/inspect/workflows/runs/:id/update
|
|
1246
1340
|
```
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.97.0",
|
|
14
14
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/ai": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/ai": "0.26.0",
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/protocol": "0.26.0",
|
|
20
|
+
"@voltro/runtime": "0.26.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.26.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.1.10"
|
|
27
27
|
}
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-auth": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/plugin-auth": "0.26.0",
|
|
20
|
+
"@voltro/protocol": "0.26.0",
|
|
21
|
+
"@voltro/runtime": "0.26.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.26.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.26.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"vitest": "^4.1.10"
|
|
29
29
|
}
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.26.0",
|
|
20
|
+
"@voltro/protocol": "0.26.0",
|
|
21
|
+
"@voltro/runtime": "0.26.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.26.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
27
|
"vitest": "^4.1.10"
|
|
28
28
|
}
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-deactivation": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.26.0",
|
|
17
|
+
"@voltro/database": "0.26.0",
|
|
18
|
+
"@voltro/env": "0.26.0",
|
|
19
|
+
"@voltro/plugin-deactivation": "0.26.0",
|
|
20
|
+
"@voltro/protocol": "0.26.0",
|
|
21
|
+
"@voltro/runtime": "0.26.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.26.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
27
|
"vitest": "^4.1.10"
|
|
28
28
|
}
|