@mastra/mongodb 1.23.1-alpha.0 → 1.23.1-alpha.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.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { v4 } from "@lukeed/uuid";
2
2
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
- import { AgentsStorage, BackgroundTasksStorage, BlobStore, DatasetsStorage, ExperimentsStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MastraCompositeStore, MemoryStorage, NotificationsStorage, ObservabilityStorage, PromptBlocksStorage, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, canonicalizeKnowledgeScope, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, hasErrorCode, isKnowledgeScopeVisible, knowledgeScopeKey, knowledgeSemanticDocumentId, knowledgeSemanticIdempotencyKey, listTracesArgsSchema, normalizePerPage, normalizeScheduleTarget, parseDuration, parseKnowledgeNodeCursor, parseKnowledgeWikilinks, resolveListOrderBy, safelyParseJSON, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
3
+ import { AgentsStorage, BackgroundTasksStorage, BlobStore, DatasetsStorage, ExperimentsStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MastraCompositeStore, MemoryStorage, NotificationsStorage, ObservabilityStorage, PromptBlocksStorage, RunFenceConflictError, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, canonicalizeKnowledgeScope, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, hasErrorCode, isKnowledgeScopeVisible, isRunFenceConflictError, knowledgeScopeKey, knowledgeSemanticDocumentId, knowledgeSemanticIdempotencyKey, listTracesArgsSchema, normalizePerPage, normalizeScheduleTarget, parseDuration, parseKnowledgeNodeCursor, parseKnowledgeWikilinks, resolveListOrderBy, resolveRunFence, safelyParseJSON, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
4
4
  import { MastraVector, validateUpsertInput, validateVectorValues } from "@mastra/core/vector";
5
5
  import { MongoBulkWriteError, MongoClient, ObjectId } from "mongodb";
6
6
  import { BaseFilterTranslator } from "@mastra/core/vector/filter";
@@ -9,7 +9,7 @@ import { MessageList } from "@mastra/core/agent";
9
9
  import { saveScorePayloadSchema } from "@mastra/core/evals";
10
10
  import { skillSnapshotFieldValuesEqual } from "@mastra/core/storage/domains/skills";
11
11
  //#region package.json
12
- var version = "1.23.1-alpha.0";
12
+ var version = "1.23.1-alpha.1";
13
13
  //#endregion
14
14
  //#region src/vector/filter.ts
15
15
  /**
@@ -1618,20 +1618,29 @@ var MongoDBConnector = class MongoDBConnector {
1618
1618
  return (await this.getConnection()).listCollections({ name: collectionName }).hasNext();
1619
1619
  }
1620
1620
  /**
1621
- * Returns true when the deployment supports multi-document transactions
1622
- * (replica set or sharded cluster). Standalone servers and custom connector
1623
- * handlers return false. Probed once and cached.
1621
+ * Whether the deployment supports multi-document transactions (replica set
1622
+ * or sharded cluster). Standalone servers and custom connector handlers
1623
+ * don't. The answer is cached once a probe completes; a probe that fails
1624
+ * rejects and is retried on the next call, so the answer never changes.
1624
1625
  */
1625
- async supportsTransactions() {
1626
+ async probeTransactions() {
1626
1627
  if (this.#supportsTransactions !== void 0) return this.#supportsTransactions;
1627
1628
  if (!this.#client) {
1628
1629
  this.#supportsTransactions = false;
1629
1630
  return false;
1630
1631
  }
1632
+ const hello = await (await this.getConnection()).admin().command({ hello: 1 });
1633
+ this.#supportsTransactions = Boolean(hello.setName) || hello.msg === "isdbgrid";
1634
+ return this.#supportsTransactions;
1635
+ }
1636
+ /**
1637
+ * Like {@link probeTransactions}, but a failed probe reads as no
1638
+ * transaction support (and is retried on the next call), so a momentary
1639
+ * outage degrades writes to non-atomic instead of failing them.
1640
+ */
1641
+ async supportsTransactions() {
1631
1642
  try {
1632
- const hello = await (await this.getConnection()).admin().command({ hello: 1 });
1633
- this.#supportsTransactions = Boolean(hello.setName) || hello.msg === "isdbgrid";
1634
- return this.#supportsTransactions;
1643
+ return await this.probeTransactions();
1635
1644
  } catch {
1636
1645
  return false;
1637
1646
  }
@@ -2278,14 +2287,19 @@ function cutoffFor(policy, encoding, now = Date.now()) {
2278
2287
  * Delete up to `limit` documents whose anchor field is older than `cutoff`.
2279
2288
  *
2280
2289
  * `deleteMany` has no limit clause, so each batch first collects `_id`s with a
2281
- * bounded `find`, then deletes exactly that set — the Mongo equivalent of the
2282
- * SQL stores' `DELETE ... WHERE rowid/ctid IN (SELECT ... LIMIT n)` pattern.
2290
+ * bounded `find`, then deletes that set — the Mongo equivalent of the SQL
2291
+ * stores' `DELETE ... WHERE rowid/ctid IN (SELECT ... LIMIT n)` pattern. The
2292
+ * delete re-checks the anchor, so a document updated after the `find` (a
2293
+ * renewed run ownership, a touched snapshot) survives.
2283
2294
  */
2284
2295
  async function pruneCollectionBatch({ connector, table, column, cutoff, limit }) {
2285
2296
  const collection = await connector.getCollection(table);
2286
2297
  const docs = await collection.find({ [column]: { $lt: cutoff } }).project({ _id: 1 }).limit(limit).toArray();
2287
2298
  if (docs.length === 0) return 0;
2288
- return (await collection.deleteMany({ _id: { $in: docs.map((doc) => doc._id) } })).deletedCount;
2299
+ return (await collection.deleteMany({
2300
+ _id: { $in: docs.map((doc) => doc._id) },
2301
+ [column]: { $lt: cutoff }
2302
+ })).deletedCount;
2289
2303
  }
2290
2304
  /**
2291
2305
  * Run the bounded/cancellable batched-delete loop for a single logical target,
@@ -6560,6 +6574,40 @@ function isAppendOnlySince(storedObservations, snapshotObservations) {
6560
6574
  return stored === snapshot || stored.startsWith(snapshot);
6561
6575
  }
6562
6576
  //#endregion
6577
+ //#region src/storage/domains/run-fencing.ts
6578
+ async function getRunClaims(connector, name) {
6579
+ return await connector.getCollection(name);
6580
+ }
6581
+ /** Duplicate-key error: an upsert lost the race to insert the document. */
6582
+ function isDuplicateKeyError(error) {
6583
+ return error?.code === 11e3;
6584
+ }
6585
+ /**
6586
+ * Runs a write, fenced when `check` is set. A fenced write runs in a
6587
+ * transaction that first updates the run's claim document, matching it on
6588
+ * the fence. A fence that is no longer current matches nothing, and the write
6589
+ * throws `RunFenceConflictError` before anything is written. A claim that
6590
+ * changes while the transaction runs makes the update conflict, so the
6591
+ * transaction retries and sees the new claim; a claim attempted after the
6592
+ * update waits until the transaction commits. Updating the claim document,
6593
+ * rather than reading it, is what makes concurrent claims conflict.
6594
+ *
6595
+ * Fencing is declared only on deployments that support transactions.
6596
+ */
6597
+ async function withRunFence(connector, check, write) {
6598
+ if (!check) return write();
6599
+ const { claims, fence, operation } = check;
6600
+ const collection = await getRunClaims(connector, claims);
6601
+ return connector.withTransaction(async (session) => {
6602
+ if ((await collection.updateOne({
6603
+ _id: fence.runId,
6604
+ generation: fence.generation,
6605
+ ownerId: fence.ownerId
6606
+ }, { $inc: { fencedWrites: 1 } }, { session })).matchedCount === 0) throw new RunFenceConflictError(fence, operation);
6607
+ return write(session);
6608
+ });
6609
+ }
6610
+ //#endregion
6563
6611
  //#region src/storage/domains/memory/index.ts
6564
6612
  /**
6565
6613
  * Local constant for the observational memory table name.
@@ -6629,9 +6677,6 @@ function buildSuccessorDocument(old, pending) {
6629
6677
  function matchStoredFields(doc, fields) {
6630
6678
  return Object.fromEntries(fields.map((f) => [f, doc[f] === void 0 ? { $exists: false } : doc[f]]));
6631
6679
  }
6632
- function isDuplicateKeyError(error) {
6633
- return error?.code === 11e3;
6634
- }
6635
6680
  function omNotFound(operation, id) {
6636
6681
  return new MastraError({
6637
6682
  id: createStorageErrorId("MONGODB", operation, "NOT_FOUND"),
@@ -6671,11 +6716,14 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6671
6716
  TABLE_THREADS,
6672
6717
  TABLE_MESSAGES,
6673
6718
  TABLE_RESOURCES,
6674
- OM_TABLE
6719
+ OM_TABLE,
6720
+ TABLE_MEMORY_RUN_FENCES
6675
6721
  ];
6676
6722
  /**
6677
6723
  * Retention-eligible collections. The observational-memory collection is
6678
6724
  * excluded: it has no timestamp anchor to age on. All anchors are BSON dates.
6725
+ * A run fence's `retiredAt` stays null until the execution holding it
6726
+ * settles, and null is never pruned.
6679
6727
  */
6680
6728
  static retentionTables = {
6681
6729
  messages: {
@@ -6692,6 +6740,11 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6692
6740
  table: TABLE_THREADS,
6693
6741
  column: "createdAt",
6694
6742
  indexed: true
6743
+ },
6744
+ runFences: {
6745
+ table: TABLE_MEMORY_RUN_FENCES,
6746
+ column: "retiredAt",
6747
+ indexed: true
6695
6748
  }
6696
6749
  };
6697
6750
  constructor(config) {
@@ -6710,11 +6763,76 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6710
6763
  await this.#maintainSupersededBy();
6711
6764
  }
6712
6765
  /**
6766
+ * Fenced writes need multi-document transactions, so only replica sets and
6767
+ * sharded clusters fence. Rejects while the deployment can't be probed.
6768
+ */
6769
+ supportsRunFencing() {
6770
+ return this.#connector.probeTransactions();
6771
+ }
6772
+ async raiseRunFence(fence) {
6773
+ try {
6774
+ const fences = await getRunClaims(this.#connector, TABLE_MEMORY_RUN_FENCES);
6775
+ for (;;) {
6776
+ try {
6777
+ await fences.updateOne({
6778
+ _id: fence.runId,
6779
+ $or: [{ generation: { $lt: fence.generation } }, {
6780
+ generation: fence.generation,
6781
+ ownerId: fence.ownerId
6782
+ }]
6783
+ }, { $set: {
6784
+ generation: fence.generation,
6785
+ ownerId: fence.ownerId,
6786
+ retiredAt: null
6787
+ } }, { upsert: true });
6788
+ return true;
6789
+ } catch (error) {
6790
+ if (!isDuplicateKeyError(error)) throw error;
6791
+ }
6792
+ const current = await fences.findOne({ _id: fence.runId });
6793
+ if (current && current.generation >= fence.generation) return current.generation === fence.generation && current.ownerId === fence.ownerId;
6794
+ }
6795
+ } catch (error) {
6796
+ throw new MastraError({
6797
+ id: createStorageErrorId("MONGODB", "RAISE_RUN_FENCE", "FAILED"),
6798
+ domain: ErrorDomain.STORAGE,
6799
+ category: ErrorCategory.THIRD_PARTY,
6800
+ details: { runId: fence.runId }
6801
+ }, error);
6802
+ }
6803
+ }
6804
+ async retireRunFence(fence) {
6805
+ try {
6806
+ return (await (await getRunClaims(this.#connector, TABLE_MEMORY_RUN_FENCES)).updateOne({
6807
+ _id: fence.runId,
6808
+ generation: fence.generation,
6809
+ ownerId: fence.ownerId
6810
+ }, [{ $set: { retiredAt: "$$NOW" } }])).matchedCount > 0;
6811
+ } catch (error) {
6812
+ throw new MastraError({
6813
+ id: createStorageErrorId("MONGODB", "RETIRE_RUN_FENCE", "FAILED"),
6814
+ domain: ErrorDomain.STORAGE,
6815
+ category: ErrorCategory.THIRD_PARTY,
6816
+ details: { runId: fence.runId }
6817
+ }, error);
6818
+ }
6819
+ }
6820
+ /** The check a write must pass: its own fence, otherwise the one in scope. */
6821
+ #runFenceCheck(fence, operation) {
6822
+ const resolved = resolveRunFence(this, fence);
6823
+ return resolved && {
6824
+ claims: TABLE_MEMORY_RUN_FENCES,
6825
+ fence: resolved,
6826
+ operation
6827
+ };
6828
+ }
6829
+ /**
6713
6830
  * Delete memory rows older than each table's `maxAge`, batched. Order is
6714
6831
  * messages → resources → threads so child rows never outlive the delete of
6715
- * their thread. Like `deleteThread()`, this does not sweep vector-store
6716
- * embeddings — semantic-recall vectors live in a separate vector store the
6717
- * memory domain cannot reach; cleaning those up is the operator's concern.
6832
+ * their thread; run fences are independent and go last. Like
6833
+ * `deleteThread()`, this does not sweep vector-store embeddings —
6834
+ * semantic-recall vectors live in a separate vector store the memory domain
6835
+ * cannot reach; cleaning those up is the operator's concern.
6718
6836
  */
6719
6837
  async prune(policies, options) {
6720
6838
  const targets = resolveTargets({
@@ -6723,7 +6841,8 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6723
6841
  order: [
6724
6842
  "messages",
6725
6843
  "resources",
6726
- "threads"
6844
+ "threads",
6845
+ "runFences"
6727
6846
  ]
6728
6847
  });
6729
6848
  return runPrune({
@@ -6829,17 +6948,19 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6829
6948
  }
6830
6949
  }
6831
6950
  async dangerouslyClearAll() {
6832
- const [threadsCollection, messagesCollection, resourcesCollection, omCollection] = await Promise.all([
6951
+ const [threadsCollection, messagesCollection, resourcesCollection, omCollection, fencesCollection] = await Promise.all([
6833
6952
  this.getCollection(TABLE_THREADS),
6834
6953
  this.getCollection(TABLE_MESSAGES),
6835
6954
  this.getCollection(TABLE_RESOURCES),
6836
- this.getCollection(OM_TABLE)
6955
+ this.getCollection(OM_TABLE),
6956
+ this.getCollection(TABLE_MEMORY_RUN_FENCES)
6837
6957
  ]);
6838
6958
  await Promise.all([
6839
6959
  threadsCollection.deleteMany({}),
6840
6960
  messagesCollection.deleteMany({}),
6841
6961
  resourcesCollection.deleteMany({}),
6842
- omCollection.deleteMany({})
6962
+ omCollection.deleteMany({}),
6963
+ fencesCollection.deleteMany({})
6843
6964
  ]);
6844
6965
  }
6845
6966
  parseRow(row) {
@@ -7195,7 +7316,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7195
7316
  throw mastraError;
7196
7317
  }
7197
7318
  }
7198
- async saveMessages({ messages }) {
7319
+ async saveMessages({ messages, fence }) {
7199
7320
  if (messages.length === 0) return { messages: [] };
7200
7321
  try {
7201
7322
  if (!messages[0]?.threadId) throw new Error("Thread ID is required");
@@ -7223,12 +7344,16 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7223
7344
  });
7224
7345
  const allThreadIds = new Set(messages.map((m) => m.threadId));
7225
7346
  const now = /* @__PURE__ */ new Date();
7226
- await this.#connector.withTransaction(async (session) => {
7347
+ const writeMessages = async (session) => {
7227
7348
  await collection.bulkWrite(messagesToInsert, { session });
7228
7349
  for (const tid of allThreadIds) await threadsCollection.updateOne({ id: tid }, { $set: { updatedAt: now } }, { session });
7229
- });
7350
+ };
7351
+ const check = this.#runFenceCheck(fence, "saveMessages");
7352
+ if (check) await withRunFence(this.#connector, check, writeMessages);
7353
+ else await this.#connector.withTransaction(writeMessages);
7230
7354
  return { messages: new MessageList().add(messages, "memory").get.all.db() };
7231
7355
  } catch (error) {
7356
+ if (isRunFenceConflictError(error)) throw error;
7232
7357
  throw new MastraError({
7233
7358
  id: createStorageErrorId("MONGODB", "SAVE_MESSAGES", "FAILED"),
7234
7359
  domain: ErrorDomain.STORAGE,
@@ -7236,49 +7361,51 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7236
7361
  }, error);
7237
7362
  }
7238
7363
  }
7239
- async updateMessages({ messages }) {
7364
+ async updateMessages({ messages, fence }) {
7240
7365
  if (messages.length === 0) return [];
7241
7366
  const messageIds = messages.map((m) => m.id);
7242
7367
  const collection = await this.getCollection(TABLE_MESSAGES);
7243
- const existingMessagesParsed = (await collection.find({ id: { $in: messageIds } }).toArray()).map((msg) => this.parseRow(msg));
7244
- if (existingMessagesParsed.length === 0) return [];
7245
- const threadIdsToUpdate = /* @__PURE__ */ new Set();
7246
- const bulkOps = [];
7247
- for (const existingMessage of existingMessagesParsed) {
7248
- const updatePayload = messages.find((m) => m.id === existingMessage.id);
7249
- if (!updatePayload) continue;
7250
- const { id, ...fieldsToUpdate } = updatePayload;
7251
- if (Object.keys(fieldsToUpdate).length === 0) continue;
7252
- threadIdsToUpdate.add(existingMessage.threadId);
7253
- if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) threadIdsToUpdate.add(updatePayload.threadId);
7254
- const updateDoc = {};
7255
- const updatableFields = { ...fieldsToUpdate };
7256
- if (updatableFields.content) {
7257
- const newContent = {
7258
- ...existingMessage.content,
7259
- ...updatableFields.content,
7260
- ...existingMessage.content?.metadata && updatableFields.content.metadata ? { metadata: {
7261
- ...existingMessage.content.metadata,
7262
- ...updatableFields.content.metadata
7263
- } } : {}
7264
- };
7265
- updateDoc.content = JSON.stringify(newContent);
7266
- delete updatableFields.content;
7267
- }
7268
- for (const key in updatableFields) if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
7269
- const dbKey = key === "threadId" ? "thread_id" : key;
7270
- let value = updatableFields[key];
7271
- if (typeof value === "object" && value !== null) value = JSON.stringify(value);
7272
- updateDoc[dbKey] = value;
7368
+ return withRunFence(this.#connector, this.#runFenceCheck(fence, "updateMessages"), async (session) => {
7369
+ const existingMessagesParsed = (await collection.find({ id: { $in: messageIds } }, { session }).toArray()).map((msg) => this.parseRow(msg));
7370
+ if (existingMessagesParsed.length === 0) return [];
7371
+ const threadIdsToUpdate = /* @__PURE__ */ new Set();
7372
+ const bulkOps = [];
7373
+ for (const existingMessage of existingMessagesParsed) {
7374
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
7375
+ if (!updatePayload) continue;
7376
+ const { id, ...fieldsToUpdate } = updatePayload;
7377
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
7378
+ threadIdsToUpdate.add(existingMessage.threadId);
7379
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) threadIdsToUpdate.add(updatePayload.threadId);
7380
+ const updateDoc = {};
7381
+ const updatableFields = { ...fieldsToUpdate };
7382
+ if (updatableFields.content) {
7383
+ const newContent = {
7384
+ ...existingMessage.content,
7385
+ ...updatableFields.content,
7386
+ ...existingMessage.content?.metadata && updatableFields.content.metadata ? { metadata: {
7387
+ ...existingMessage.content.metadata,
7388
+ ...updatableFields.content.metadata
7389
+ } } : {}
7390
+ };
7391
+ updateDoc.content = JSON.stringify(newContent);
7392
+ delete updatableFields.content;
7393
+ }
7394
+ for (const key in updatableFields) if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
7395
+ const dbKey = key === "threadId" ? "thread_id" : key;
7396
+ let value = updatableFields[key];
7397
+ if (typeof value === "object" && value !== null) value = JSON.stringify(value);
7398
+ updateDoc[dbKey] = value;
7399
+ }
7400
+ if (Object.keys(updateDoc).length > 0) bulkOps.push({ updateOne: {
7401
+ filter: { id },
7402
+ update: { $set: updateDoc }
7403
+ } });
7273
7404
  }
7274
- if (Object.keys(updateDoc).length > 0) bulkOps.push({ updateOne: {
7275
- filter: { id },
7276
- update: { $set: updateDoc }
7277
- } });
7278
- }
7279
- if (bulkOps.length > 0) await collection.bulkWrite(bulkOps);
7280
- if (threadIdsToUpdate.size > 0) await (await this.getCollection(TABLE_THREADS)).updateMany({ id: { $in: Array.from(threadIdsToUpdate) } }, { $set: { updatedAt: /* @__PURE__ */ new Date() } });
7281
- return (await collection.find({ id: { $in: messageIds } }).toArray()).map((row) => this.parseRow(row));
7405
+ if (bulkOps.length > 0) await collection.bulkWrite(bulkOps, { session });
7406
+ if (threadIdsToUpdate.size > 0) await (await this.getCollection(TABLE_THREADS)).updateMany({ id: { $in: Array.from(threadIdsToUpdate) } }, { $set: { updatedAt: /* @__PURE__ */ new Date() } }, { session });
7407
+ return (await collection.find({ id: { $in: messageIds } }, { session }).toArray()).map((row) => this.parseRow(row));
7408
+ });
7282
7409
  }
7283
7410
  async getResourceById({ resourceId }) {
7284
7411
  try {
@@ -7313,9 +7440,11 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7313
7440
  }, error);
7314
7441
  }
7315
7442
  }
7316
- async updateResource({ resourceId, workingMemory, metadata }) {
7443
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
7317
7444
  try {
7445
+ const check = this.#runFenceCheck(fence, "updateResource");
7318
7446
  const existingResource = await this.getResourceById({ resourceId });
7447
+ const collection = await this.getCollection(TABLE_RESOURCES);
7319
7448
  if (!existingResource) {
7320
7449
  const newResource = {
7321
7450
  id: resourceId,
@@ -7324,7 +7453,11 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7324
7453
  createdAt: /* @__PURE__ */ new Date(),
7325
7454
  updatedAt: /* @__PURE__ */ new Date()
7326
7455
  };
7327
- return this.saveResource({ resource: newResource });
7456
+ await withRunFence(this.#connector, check, (session) => collection.updateOne({ id: resourceId }, { $set: { ...newResource } }, {
7457
+ upsert: true,
7458
+ session
7459
+ }));
7460
+ return newResource;
7328
7461
  }
7329
7462
  const updatedResource = {
7330
7463
  ...existingResource,
@@ -7335,13 +7468,13 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7335
7468
  } : existingResource.metadata,
7336
7469
  updatedAt: /* @__PURE__ */ new Date()
7337
7470
  };
