@mastra/dynamodb 1.4.1 → 1.4.2-alpha.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/dist/index.cjs CHANGED
@@ -499,6 +499,80 @@ const resourceEntity = new electrodb.Entity({
499
499
  } }
500
500
  });
501
501
  //#endregion
502
+ //#region src/entities/run-fencing.ts
503
+ /**
504
+ * The current claim on a durable workflow run. `leaseExpiresAt` holds the
505
+ * lease's expiry in epoch milliseconds and is removed when the owner releases
506
+ * the run; `generation` and `ownerId` are kept. `updatedAt` is the epoch
507
+ * milliseconds of the last claim, renewal or release.
508
+ */
509
+ const workflowRunOwnerEntity = new electrodb.Entity({
510
+ model: {
511
+ entity: "workflow_run_owner",
512
+ version: "1",
513
+ service: "mastra"
514
+ },
515
+ attributes: {
516
+ entity: {
517
+ type: "string",
518
+ required: true
519
+ },
520
+ run_id: {
521
+ type: "string",
522
+ required: true
523
+ },
524
+ generation: { type: "number" },
525
+ ownerId: { type: "string" },
526
+ leaseExpiresAt: { type: "number" },
527
+ updatedAt: { type: "number" }
528
+ },
529
+ indexes: { primary: {
530
+ pk: {
531
+ field: "pk",
532
+ composite: ["entity", "run_id"]
533
+ },
534
+ sk: {
535
+ field: "sk",
536
+ composite: ["entity"]
537
+ }
538
+ } }
539
+ });
540
+ /**
541
+ * The newest fence memory has seen for a run. Fenced memory writes check it.
542
+ * `retiredAt` is set, in epoch milliseconds, once the execution holding the
543
+ * fence settles, and removed when a fence is raised again.
544
+ */
545
+ const memoryRunFenceEntity = new electrodb.Entity({
546
+ model: {
547
+ entity: "memory_run_fence",
548
+ version: "1",
549
+ service: "mastra"
550
+ },
551
+ attributes: {
552
+ entity: {
553
+ type: "string",
554
+ required: true
555
+ },
556
+ run_id: {
557
+ type: "string",
558
+ required: true
559
+ },
560
+ generation: { type: "number" },
561
+ ownerId: { type: "string" },
562
+ retiredAt: { type: "number" }
563
+ },
564
+ indexes: { primary: {
565
+ pk: {
566
+ field: "pk",
567
+ composite: ["entity", "run_id"]
568
+ },
569
+ sk: {
570
+ field: "sk",
571
+ composite: ["entity"]
572
+ }
573
+ } }
574
+ });
575
+ //#endregion
502
576
  //#region src/entities/score.ts
