@rivetkit/workflow-engine 2.1.4 → 2.1.6-rc.1

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.
@@ -1,3 +1,53 @@
1
+ // src/error-utils.ts
2
+ var WORKFLOW_ERROR_REPORTED_SYMBOL = /* @__PURE__ */ Symbol("workflow.error.reported");
3
+ function extractErrorInfo(error) {
4
+ if (error instanceof Error) {
5
+ const result = {
6
+ name: error.name,
7
+ message: error.message,
8
+ stack: error.stack
9
+ };
10
+ const metadata = {};
11
+ for (const key of Object.keys(error)) {
12
+ if (key !== "name" && key !== "message" && key !== "stack") {
13
+ const value = error[key];
14
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
15
+ metadata[key] = value;
16
+ }
17
+ }
18
+ }
19
+ if (Object.keys(metadata).length > 0) {
20
+ result.metadata = metadata;
21
+ }
22
+ return result;
23
+ }
24
+ return {
25
+ name: "Error",
26
+ message: String(error)
27
+ };
28
+ }
29
+ function markErrorReported(error) {
30
+ error[WORKFLOW_ERROR_REPORTED_SYMBOL] = true;
31
+ return error;
32
+ }
33
+ function isErrorReported(error) {
34
+ if (!(error instanceof Error)) {
35
+ return false;
36
+ }
37
+ return Boolean(
38
+ error[WORKFLOW_ERROR_REPORTED_SYMBOL]
39
+ );
40
+ }
41
+ function getErrorEventTag(event) {
42
+ if ("step" in event) {
43
+ return "step";
44
+ }
45
+ if ("rollback" in event) {
46
+ return "rollback";
47
+ }
48
+ return "workflow";
49
+ }
50
+
1
51
  // src/errors.ts