7338
- const collection = await this.getCollection(TABLE_RESOURCES);
7339
7471
  const updateDoc = { updatedAt: updatedResource.updatedAt };
7340
7472
  if (workingMemory !== void 0) updateDoc.workingMemory = workingMemory;
7341
7473
  if (metadata) updateDoc.metadata = updatedResource.metadata;
7342
- await collection.updateOne({ id: resourceId }, { $set: updateDoc });
7474
+ await withRunFence(this.#connector, check, (session) => collection.updateOne({ id: resourceId }, { $set: updateDoc }, { session }));
7343
7475
  return updatedResource;
7344
7476
  } catch (error) {
7477
+ if (isRunFenceConflictError(error)) throw error;
7345
7478
  throw new MastraError({
7346
7479
  id: createStorageErrorId("MONGODB", "UPDATE_RESOURCE", "FAILED"),
7347
7480
  domain: ErrorDomain.STORAGE,
@@ -7441,14 +7574,19 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7441
7574
  throw mastraError;
7442
7575
  }
7443
7576
  }
7444
- async saveThread({ thread }) {
7577
+ async saveThread({ thread, fence }) {
7445
7578
  try {
7446
- await (await this.getCollection(TABLE_THREADS)).updateOne({ id: thread.id }, { $set: {
7579
+ const collection = await this.getCollection(TABLE_THREADS);
7580
+ await withRunFence(this.#connector, this.#runFenceCheck(fence, "saveThread"), (session) => collection.updateOne({ id: thread.id }, { $set: {
7447
7581
  ...thread,
7448
7582
  metadata: thread.metadata
7449
- } }, { upsert: true });
7583
+ } }, {
7584
+ upsert: true,
7585
+ session
7586
+ }));
7450
7587
  return thread;
7451
7588
  } catch (error) {
7589
+ if (isRunFenceConflictError(error)) throw error;
7452
7590
  throw new MastraError({
7453
7591
  id: createStorageErrorId("MONGODB", "SAVE_THREAD", "FAILED"),
7454
7592
  domain: ErrorDomain.STORAGE,
@@ -7457,7 +7595,8 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7457
7595
  }, error);
7458
7596
  }