503
577
  const scoreEntity = new electrodb.Entity({
504
578
  model: {
@@ -1129,7 +1203,9 @@ function getElectroDbService(client, tableName) {
1129
1203
  workflow_snapshot: workflowSnapshotEntity,
1130
1204
  resource: resourceEntity,
1131
1205
  score: scoreEntity,
1132
- background_task: backgroundTaskEntity
1206
+ background_task: backgroundTaskEntity,
1207
+ workflow_run_owner: workflowRunOwnerEntity,
1208
+ memory_run_fence: memoryRunFenceEntity
1133
1209
  }, {
1134
1210
  client,
1135
1211
  table: tableName
@@ -1167,7 +1243,9 @@ const ENTITY_MAP = {
1167
1243
  [_mastra_core_storage.TABLE_RESOURCES]: "resource",
1168
1244
  [_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT]: "workflow_snapshot",
1169
1245
  [_mastra_core_storage.TABLE_SCORERS]: "score",
1170
- [_mastra_core_storage.TABLE_BACKGROUND_TASKS]: "background_task"
1246
+ [_mastra_core_storage.TABLE_BACKGROUND_TASKS]: "background_task",
1247
+ [_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]: "workflow_run_owner",
1248
+ [_mastra_core_storage.TABLE_MEMORY_RUN_FENCES]: "memory_run_fence"
1171
1249
  };
1172
1250
  /**
1173
1251
  * Gets the primary key fields for an entity based on the scanned item
@@ -1186,6 +1264,10 @@ function getDeleteKey(entityName, item) {
1186
1264
  key.workflow_name = item.workflow_name;
1187
1265
  key.run_id = item.run_id;
1188
1266
  break;
1267
+ case "workflow_run_owner":
1268
+ case "memory_run_fence":
1269
+ key.run_id = item.run_id;
1270
+ break;
1189
1271
  default: key.id = item.id;
1190
1272
  }
1191
1273
  return key;
@@ -1521,6 +1603,91 @@ function getTtlProps(entityName, ttlConfig, customTtlSeconds) {
1521
1603
  return { [getTtlAttributeName(entityName, ttlConfig)]: ttlValue };
1522
1604
  }
1523
1605
  //#endregion
1606
+ //#region src/storage/domains/run-fencing.ts
1607
+ const MAX_CONFLICT_RETRIES = 8;
1608
+ const BASE_DELAY_MS$1 = 20;
1609
+ async function backoff(attempt) {
1610
+ const delay = BASE_DELAY_MS$1 * 2 ** attempt;
1611
+ await new Promise((resolve) => setTimeout(resolve, delay + Math.random() * delay * .5));
1612
+ }
1613
+ function hasErrorName(error, errorName) {
1614
+ let current = error;
1615
+ for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
1616
+ const { name, code } = current;
1617
+ if (name === errorName || code === errorName) return true;
1618
+ current = current.cause;
1619
+ }
1620
+ return false;
1621
+ }
1622
+ function isTransactionConflict(error) {
1623
+ return hasErrorName(error, "TransactionConflictException");
1624
+ }
1625
+ /** Whether a write failed its condition, either directly or wrapped by ElectroDB. */
1626
+ function isConditionalCheckFailed(error) {
1627
+ return hasErrorName(error, "ConditionalCheckFailedException");
1628
+ }
1629
+ /**
1630
+ * Runs a single-item write, retrying while a transaction is writing the same
1631
+ * item. DynamoDB rejects such a write with TransactionConflictException
1632
+ * instead of waiting for the transaction, and the SDK does not retry it.
1633
+ */
1634
+ async function retryOnTransactionConflict(write) {
1635
+ for (let attempt = 0;; attempt++) try {
1636
+ return await write();
1637
+ } catch (error) {
1638
+ if (attempt >= MAX_CONFLICT_RETRIES || !isTransactionConflict(error)) throw error;
1639
+ await backoff(attempt);
1640
+ }
1641
+ }
1642
+ /**
1643
+ * Rejects a fenced write that turns out to have nothing to write when its
1644
+ * fence is no longer current, as the write itself would have been rejected.
1645
+ */
1646
+ async function assertRunFence(service, check) {
1647
+ if (!check) return;
1648
+ const { claims, fence, operation } = check;
1649
+ const { data } = await service.entities[claims].get({
1650
+ entity: claims,
1651
+ run_id: fence.runId
1652
+ }).go({ consistent: true });
1653
+ if (!(0, _mastra_core_storage.matchesRunFence)(data, fence)) throw new _mastra_core_storage.RunFenceConflictError(fence, operation);
1654
+ }
1655
+ /**
1656
+ * Runs a write, fenced when `check` is set. A fenced write is a transaction
1657
+ * whose first item is a condition check on the run's claim, matching it on
1658
+ * the fence. A fence that is no longer current fails the check, and the write
1659
+ * throws `RunFenceConflictError` without writing anything. Transactions are
1660
+ * serializable with the single-item writes that claim a run, so a takeover
1661
+ * lands either before the transaction (failing its check) or after it.
1662
+ *
1663
+ * A fenced write whose own condition fails throws an error named
1664
+ * `ConditionalCheckFailedException`, as the unfenced write would.
1665
+ */
1666
+ async function goWithRunFence(service, check, write) {
1667
+ if (!check) {
1668
+ await write.go();
1669
+ return;
1670
+ }
1671
+ const { claims, fence, operation } = check;
1672
+ for (let attempt = 0;; attempt++) {
1673
+ const result = await service.transaction.write((entities) => [entities[claims].check({
1674
+ entity: claims,
1675
+ run_id: fence.runId
1676
+ }).where((attr, op) => `${op.eq(attr.generation, fence.generation)} AND ${op.eq(attr.ownerId, fence.ownerId)}`).commit(), write.commit()]).go();
1677
+ if (!result.canceled) return;
1678
+ const [claim, target] = result.data;
1679
+ if (claim?.code === "ConditionalCheckFailed") throw new _mastra_core_storage.RunFenceConflictError(fence, operation);
1680
+ if (target?.code === "ConditionalCheckFailed") {
1681
+ const error = new Error(target.message ?? "The conditional request failed");
1682
+ error.name = "ConditionalCheckFailedException";
1683
+ throw error;
1684
+ }
1685
+ const reasons = result.data.map((item) => item?.code ?? "None");
1686
+ if (attempt >= MAX_CONFLICT_RETRIES || !reasons.includes("TransactionConflict")) throw new Error(`DynamoDB canceled the fenced ${operation} transaction: ${reasons.join(", ")}`);
1687
+ await backoff(attempt);
1688
+ }
1689
+ }
1690
+ //#endregion
1524
1691
  //#region src/storage/domains/memory/index.ts
1525
1692
  var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1526
1693
  supportsPartialThreadUpdate = true;
@@ -1532,15 +1699,81 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1532
1699
  this.service = resolved.service;
1533
1700
  this.ttlConfig = resolved.ttl;
1534
1701
  }
1702
+ supportsRunFencing() {
1703
+ return true;
1704
+ }
1535
1705
  async dangerouslyClearAll() {
1536
1706
  await deleteTableData(this.service, _mastra_core_storage.TABLE_THREADS);
1537
1707
  await deleteTableData(this.service, _mastra_core_storage.TABLE_MESSAGES);
1538
1708
  await deleteTableData(this.service, _mastra_core_storage.TABLE_RESOURCES);
1709
+ await deleteTableData(this.service, _mastra_core_storage.TABLE_MEMORY_RUN_FENCES);
1539
1710
  }
1540
- async deleteMessages(messageIds) {
1711
+ async raiseRunFence(fence) {
1712
+ const fences = this.service.entities.memory_run_fence;
1713
+ const key = {
1714
+ entity: "memory_run_fence",
1715
+ run_id: fence.runId
1716
+ };
1717
+ try {
1718
+ await retryOnTransactionConflict(() => fences.update(key).set({
1719
+ generation: fence.generation,
1720
+ ownerId: fence.ownerId
1721
+ }).remove(["retiredAt"]).where((attr, op) => `${op.notExists(attr.generation)} OR ${op.lt(attr.generation, fence.generation)} OR (${op.eq(attr.generation, fence.generation)} AND ${op.eq(attr.ownerId, fence.ownerId)})`).go());
1722
+ return true;
1723
+ } catch (error) {
1724
+ try {
1725
+ if (!isConditionalCheckFailed(error)) throw error;
1726
+ const { data } = await fences.get(key).go({ consistent: true });
1727
+ return (0, _mastra_core_storage.matchesRunFence)(data, fence);
1728
+ } catch (cause) {
1729
+ throw new _mastra_core_error.MastraError({
1730
+ id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "RAISE_RUN_FENCE", "FAILED"),
1731
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
1732
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
1733
+ details: { runId: fence.runId }
1734
+ }, cause);
1735
+ }
1736
+ }
1737
+ }
1738
+ async retireRunFence(fence) {
1739
+ try {
1740
+ await retryOnTransactionConflict(() => this.service.entities.memory_run_fence.patch({
1741
+ entity: "memory_run_fence",
1742
+ run_id: fence.runId
1743
+ }).set({ retiredAt: Date.now() }).where((attr, op) => `${op.eq(attr.generation, fence.generation)} AND ${op.eq(attr.ownerId, fence.ownerId)}`).go());
1744
+ return true;
1745
+ } catch (error) {
1746
+ if (isConditionalCheckFailed(error)) return false;
1747
+ throw new _mastra_core_error.MastraError({
1748
+ id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "RETIRE_RUN_FENCE", "FAILED"),
1749
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
1750
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
1751
+ details: { runId: fence.runId }
1752
+ }, error);
1753
+ }
1754
+ }
1755
+ /** The check a write must pass: its own fence, otherwise the one in scope. */
1756
+ #runFenceCheck(fence, operation) {
1757
+ const resolved = (0, _mastra_core_storage.resolveRunFence)(this, fence);
1758
+ return resolved && {
1759
+ claims: "memory_run_fence",
1760
+ fence: resolved,
1761
+ operation
1762
+ };
1763
+ }
1764
+ /** Bumps a thread's updatedAt after a write to its messages. */
1765
+ #touchThread(threadId, check) {
1766
+ return goWithRunFence(this.service, check, this.service.entities.thread.update({
1767
+ entity: "thread",
1768
+ id: threadId
1769
+ }).set({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
1770
+ }
1771
+ async deleteMessages(messageIds, options) {
1541
1772
  if (!messageIds || messageIds.length === 0) return;
1542
1773
  this.logger.debug("Deleting messages", { count: messageIds.length });
1774
+ const check = this.#runFenceCheck(options?.fence, "deleteMessages");
1543
1775
  try {
1776
+ let deleted = 0;
1544
1777
  const threadIds = /* @__PURE__ */ new Set();
1545
1778
  const batchSize = 25;
1546
1779
  for (let i = 0; i < messageIds.length; i += batchSize) {
@@ -1553,18 +1786,17 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1553
1786
  }));
1554
1787
  for (const message of messagesToDelete) if (message) {
1555
1788
  if (message.threadId) threadIds.add(message.threadId);
1556
- await this.service.entities.message.delete({
1789
+ await goWithRunFence(this.service, check, this.service.entities.message.delete({
1557
1790
  entity: "message",
1558
1791
  id: message.id
1559
- }).go();
1792
+ }));
1793
+ deleted++;
1560
1794
  }
1561
1795
  }
1562
- const now = (/* @__PURE__ */ new Date()).toISOString();
1563
- for (const threadId of threadIds) await this.service.entities.thread.update({
1564
- entity: "thread",
1565
- id: threadId
1566
- }).set({ updatedAt: now }).go();
1796
+ if (deleted === 0) await assertRunFence(this.service, check);
1797
+ for (const threadId of threadIds) await this.#touchThread(threadId, check);
1567
1798
  } catch (error) {
1799
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
1568
1800
  throw new _mastra_core_error.MastraError({
1569
1801
  id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "DELETE_MESSAGES", "FAILED"),
1570
1802
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -1618,7 +1850,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1618
1850
  }, error);
1619
1851
  }
