@voltro/workflow 0.60.0 → 0.62.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,49 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.62.0] — 2026-09-01
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/workflow, @voltro/cli, @voltro/voltro** — `timeouts.finish` now filters running rows by the declared workflow, durably cancels each run before notification, and emits `onFailure` only for the successful terminal transition. Finish-timeout reports carry the run's stable `runId`, `executionId`, and original payload; handler-start logs expose the returned status instead of unconditionally claiming a new start.
47
+
48
+ Low-level adapters that construct `AdmissionDrainDeps` must update `cancelRun` to accept the run object and return whether it made the terminal transition, and return the run identity, workflow name, and payload from `listRunningRuns`. Ordinary workflow declarations need no source change; `voltro update` prints the adapter migration only when the project references this low-level surface.
49
+
50
+ ### Added
51
+
52
+ - **@voltro/ai** — `streamText` and `resumableStreamText` now accept an opt-in `smooth` configuration for word- or line-based text and reasoning chunks. Smoothing runs before resumable journaling, preserves provider-native behavior when omitted, and shares the stream abort signal so cancellation also stops a pending smoothing delay.
53
+
54
+ ### Fixed
55
+
56
+ - **@voltro/web** — Production `framework_debug` now enables local framework diagnostics without opening browser-to-inspect HTTP or SSE transports. Development log relays are single-flight and rate-limited, classify permanent response failures, back off transient failures, expose dropped-log counts, and open a page-scoped circuit after repeated failure; the SSE relay also probes the endpoint before creating an `EventSource` and closes failed sources before retrying.
57
+
58
+ ---
59
+
60
+ ## [0.61.0] — 2026-08-31
61
+
62
+ ### ⚠ BREAKING
63
+
64
+ - **@voltro/client, @voltro/web, @voltro/cli** — `RpcError.kind` now distinguishes transport, handler, client, and unknown failures; manually emitted events must add the field, while `useConnectionStatus` now degrades only for transport failures and clears on the next rpc success.
65
+
66
+ **`voltro update` carries you across this** — codemod `0.61.0/01_rpc_errors_have_kinds`. 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.61.0).
67
+ - **@voltro/plugin-sentry, @voltro/cli** — Browser RPC spans now join the active page/navigation transaction and link to the Effect/server trace instead of creating one root transaction per call; `tracesSampleRate`, `browserTracing`, and the new `rpcSpans` switch control the resulting browser trace volume.
68
+
69
+ **`voltro update` carries you across this** — codemod `0.61.0/02_sentry_rpc_spans_join_page`. 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.61.0).
70
+
71
+ ### Fixed
72
+
73
+ - **@voltro/cli** — **Six SSR failure sites report to Sentry, not three.**
74
+
75
+ 0.60.0 gave the web server its own Sentry and wired the three loud sites: the SSR shell throw, the SPA layout shell throw, and the general render error (which covers a loader throw). The docs said "shell throw, loader throw, PPR/SWR refresh" — and an enumeration in a claim is read as exhaustive.
76
+
77
+ The other three were `warn`-level and reported to nobody: a failed PPR hole pass, a failed background SWR refresh, and a `not-found.tsx` that throws while rendering. They are quieter because the request still serves something — stale HTML, an unfilled hole, a plain 404 — which says something about the REQUEST and nothing about who else could find out. The answer to that is nobody: none of them reaches a browser boundary, so the pod's stdout was the only record.
78
+
79
+ They carry their own `voltro.stage` (`not-found-render`, `ppr-holes`, `swr-refresh`), so a quota-conscious project can drop them by stage without losing the three that fail the request.
80
+
81
+ `webSentry.test.ts` now fails if a render-path `log.error`/`log.warn` gains no reporter beside it — the property, rather than the six call sites.
82
+
83
+ ---
84
+
42
85
  ## [0.60.0] — 2026-08-31
43
86
 
44
87
  ### ⚠ BREAKING
@@ -1,5 +1,5 @@
1
1
  import { I as e, P as t, S as n, g as r, v as i } from "./primitives-B-HlFCap.js";