7459
7597
  }
7460
- async updateThread({ id, title, metadata }) {
7598
+ async updateThread({ id, title, metadata, fence }) {
7599
+ const check = this.#runFenceCheck(fence, "updateThread");
7461
7600
  const thread = await this.getThreadById({ threadId: id });
7462
7601
  if (!thread) throw new MastraError({
7463
7602
  id: createStorageErrorId("MONGODB", "UPDATE_THREAD", "NOT_FOUND"),
@@ -7480,12 +7619,14 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7480
7619
  updatedAt: now
7481
7620
  };
7482
7621
  try {
7483
- await (await this.getCollection(TABLE_THREADS)).updateOne({ id }, { $set: {
7622
+ const collection = await this.getCollection(TABLE_THREADS);
7623
+ await withRunFence(this.#connector, check, (session) => collection.updateOne({ id }, { $set: {
7484
7624
  title: updatedThread.title,
7485
7625
  metadata: updatedThread.metadata,
7486
7626
  updatedAt: now
7487
- } });
7627
+ } }, { session }));
7488
7628
  } catch (error) {
7629
+ if (isRunFenceConflictError(error)) throw error;
7489
7630
  throw new MastraError({
7490
7631
  id: createStorageErrorId("MONGODB", "UPDATE_THREAD", "FAILED"),
7491
7632
  domain: ErrorDomain.STORAGE,
@@ -7508,16 +7649,19 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7508
7649
  }, error);
7509
7650
  }