1620
1852
  }
1621
- async saveThread({ thread }) {
1853
+ async saveThread({ thread, fence }) {
1622
1854
  this.logger.debug("Saving thread", { threadId: thread.id });
1623
1855
  const now = /* @__PURE__ */ new Date();
1624
1856
  const threadData = {
@@ -1632,7 +1864,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1632
1864
  ...getTtlProps("thread", this.ttlConfig)
1633
1865
  };
1634
1866
  try {
1635
- await this.service.entities.thread.upsert(threadData).go();
1867
+ await goWithRunFence(this.service, this.#runFenceCheck(fence, "saveThread"), this.service.entities.thread.upsert(threadData));
1636
1868
  return {
1637
1869
  id: thread.id,
1638
1870
  resourceId: thread.resourceId,
@@ -1642,6 +1874,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1642
1874
  metadata: thread.metadata
1643
1875
  };
1644
1876
  } catch (error) {
1877
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
1645
1878
  throw new _mastra_core_error.MastraError({
1646
1879
  id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "SAVE_THREAD", "FAILED"),
1647
1880
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -1650,8 +1883,9 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1650
1883
  }, error);
1651
1884
  }
1652
1885
  }
1653
- async updateThread({ id, title, metadata }) {
1886
+ async updateThread({ id, title, metadata, fence }) {
1654
1887
  this.logger.debug("Updating thread", { threadId: id });
1888
+ const check = this.#runFenceCheck(fence, "updateThread");
1655
1889
  try {
1656
1890
  const existingThread = await this.getThreadById({ threadId: id });
1657
1891
  if (!existingThread) throw new Error(`Thread not found: ${id}`);
@@ -1665,10 +1899,10 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1665
1899
  };
1666
1900
  updateData.metadata = JSON.stringify(mergedMetadata);
1667
1901
  }
1668
- await this.service.entities.thread.update({
1902
+ await goWithRunFence(this.service, check, this.service.entities.thread.update({
1669
1903
  entity: "thread",
1670
1904
  id
1671
- }).set(updateData).go();
1905
+ }).set(updateData));
1672
1906
  return {
1673
1907
  ...existingThread,
1674
1908
  title: title || existingThread.title,
@@ -1679,6 +1913,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1679
1913
  updatedAt: now
1680
1914
  };
1681
1915
  } catch (error) {
1916
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
1682
1917
  throw new _mastra_core_error.MastraError({
1683
1918
  id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "UPDATE_THREAD", "FAILED"),
1684
1919
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -1926,6 +2161,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1926
2161
  ...getTtlProps("message", this.ttlConfig)
1927
2162
  };
1928
2163
  });