2
52
  var CriticalError = class extends Error {
3
53
  constructor(message) {
@@ -63,11 +113,12 @@ var StepExhaustedError = class extends Error {
63
113
  }
64
114
  };
65
115
  var StepFailedError = class extends Error {
66
- constructor(stepName, originalError, attempts) {
116
+ constructor(stepName, originalError, attempts, retryAt) {
67
117
  super(`Step "${stepName}" failed (attempt ${attempts})`);
68
118
  this.stepName = stepName;
69
119
  this.originalError = originalError;
70
120
  this.attempts = attempts;
121
+ this.retryAt = retryAt;
71
122
  this.name = "StepFailedError";
72
123
  this.cause = originalError;
73
124
  }
@@ -101,6 +152,126 @@ var EntryInProgressError = class extends Error {
101
152
  }
102
153
  };
103
154
 
155
+ // src/keys.ts
156
+ import * as tuple from "fdb-tuple";
157
+ var KEY_PREFIX = {
158
+ NAMES: 1,
159
+ // Name registry: [1, index]
160
+ HISTORY: 2,
161
+ // History entries: [2, ...locationSegments]
162
+ WORKFLOW: 3,
163
+ // Workflow metadata: [3, field]
164
+ ENTRY_METADATA: 4
165
+ // Entry metadata: [4, entryId]
166
+ };
167
+ var WORKFLOW_FIELD = {
168
+ STATE: 1,
169
+ OUTPUT: 2,
170
+ ERROR: 3,
171
+ INPUT: 4
172
+ };
173
+ function segmentToTuple(segment) {
174
+ if (typeof segment === "number") {
175
+ return segment;
176
+ }
177
+ return [segment.loop, segment.iteration];
178
+ }
179
+ function locationToTupleElements(location) {
180
+ return location.map(segmentToTuple);
181
+ }
182
+ function bufferToUint8Array(buf) {
183
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
184
+ }
185
+ function uint8ArrayToBuffer(arr) {
186
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
187
+ }
188
+ function pack2(items) {
189
+ const buf = tuple.pack(items);
190
+ return bufferToUint8Array(buf);
191
+ }
192
+ function unpack2(data) {
193
+ const buf = uint8ArrayToBuffer(data);
194
+ return tuple.unpack(buf);
195
+ }
196
+ function buildNameKey(index) {
197
+ return pack2([KEY_PREFIX.NAMES, index]);
198
+ }
199
+ function buildNamePrefix() {
200
+ return pack2([KEY_PREFIX.NAMES]);
201
+ }
202
+ function buildHistoryKey(location) {
203
+ return pack2([KEY_PREFIX.HISTORY, ...locationToTupleElements(location)]);
204
+ }
205
+ function buildHistoryPrefix(location) {
206
+ return pack2([KEY_PREFIX.HISTORY, ...locationToTupleElements(location)]);
207
+ }
208
+ function buildLoopIterationRange(loopLocation, loopSegment, fromIteration, toIteration) {
209
+ const loopLocationSegments = locationToTupleElements(loopLocation);
210
+ return {
211
+ start: pack2([
212
+ KEY_PREFIX.HISTORY,
213
+ ...loopLocationSegments,
214
+ [loopSegment, fromIteration]
215
+ ]),
216
+ end: pack2([
217
+ KEY_PREFIX.HISTORY,
218
+ ...loopLocationSegments,
219
+ [loopSegment, toIteration]
220
+ ])
221
+ };
222
+ }
223
+ function buildHistoryPrefixAll() {
224
+ return pack2([KEY_PREFIX.HISTORY]);
225
+ }
226
+ function buildWorkflowStateKey() {
227
+ return pack2([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.STATE]);
228
+ }
229
+ function buildWorkflowOutputKey() {
230
+ return pack2([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.OUTPUT]);
231
+ }
232
+ function buildWorkflowErrorKey() {
233
+ return pack2([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.ERROR]);
234
+ }
235
+ function buildWorkflowInputKey() {
236
+ return pack2([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.INPUT]);
237
+ }
238
+ function buildEntryMetadataKey(entryId) {
239
+ return pack2([KEY_PREFIX.ENTRY_METADATA, entryId]);
240
+ }
241
+ function buildEntryMetadataPrefix() {
242
+ return pack2([KEY_PREFIX.ENTRY_METADATA]);
243
+ }
244
+ function parseNameKey(key) {
245
+ const elements = unpack2(key);
246
+ if (elements.length !== 2 || elements[0] !== KEY_PREFIX.NAMES) {
247
+ throw new Error("Invalid name key");
248
+ }
249
+ return elements[1];
250
+ }
251
+ function keyStartsWith(key, prefix) {
252
+ if (key.length < prefix.length) {
253
+ return false;
254
+ }
255
+ for (let i = 0; i < prefix.length; i++) {
256
+ if (key[i] !== prefix[i]) {
257
+ return false;
258
+ }
259
+ }
260
+ return true;
261
+ }
262
+ function compareKeys(a, b) {
263
+ const minLen = Math.min(a.length, b.length);
264
+ for (let i = 0; i < minLen; i++) {
265
+ if (a[i] !== b[i]) {
266
+ return a[i] - b[i];
267
+ }
268
+ }
269
+ return a.length - b.length;
270
+ }
271
+ function keyToHex(key) {
272
+ return Array.from(key).map((b) => b.toString(16).padStart(2, "0")).join("");
273
+ }
274
+
104
275
  // src/location.ts
105
276
  function isLoopIterationMarker(segment) {
106
277
  return typeof segment === "object" && "loop" in segment;
@@ -1223,111 +1394,6 @@ function deserializeName(bytes) {
1223
1394
  return decoder.decode(bytes);
1224
1395
  }
1225
1396
 
1226
- // src/keys.ts
1227
- import * as tuple from "fdb-tuple";
1228
- var KEY_PREFIX = {
1229
- NAMES: 1,
1230
- // Name registry: [1, index]
1231
- HISTORY: 2,
1232
- // History entries: [2, ...locationSegments]
1233
- WORKFLOW: 3,
1234
- // Workflow metadata: [3, field]
1235
- ENTRY_METADATA: 4
1236
- // Entry metadata: [4, entryId]
1237
- };
1238
- var WORKFLOW_FIELD = {
1239
- STATE: 1,
1240
- OUTPUT: 2,
1241
- ERROR: 3,
1242
- INPUT: 4
1243
- };
1244
- function segmentToTuple(segment) {
1245
- if (typeof segment === "number") {
1246
- return segment;
1247
- }
1248
- return [segment.loop, segment.iteration];
1249
- }
1250
- function locationToTupleElements(location) {
1251
- return location.map(segmentToTuple);
1252
- }
1253
- function bufferToUint8Array(buf) {
1254
- return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
1255
- }
1256
- function uint8ArrayToBuffer(arr) {
1257
- return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
1258
- }
1259
- function pack2(items) {
1260
- const buf = tuple.pack(items);
1261
- return bufferToUint8Array(buf);
1262
- }
1263
- function unpack2(data) {
1264
- const buf = uint8ArrayToBuffer(data);
1265
- return tuple.unpack(buf);
1266
- }
1267
- function buildNameKey(index) {
1268
- return pack2([KEY_PREFIX.NAMES, index]);
1269
- }
1270
- function buildNamePrefix() {
1271
- return pack2([KEY_PREFIX.NAMES]);
1272
- }
1273
- function buildHistoryKey(location) {
1274
- return pack2([KEY_PREFIX.HISTORY, ...locationToTupleElements(location)]);
1275
- }
1276
- function buildHistoryPrefix(location) {
1277
- return pack2([KEY_PREFIX.HISTORY, ...locationToTupleElements(location)]);
1278
- }
1279
- function buildHistoryPrefixAll() {
1280
- return pack2([KEY_PREFIX.HISTORY]);
1281
- }
1282
- function buildWorkflowStateKey() {
1283
- return pack2([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.STATE]);
1284
- }
1285
- function buildWorkflowOutputKey() {
1286
- return pack2([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.OUTPUT]);
1287
- }
1288
- function buildWorkflowErrorKey() {
1289
- return pack2([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.ERROR]);
1290
- }
1291
- function buildWorkflowInputKey() {
1292
- return pack2([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.INPUT]);
1293
- }
1294
- function buildEntryMetadataKey(entryId) {
1295
- return pack2([KEY_PREFIX.ENTRY_METADATA, entryId]);
1296
- }
1297
- function buildEntryMetadataPrefix() {
1298
- return pack2([KEY_PREFIX.ENTRY_METADATA]);
1299
- }
1300
- function parseNameKey(key) {
1301
- const elements = unpack2(key);
1302
- if (elements.length !== 2 || elements[0] !== KEY_PREFIX.NAMES) {
1303
- throw new Error("Invalid name key");
1304
- }
1305
- return elements[1];
1306
- }
1307
- function keyStartsWith(key, prefix) {
1308
- if (key.length < prefix.length) {
1309
- return false;
1310
- }
1311
- for (let i = 0; i < prefix.length; i++) {
1312
- if (key[i] !== prefix[i]) {
1313
- return false;
1314
- }
1315
- }
1316
- return true;
1317
- }
1318
- function compareKeys(a, b) {
1319
- const minLen = Math.min(a.length, b.length);
1320
- for (let i = 0; i < minLen; i++) {
1321
- if (a[i] !== b[i]) {
1322
- return a[i] - b[i];
1323
- }
1324
- }
1325
- return a.length - b.length;
1326
- }
1327
- function keyToHex(key) {
1328
- return Array.from(key).map((b) => b.toString(16).padStart(2, "0")).join("");
1329
- }
1330
-
1331
1397
  // src/storage.ts
1332
1398
  function createStorage() {
1333
1399
  return {
@@ -1437,7 +1503,7 @@ async function loadMetadata(storage, driver, entryId) {
1437
1503
  }
1438
1504
  return getOrCreateMetadata(storage, entryId);
1439
1505
  }
1440
- async function flush(storage, driver, onHistoryUpdated) {
1506
+ async function flush(storage, driver, onHistoryUpdated, pendingDeletions) {
1441
1507
  const writes = [];
1442
1508
  let historyUpdated = false;
1443
1509
  for (let i = storage.flushedNameCount; i < storage.nameRegistry.length; i++) {
@@ -1492,6 +1558,22 @@ async function flush(storage, driver, onHistoryUpdated) {
1492
1558
  if (writes.length > 0) {
1493
1559
  await driver.batch(writes);
1494
1560
  }
1561
+ if (pendingDeletions) {
1562
+ const deleteOps = [];
1563
+ for (const prefix of pendingDeletions.prefixes) {
1564
+ deleteOps.push(driver.deletePrefix(prefix));
1565
+ }
1566
+ for (const range of pendingDeletions.ranges) {
1567
+ deleteOps.push(driver.deleteRange(range.start, range.end));
1568
+ }
1569
+ for (const key of pendingDeletions.keys) {
1570
+ deleteOps.push(driver.delete(key));
1571
+ }
1572
+ if (deleteOps.length > 0) {
1573
+ await Promise.all(deleteOps);
1574
+ historyUpdated = true;
1575
+ }
1576
+ }
1495
1577
  storage.flushedNameCount = storage.nameRegistry.length;
1496
1578
  storage.flushedState = storage.state;
1497
1579
  storage.flushedOutput = storage.output;
@@ -1501,21 +1583,27 @@ async function flush(storage, driver, onHistoryUpdated) {
1501
1583
  }
1502
1584
  }
1503
1585
  async function deleteEntriesWithPrefix(storage, driver, prefixLocation, onHistoryUpdated) {
1504
- const entryIds = [];
1586
+ const deletions = collectDeletionsForPrefix(storage, prefixLocation);
1587
+ await driver.deletePrefix(deletions.prefixes[0]);
1588
+ await Promise.all(deletions.keys.map((key) => driver.delete(key)));
1589
+ if (deletions.keys.length > 0 && onHistoryUpdated) {
1590
+ onHistoryUpdated();
1591
+ }
1592
+ }
1593
+ function collectDeletionsForPrefix(storage, prefixLocation) {
1594
+ const pending = {
1595
+ prefixes: [buildHistoryPrefix(prefixLocation)],
1596
+ keys: [],
1597
+ ranges: []
1598
+ };
1505
1599
  for (const [key, entry] of storage.history.entries) {
1506
1600
  if (isLocationPrefix(prefixLocation, entry.location)) {
1507
- entryIds.push(entry.id);
1601
+ pending.keys.push(buildEntryMetadataKey(entry.id));
1508
1602
  storage.entryMetadata.delete(entry.id);
1509
1603
  storage.history.entries.delete(key);
1510
1604
  }
1511
1605
  }
1512
- await driver.deletePrefix(buildHistoryPrefix(prefixLocation));
1513
- await Promise.all(
1514
- entryIds.map((id) => driver.delete(buildEntryMetadataKey(id)))
1515
- );
1516
- if (entryIds.length > 0 && onHistoryUpdated) {
1517
- onHistoryUpdated();
1518
- }
1606
+ return pending;
1519
1607
  }
1520
1608
  function getEntry(storage, location) {
1521
1609
  const key = locationToKey(storage, location);
@@ -1554,9 +1642,7 @@ function setLongTimeout(listener, after) {
1554
1642
  var DEFAULT_MAX_RETRIES = 3;
1555
1643
  var DEFAULT_RETRY_BACKOFF_BASE = 100;
1556
1644
  var DEFAULT_RETRY_BACKOFF_MAX = 3e4;
1557
- var DEFAULT_LOOP_COMMIT_INTERVAL = 20;
1558
- var DEFAULT_LOOP_HISTORY_EVERY = 20;
1559
- var DEFAULT_LOOP_HISTORY_KEEP = 20;
1645
+ var DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL = 20;
1560
1646
  var DEFAULT_STEP_TIMEOUT = 3e4;
1561
1647
  var QUEUE_HISTORY_MESSAGE_MARKER = "__rivetWorkflowQueueMessage";
1562
1648
  function calculateBackoff(attempts, base, max) {
@@ -1571,7 +1657,7 @@ var StepTimeoutError = class extends Error {
1571
1657
  }
1572
1658
  };
1573
1659
  var WorkflowContextImpl = class _WorkflowContextImpl {
1574
- constructor(workflowId, storage, driver, messageDriver, location = emptyLocation(), abortController, mode = "forward", rollbackActions, rollbackCheckpointSet = false, historyNotifier, logger) {
1660
+ constructor(workflowId, storage, driver, messageDriver, location = emptyLocation(), abortController, mode = "forward", rollbackActions, rollbackCheckpointSet = false, historyNotifier, onError, logger, visitedKeys) {
1575
1661
  this.workflowId = workflowId;
1576
1662
  this.storage = storage;
1577
1663
  this.driver = driver;
@@ -1582,12 +1668,14 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1582
1668
  this.rollbackActions = rollbackActions;
1583
1669
  this.rollbackCheckpointSet = rollbackCheckpointSet;
1584
1670
  this.historyNotifier = historyNotifier;
1671
+ this.onError = onError;
1585
1672
  this.logger = logger;
1673
+ this.visitedKeys = visitedKeys ?? /* @__PURE__ */ new Set();
1586
1674
  }
1587
1675
  entryInProgress = false;
1588
1676
  abortController;
1589
1677
  currentLocation;
1590
- visitedKeys = /* @__PURE__ */ new Set();
1678
+ visitedKeys;
1591
1679
  mode;
1592
1680
  rollbackActions;
1593
1681
  rollbackCheckpointSet;
@@ -1595,6 +1683,7 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1595
1683
  usedNamesInExecution = /* @__PURE__ */ new Set();
1596
1684
  pendingCompletableMessageIds = /* @__PURE__ */ new Set();
1597
1685
  historyNotifier;
1686
+ onError;
1598
1687
  logger;
1599
1688
  get abortSignal() {
1600
1689
  return this.abortController.signal;
@@ -1637,7 +1726,9 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1637
1726
  this.rollbackActions,
1638
1727
  this.rollbackCheckpointSet,
1639
1728
  this.historyNotifier,
1640
- this.logger
1729
+ this.onError,
1730
+ this.logger,
1731
+ this.visitedKeys
1641
1732
  );
1642
1733
  }
1643
1734
  /**
@@ -1647,6 +1738,36 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1647
1738
  if (!this.logger) return;
1648
1739
  this.logger[level](data);
1649
1740
  }
1741
+ async notifyError(event) {
1742
+ if (!this.onError) {
1743
+ return;
1744
+ }
1745
+ try {
1746
+ await this.onError(event);
1747
+ } catch (error) {
1748
+ this.log("warn", {
1749
+ msg: "workflow error hook failed",
1750
+ hookEventType: getErrorEventTag(event),
1751
+ error: extractErrorInfo(error)
1752
+ });
1753
+ }
1754
+ }
1755
+ async notifyStepError(config2, attempt, error, opts) {
1756
+ const maxRetries = config2.maxRetries ?? DEFAULT_MAX_RETRIES;
1757
+ await this.notifyError({
1758
+ step: {
1759
+ workflowId: this.workflowId,
1760
+ stepName: config2.name,
1761
+ attempt,
1762
+ maxRetries,
1763
+ remainingRetries: Math.max(0, maxRetries - (attempt - 1)),
1764
+ willRetry: opts.willRetry,
1765
+ retryDelay: opts.retryDelay,
1766
+ retryAt: opts.retryAt,
1767
+ error: extractErrorInfo(error)
1768
+ }
1769
+ });
1770
+ }
1650
1771
  /**
1651
1772
  * Mark a key as visited.
1652
1773
  */
@@ -1790,10 +1911,24 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1790
1911
  if (metadata2.status === "completed" || stepData.output !== void 0) {
1791
1912
  return stepData.output;
1792
1913
  }
1793
- const maxRetries = config2.maxRetries ?? DEFAULT_MAX_RETRIES;
1794
- if (metadata2.attempts >= maxRetries) {
1914
+ const maxRetries2 = config2.maxRetries ?? DEFAULT_MAX_RETRIES;
1915
+ if (metadata2.attempts > maxRetries2) {
1795
1916
  const lastError = stepData.error ?? metadata2.error;
1796
- throw new StepExhaustedError(config2.name, lastError);
1917
+ const exhaustedError = markErrorReported(
1918
+ new StepExhaustedError(config2.name, lastError)
1919
+ );
1920
+ if (metadata2.status !== "exhausted") {
1921
+ metadata2.status = "exhausted";
1922
+ metadata2.dirty = true;
1923
+ await this.flushStorage();
1924
+ await this.notifyStepError(
1925
+ config2,
1926
+ metadata2.attempts,
1927
+ exhaustedError,
1928
+ { willRetry: false }
1929
+ );
1930
+ }
1931
+ throw exhaustedError;
1797
1932
  }
1798
1933
  const backoffDelay = calculateBackoff(
1799
1934
  metadata2.attempts,
@@ -1808,7 +1943,11 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1808
1943
  }
1809
1944
  const entry = existing ?? createEntry(location, { type: "step", data: {} });
1810
1945
  if (!existing) {
1811
- this.log("debug", { msg: "executing new step", step: config2.name, key });
1946
+ this.log("debug", {
1947
+ msg: "executing new step",
1948
+ step: config2.name,
1949
+ key
1950
+ });
1812
1951
  const nameIndex = registerName(this.storage, config2.name);
1813
1952
  entry.location = [...location];
1814
1953
  entry.location[entry.location.length - 1] = nameIndex;
@@ -1817,6 +1956,9 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1817
1956
  this.log("debug", { msg: "retrying step", step: config2.name, key });
1818
1957
  }
1819
1958
  const metadata = getOrCreateMetadata(this.storage, entry.id);
1959
+ const maxRetries = config2.maxRetries ?? DEFAULT_MAX_RETRIES;
1960
+ const retryBackoffBase = config2.retryBackoffBase ?? DEFAULT_RETRY_BACKOFF_BASE;
1961
+ const retryBackoffMax = config2.retryBackoffMax ?? DEFAULT_RETRY_BACKOFF_MAX;
1820
1962
  metadata.status = "running";
1821
1963
  metadata.attempts++;
1822
1964
  metadata.lastAttemptAt = Date.now();
@@ -1836,10 +1978,18 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1836
1978
  metadata.error = void 0;
1837
1979
  metadata.completedAt = Date.now();
1838
1980
  if (!config2.ephemeral) {
1839
- this.log("debug", { msg: "flushing step", step: config2.name, key });
1981
+ this.log("debug", {
1982
+ msg: "flushing step",
1983
+ step: config2.name,
1984
+ key
1985
+ });
1840
1986
  await this.flushStorage();
1841
1987
  }
1842
- this.log("debug", { msg: "step completed", step: config2.name, key });
1988
+ this.log("debug", {
1989
+ msg: "step completed",
1990
+ step: config2.name,
1991
+ key
1992
+ });
1843
1993
  return output;
1844
1994
  } catch (error) {
1845
1995
  if (error instanceof StepTimeoutError) {
@@ -1850,7 +2000,10 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1850
2000
  metadata.status = "exhausted";
1851
2001
  metadata.error = String(error);
1852
2002
  await this.flushStorage();
1853
- throw new CriticalError(error.message);
2003
+ await this.notifyStepError(config2, metadata.attempts, error, {
2004
+ willRetry: false
2005
+ });
2006
+ throw markErrorReported(new CriticalError(error.message));
1854
2007
  }
1855
2008
  if (error instanceof CriticalError || error instanceof RollbackError) {
1856
2009
  if (entry.kind.type === "step") {
@@ -1860,16 +2013,53 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1860
2013
  metadata.status = "exhausted";
1861
2014
  metadata.error = String(error);
1862
2015
  await this.flushStorage();
1863
- throw error;
2016
+ await this.notifyStepError(config2, metadata.attempts, error, {
2017
+ willRetry: false
2018
+ });
2019
+ throw markErrorReported(error);
1864
2020
  }
1865
2021
  if (entry.kind.type === "step") {
1866
2022
  entry.kind.data.error = String(error);
1867
2023
  }
1868
2024
  entry.dirty = true;
1869
- metadata.status = "failed";
2025
+ const willRetry = metadata.attempts <= maxRetries;
2026
+ metadata.status = willRetry ? "failed" : "exhausted";
1870
2027
  metadata.error = String(error);
1871
2028
  await this.flushStorage();
1872
- throw new StepFailedError(config2.name, error, metadata.attempts);
2029
+ if (willRetry) {
2030
+ const retryDelay = calculateBackoff(
2031
+ metadata.attempts,
2032
+ retryBackoffBase,
2033
+ retryBackoffMax
2034
+ );
2035
+ const retryAt = metadata.lastAttemptAt + retryDelay;
2036
+ await this.notifyStepError(
2037
+ config2,
2038
+ metadata.attempts,
2039
+ error,
2040
+ {
2041
+ willRetry: true,
2042
+ retryDelay,
2043
+ retryAt
2044
+ }
2045
+ );
2046
+ throw new StepFailedError(
2047
+ config2.name,
2048
+ error,
2049
+ metadata.attempts,
2050
+ retryAt
2051
+ );
2052
+ }
2053
+ const exhaustedError = markErrorReported(
2054
+ new StepExhaustedError(config2.name, String(error))
2055
+ );
2056
+ await this.notifyStepError(
2057
+ config2,
2058
+ metadata.attempts,
2059
+ error,
2060
+ { willRetry: false }
2061
+ );
2062
+ throw exhaustedError;
1873
2063
  }
1874
2064
  }
1875
2065
  /**
@@ -1970,7 +2160,11 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
1970
2160
  );
1971
2161
  }
1972
2162
  const loopData = existing.kind.data;
1973
- metadata = await loadMetadata(this.storage, this.driver, existing.id);
2163
+ metadata = await loadMetadata(
2164
+ this.storage,
2165
+ this.driver,
2166
+ existing.id
2167
+ );
1974
2168
  if (rollbackMode) {
1975
2169
  if (loopData.output !== void 0) {
1976
2170
  return loopData.output;
@@ -2008,10 +2202,15 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
2008
2202
  metadata.error = void 0;
2009
2203
  metadata.dirty = true;
2010
2204
  }
2011
- const commitInterval = config2.commitInterval ?? DEFAULT_LOOP_COMMIT_INTERVAL;
2012
- const historyEvery = config2.historyEvery ?? config2.commitInterval ?? DEFAULT_LOOP_HISTORY_EVERY;
2013
- const historyKeep = config2.historyKeep ?? config2.commitInterval ?? DEFAULT_LOOP_HISTORY_KEEP;
2205
+ const historyPruneInterval = config2.historyPruneInterval ?? DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL;
2206
+ const historySize = config2.historySize ?? historyPruneInterval;
2207
+ let lastPrunedUpTo = 0;
2208
+ let deferredFlush = null;
2014
2209
  while (true) {
2210
+ if (deferredFlush) {
2211
+ await deferredFlush;
2212
+ deferredFlush = null;
2213
+ }
2015
2214
  if (rollbackMode && rollbackSingleIteration) {
2016
2215
  if (rollbackIterationRan) {
2017
2216
  return rollbackOutput;
@@ -2046,13 +2245,13 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
2046
2245
  metadata.completedAt = Date.now();
2047
2246
  metadata.dirty = true;
2048
2247
  }
2049
- await this.flushStorage();
2050
- await this.forgetOldIterations(
2248
+ const deletions = this.collectLoopPruning(
2051
2249
  location,
2052
2250
  iteration + 1,
2053
- historyEvery,
2054
- historyKeep
2251
+ historySize,
2252
+ lastPrunedUpTo
2055
2253
  );
2254
+ await this.flushStorageWithDeletions(deletions);
2056
2255
  if (rollbackMode && rollbackSingleIteration) {
2057
2256
  rollbackOutput = result.value;
2058
2257
  rollbackIterationRan = true;
@@ -2064,60 +2263,75 @@ var WorkflowContextImpl = class _WorkflowContextImpl {
2064
2263
  state = result.state;
2065
2264
  }
2066
2265
  iteration++;
2067
- if (iteration % commitInterval === 0) {
2266
+ if (!rollbackMode) {
2068
2267
  if (entry.kind.type === "loop") {
2069
2268
  entry.kind.data.state = state;
2070
2269
  entry.kind.data.iteration = iteration;
2071
2270
  }
2072
2271
  entry.dirty = true;
2073
- await this.flushStorage();
2074
- await this.forgetOldIterations(
2272
+ }
2273
+ if (iteration % historyPruneInterval === 0) {
2274
+ const deletions = this.collectLoopPruning(
2075
2275
  location,
2076
2276
  iteration,
2077
- historyEvery,
2078
- historyKeep
2277
+ historySize,
2278
+ lastPrunedUpTo
2079
2279
  );
2280
+ lastPrunedUpTo = Math.max(0, iteration - historySize);
2281
+ deferredFlush = this.flushStorageWithDeletions(deletions);
2080
2282
  }
2081
2283
  }
2082
2284
  }
2083
2285
  /**
2084
- * Delete old loop iteration entries to save storage space.
2085
- *
2086
- * Loop locations always end with a NameIndex (number) because loops are
2087
- * created via appendName(). Even for nested loops, the innermost loop's
2088
- * location ends with its name index:
2089
- *
2090
- * ctx.loop("outer") → location: [outerIndex]
2091
- * iteration 0 → location: [{ loop: outerIndex, iteration: 0 }]
2092
- * ctx.loop("inner") → location: [{ loop: outerIndex, iteration: 0 }, innerIndex]
2286
+ * Collect pending deletions for loop history pruning.
2093
2287
  *
2094
- * This function removes iterations older than (currentIteration - historyKeep)
2095
- * every historyEvery iterations.
2288
+ * Only deletes iterations in the range [fromIteration, keepFrom) where
2289
+ * keepFrom = currentIteration - historySize. This avoids re-scanning
2290
+ * already-deleted iterations.
2096
2291
  */
2097
- async forgetOldIterations(loopLocation, currentIteration, historyEvery, historyKeep) {
2098
- if (historyEvery <= 0 || historyKeep <= 0) {
2099
- return;
2292
+ collectLoopPruning(loopLocation, currentIteration, historySize, fromIteration) {
2293
+ if (currentIteration <= historySize) {
2294
+ return void 0;
2100
2295
  }
2101
- if (currentIteration === 0 || currentIteration % historyEvery !== 0) {
2102
- return;
2296
+ const keepFrom = Math.max(0, currentIteration - historySize);
2297
+ if (fromIteration >= keepFrom) {
2298
+ return void 0;
2103
2299
  }
2104
- const keepFrom = Math.max(0, currentIteration - historyKeep);
2105
2300
  const loopSegment = loopLocation[loopLocation.length - 1];
2106
2301
  if (typeof loopSegment !== "number") {
2107
2302
  throw new Error("Expected loop location to end with a name index");
2108
2303
  }
2109
- for (let i = 0; i < keepFrom; i++) {
2110
- const iterationLocation = [
2111
- ...loopLocation,
2112
- { loop: loopSegment, iteration: i }
2113
- ];
2114
- await deleteEntriesWithPrefix(
2115
- this.storage,
2116
- this.driver,
2117
- iterationLocation,
2118
- this.historyNotifier
2119
- );
2304
+ const range = buildLoopIterationRange(
2305
+ loopLocation,
2306
+ loopSegment,
2307
+ fromIteration,
2308
+ keepFrom
2309
+ );
2310
+ const metadataKeys = [];
2311
+ for (const [key, entry] of this.storage.history.entries) {
2312
+ if (!isLocationPrefix(loopLocation, entry.location)) {
2313
+ continue;
2314
+ }
2315
+ const iterationSegment = entry.location[loopLocation.length];
2316
+ if (!iterationSegment || typeof iterationSegment === "number" || iterationSegment.loop !== loopSegment || iterationSegment.iteration < fromIteration || iterationSegment.iteration >= keepFrom) {
2317
+ continue;
2318
+ }
2319
+ metadataKeys.push(buildEntryMetadataKey(entry.id));
2320
+ this.storage.entryMetadata.delete(entry.id);
2321
+ this.storage.history.entries.delete(key);
2120
2322
  }
2323
+ return {
2324
+ prefixes: [],
2325
+ keys: metadataKeys,
2326
+ ranges: [range]
2327
+ };
2328
+ }
2329
+ /**
2330
+ * Flush storage with optional pending deletions so pruning
2331
+ * happens alongside the state write.
2332
+ */
2333
+ async flushStorageWithDeletions(deletions) {
2334
+ await flush(this.storage, this.driver, this.historyNotifier, deletions);
2121
2335
  }
2122
2336
  // === Sleep ===
2123
2337
  async sleep(name, durationMs) {
@@ -2945,7 +3159,7 @@ async function awaitWithEviction(promise, abortSignal) {
2945
3159
  cleanup();
2946
3160
  }
2947
3161
  }
2948
- async function executeRollback(workflowId, workflowFn, input, driver, messageDriver, abortController, storage, historyNotifier, logger) {
3162
+ async function executeRollback(workflowId, workflowFn, input, driver, messageDriver, abortController, storage, historyNotifier, onError, logger) {
2949
3163
  const rollbackActions = [];
2950
3164
  const ctx = new WorkflowContextImpl(
2951
3165
  workflowId,
@@ -2958,6 +3172,7 @@ async function executeRollback(workflowId, workflowFn, input, driver, messageDri
2958
3172
  rollbackActions,
2959
3173
  false,
2960
3174
  historyNotifier,
3175
+ onError,
2961
3176
  logger
2962
3177
  );
2963
3178
  try {
@@ -2983,6 +3198,7 @@ async function executeRollback(workflowId, workflowFn, input, driver, messageDri
2983
3198
  if (metadata.rollbackCompletedAt !== void 0) {
2984
3199
  continue;
2985
3200
  }
3201
+ let rollbackEvent;
2986
3202
  try {
2987
3203
  await awaitWithEviction(
2988
3204
  action.rollback(rollbackContext, action.output),
@@ -2995,13 +3211,39 @@ async function executeRollback(workflowId, workflowFn, input, driver, messageDri
2995
3211
  throw error;
2996
3212
  }
2997
3213
  metadata.rollbackError = error instanceof Error ? error.message : String(error);
3214
+ if (onError) {
3215
+ rollbackEvent = {
3216
+ rollback: {
3217
+ workflowId,
3218
+ stepName: action.name,
3219
+ error: extractErrorInfo(error)
3220
+ }
3221
+ };
3222
+ }
3223
+ if (error instanceof Error) {
3224
+ markErrorReported(error);
3225
+ }
2998
3226
  throw error;
2999
3227
  } finally {
3000
3228
  metadata.dirty = true;
3001
3229
  await flush(storage, driver, historyNotifier);
3230
+ if (rollbackEvent && onError) {
3231
+ await notifyError(onError, logger, rollbackEvent);
3232
+ }
3002
3233
  }
3003
3234
  }
3004
3235
  }
3236
+ async function notifyError(onError, logger, event) {
3237
+ try {
3238
+ await onError(event);
3239
+ } catch (error) {
3240
+ logger == null ? void 0 : logger.warn({
3241
+ msg: "workflow error hook failed",
3242
+ hookEventType: getErrorEventTag(event),
3243
+ error: extractErrorInfo(error)
3244
+ });
3245
+ }
3246
+ }
3005
3247
  async function setSleepState(storage, driver, workflowId, deadline, messageNames, historyNotifier) {
3006
3248
  storage.state = "sleeping";
3007
3249
  await flush(storage, driver, historyNotifier);
@@ -3021,10 +3263,9 @@ async function setEvictedState(storage, driver, historyNotifier) {
3021
3263
  await flush(storage, driver, historyNotifier);
3022
3264
  return { state: storage.state };
3023
3265
  }
3024
- async function setRetryState(storage, driver, workflowId, historyNotifier) {
3266
+ async function setRetryState(storage, driver, workflowId, retryAt, historyNotifier) {
3025
3267
  storage.state = "sleeping";
3026
3268
  await flush(storage, driver, historyNotifier);
3027
- const retryAt = Date.now() + 100;
3028
3269
  await driver.setAlarm(workflowId, retryAt);
3029
3270
  return { state: "sleeping", sleepUntil: retryAt };
3030
3271
  }
@@ -3065,7 +3306,7 @@ async function waitForSleep(runtime, deadline, abortSignal) {
3065
3306
  }
3066
3307
  }
3067
3308
  }
3068
- async function executeLiveWorkflow(workflowId, workflowFn, input, driver, messageDriver, abortController, runtime, onHistoryUpdated, logger) {
3309
+ async function executeLiveWorkflow(workflowId, workflowFn, input, driver, messageDriver, abortController, runtime, onHistoryUpdated, onError, logger) {
3069
3310
  let lastResult;
3070
3311
  while (true) {
3071
3312
  const result = await executeWorkflow(
@@ -3076,6 +3317,7 @@ async function executeLiveWorkflow(workflowId, workflowFn, input, driver, messag
3076
3317
  messageDriver,
3077
3318
  abortController,
3078
3319
  onHistoryUpdated,
3320
+ onError,
3079
3321
  logger
3080
3322
  );
3081
3323
  lastResult = result;
@@ -3157,6 +3399,7 @@ function runWorkflow(workflowId, workflowFn, input, driver, options = {}) {
3157
3399
  abortController,
3158
3400
  liveRuntime,
3159
3401
  options.onHistoryUpdated,
3402
+ options.onError,
3160
3403
  logger
3161
3404
  ) : executeWorkflow(
3162
3405
  workflowId,
@@ -3166,6 +3409,7 @@ function runWorkflow(workflowId, workflowFn, input, driver, options = {}) {
3166
3409
  messageDriver,
3167
3410
  abortController,
3168
3411
  options.onHistoryUpdated,
3412
+ options.onError,
3169
3413
  logger
3170
3414
  );
3171
3415
  return {
@@ -3257,7 +3501,7 @@ function runWorkflow(workflowId, workflowFn, input, driver, options = {}) {
3257
3501
  }
3258
3502
  };
3259
3503
  }
3260
- async function executeWorkflow(workflowId, workflowFn, input, driver, messageDriver, abortController, onHistoryUpdated, logger) {
3504
+ async function executeWorkflow(workflowId, workflowFn, input, driver, messageDriver, abortController, onHistoryUpdated, onError, logger) {
3261
3505
  var _a;
3262
3506
  const storage = await loadStorage(driver);
3263
3507
  const historyNotifier = onHistoryUpdated ? () => onHistoryUpdated(createHistorySnapshot(storage)) : void 0;
@@ -3299,6 +3543,7 @@ async function executeWorkflow(workflowId, workflowFn, input, driver, messageDri
3299
3543
  abortController,
3300
3544
  storage,
3301
3545
  historyNotifier,
3546
+ onError,
3302
3547
  logger
3303
3548
  );
3304
3549
  } catch (error) {
@@ -3326,6 +3571,7 @@ async function executeWorkflow(workflowId, workflowFn, input, driver, messageDri
3326
3571
  void 0,
3327
3572
  false,
3328
3573
  historyNotifier,
3574
+ onError,
3329
3575
  logger
3330
3576
  );
3331
3577
  storage.state = "running";
@@ -3363,11 +3609,20 @@ async function executeWorkflow(workflowId, workflowFn, input, driver, messageDri
3363
3609
  storage,
3364
3610
  driver,
3365
3611
  workflowId,
3612
+ error.retryAt,
3366
3613
  historyNotifier
3367
3614
  );
3368
3615
  }
3369
3616
  if (error instanceof RollbackCheckpointError) {
3370
3617
  await setFailedState(storage, driver, error, historyNotifier);
3618
+ if (onError && !isErrorReported(error)) {
3619
+ await notifyError(onError, logger, {
3620
+ workflow: {
3621
+ workflowId,
3622
+ error: extractErrorInfo(error)
3623
+ }
3624
+ });
3625
+ }
3371
3626
  throw error;
3372
3627
  }
3373
3628
  storage.error = extractErrorInfo(error);
@@ -3383,6 +3638,7 @@ async function executeWorkflow(workflowId, workflowFn, input, driver, messageDri
3383
3638
  abortController,
3384
3639
  storage,
3385
3640
  historyNotifier,
3641
+ onError,
3386
3642
  logger
3387
3643
  );
3388
3644
  } catch (rollbackError) {
@@ -3393,37 +3649,23 @@ async function executeWorkflow(workflowId, workflowFn, input, driver, messageDri
3393
3649
  }
3394
3650
  storage.state = "failed";
3395
3651
  await flush(storage, driver, historyNotifier);
3396
- throw error;
3397
- }
3398
- }
3399
- function extractErrorInfo(error) {
3400
- if (error instanceof Error) {
3401
- const result = {
3402
- name: error.name,
3403
- message: error.message,
3404
- stack: error.stack
3405
- };
3406
- const metadata = {};
3407
- for (const key of Object.keys(error)) {
3408
- if (key !== "name" && key !== "message" && key !== "stack") {
3409
- const value = error[key];
3410
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
3411
- metadata[key] = value;
3652
+ if (onError && !isErrorReported(error)) {
3653
+ await notifyError(onError, logger, {
3654
+ workflow: {
3655
+ workflowId,
3656
+ error: extractErrorInfo(error)
3412
3657
  }
3658
+ });
3659
+ if (error instanceof CriticalError || error instanceof RollbackError || error instanceof StepExhaustedError) {
3660
+ markErrorReported(error);
3413
3661
  }
3414
3662
  }
3415
- if (Object.keys(metadata).length > 0) {
3416
- result.metadata = metadata;
3417
- }
3418
- return result;
3663
+ throw error;
3419
3664
  }
3420
- return {
3421
- name: "Error",
3422
- message: String(error)
3423
- };
3424
3665
  }
3425
3666
 
3426
3667
  export {
3668
+ extractErrorInfo,
3427
3669
  CriticalError,
3428
3670
  RollbackError,
3429
3671
  RollbackCheckpointError,
@@ -3437,6 +3679,9 @@ export {
3437
3679
  RaceError,
3438
3680
  CancelledError,
3439
3681
  EntryInProgressError,
3682
+ keyStartsWith,
3683
+ compareKeys,
3684
+ keyToHex,
3440
3685
  isLoopIterationMarker,
3441
3686
  registerName,
3442
3687
  resolveName,
@@ -3447,9 +3692,6 @@ export {
3447
3692
  parentLocation,
3448
3693
  isLocationPrefix,
3449
3694
  locationsEqual,
3450
- keyStartsWith,
3451
- compareKeys,
3452
- keyToHex,
3453
3695
  createStorage,
3454
3696
  createHistorySnapshot,
3455
3697
  generateId,
@@ -3465,12 +3707,10 @@ export {
3465
3707
  DEFAULT_MAX_RETRIES,
3466
3708
  DEFAULT_RETRY_BACKOFF_BASE,
3467
3709
  DEFAULT_RETRY_BACKOFF_MAX,
3468
- DEFAULT_LOOP_COMMIT_INTERVAL,
3469
- DEFAULT_LOOP_HISTORY_EVERY,
3470
- DEFAULT_LOOP_HISTORY_KEEP,
3710
+ DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL,
3471
3711
  DEFAULT_STEP_TIMEOUT,
3472
3712
  WorkflowContextImpl,
3473
3713
  Loop,
3474
3714
  runWorkflow
3475
3715
  };
3476
- //# sourceMappingURL=chunk-JTLDEP6X.js.map
3716
+ //# sourceMappingURL=chunk-MMWB37UG.js.map