7510
7651
  }
7511
- async deleteMessages(messageIds) {
7652
+ async deleteMessages(messageIds, options) {
7512
7653
  if (messageIds.length === 0) return;
7513
7654
  try {
7514
7655
  const messagesCollection = await this.getCollection(TABLE_MESSAGES);
7515
7656
  const threadsCollection = await this.getCollection(TABLE_THREADS);
7516
- const messagesToDelete = await messagesCollection.find({ id: { $in: messageIds } }).toArray();
7517
- const threadIds = [...new Set(messagesToDelete.map((m) => m.thread_id))];
7518
- await messagesCollection.deleteMany({ id: { $in: messageIds } });
7519
- if (threadIds.length > 0) await threadsCollection.updateMany({ id: { $in: threadIds } }, { $set: { updatedAt: /* @__PURE__ */ new Date() } });
7657
+ await withRunFence(this.#connector, this.#runFenceCheck(options?.fence, "deleteMessages"), async (session) => {
7658
+ const messagesToDelete = await messagesCollection.find({ id: { $in: messageIds } }, { session }).toArray();
7659
+ const threadIds = [...new Set(messagesToDelete.map((m) => m.thread_id))];
7660
+ await messagesCollection.deleteMany({ id: { $in: messageIds } }, { session });
7661
+ if (threadIds.length > 0) await threadsCollection.updateMany({ id: { $in: threadIds } }, { $set: { updatedAt: /* @__PURE__ */ new Date() } }, { session });
7662
+ });
7520
7663
  } catch (error) {
7664
+ if (isRunFenceConflictError(error)) throw error;
7521
7665
  throw new MastraError({
7522
7666
  id: createStorageErrorId("MONGODB", "DELETE_MESSAGES", "FAILED"),
7523
7667
  domain: ErrorDomain.STORAGE,
@@ -7884,7 +8028,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7884
8028
  applied: false,
7885
8029
  reason: "conflict"
7886
8030
  };
7887
- if ((await collection.updateOne({
8031
+ if ((await withRunFence(this.#connector, this.#runFenceCheck(void 0, "updateActiveObservations"), (session) => collection.updateOne({
7888
8032
  id: input.id,
7889
8033
  supersededBy: null,
7890
8034
  ...matchStoredFields(doc, ["activeObservations", "lastObservedAt"])
@@ -7898,7 +8042,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7898
8042
  updatedAt: /* @__PURE__ */ new Date()
7899
8043
  },
7900
8044
  $inc: { totalTokensObserved: safeTokenCount }
7901
- })).matchedCount === 1) return { applied: true };
8045
+ }, { session }))).matchedCount === 1) return { applied: true };
7902
8046
  }