2164
+ const check = this.#runFenceCheck(args.fence, "saveMessages");
1929
2165
  try {
1930
2166
  const savedMessageIds = [];
1931
2167
  for (const messageData of messagesToSave) {
@@ -1934,14 +2170,14 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1934
2170
  throw new Error("Internal error: Missing entity property during saveMessages");
1935
2171
  }
1936
2172
  try {
1937
- await this.service.entities.message.put(messageData).go();
2173
+ await goWithRunFence(this.service, check, this.service.entities.message.put(messageData));
1938
2174
  savedMessageIds.push(messageData.id);
1939
2175
  } catch (error) {
1940
- for (const savedId of savedMessageIds) try {
1941
- await this.service.entities.message.delete({
2176
+ for (const savedId of (0, _mastra_core_storage.isRunFenceConflictError)(error) ? [] : savedMessageIds) try {
2177
+ await goWithRunFence(this.service, check, this.service.entities.message.delete({
1942
2178
  entity: "message",
1943
2179
  id: savedId
1944
- }).go();
2180
+ }));
1945
2181
  } catch (rollbackError) {
1946
2182
  this.logger.error("Failed to rollback message during save error", {
1947
2183
  messageId: savedId,
@@ -1951,12 +2187,10 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
1951
2187
  throw error;
1952
2188
  }
1953
2189
  }
1954
- await this.service.entities.thread.update({
1955
- entity: "thread",
1956
- id: threadId
1957
- }).set({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).go();
2190
+ await this.#touchThread(threadId, check);
1958
2191
  return { messages: new _mastra_core_agent.MessageList().add(messages, "memory").get.all.db() };
1959
2192
  } catch (error) {
2193
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
1960
2194
  throw new _mastra_core_error.MastraError({
1961
2195
  id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "SAVE_MESSAGES", "FAILED"),
1962
2196
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2110,6 +2344,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
2110
2344
  const { messages } = args;
2111
2345
  this.logger.debug("Updating messages", { count: messages.length });
2112
2346
  if (!messages.length) return [];
2347
+ const check = this.#runFenceCheck(args.fence, "updateMessages");
2113
2348
  const updatedMessages = [];
2114
2349
  const affectedThreadIds = /* @__PURE__ */ new Set();
2115
2350
  try {
@@ -2145,22 +2380,21 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
2145
2380
  if ("parts" in updates.content && updates.content.parts !== void 0) newContent.parts = updates.content.parts;
2146
2381
  updatePayload.content = JSON.stringify(newContent);
2147
2382
  }
2148
- await this.service.entities.message.update({
2383
+ await goWithRunFence(this.service, check, this.service.entities.message.update({
2149
2384
  entity: "message",
2150
2385
  id
2151
- }).set(updatePayload).go();
2386
+ }).set(updatePayload));
2152
2387
  const updatedMessage = await this.service.entities.message.get({
2153
2388
  entity: "message",
2154
2389
  id
2155
2390
  }).go();
2156
2391
  if (updatedMessage.data) updatedMessages.push(this.parseMessageData(updatedMessage.data));
2157
2392
  }
2158
- for (const threadId of affectedThreadIds) await this.service.entities.thread.update({
2159
- entity: "thread",
2160
- id: threadId
2161
- }).set({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).go();
2393
+ if (affectedThreadIds.size === 0) await assertRunFence(this.service, check);
2394
+ for (const threadId of affectedThreadIds) await this.#touchThread(threadId, check);
2162
2395
  return updatedMessages;
2163
2396
  } catch (error) {
2397
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2164
2398
  throw new _mastra_core_error.MastraError({
2165
2399
  id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "UPDATE_MESSAGES", "FAILED"),
2166
2400
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2194,6 +2428,9 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
2194
2428
  }
2195
2429
  }
2196
2430
  async saveResource({ resource }) {
2431
+ return this.#saveResource(resource, void 0);
2432
+ }
2433
+ async #saveResource(resource, check) {
2197
2434
  this.logger.debug("Saving resource", { resourceId: resource.id });
2198
2435
  const now = /* @__PURE__ */ new Date();
2199
2436
  const resourceData = {
@@ -2206,7 +2443,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
2206
2443
  ...getTtlProps("resource", this.ttlConfig)
2207
2444
  };
2208
2445
  try {
2209
- await this.service.entities.resource.upsert(resourceData).go();
2446
+ await goWithRunFence(this.service, check, this.service.entities.resource.upsert(resourceData));
2210
2447
  return {
2211
2448
  id: resource.id,
2212
2449
  workingMemory: resource.workingMemory,
@@ -2215,6 +2452,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
2215
2452
  updatedAt: now
2216
2453
  };
2217
2454
  } catch (error) {
2455
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2218
2456
  throw new _mastra_core_error.MastraError({
2219
2457
  id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "SAVE_RESOURCE", "FAILED"),
2220
2458
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2223,8 +2461,9 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
2223
2461
  }, error);
2224
2462
  }
2225
2463
  }
2226
- async updateResource({ resourceId, workingMemory, metadata }) {
2464
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
2227
2465
  this.logger.debug("Updating resource", { resourceId });
2466
+ const check = this.#runFenceCheck(fence, "updateResource");
2228
2467
  try {
2229
2468
  const existingResource = await this.getResourceById({ resourceId });
2230
2469
  if (!existingResource) {
@@ -2235,7 +2474,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
2235
2474
  createdAt: /* @__PURE__ */ new Date(),
2236
2475
  updatedAt: /* @__PURE__ */ new Date()
2237
2476
  };
2238
- return this.saveResource({ resource: newResource });
2477
+ return this.#saveResource(newResource, check);
2239
2478
  }
2240
2479
  const now = /* @__PURE__ */ new Date();
2241
2480
  const updateData = { updatedAt: now.toISOString() };
@@ -2247,10 +2486,10 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
2247
2486
  };
2248
2487
  updateData.metadata = JSON.stringify(mergedMetadata);
2249
2488
  }
2250
- await this.service.entities.resource.update({
2489
+ await goWithRunFence(this.service, check, this.service.entities.resource.update({
2251
2490
  entity: "resource",
2252
2491
  id: resourceId
2253
- }).set(updateData).go();
2492
+ }).set(updateData));
2254
2493
  return {
2255
2494
  ...existingResource,
2256
2495
  workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
@@ -2261,6 +2500,7 @@ var MemoryStorageDynamoDB = class extends _mastra_core_storage.MemoryStorage {
2261
2500
  updatedAt: now
2262
2501
  };
2263
2502
  } catch (error) {
2503
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2264
2504
  throw new _mastra_core_error.MastraError({
2265
2505
  id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "UPDATE_RESOURCE", "FAILED"),
2266
2506
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2582,6 +2822,15 @@ function formatWorkflowRun(snapshotData) {
2582
2822
  resourceId: snapshotData.resourceId
2583
2823
  };
2584
2824
  }
2825
+ function toRunOwnershipRecord(item) {
2826
+ return {
2827
+ runId: item.run_id,
2828
+ generation: Number(item.generation),
2829
+ ownerId: item.ownerId,
2830
+ leaseExpiresAt: item.leaseExpiresAt === void 0 ? null : new Date(item.leaseExpiresAt),
2831
+ live: item.leaseExpiresAt !== void 0 && item.leaseExpiresAt > Date.now()
2832
+ };
2833
+ }
2585
2834
  const MAX_RETRIES = 5;
2586
2835
  const BASE_DELAY_MS = 50;
2587
2836
  var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorage {
@@ -2596,8 +2845,126 @@ var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorag
2596
2845
  supportsConcurrentUpdates() {
2597
2846
  return true;
2598
2847
  }
2848
+ supportsRunFencing() {
2849
+ return true;
2850
+ }
2599
2851
  async dangerouslyClearAll() {
2600
2852
  await deleteTableData(this.service, _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT);
2853
+ await deleteTableData(this.service, _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS);
2854
+ }
2855
+ #runFenceCheck(fence, runId, operation) {
2856
+ const resolved = (0, _mastra_core_storage.resolveRunFence)(this, fence, runId);
2857
+ return resolved && {
2858
+ claims: "workflow_run_owner",
2859
+ fence: resolved,
2860
+ operation
2861
+ };
2862
+ }
2863
+ #runOwnerKey(runId) {
2864
+ return {
2865
+ entity: "workflow_run_owner",
2866
+ run_id: runId
2867
+ };
2868
+ }
2869
+ async #readRunOwner(runId) {
2870
+ const { data } = await this.service.entities.workflow_run_owner.get(this.#runOwnerKey(runId)).go({ consistent: true });
2871
+ return data ? toRunOwnershipRecord(data) : null;
2872
+ }
2873
+ #ownershipError(operation, runId, error) {
2874
+ return new _mastra_core_error.MastraError({
2875
+ id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", operation, "FAILED"),
2876
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2877
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
2878
+ details: { runId }
2879
+ }, error);
2880
+ }
2881
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
2882
+ try {
2883
+ const owners = this.service.entities.workflow_run_owner;
2884
+ const now = Date.now();
2885
+ if (!expectedGeneration) {
2886
+ try {
2887
+ const item = {
2888
+ ...this.#runOwnerKey(runId),
2889
+ generation: Math.max(1, now),
2890
+ ownerId,
2891
+ leaseExpiresAt: now + leaseMs,
2892
+ updatedAt: now
2893
+ };
2894
+ await retryOnTransactionConflict(() => owners.create(item).go());
2895
+ return {
2896
+ acquired: true,
2897
+ record: toRunOwnershipRecord(item)
2898
+ };
2899
+ } catch (error) {
2900
+ if (!this.isConditionalCheckFailed(error)) throw error;
2901
+ }
2902
+ if (expectedGeneration === 0) return {
2903
+ acquired: false,
2904
+ record: await this.#readRunOwner(runId)
2905
+ };
2906
+ }
2907
+ try {
2908
+ const { data } = await retryOnTransactionConflict(() => owners.patch(this.#runOwnerKey(runId)).set({
2909
+ ownerId,
2910
+ leaseExpiresAt: now + leaseMs,
2911
+ updatedAt: now
2912
+ }).add({ generation: 1 }).where((attr, op) => [expectedGeneration !== void 0 ? op.eq(attr.generation, expectedGeneration) : "", force ? "" : `(${op.notExists(attr.leaseExpiresAt)} OR ${op.lte(attr.leaseExpiresAt, now)})`].filter(Boolean).join(" AND ")).go({ response: "all_new" }));
2913
+ return {
2914
+ acquired: true,
2915
+ record: toRunOwnershipRecord(data)
2916
+ };
2917
+ } catch (error) {
2918
+ if (!this.isConditionalCheckFailed(error)) throw error;
2919
+ return {
2920
+ acquired: false,
2921
+ record: await this.#readRunOwner(runId)
2922
+ };
2923
+ }
2924
+ } catch (error) {
2925
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", runId, error);
2926
+ }
2927
+ }
2928
+ async renewRunOwnership({ leaseMs, ...fence }) {
2929
+ try {
2930
+ const { data } = await retryOnTransactionConflict(() => {
2931
+ const now = Date.now();
2932
+ return this.service.entities.workflow_run_owner.patch(this.#runOwnerKey(fence.runId)).set({
2933
+ leaseExpiresAt: now + leaseMs,
2934
+ updatedAt: now
2935
+ }).where((attr, op) => `${op.eq(attr.generation, fence.generation)} AND ${op.eq(attr.ownerId, fence.ownerId)} AND ${op.exists(attr.leaseExpiresAt)}`).go({ response: "all_new" });
2936
+ });
2937
+ return {
2938
+ renewed: true,
2939
+ record: toRunOwnershipRecord(data)
2940
+ };
2941
+ } catch (error) {
2942
+ if (!this.isConditionalCheckFailed(error)) throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, error);
2943
+ try {
2944
+ return {
2945
+ renewed: false,
2946
+ record: await this.#readRunOwner(fence.runId)
2947
+ };
2948
+ } catch (readError) {
2949
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, readError);
2950
+ }
2951
+ }
2952
+ }
2953
+ async releaseRunOwnership(fence) {
2954
+ try {
2955
+ await retryOnTransactionConflict(() => this.service.entities.workflow_run_owner.patch(this.#runOwnerKey(fence.runId)).set({ updatedAt: Date.now() }).remove(["leaseExpiresAt"]).where((attr, op) => `${op.eq(attr.generation, fence.generation)} AND ${op.eq(attr.ownerId, fence.ownerId)}`).go());
2956
+ return true;
2957
+ } catch (error) {
2958
+ if (this.isConditionalCheckFailed(error)) return false;
2959
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
2960
+ }
2961
+ }
2962
+ async getRunOwnership({ runId }) {
2963
+ try {
2964
+ return await this.#readRunOwner(runId);
2965
+ } catch (error) {
2966
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
2967
+ }
2601
2968
  }
2602
2969
  /**
2603
2970
  * Helper to check if an error is a DynamoDB conditional check failure
@@ -2622,7 +2989,8 @@ var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorag
2622
2989
  const jitter = Math.random() * backoff * .5;
2623
2990
  await new Promise((resolve) => setTimeout(resolve, backoff + jitter));
2624
2991
  }
2625
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
2992
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence }) {
2993
+ const check = this.#runFenceCheck(fence, runId, "updateWorkflowResults");
2626
2994
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) try {
2627
2995
  const existingRecord = await this.service.entities.workflow_snapshot.get({
2628
2996
  entity: "workflow_snapshot",
@@ -2665,8 +3033,8 @@ var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorag
2665
3033
  updatedAt: now.toISOString(),
2666
3034
  ...getTtlProps("workflow_snapshot", this.ttlConfig)
2667
3035
  };
2668
- if (previousUpdatedAt) await this.service.entities.workflow_snapshot.upsert(data).where((attr, op) => op.eq(attr.updatedAt, previousUpdatedAt)).go();
2669
- else await this.service.entities.workflow_snapshot.create(data).where((attr, op) => op.notExists(attr.run_id)).go();
3036
+ const write = previousUpdatedAt ? this.service.entities.workflow_snapshot.upsert(data).where((attr, op) => op.eq(attr.updatedAt, previousUpdatedAt)) : this.service.entities.workflow_snapshot.create(data).where((attr, op) => op.notExists(attr.run_id));
3037
+ await goWithRunFence(this.service, check, write);
2670
3038
  return snapshot.context;
2671
3039
  } catch (error) {
2672
3040
  if (this.isConditionalCheckFailed(error)) {
@@ -2699,19 +3067,21 @@ var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorag
2699
3067
  }
2700
3068
  }, /* @__PURE__ */ new Error("Max retries exceeded for optimistic locking"));
2701
3069
  }