2
- import { o as a } from "./src-CoPlQmC5.js";
2
+ import { o as a } from "./src-B1XsZ6OO.js";
3
3
  import { i as o, p as s } from "./cluster-OUXsLZ2x.js";
4
4
  import { Cron as c, Deferred as l, Effect as u, Fiber as d, Layer as f, Schema as p } from "effect";
5
5
  import { SqlClient as m } from "@effect/sql";
package/dist/index.d.ts CHANGED
@@ -81,11 +81,21 @@ export declare interface AdmissionDrainDeps {
81
81
  * `admitted` marker that skips the gate.
82
82
  */
83
83
  readonly startAdmitted: (workflowName: string, payload: unknown, callerContext: unknown) => Promise<AdmittedStart>;
84
- /** Cancel a run — used by singleton eviction and by `timeouts.finish`. */
85
- readonly cancelRun: (workflowName: string, executionId: string) => Promise<void>;
86
84
  /**
87
- * A start that will never happen: `timeouts.start` elapsed, or the workflow
88
- * was deleted while its intents were queued. Wired to the same `onFailure`
85
+ * Cancel a run through the durable run-state boundary engine interrupt,
86
+ * terminal row, event, and child cleanup used by singleton eviction and
87
+ * `timeouts.finish`. `false` means the run was already terminal or absent.
88
+ */
89
+ readonly cancelRun: (input: {
90
+ readonly workflowName: string;
91
+ readonly runId: string;
92
+ readonly executionId: string;
93
+ readonly reason: string;
94
+ readonly detail?: Record<string, unknown>;
95
+ }) => Promise<boolean>;
96
+ /**
97
+ * Work that will not complete: a queued start expired or lost its workflow,
98
+ * or `timeouts.finish` durably cancelled a run. Wired to the same `onFailure`
89
99
  * path an exhausted retry takes — a job that silently did not happen is the
90
100
  * failure this whole feature exists to remove, so it must not be the one
91
101
  * outcome with no notification.
@@ -95,10 +105,15 @@ export declare interface AdmissionDrainDeps {
95
105
  readonly payload: unknown;
96
106
  readonly callerContext: unknown;
97
107
  readonly reason: string;
108
+ readonly runId?: string | null;
109
+ readonly executionId?: string | null;
98
110
  }) => Promise<void>;
99
111
  /** Runs of a controlled workflow that are still running, for `timeouts.finish`. */
100
112
  readonly listRunningRuns?: (workflowName: string) => Promise<ReadonlyArray<{
113
+ readonly runId: string;
114
+ readonly workflowName: string;
101
115
  readonly executionId: string;
116
+ readonly payload: unknown;
102
117
  readonly startedAt: number;
103
118
  }>>;
104
119
  /**
@@ -2488,14 +2503,13 @@ export declare interface WorkflowExecuteRecordingOptions extends WorkflowRunReco
2488
2503
  export declare interface WorkflowFailureReport {
2489
2504
  /** The workflow that failed. */
2490
2505
  readonly workflow: string;
2491
- /** Its payload, as it was started with. Null when the failure happened before
2492
- * a payload existed (a `timeouts.finish` sweep reads the run row, not the
2493
- * intent). */
2506
+ /** Its payload, as it was started with. */
2494
2507
  readonly payload: unknown;
2495
2508
  /** Tagged error name, when the failure carried one. */
2496
2509
  readonly errorTag: string | null;
2497
2510
  readonly errorMessage: string | null;
2498
- /** `_voltro_workflow_runs.id`, when a run existed. */
2511
+ /** `_voltro_workflow_runs.id`, when a run existed. Always populated for
2512
+ * `timeouts.finish`, so the report has a stable identity. */
2499
2513
  readonly runId: string | null;
2500
2514
  readonly executionId: string | null;
2501
2515
  /** Human-readable, always present: which of the four paths this was. */
@@ -3113,10 +3127,11 @@ export declare interface WorkflowThrottle<Payload> {
3113
3127
  * concurrency-queued run that never gets a slot is a job that silently did not
3114
3128
  * happen. `finish` bounds the run itself once admitted.
3115
3129
  *
3116
- * Both expire into the SAME path as an exhausted retry: the run is recorded
3117
- * `failed` with an `errorTag` naming which bound was hit, and `onFailure` fires.
3118
- * A timeout that expired quietly would be the sweep-instead-of-signal shape this
3119
- * whole feature exists to remove.
3130
+ * Both reach the same `onFailure` signal as an exhausted retry. `start` expires
3131
+ * the queued intent before a run exists; `finish` durably cancels the running
3132
+ * row and signals only when that transition wins. A timeout that expired
3133
+ * quietly or signalled again on every sweep — would make the notification
3134
+ * path less trustworthy than the work it reports.
3120
3135
  */
3121
3136
  export declare interface WorkflowTimeouts {
3122
3137
  readonly start?: FlowDuration;
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { A as e, B as t, C as n, D as r, E as i, G as a, H as o, I as s, L as c, M as l, N as u, O as d, P as f, R as p, S as m, T as h, U as g, V as _, W as v, _ as y, a as b, b as x, c as S, d as C, f as w, g as T, h as E, i as D, j as O, k, l as A, m as j, n as M, o as N, p as P, r as F, s as I, t as L, u as R, v as z, w as B, x as V, y as H, z as U } from "./primitives-B-HlFCap.js";
2
- import { $ as W, A as G, At as K, B as q, C as J, Ct as Y, D as X, Dt as Z, E as Q, Et as $, F as ee, Ft as te, G as ne, H as re, I as ie, It as ae, J as oe, K as se, L as ce, Lt as le, M as ue, Mt as de, N as fe, Nt as pe, O as me, Ot as he, P as ge, Pt as _e, Q as ve, R as ye, Rt as be, S as xe, St as Se, T as Ce, Tt as we, U as Te, V as Ee, W as De, X as Oe, Y as ke, Z as Ae, _ as je, _t as Me, a as Ne, at as Pe, b as Fe, bt as Ie, c as Le, ct as Re, d as ze, dt as Be, et as Ve, f as He, ft as Ue, g as We, gt as Ge, h as Ke, ht as qe, i as Je, it as Ye, j as Xe, jt as Ze, k as Qe, kt as $e, l as et, lt as tt, m as nt, mt as rt, n as it, nt as at, o as ot, ot as st, p as ct, pt as lt, q as ut, r as dt, rt as ft, s as pt, st as mt, t as ht, tt as gt, u as _t, ut as vt, v as yt, vt as bt, w as xt, wt as St, x as Ct, xt as wt, y as Tt, yt as Et, z as Dt, zt as Ot } from "./src-CoPlQmC5.js";
2
+ import { $ as W, A as G, At as K, B as q, C as J, Ct as Y, D as X, Dt as Z, E as Q, Et as $, F as ee, Ft as te, G as ne, H as re, I as ie, It as ae, J as oe, K as se, L as ce, Lt as le, M as ue, Mt as de, N as fe, Nt as pe, O as me, Ot as he, P as ge, Pt as _e, Q as ve, R as ye, Rt as be, S as xe, St as Se, T as Ce, Tt as we, U as Te, V as Ee, W as De, X as Oe, Y as ke, Z as Ae, _ as je, _t as Me, a as Ne, at as Pe, b as Fe, bt as Ie, c as Le, ct as Re, d as ze, dt as Be, et as Ve, f as He, ft as Ue, g as We, gt as Ge, h as Ke, ht as qe, i as Je, it as Ye, j as Xe, jt as Ze, k as Qe, kt as $e, l as et, lt as tt, m as nt, mt as rt, n as it, nt as at, o as ot, ot as st, p as ct, pt as lt, q as ut, r as dt, rt as ft, s as pt, st as mt, t as ht, tt as gt, u as _t, ut as vt, v as yt, vt as bt, w as xt, wt as St, x as Ct, xt as wt, y as Tt, yt as Et, z as Dt, zt as Ot } from "./src-B1XsZ6OO.js";
3
3
  export { ft as ADMISSIONS_TABLE, ct as BUDGET_HOLDS_TABLE, nt as BudgetHoldExpired, De as CANCEL_ON_WATERMARK, l as CurrentWorkflowExecutionId, u as CurrentWorkflowPatches, B as CurrentWorkflowReplayShape, f as CurrentWorkflowRunId, Ke as DEFAULT_BUDGET_HOLD_TIMEOUT_MS, ne as DEFAULT_COLD_LOOKBACK_MS, ye as DEFAULT_DRAIN_BATCH, se as DEFAULT_EVENT_BATCH, Ye as DEFAULT_LEASE_MS, Je as DEFAULT_MAX_RUN_RECLAIMS, Ne as DEFAULT_REPLAY_SHAPE_LIMIT, q as DEFAULT_STALL_AFTER_MS, Ee as DEFAULT_STALL_EVENT_LOOKBACK, re as DEFAULT_STALL_RUN_PAGE, fe as DEFAULT_SUSPEND_HINT_MS, d as DEFAULT_WORKFLOW_SCHEDULE_MAX_RUNTIME_MS, t as DEFERRING_CONTROLS, ut as EVENTS_TABLE, St as FlowControlKeyError, Pe as PAUSES_TABLE, st as PENDING_TABLE, oe as WATERMARKS_TABLE, L as WorkflowFlowControlProperty, M as WorkflowMessagesProperty, s as WorkflowRunRecorder, k as WorkflowScheduleProperty, c as WorkflowStepInterceptorTag, F as WorkflowVersionTypeId, D as WorkflowWorkerLayerTypeId, We as _voltroBudgetHoldsTable, de as _voltroWorkflowAdmissionsTable, pe as _voltroWorkflowPausesTable, _e as _voltroWorkflowPendingTable, ae as _voltroWorkflowRunEventsTable, le as _voltroWorkflowRunStepsTable, be as _voltroWorkflowRunsTable, te as _voltroWorkflowStartContextsTable, Ve as _voltroWorkflowWatermarksTable, gt as admitStart, mt as allIntents, _ as assertConsistentPools, Xe as awaitEvent, ue as awaitSignal, xt as awaitSignalSuspending, He as awaitUpdate, je as budgetHoldGeneration, yt as budgetHoldKey, Tt as budgetHoldSignalName, dt as closeWorkflowChildrenForParent, we as comparePendingOrder, Ce as completeSuspendingSignal, $ as decideAdmission, o as deferringControlsOf, Re as deleteIntent, tt as discardIntent, Dt as drainTick, vt as dueIntents, b as durableClock, N as durableQueue, I as durableQueueModule, S as durableRateLimiterModule, Be as evaluateAdmission, p as getCurrentWorkflowExecutionId, U as getCurrentWorkflowRunId, A as getWorkflowFlowControl, e as getWorkflowSchedule, R as getWorkflowVersionMetadata, g as hasCancelOn, v as hasFlowControl, Fe as heldBudgets, Ot as inMemoryWorkflowEngineLayer, it as inspectWorkflow, Z as isFinishExpired, he as isStartExpired, C as isWorkflowWorkerLayer, at as linkExecution, ht as makeInMemoryRecorder, h as makeReplayShapeTracker, ot as makeWorkflowRunRecorder, X as makeWorkflowUpdateId, ge as maybeHintSuspendingSignal, ve as newestEventAt, $e as nextSlotAt, i as nondeterminismEventPayload, pt as normaliseWorkflowPatches, Ue as noteIntentAttempt, lt as parkDelayedStart, w as patch, rt as pauseWorkflow, Ct as pendingBudgetHolds, qe as pendingSlotId, Ge as pendingWindow, Me as persistDefer, W as planCancellations, K as pooledConcurrencyKey, P as processQueue, bt as pruneAdmissions, j as queueWorker, E as rateLimit, Et as readAdmissionState, Ie as readPausedWorkflows, ke as readWatermark, wt as recordAdmission, xe as releaseBudgetHolds, Se as releaseLease, r as reportNondeterminism, ee as resetSuspendHintsForTest, Ze as resolveAdmissionKeys, Le as resolveRunGuardTuning, ie as resolveSuspendHintMs, me as resolveWorkflowMessageRun, Y as resumeWorkflow, Qe as sendWorkflowSignal, G as sendWorkflowUpdate, et as serialiseWorkflowRowForWire, ce as setSuspendSignalHintMs, T as sleep, y as sleepUntil, z as step, H as stepIdempotencyKey, x as stepModule, J as suspendForBudget, Q as suspendingSignalDeferredName, Oe as sweepCancelOn, Te as sweepStalledRuns, _t as truncateWorkflowValue, a as validateFlowControl, O as validateWorkflowSchedule, V as withCompensation, m as workflow, n as workflowModule, ze as wrapWorkflowExecuteWithRunRecording, Ae as writeWatermark };
@@ -737,14 +737,13 @@ export declare interface WorkflowDebounce<Payload> {
737
737
  export declare interface WorkflowFailureReport {
738
738
  /** The workflow that failed. */
739
739
  readonly workflow: string;
740
- /** Its payload, as it was started with. Null when the failure happened before
741
- * a payload existed (a `timeouts.finish` sweep reads the run row, not the
742
- * intent). */
740
+ /** Its payload, as it was started with. */
743
741
  readonly payload: unknown;
744
742
  /** Tagged error name, when the failure carried one. */
745
743
  readonly errorTag: string | null;
746
744
  readonly errorMessage: string | null;
747
- /** `_voltro_workflow_runs.id`, when a run existed. */
745
+ /** `_voltro_workflow_runs.id`, when a run existed. Always populated for
746
+ * `timeouts.finish`, so the report has a stable identity. */
748
747
  readonly runId: string | null;
749
748
  readonly executionId: string | null;
750
749
  /** Human-readable, always present: which of the four paths this was. */
@@ -1045,10 +1044,11 @@ export declare interface WorkflowThrottle<Payload> {
1045
1044
  * concurrency-queued run that never gets a slot is a job that silently did not
1046
1045
  * happen. `finish` bounds the run itself once admitted.
1047
1046
  *
1048
- * Both expire into the SAME path as an exhausted retry: the run is recorded
1049
- * `failed` with an `errorTag` naming which bound was hit, and `onFailure` fires.
1050
- * A timeout that expired quietly would be the sweep-instead-of-signal shape this
1051
- * whole feature exists to remove.
1047
+ * Both reach the same `onFailure` signal as an exhausted retry. `start` expires
1048
+ * the queued intent before a run exists; `finish` durably cancels the running
1049
+ * row and signals only when that transition wins. A timeout that expired
1050
+ * quietly or signalled again on every sweep — would make the notification
1051
+ * path less trustworthy than the work it reports.
1052
1052
  */
1053
1053
  export declare interface WorkflowTimeouts {
1054
1054
  readonly start?: FlowDuration;
@@ -1213,7 +1213,16 @@ var le = f.layerMemory, P = j("_voltro_workflow_runs", {
1213
1213
  return;
1214
1214
  }
1215
1215
  let s = await e.startAdmitted(a.workflowName, o.payload, t.callerContext);
1216
- o.evict !== void 0 && (await e.cancelRun(a.workflowName, o.evict.executionId).catch(() => {}), await W(e.store, o.evict.executionId, i).catch(() => {}), r.evicted++);
1216
+ o.evict !== void 0 && (await e.cancelRun({
1217
+ workflowName: a.workflowName,
1218
+ runId: o.evict.runId,
1219
+ executionId: o.evict.executionId,
1220
+ reason: "cancelled to make room for a newer start (singleton mode 'cancel')",
1221
+ detail: {
1222
+ source: "flow-control",
1223
+ mode: "singleton"
1224
+ }
1225
+ }).catch(() => !1), await W(e.store, o.evict.executionId, i).catch(() => {}), r.evicted++);
1217
1226
  for (let t of o.consume) await G(e.store, t).catch(() => {});
1218
1227
  o.ledgerId !== void 0 && await Ge(e.store, o.ledgerId, s.executionId, s.runId ?? null).catch(() => {}), await G(e.store, t.id), r.admitted++;
1219
1228
  }, yt = async (e, t, n, r, i) => {
@@ -1307,7 +1316,16 @@ var le = f.layerMemory, P = j("_voltro_workflow_runs", {
1307
1316
  o.skipped++;
1308
1317
  return;
1309
1318
  }
1310
- i.evict !== void 0 && (await e.cancelRun(n.workflowName, i.evict.executionId), await W(e.store, i.evict.executionId, s), await U({
1319
+ i.evict !== void 0 && (await e.cancelRun({
1320
+ workflowName: n.workflowName,
1321
+ runId: i.evict.runId,
1322
+ executionId: i.evict.executionId,
1323
+ reason: "cancelled to make room for a newer start (singleton mode 'cancel')",
1324
+ detail: {
1325
+ source: "flow-control",
1326
+ mode: "singleton"
1327
+ }
1328
+ }), await W(e.store, i.evict.executionId, s), await U({
1311
1329
  store: e.store,
1312
1330
  control: n,
1313
1331
  keys: a,
@@ -1344,15 +1362,30 @@ var le = f.layerMemory, P = j("_voltro_workflow_runs", {
1344
1362
  for (let r of e.controls.values()) {
1345
1363
  if (r.timeouts?.finishMs === void 0) continue;
1346
1364
  let i = await e.listRunningRuns(r.workflowName);
1347
- for (let a of i) Se(r, {
1348
- startedAt: a.startedAt,
1349
- terminal: !1
1350
- }, t) && (await e.cancelRun(r.workflowName, a.executionId), await W(e.store, a.executionId, t), await e.onAbandoned({
1351
- workflowName: r.workflowName,
1352
- payload: null,
1353
- callerContext: null,
1354
- reason: `timeouts.finish (${r.timeouts.finishMs}ms) elapsed; the run was cancelled after ${t - a.startedAt}ms`
1355
- }), n++);
1365
+ for (let a of i) {
1366
+ if (!Se(r, {
1367
+ startedAt: a.startedAt,
1368
+ terminal: !1
1369
+ }, t)) continue;
1370
+ let i = `timeouts.finish (${r.timeouts.finishMs}ms) elapsed; the run was cancelled after ${t - a.startedAt}ms`;
1371
+ await e.cancelRun({
1372
+ workflowName: a.workflowName,
1373
+ runId: a.runId,
1374
+ executionId: a.executionId,
1375
+ reason: i,
1376
+ detail: {
1377
+ source: "flow-control",
1378
+ timeout: "finish"
1379
+ }
1380
+ }) && (await W(e.store, a.executionId, t), await e.onAbandoned({
1381
+ workflowName: a.workflowName,
1382
+ payload: a.payload,
1383
+ callerContext: null,
1384
+ reason: i,
1385
+ runId: a.runId,
1386
+ executionId: a.executionId
1387
+ }), n++);
1388
+ }
1356
1389
  }
1357
1390
  return n;
1358
1391
  }, St = 3e5, Ct, wt = (e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/workflow",
3
- "version": "0.60.0",
3
+ "version": "0.62.0",
4
4
  "description": "Durable workflows for Voltro — the workflow() descriptor + step() / awaitSignal primitives over @effect/cluster, with a browser-safe define subpath.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -51,9 +51,9 @@
51
51
  "@effect/cluster": "^0.60.0",
52
52
  "@effect/sql": "^0.52.0",
53
53
  "@effect/workflow": "^0.19.0",
54
- "@voltro/database": "0.60.0",
55
- "@voltro/logger": "0.60.0",
56
- "@voltro/protocol": "0.60.0"
54
+ "@voltro/database": "0.62.0",
55
+ "@voltro/logger": "0.62.0",
56
+ "@voltro/protocol": "0.62.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "effect": "^3.22.0",