7903
8047
  throw omConflict("UPDATE_ACTIVE_OBSERVATIONS", input.id);
7904
8048
  } catch (error) {
@@ -7914,7 +8058,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7914
8058
  async createReflectionGeneration(input) {
7915
8059
  try {
7916
8060
  const { currentRecord } = input;
7917
- return await this.#rollOver(currentRecord, (doc) => {
8061
+ return await this.#rollOver(currentRecord, "createReflectionGeneration", (doc) => {
7918
8062
  const stored = this.parseOMDocument(doc);
7919
8063
  const plan = planReflectionGenerationText({
7920
8064
  storedObservations: stored.activeObservations,
@@ -8151,7 +8295,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8151
8295
  let hops = 0;
8152
8296
  let retries = 0;
8153
8297
  for (;;) {
8154
- if ((await collection.updateOne({
8298
+ if ((await withRunFence(this.#connector, this.#runFenceCheck(void 0, "updateBufferedObservations"), (session) => collection.updateOne({
8155
8299
  id: targetId,
8156
8300
  supersededBy: null,
8157
8301
  "bufferedObservationChunks.cycleId": { $ne: input.chunk.cycleId },
@@ -8165,7 +8309,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8165
8309
  bufferedObservationChunks: { $concatArrays: [{ $ifNull: ["$bufferedObservationChunks", []] }, { $literal: [newChunk] }] },
8166
8310
  ...lastBufferedAtTime ? { lastBufferedAtTime: { $max: ["$lastBufferedAtTime", { $literal: lastBufferedAtTime }] } } : {},
8167
8311
  updatedAt: { $literal: /* @__PURE__ */ new Date() }
8168
- } }])).matchedCount === 1) return {
8312
+ } }], { session }))).matchedCount === 1) return {
8169
8313
  persisted: true,
8170
8314
  recordId: targetId
8171
8315
  };
@@ -8188,7 +8332,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8188
8332
  recordId: targetId
8189
8333
  };
8190
8334
  if (stored !== void 0 && stored !== null && !Array.isArray(stored)) {
8191
- if ((await collection.updateOne({
8335
+ if ((await withRunFence(this.#connector, this.#runFenceCheck(void 0, "updateBufferedObservations"), (session) => collection.updateOne({
8192
8336
  id: targetId,
8193
8337
  supersededBy: null,
8194
8338
  bufferedObservationChunks: stored,
@@ -8197,7 +8341,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8197
8341
  bufferedObservationChunks: [newChunk],
8198
8342
  ...lastBufferedAtTime ? { lastBufferedAtTime: maxObservationCursor(doc.lastBufferedAtTime, lastBufferedAtTime) } : {},
8199
8343
  updatedAt: /* @__PURE__ */ new Date()
8200
- } })).matchedCount === 1) return {
8344
+ } }, { session }))).matchedCount === 1) return {
8201
8345
  persisted: true,
8202
8346
  recordId: targetId
8203
8347
  };
@@ -8287,7 +8431,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8287
8431
  const newTokenCount = existingTokenCount + activatedTokens;
8288
8432
  const existingPending = Number(doc.pendingMessageTokens || 0);
8289
8433
  const newPending = Math.max(0, existingPending - activatedMessageTokens);