2702
- async updateWorkflowState({ workflowName, runId, opts }) {
3070
+ async updateWorkflowState({ workflowName, runId, opts, fence }) {
3071
+ const check = this.#runFenceCheck(fence, runId, "updateWorkflowState");
2703
3072
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) try {
2704
3073
  const existingRecord = await this.service.entities.workflow_snapshot.get({
2705
3074
  entity: "workflow_snapshot",
2706
3075
  workflow_name: workflowName,
2707
3076
  run_id: runId
2708
3077
  }).go();
2709
- if (!existingRecord.data) return;
2710
- const existingSnapshot = existingRecord.data.snapshot;
2711
- if (!existingSnapshot || !existingSnapshot.context) return;
2712
- const previousUpdatedAt = existingRecord.data.updatedAt;
3078
+ const existingSnapshot = existingRecord.data?.snapshot;
2713
3079
  const { expectedStatus, ...state } = opts;
2714
- if (!(0, _mastra_core_storage.matchesExpectedWorkflowStatus)(existingSnapshot.status, expectedStatus)) return;
3080
+ if (!existingSnapshot?.context || !(0, _mastra_core_storage.matchesExpectedWorkflowStatus)(existingSnapshot.status, expectedStatus)) {
3081
+ await assertRunFence(this.service, check);
3082
+ return;
3083
+ }
3084
+ const previousUpdatedAt = existingRecord.data.updatedAt;
2715
3085
  const updatedSnapshot = {
2716
3086
  ...existingSnapshot,
2717
3087
  ...state
@@ -2727,7 +3097,7 @@ var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorag
2727
3097
  resourceId: existingRecord.data.resourceId,
2728
3098
  ...getTtlProps("workflow_snapshot", this.ttlConfig)
2729
3099
  };
2730
- await this.service.entities.workflow_snapshot.upsert(data).where((attr, op) => op.eq(attr.updatedAt, previousUpdatedAt)).go();
3100
+ await goWithRunFence(this.service, check, this.service.entities.workflow_snapshot.upsert(data).where((attr, op) => op.eq(attr.updatedAt, previousUpdatedAt)));
2731
3101
  return updatedSnapshot;
2732
3102
  } catch (error) {
2733
3103
  if (this.isConditionalCheckFailed(error)) {
@@ -2758,11 +3128,12 @@ var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorag
2758
3128
  }
2759
3129
  }, /* @__PURE__ */ new Error("Max retries exceeded for optimistic locking"));
2760
3130
  }
2761
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
3131
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
2762
3132
  this.logger.debug("Persisting workflow snapshot", {
2763
3133
  workflowName,
2764
3134
  runId
2765
3135
  });
3136
+ const check = this.#runFenceCheck(fence, runId, "persistWorkflowSnapshot");
2766
3137
  try {
2767
3138
  const now = /* @__PURE__ */ new Date();
2768
3139
  const createdAtValue = (createdAt ?? now).toISOString();
@@ -2778,10 +3149,10 @@ var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorag
2778
3149
  ...getTtlProps("workflow_snapshot", this.ttlConfig)
2779
3150
  };
2780
3151
  try {
2781
- await this.service.entities.workflow_snapshot.create(data).where((attr, op) => op.notExists(attr.run_id)).go();
3152
+ await goWithRunFence(this.service, check, this.service.entities.workflow_snapshot.create(data).where((attr, op) => op.notExists(attr.run_id)));
2782
3153
  } catch (error) {
2783
3154
  if (!this.isConditionalCheckFailed(error)) throw error;
2784
- await this.service.entities.workflow_snapshot.update({
3155
+ await goWithRunFence(this.service, check, this.service.entities.workflow_snapshot.update({
2785
3156
  entity: "workflow_snapshot",
2786
3157
  workflow_name: workflowName,
2787
3158
  run_id: runId
@@ -2790,9 +3161,10 @@ var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorag
2790
3161
  updatedAt: updatedAtValue,
2791
3162
  resourceId,
2792
3163
  ...getTtlProps("workflow_snapshot", this.ttlConfig)
2793
- }).go();
3164
+ }));
2794
3165
  }