8290
- if ((await collection.updateOne({
8434
+ if ((await withRunFence(this.#connector, this.#runFenceCheck(void 0, "swapBufferedToActive"), (session) => collection.updateOne({
8291
8435
  id: input.id,
8292
8436
  supersededBy: null,
8293
8437
  ...matchStoredFields(doc, [
@@ -8304,7 +8448,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8304
8448
  bufferedObservationChunks: remainingChunks,
8305
8449
  lastObservedAt: maxObservationCursor(doc.lastObservedAt, lastObservedAt),
8306
8450
  updatedAt: /* @__PURE__ */ new Date()
8307
- } })).matchedCount === 0) continue;
8451
+ } }, { session }))).matchedCount === 0) continue;
8308
8452
  const latestChunkHints = activatedChunks[activatedChunks.length - 1];
8309
8453
  return {
8310
8454
  chunksActivated: activatedChunks.length,
@@ -8353,13 +8497,13 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8353
8497
  const newContent = existingContent ? `${existingContent}\n\n${input.reflection}` : input.reflection;
8354
8498
  const newTokens = existingTokens + input.tokenCount;
8355
8499
  const newInputTokens = existingInputTokens + input.inputTokenCount;
8356
- if ((await collection.updateOne({ id: input.id }, { $set: {
8500
+ if ((await withRunFence(this.#connector, this.#runFenceCheck(void 0, "updateBufferedReflection"), (session) => collection.updateOne({ id: input.id }, { $set: {
8357
8501
  bufferedReflection: newContent,
8358
8502
  bufferedReflectionTokens: newTokens,
8359
8503
  bufferedReflectionInputTokens: newInputTokens,
8360
8504
  reflectedObservationLineCount: input.reflectedObservationLineCount,
8361
8505
  updatedAt: /* @__PURE__ */ new Date()
8362
- } })).matchedCount === 0) throw new MastraError({
8506
+ } }, { session }))).matchedCount === 0) throw new MastraError({
8363
8507
  id: createStorageErrorId("MONGODB", "UPDATE_BUFFERED_REFLECTION", "NOT_FOUND"),
8364
8508
  text: `Observational memory record not found: ${input.id}`,
8365
8509
  domain: ErrorDomain.STORAGE,
@@ -8379,7 +8523,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8379
8523
  async swapBufferedReflectionToActive(input) {
8380
8524
  try {
8381
8525
  const { currentRecord } = input;
8382
- return await this.#rollOver(currentRecord, (doc) => {
8526
+ return await this.#rollOver(currentRecord, "swapBufferedReflectionToActive", (doc) => {
8383
8527
  const stored = this.parseOMDocument(doc);
8384
8528
  const bufferedReflection = stored.bufferedReflection || "";
8385
8529
  if (!bufferedReflection) throw new MastraError({
@@ -8465,7 +8609,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8465
8609
  *
8466
8610
  * A crash between 2 and 3 is finished by the next head read (`#getHeadDoc`) or startup.
8467
8611
  */
8468
- async #rollOver(currentRecord, decide) {
8612
+ async #rollOver(currentRecord, operation, decide) {
8469
8613
  const collection = await this.getCollection(OM_TABLE);
8470
8614
  for (let attempt = 1; attempt <= OM_MAX_CONDITIONAL_ATTEMPTS; attempt++) {
8471
8615
  const doc = await collection.findOne({ id: currentRecord.id });
@@ -8476,7 +8620,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8476
8620
  }
8477
8621
  const pending = decide(doc);
8478
8622
  if (!pending) return this.parseOMDocument(doc);
8479
- const frozen = await collection.findOneAndUpdate({
8623
+ const frozen = await withRunFence(this.#connector, this.#runFenceCheck(void 0, operation), (session) => collection.findOneAndUpdate({
8480
8624
  id: doc.id,
8481
8625
  supersededBy: null,
8482
8626
  ...matchStoredFields(doc, [
@@ -8488,7 +8632,10 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8488
8632
  }, { $set: {
8489
8633
  supersededBy: pending.newId,
8490
8634
  pendingSuccessor: pending
8491
- } }, { returnDocument: "before" });
8635
+ } }, {
8636
+ returnDocument: "before",
8637
+ session
8638
+ }));
8492
8639
  if (!frozen) continue;
8493
8640
  return this.parseOMDocument(await this.#completeRollover(collection, frozen, pending));
8494
8641
  }
@@ -11803,33 +11950,58 @@ var MongoDBWorkflowDefinitionsStore = class MongoDBWorkflowDefinitionsStore exte
11803
11950
  };
11804
11951
  //#endregion
11805
11952
  //#region src/storage/domains/workflows/index.ts
11953
+ const RUN_OWNER_PROJECTION = {
11954
+ generation: 1,
11955
+ ownerId: 1,
11956
+ leaseExpiresAt: 1,
11957
+ live: { $gt: ["$leaseExpiresAt", "$$NOW"] }
11958
+ };
11959
+ const NOT_LIVE = { $or: [{ leaseExpiresAt: null }, { $expr: { $lte: ["$leaseExpiresAt", "$$NOW"] } }] };
11960
+ function toRunOwnershipRecord(doc) {
11961
+ return {
11962
+ runId: doc._id,
11963
+ generation: Number(doc.generation),
11964
+ ownerId: doc.ownerId,
11965
+ leaseExpiresAt: doc.leaseExpiresAt ?? null,
11966
+ live: Boolean(doc.live)
11967
+ };
11968
+ }
11806
11969
  var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsStorage {
11807
11970
  #connector;
11808
11971
  #skipDefaultIndexes;
11809
11972
  #indexes;
11810
11973
  /** Collections managed by this domain */
11811
- static MANAGED_COLLECTIONS = [TABLE_WORKFLOW_SNAPSHOT];
11974
+ static MANAGED_COLLECTIONS = [TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKFLOW_RUN_OWNERS];
11812
11975
  /**
11813
- * Anchor is `updatedAt` (BSON date), so the policy reads as inactivity:
11814
- * a run is pruned only after its snapshot has not been touched for `maxAge`.
11976
+ * Both anchors are `updatedAt` (BSON date), so the policies read as
11977
+ * inactivity: a run is pruned only after its snapshot has not been touched
11978
+ * for `maxAge`, and an ownership document only after no claim, renewal or
11979
+ * release has touched it for `maxAge`.
11815
11980
  */
11816
- static retentionTables = { workflowSnapshot: {
11817
- table: TABLE_WORKFLOW_SNAPSHOT,
11818
- column: "updatedAt",
11819
- indexed: true
11820
- } };
11981
+ static retentionTables = {
11982
+ workflowSnapshot: {
11983
+ table: TABLE_WORKFLOW_SNAPSHOT,
11984
+ column: "updatedAt",
11985
+ indexed: true
11986
+ },
11987
+ runOwnership: {
11988
+ table: TABLE_WORKFLOW_RUN_OWNERS,
11989
+ column: "updatedAt",
11990
+ indexed: true
11991
+ }
11992
+ };
11821
11993
  constructor(config) {
11822
11994
  super();
11823
11995
  this.#connector = resolveMongoDBConfig(config);
11824
11996
  this.#skipDefaultIndexes = config.skipDefaultIndexes;
11825
11997
  this.#indexes = config.indexes?.filter((idx) => WorkflowsStorageMongoDB.MANAGED_COLLECTIONS.includes(idx.collection));
11826
11998
  }
11827
- /** Delete workflow snapshots idle for longer than the policy's `maxAge`, batched. */
11999
+ /** Delete workflow snapshots and ownership documents idle for longer than each policy's `maxAge`, batched. */
11828
12000
  async prune(policies, options) {
11829
12001
  const targets = resolveTargets({
11830
12002
  policies,
11831
12003
  descriptor: WorkflowsStorageMongoDB.retentionTables,
11832
- order: ["workflowSnapshot"]
12004
+ order: ["workflowSnapshot", "runOwnership"]
11833
12005
  });
11834
12006
  return runPrune({
11835
12007
  connector: this.#connector,
@@ -11842,6 +12014,13 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11842
12014
  supportsConcurrentUpdates() {
11843
12015
  return true;
11844
12016
  }
12017
+ /**
12018
+ * Fenced writes need multi-document transactions, so only replica sets and
12019
+ * sharded clusters fence. Rejects while the deployment can't be probed.
12020
+ */
12021
+ supportsRunFencing() {
12022
+ return this.#connector.probeTransactions();
12023
+ }
11845
12024
  async getCollection(name) {
11846
12025
  return this.#connector.getCollection(name);
11847
12026
  }
@@ -11849,6 +12028,124 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11849
12028
  await this.createDefaultIndexes();
11850
12029
  await this.createCustomIndexes();
11851
12030
  }
12031
+ #runFenceCheck(fence, runId, operation) {
12032
+ const resolved = resolveRunFence(this, fence, runId);
12033
+ return resolved && {
12034
+ claims: TABLE_WORKFLOW_RUN_OWNERS,
12035
+ fence: resolved,
12036
+ operation
12037
+ };
12038
+ }
12039
+ async #readRunOwner(runId) {
12040
+ const doc = await (await getRunClaims(this.#connector, TABLE_WORKFLOW_RUN_OWNERS)).findOne({ _id: runId }, { projection: RUN_OWNER_PROJECTION });
12041
+ return doc ? toRunOwnershipRecord(doc) : null;
12042
+ }
12043
+ #ownershipError(operation, runId, error) {
12044
+ return new MastraError({
12045
+ id: createStorageErrorId("MONGODB", operation, "FAILED"),
12046
+ domain: ErrorDomain.STORAGE,
12047
+ category: ErrorCategory.THIRD_PARTY,
12048
+ details: { runId }
12049
+ }, error);
12050
+ }
12051
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
12052
+ try {
12053
+ const owners = await getRunClaims(this.#connector, TABLE_WORKFLOW_RUN_OWNERS);
12054
+ const claim = (generation) => [{ $set: {
12055
+ generation,
12056
+ ownerId: { $literal: ownerId },
12057
+ leaseExpiresAt: { $add: ["$$NOW", leaseMs] },
12058
+ updatedAt: "$$NOW"
12059
+ } }];
12060
+ if (!expectedGeneration) {
12061
+ try {
12062
+ const created = await owners.findOneAndUpdate({
12063
+ _id: runId,
12064
+ generation: { $exists: false }
12065
+ }, claim({ $max: [1, { $toLong: "$$NOW" }] }), {
12066
+ upsert: true,
12067
+ returnDocument: "after",
12068
+ projection: RUN_OWNER_PROJECTION
12069
+ });
12070
+ if (created) return {
12071
+ acquired: true,
12072
+ record: toRunOwnershipRecord(created)
12073
+ };
12074
+ } catch (error) {
12075
+ if (!isDuplicateKeyError(error)) throw error;
12076
+ }
12077
+ if (expectedGeneration === 0) return {
12078
+ acquired: false,
12079
+ record: await this.#readRunOwner(runId)
12080
+ };
12081
+ }
12082
+ const advanced = await owners.findOneAndUpdate({
12083
+ _id: runId,
12084
+ ...expectedGeneration !== void 0 ? { generation: expectedGeneration } : {},
12085
+ ...force ? {} : NOT_LIVE
12086
+ }, claim({ $add: ["$generation", 1] }), {
12087
+ returnDocument: "after",
12088
+ projection: RUN_OWNER_PROJECTION
12089
+ });
12090
+ if (advanced) return {
12091
+ acquired: true,
12092
+ record: toRunOwnershipRecord(advanced)
12093
+ };
12094
+ return {
12095
+ acquired: false,
12096
+ record: await this.#readRunOwner(runId)
12097
+ };
12098
+ } catch (error) {
12099
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", runId, error);
12100
+ }
12101
+ }
12102
+ async renewRunOwnership({ leaseMs, ...fence }) {
12103
+ try {
12104
+ const renewed = await (await getRunClaims(this.#connector, TABLE_WORKFLOW_RUN_OWNERS)).findOneAndUpdate({
12105
+ _id: fence.runId,
12106
+ generation: fence.generation,
12107
+ ownerId: fence.ownerId,
12108
+ leaseExpiresAt: { $ne: null }
12109
+ }, [{ $set: {
12110
+ leaseExpiresAt: { $add: ["$$NOW", leaseMs] },
12111
+ updatedAt: "$$NOW"
12112
+ } }], {
12113
+ returnDocument: "after",
12114
+ projection: RUN_OWNER_PROJECTION
12115
+ });
12116
+ if (renewed) return {
12117
+ renewed: true,
12118
+ record: toRunOwnershipRecord(renewed)
12119
+ };
12120
+ return {
12121
+ renewed: false,
12122
+ record: await this.#readRunOwner(fence.runId)
12123
+ };
12124
+ } catch (error) {
12125
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, error);
12126
+ }
12127
+ }
12128
+ async releaseRunOwnership(fence) {
12129
+ try {
12130
+ return (await (await getRunClaims(this.#connector, TABLE_WORKFLOW_RUN_OWNERS)).updateOne({
12131
+ _id: fence.runId,
12132
+ generation: fence.generation,
12133
+ ownerId: fence.ownerId
12134
+ }, [{ $set: {
12135
+ leaseExpiresAt: null,
12136
+ updatedAt: "$$NOW"
12137
+ } }])).matchedCount > 0;
12138
+ } catch (error) {
12139
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
12140
+ }
12141
+ }
12142
+ async getRunOwnership({ runId }) {
12143
+ try {
12144
+ return await this.#readRunOwner(runId);
12145
+ } catch (error) {
12146
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
12147
+ }
12148
+ }
11852
12149
  /**
11853
12150
  * Returns default index definitions for the workflows domain collections.
11854
12151
  */
@@ -11908,8 +12205,10 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11908
12205
  }
11909
12206
  async dangerouslyClearAll() {
11910
12207
  await (await this.getCollection(TABLE_WORKFLOW_SNAPSHOT)).deleteMany({});
12208
+ await (await this.getCollection(TABLE_WORKFLOW_RUN_OWNERS)).deleteMany({});
11911
12209
  }
11912
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
12210
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence }) {
12211
+ const check = this.#runFenceCheck(fence, runId, "updateWorkflowResults");
11913
12212
  try {
11914
12213
  const collection = await this.getCollection(TABLE_WORKFLOW_SNAPSHOT);
11915
12214
  const now = /* @__PURE__ */ new Date();
@@ -11929,7 +12228,7 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11929
12228
  runId,
11930
12229
  requestContext: {}
11931
12230
  };
11932
- const updatedDoc = await collection.findOneAndUpdate({
12231
+ const updatedDoc = await withRunFence(this.#connector, check, (session) => collection.findOneAndUpdate({
11933
12232
  workflow_name: workflowName,
11934
12233
  run_id: runId
11935
12234
  }, [{ $set: {
@@ -11944,10 +12243,12 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11944
12243
  createdAt: { $ifNull: ["$createdAt", now] }
11945
12244
  } }], {
11946
12245
  upsert: true,
11947
- returnDocument: "after"
11948
- });
12246
+ returnDocument: "after",
12247
+ session
12248
+ }));
11949
12249
  return (typeof updatedDoc?.snapshot === "string" ? JSON.parse(updatedDoc.snapshot) : updatedDoc?.snapshot)?.context || {};
11950
12250
  } catch (error) {
12251
+ if (isRunFenceConflictError(error)) throw error;
11951
12252
  throw new MastraError({
11952
12253
  id: createStorageErrorId("MONGODB", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
11953
12254
  domain: ErrorDomain.STORAGE,
@@ -11960,7 +12261,8 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11960
12261
  }, error);
11961
12262
  }
11962
12263
  }
11963
- async updateWorkflowState({ workflowName, runId, opts }) {
12264
+ async updateWorkflowState({ workflowName, runId, opts, fence }) {
12265
+ const check = this.#runFenceCheck(fence, runId, "updateWorkflowState");
11964
12266
  try {
11965
12267
  const collection = await this.getCollection(TABLE_WORKFLOW_SNAPSHOT);
11966
12268
  const { expectedStatus, ...state } = opts;
@@ -11969,10 +12271,13 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11969
12271
  run_id: runId
11970
12272
  };
11971
12273
  if (expectedStatus !== void 0) filter["snapshot.status"] = { $in: Array.isArray(expectedStatus) ? expectedStatus : [expectedStatus] };
11972
- const updatedDoc = await collection.findOneAndUpdate(filter, [{ $set: {
12274
+ const updatedDoc = await withRunFence(this.#connector, check, (session) => collection.findOneAndUpdate(filter, [{ $set: {
11973
12275
  snapshot: { $mergeObjects: ["$snapshot", state] },
11974
12276
  updatedAt: /* @__PURE__ */ new Date()
11975
- } }], { returnDocument: "after" });
12277
+ } }], {
12278
+ returnDocument: "after",
12279
+ session
12280
+ }));
11976
12281
  if (!updatedDoc) return;