2795
3166
  } catch (error) {
3167
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2796
3168
  throw new _mastra_core_error.MastraError({
2797
3169
  id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
2798
3170
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2951,18 +3323,19 @@ var WorkflowStorageDynamoDB = class extends _mastra_core_storage.WorkflowsStorag
2951
3323
  }, error);
2952
3324
  }
2953
3325
  }
2954
- async deleteWorkflowRunById({ runId, workflowName }) {
3326
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
2955
3327
  this.logger.debug("Deleting workflow run by ID", {
2956
3328
  runId,
2957
3329
  workflowName
2958
3330
  });
2959
3331
  try {
2960
- await this.service.entities.workflow_snapshot.delete({
3332
+ await goWithRunFence(this.service, this.#runFenceCheck(fence, runId, "deleteWorkflowRunById"), this.service.entities.workflow_snapshot.delete({
2961
3333
  entity: "workflow_snapshot",
2962
3334
  workflow_name: workflowName,
2963
3335
  run_id: runId
2964
- }).go();
3336
+ }));
2965
3337
  } catch (error) {
3338
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2966
3339
  throw new _mastra_core_error.MastraError({
2967
3340
  id: (0, _mastra_core_storage.createStorageErrorId)("DYNAMODB", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
2968
3341
  domain: _mastra_core_error.ErrorDomain.STORAGE,