11977
12282
  const snapshot = typeof updatedDoc.snapshot === "string" ? JSON.parse(updatedDoc.snapshot) : updatedDoc.snapshot;
11978
12283
  if (!snapshot?.context) throw new MastraError({
@@ -11987,7 +12292,7 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11987
12292
  });
11988
12293
  return snapshot;
11989
12294
  } catch (error) {
11990
- if (error instanceof MastraError) throw error;
12295
+ if (error instanceof MastraError || isRunFenceConflictError(error)) throw error;
11991
12296
  throw new MastraError({
11992
12297
  id: createStorageErrorId("MONGODB", "UPDATE_WORKFLOW_STATE", "FAILED"),
11993
12298
  domain: ErrorDomain.STORAGE,
@@ -11999,10 +12304,12 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11999
12304
  }, error);
12000
12305
  }
12001
12306
  }
12002
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
12307
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
12308
+ const check = this.#runFenceCheck(fence, runId, "persistWorkflowSnapshot");
12003
12309
  try {
12004
12310
  const now = /* @__PURE__ */ new Date();
12005
- await (await this.getCollection(TABLE_WORKFLOW_SNAPSHOT)).updateOne({
12311
+ const collection = await this.getCollection(TABLE_WORKFLOW_SNAPSHOT);
12312
+ await withRunFence(this.#connector, check, (session) => collection.updateOne({
12006
12313
  workflow_name: workflowName,
12007
12314
  run_id: runId
12008
12315
  }, {
@@ -12014,8 +12321,12 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
12014
12321
  updatedAt: updatedAt ?? now
12015
12322
  },
12016
12323
  $setOnInsert: { createdAt: createdAt ?? now }
12017
- }, { upsert: true });
12324
+ }, {
12325
+ upsert: true,
12326
+ session
12327
+ }));
12018
12328
  } catch (error) {
12329
+ if (isRunFenceConflictError(error)) throw error;
12019
12330
  throw new MastraError({
12020
12331
  id: createStorageErrorId("MONGODB", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
12021
12332
  domain: ErrorDomain.STORAGE,
@@ -12108,13 +12419,16 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
12108
12419
  }, error);
12109
12420
  }
12110
12421
  }
12111
- async deleteWorkflowRunById({ runId, workflowName }) {
12422
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
12423
+ const check = this.#runFenceCheck(fence, runId, "deleteWorkflowRunById");
12112
12424
  try {
12113
- await (await this.getCollection(TABLE_WORKFLOW_SNAPSHOT)).deleteOne({
12425
+ const collection = await this.getCollection(TABLE_WORKFLOW_SNAPSHOT);
12426
+ await withRunFence(this.#connector, check, (session) => collection.deleteOne({
12114
12427
  workflow_name: workflowName,
12115
12428
  run_id: runId
12116
- });
12429
+ }, { session }));
12117
12430
  } catch (error) {
12431
+ if (isRunFenceConflictError(error)) throw error;
12118
12432
  throw new MastraError({
12119
12433
  id: createStorageErrorId("MONGODB", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
12120
12434
  domain: ErrorDomain.STORAGE,