@mastra/mongodb 1.23.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,14 +1,15 @@
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";
7
+ import { createHash } from "crypto";
7
8
  import { MessageList } from "@mastra/core/agent";
8
9
  import { saveScorePayloadSchema } from "@mastra/core/evals";
9
10
  import { skillSnapshotFieldValuesEqual } from "@mastra/core/storage/domains/skills";
10
11
  //#region package.json
11
- var version = "1.23.0";
12
+ var version = "1.23.1-alpha.1";
12
13
  //#endregion
13
14
  //#region src/vector/filter.ts
14
15
  /**
@@ -1617,20 +1618,29 @@ var MongoDBConnector = class MongoDBConnector {
1617
1618
  return (await this.getConnection()).listCollections({ name: collectionName }).hasNext();
1618
1619
  }
1619
1620
  /**
1620
- * Returns true when the deployment supports multi-document transactions
1621
- * (replica set or sharded cluster). Standalone servers and custom connector
1622
- * 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.
1623
1625
  */
1624
- async supportsTransactions() {
1626
+ async probeTransactions() {
1625
1627
  if (this.#supportsTransactions !== void 0) return this.#supportsTransactions;
1626
1628
  if (!this.#client) {
1627
1629
  this.#supportsTransactions = false;
1628
1630
  return false;
1629
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() {
1630
1642
  try {
1631
- const hello = await (await this.getConnection()).admin().command({ hello: 1 });
1632
- this.#supportsTransactions = Boolean(hello.setName) || hello.msg === "isdbgrid";
1633
- return this.#supportsTransactions;
1643
+ return await this.probeTransactions();
1634
1644
  } catch {
1635
1645
  return false;
1636
1646
  }
@@ -2277,14 +2287,19 @@ function cutoffFor(policy, encoding, now = Date.now()) {
2277
2287
  * Delete up to `limit` documents whose anchor field is older than `cutoff`.
2278
2288
  *
2279
2289
  * `deleteMany` has no limit clause, so each batch first collects `_id`s with a
2280
- * bounded `find`, then deletes exactly that set — the Mongo equivalent of the
2281
- * 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.
2282
2294
  */
2283
2295
  async function pruneCollectionBatch({ connector, table, column, cutoff, limit }) {
2284
2296
  const collection = await connector.getCollection(table);
2285
2297
  const docs = await collection.find({ [column]: { $lt: cutoff } }).project({ _id: 1 }).limit(limit).toArray();
2286
2298
  if (docs.length === 0) return 0;
2287
- 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;
2288
2303
  }
2289
2304
  /**
2290
2305
  * Run the bounded/cancellable batched-delete loop for a single logical target,
@@ -6494,6 +6509,105 @@ var MongoDBMCPServersStorage = class MongoDBMCPServersStorage extends MCPServers
6494
6509
  }
6495
6510
  };
6496
6511
  //#endregion
6512
+ //#region ../../packages/_internal-core/dist/memory/index.js
6513
+ /**
6514
+ * A buffered chunk stores `lastObservedAt = max message time + 1ms`. It is wholly covered
6515
+ * (every message it observed is already at or before the cursor) iff
6516
+ * `cursor >= chunk.lastObservedAt - 1ms`.
6517
+ */
6518
+ function isBufferedChunkCoveredByCursor(chunkLastObservedAt, cursor) {
6519
+ if (!cursor) return false;
6520
+ return new Date(cursor).getTime() >= new Date(chunkLastObservedAt).getTime() - 1;
6521
+ }
6522
+ /** The later of two cursors; `undefined` only when both are absent. */
6523
+ function maxObservationCursor(a, b) {
6524
+ if (!a) return b ? new Date(b) : void 0;
6525
+ if (!b) return new Date(a);
6526
+ return new Date(Math.max(new Date(a).getTime(), new Date(b).getTime()));
6527
+ }
6528
+ /**
6529
+ * Decide the new generation's text when a reflection built from `snapshot` commits against
6530
+ * the stored record.
6531
+ *
6532
+ * - Stored text equals the snapshot: the reflection replaces it.
6533
+ * - Stored text extends the snapshot (activation only appends): the appended tail is kept
6534
+ * after the reflection, and its tokens are added.
6535
+ * - Anything else (a non-append rewrite): `null` — the reflection must not be applied.
6536
+ */
6537
+ function planReflectionGenerationText(input) {
6538
+ const stored = input.storedObservations ?? "";
6539
+ const snapshot = input.snapshotObservations ?? "";
6540
+ if (stored === snapshot) return {
6541
+ mode: "equal",
6542
+ observations: input.reflection,
6543
+ tokenCount: input.tokenCount
6544
+ };
6545
+ if (!stored.startsWith(snapshot)) return null;
6546
+ const tail = stored.slice(snapshot.length).trimStart();
6547
+ if (!tail.trim()) return {
6548
+ mode: "equal",
6549
+ observations: input.reflection,
6550
+ tokenCount: input.tokenCount
6551
+ };
6552
+ return {
6553
+ mode: "append",
6554
+ observations: input.reflection ? `${input.reflection}\n\n${tail}` : tail,
6555
+ tokenCount: input.tokenCount + Math.max(0, (input.storedObservationTokenCount ?? 0) - (input.snapshotObservationTokenCount ?? 0))
6556
+ };
6557
+ }
6558
+ /**
6559
+ * Backs `updateActiveObservations` in adapters that implement `commitActiveObservations`.
6560
+ * Callers of `updateActiveObservations` (which resolves `void`) can't see a commit result, so a
6561
+ * write that was not applied throws instead of resolving as if it had committed.
6562
+ */
6563
+ function assertActiveObservationsApplied(result, id) {
6564
+ if (result.applied) return;
6565
+ throw new Error(result.reason === "retired" ? `Observational memory record ${id} was superseded by a newer generation; observations were not written` : `Observational memory record ${id} changed since the observations were composed; observations were not written`);
6566
+ }
6567
+ /**
6568
+ * Whether the stored text can still accept a reflection built from `snapshot`
6569
+ * (equal, or only appended to since).
6570
+ */
6571
+ function isAppendOnlySince(storedObservations, snapshotObservations) {
6572
+ const stored = storedObservations ?? "";
6573
+ const snapshot = snapshotObservations ?? "";
6574
+ return stored === snapshot || stored.startsWith(snapshot);
6575
+ }
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
6497
6611
  //#region src/storage/domains/memory/index.ts
6498
6612
  /**
6499
6613
  * Local constant for the observational memory table name.
@@ -6501,6 +6615,95 @@ var MongoDBMCPServersStorage = class MongoDBMCPServersStorage extends MCPServers
6501
6615
  * versions that don't export TABLE_OBSERVATIONAL_MEMORY.
6502
6616
  */
6503
6617
  const OM_TABLE = "mastra_observational_memory";
6618
+ /** Canonical head order: newest generation, then earliest createdAt, then lowest id. */
6619
+ const OM_HEAD_SORT = {
6620
+ generationCount: -1,
6621
+ createdAt: 1,
6622
+ id: 1
6623
+ };
6624
+ /** Max redirects when a write aimed at a retired record follows the head. */
6625
+ const OM_MAX_HEAD_HOPS = 3;
6626
+ /** Max read-decide-conditional-update rounds before giving up on a contended record. */
6627
+ const OM_MAX_CONDITIONAL_ATTEMPTS = 3;
6628
+ /** Document `_id` of a key's generation-0 record; see `initializeObservationalMemory`. */
6629
+ function getGeneration0DocumentId(lookupKey) {
6630
+ return `om0_${createHash("sha256").update(lookupKey).digest("hex").slice(0, 32)}`;
6631
+ }
6632
+ /**
6633
+ * The successor document of a fenced (frozen) document: a pure function of the old document's
6634
+ * final state and the rollover inputs, so concurrent roll-forwards insert identical documents.
6635
+ * Buffered chunks, the cursor, buffering markers, flags, and counters carry over; buffered
6636
+ * reflection state does not.
6637
+ */
6638
+ function buildSuccessorDocument(old, pending) {
6639
+ let activeObservations = pending.reflection;
6640
+ let observationTokenCount = pending.tokenCount;
6641
+ if (pending.mode === "append") {
6642
+ const tail = String(old.activeObservations ?? "").slice(pending.snapTextLength).trimStart();
6643
+ activeObservations = pending.reflection ? `${pending.reflection}\n\n${tail}` : tail;
6644
+ observationTokenCount = pending.tokenCount + Math.max(0, Number(old.observationTokenCount || 0) - pending.snapObservationTokenCount);
6645
+ }
6646
+ return {
6647
+ id: pending.newId,
6648
+ lookupKey: old.lookupKey,
6649
+ scope: old.scope,
6650
+ resourceId: old.resourceId,
6651
+ threadId: old.threadId ?? null,
6652
+ activeObservations,
6653
+ activeObservationsPendingUpdate: null,
6654
+ originType: "reflection",
6655
+ config: old.config ?? null,
6656
+ generationCount: Number(old.generationCount || 0) + 1,
6657
+ lastObservedAt: old.lastObservedAt ?? null,
6658
+ lastReflectionAt: pending.createdAt,
6659
+ pendingMessageTokens: old.pendingMessageTokens ?? 0,
6660
+ totalTokensObserved: old.totalTokensObserved ?? 0,
6661
+ observationTokenCount,
6662
+ bufferedObservationChunks: Array.isArray(old.bufferedObservationChunks) ? old.bufferedObservationChunks : [],
6663
+ isObserving: false,
6664
+ isReflecting: false,
6665
+ isBufferingObservation: Boolean(old.isBufferingObservation),
6666
+ isBufferingReflection: false,
6667
+ lastBufferedAtTokens: old.lastBufferedAtTokens ?? 0,
6668
+ lastBufferedAtTime: old.lastBufferedAtTime ?? null,
6669
+ observedTimezone: old.observedTimezone ?? null,
6670
+ metadata: old.metadata ?? null,
6671
+ supersededBy: null,
6672
+ createdAt: pending.createdAt,
6673
+ updatedAt: pending.createdAt
6674
+ };
6675
+ }
6676
+ /** Equality filters for `fields` as stored in `doc` (`$exists: false` for absent fields). */
6677
+ function matchStoredFields(doc, fields) {
6678
+ return Object.fromEntries(fields.map((f) => [f, doc[f] === void 0 ? { $exists: false } : doc[f]]));
6679
+ }
6680
+ function omNotFound(operation, id) {
6681
+ return new MastraError({
6682
+ id: createStorageErrorId("MONGODB", operation, "NOT_FOUND"),
6683
+ text: `Observational memory record not found: ${id}`,
6684
+ domain: ErrorDomain.STORAGE,
6685
+ category: ErrorCategory.THIRD_PARTY,
6686
+ details: { id }
6687
+ });
6688
+ }
6689
+ function omConflict(operation, id) {
6690
+ return new MastraError({
6691
+ id: createStorageErrorId("MONGODB", operation, "CONFLICT"),
6692
+ text: `Observational memory record ${id} kept changing during ${operation}; giving up after ${OM_MAX_CONDITIONAL_ATTEMPTS} attempts`,
6693
+ domain: ErrorDomain.STORAGE,
6694
+ category: ErrorCategory.THIRD_PARTY,
6695
+ details: { id }
6696
+ });
6697
+ }
6698
+ function omNoLiveHead(id) {
6699
+ return new MastraError({
6700
+ id: createStorageErrorId("MONGODB", "RESOLVE_OBSERVATIONAL_MEMORY_HEAD", "FAILED"),
6701
+ text: `Observational memory record ${id} is superseded but no live head was found`,
6702
+ domain: ErrorDomain.STORAGE,
6703
+ category: ErrorCategory.THIRD_PARTY,
6704
+ details: { id }
6705
+ });
6706
+ }
6504
6707
  var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6505
6708
  supportsPartialThreadUpdate = true;
6506
6709
  supportsObservationalMemory = true;
@@ -6513,11 +6716,14 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6513
6716
  TABLE_THREADS,
6514
6717
  TABLE_MESSAGES,
6515
6718
  TABLE_RESOURCES,
6516
- OM_TABLE
6719
+ OM_TABLE,
6720
+ TABLE_MEMORY_RUN_FENCES
6517
6721
  ];
6518
6722
  /**
6519
6723
  * Retention-eligible collections. The observational-memory collection is
6520
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.
6521
6727
  */
6522
6728
  static retentionTables = {
6523
6729
  messages: {
@@ -6534,6 +6740,11 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6534
6740
  table: TABLE_THREADS,
6535
6741
  column: "createdAt",
6536
6742
  indexed: true
6743
+ },
6744
+ runFences: {
6745
+ table: TABLE_MEMORY_RUN_FENCES,
6746
+ column: "retiredAt",
6747
+ indexed: true
6537
6748
  }
6538
6749
  };
6539
6750
  constructor(config) {
@@ -6548,13 +6759,80 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6548
6759
  async init() {
6549
6760
  await this.createDefaultIndexes();
6550
6761
  await this.createCustomIndexes();
6762
+ await this.#warnIfOMIdIndexMissing();
6763
+ await this.#maintainSupersededBy();
6764
+ }
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
+ };
6551
6828
  }
6552
6829
  /**
6553
6830
  * Delete memory rows older than each table's `maxAge`, batched. Order is
6554
6831
  * messages → resources → threads so child rows never outlive the delete of
6555
- * their thread. Like `deleteThread()`, this does not sweep vector-store
6556
- * embeddings — semantic-recall vectors live in a separate vector store the
6557
- * 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.
6558
6836
  */
6559
6837
  async prune(policies, options) {
6560
6838
  const targets = resolveTargets({
@@ -6563,7 +6841,8 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6563
6841
  order: [
6564
6842
  "messages",
6565
6843
  "resources",
6566
- "threads"
6844
+ "threads",
6845
+ "runFences"
6567
6846
  ]
6568
6847
  });
6569
6848
  return runPrune({
@@ -6669,17 +6948,19 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
6669
6948
  }
6670
6949
  }
6671
6950
  async dangerouslyClearAll() {
6672
- const [threadsCollection, messagesCollection, resourcesCollection, omCollection] = await Promise.all([
6951
+ const [threadsCollection, messagesCollection, resourcesCollection, omCollection, fencesCollection] = await Promise.all([
6673
6952
  this.getCollection(TABLE_THREADS),
6674
6953
  this.getCollection(TABLE_MESSAGES),
6675
6954
  this.getCollection(TABLE_RESOURCES),
6676
- this.getCollection(OM_TABLE)
6955
+ this.getCollection(OM_TABLE),
6956
+ this.getCollection(TABLE_MEMORY_RUN_FENCES)
6677
6957
  ]);
6678
6958
  await Promise.all([
6679
6959
  threadsCollection.deleteMany({}),
6680
6960
  messagesCollection.deleteMany({}),
6681
6961
  resourcesCollection.deleteMany({}),
6682
- omCollection.deleteMany({})
6962
+ omCollection.deleteMany({}),
6963
+ fencesCollection.deleteMany({})
6683
6964
  ]);
6684
6965
  }
6685
6966
  parseRow(row) {
@@ -7035,7 +7316,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7035
7316
  throw mastraError;
7036
7317
  }
7037
7318
  }
7038
- async saveMessages({ messages }) {
7319
+ async saveMessages({ messages, fence }) {
7039
7320
  if (messages.length === 0) return { messages: [] };
7040
7321
  try {
7041
7322
  if (!messages[0]?.threadId) throw new Error("Thread ID is required");
@@ -7063,12 +7344,16 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7063
7344
  });
7064
7345
  const allThreadIds = new Set(messages.map((m) => m.threadId));
7065
7346
  const now = /* @__PURE__ */ new Date();
7066
- await this.#connector.withTransaction(async (session) => {
7347
+ const writeMessages = async (session) => {
7067
7348
  await collection.bulkWrite(messagesToInsert, { session });
7068
7349
  for (const tid of allThreadIds) await threadsCollection.updateOne({ id: tid }, { $set: { updatedAt: now } }, { session });
7069
- });
7350
+ };
7351
+ const check = this.#runFenceCheck(fence, "saveMessages");
7352
+ if (check) await withRunFence(this.#connector, check, writeMessages);
7353
+ else await this.#connector.withTransaction(writeMessages);
7070
7354
  return { messages: new MessageList().add(messages, "memory").get.all.db() };
7071
7355
  } catch (error) {
7356
+ if (isRunFenceConflictError(error)) throw error;
7072
7357
  throw new MastraError({
7073
7358
  id: createStorageErrorId("MONGODB", "SAVE_MESSAGES", "FAILED"),
7074
7359
  domain: ErrorDomain.STORAGE,
@@ -7076,49 +7361,51 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7076
7361
  }, error);
7077
7362
  }
7078
7363
  }
7079
- async updateMessages({ messages }) {
7364
+ async updateMessages({ messages, fence }) {
7080
7365
  if (messages.length === 0) return [];
7081
7366
  const messageIds = messages.map((m) => m.id);
7082
7367
  const collection = await this.getCollection(TABLE_MESSAGES);
7083
- const existingMessagesParsed = (await collection.find({ id: { $in: messageIds } }).toArray()).map((msg) => this.parseRow(msg));
7084
- if (existingMessagesParsed.length === 0) return [];
7085
- const threadIdsToUpdate = /* @__PURE__ */ new Set();
7086
- const bulkOps = [];
7087
- for (const existingMessage of existingMessagesParsed) {
7088
- const updatePayload = messages.find((m) => m.id === existingMessage.id);
7089
- if (!updatePayload) continue;
7090
- const { id, ...fieldsToUpdate } = updatePayload;
7091
- if (Object.keys(fieldsToUpdate).length === 0) continue;
7092
- threadIdsToUpdate.add(existingMessage.threadId);
7093
- if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) threadIdsToUpdate.add(updatePayload.threadId);
7094
- const updateDoc = {};
7095
- const updatableFields = { ...fieldsToUpdate };
7096
- if (updatableFields.content) {
7097
- const newContent = {
7098
- ...existingMessage.content,
7099
- ...updatableFields.content,
7100
- ...existingMessage.content?.metadata && updatableFields.content.metadata ? { metadata: {
7101
- ...existingMessage.content.metadata,
7102
- ...updatableFields.content.metadata
7103
- } } : {}
7104
- };
7105
- updateDoc.content = JSON.stringify(newContent);
7106
- delete updatableFields.content;
7107
- }
7108
- for (const key in updatableFields) if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
7109
- const dbKey = key === "threadId" ? "thread_id" : key;
7110
- let value = updatableFields[key];
7111
- if (typeof value === "object" && value !== null) value = JSON.stringify(value);
7112
- 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
+ } });
7113
7404
  }
7114
- if (Object.keys(updateDoc).length > 0) bulkOps.push({ updateOne: {
7115
- filter: { id },
7116
- update: { $set: updateDoc }
7117
- } });
7118
- }
7119
- if (bulkOps.length > 0) await collection.bulkWrite(bulkOps);
7120
- if (threadIdsToUpdate.size > 0) await (await this.getCollection(TABLE_THREADS)).updateMany({ id: { $in: Array.from(threadIdsToUpdate) } }, { $set: { updatedAt: /* @__PURE__ */ new Date() } });
7121
- 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
+ });
7122
7409
  }
7123
7410
  async getResourceById({ resourceId }) {
7124
7411
  try {
@@ -7153,9 +7440,11 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7153
7440
  }, error);
7154
7441
  }
7155
7442
  }
7156
- async updateResource({ resourceId, workingMemory, metadata }) {
7443
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
7157
7444
  try {
7445
+ const check = this.#runFenceCheck(fence, "updateResource");
7158
7446
  const existingResource = await this.getResourceById({ resourceId });
7447
+ const collection = await this.getCollection(TABLE_RESOURCES);
7159
7448
  if (!existingResource) {
7160
7449
  const newResource = {
7161
7450
  id: resourceId,
@@ -7164,7 +7453,11 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7164
7453
  createdAt: /* @__PURE__ */ new Date(),
7165
7454
  updatedAt: /* @__PURE__ */ new Date()
7166
7455
  };
7167
- 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;
7168
7461
  }
7169
7462
  const updatedResource = {
7170
7463
  ...existingResource,
@@ -7175,13 +7468,13 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7175
7468
  } : existingResource.metadata,
7176
7469
  updatedAt: /* @__PURE__ */ new Date()
7177
7470
  };
7178
- const collection = await this.getCollection(TABLE_RESOURCES);
7179
7471
  const updateDoc = { updatedAt: updatedResource.updatedAt };
7180
7472
  if (workingMemory !== void 0) updateDoc.workingMemory = workingMemory;
7181
7473
  if (metadata) updateDoc.metadata = updatedResource.metadata;
7182
- await collection.updateOne({ id: resourceId }, { $set: updateDoc });
7474
+ await withRunFence(this.#connector, check, (session) => collection.updateOne({ id: resourceId }, { $set: updateDoc }, { session }));
7183
7475
  return updatedResource;
7184
7476
  } catch (error) {
7477
+ if (isRunFenceConflictError(error)) throw error;
7185
7478
  throw new MastraError({
7186
7479
  id: createStorageErrorId("MONGODB", "UPDATE_RESOURCE", "FAILED"),
7187
7480
  domain: ErrorDomain.STORAGE,
@@ -7281,14 +7574,19 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7281
7574
  throw mastraError;
7282
7575
  }
7283
7576
  }
7284
- async saveThread({ thread }) {
7577
+ async saveThread({ thread, fence }) {
7285
7578
  try {
7286
- 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: {
7287
7581
  ...thread,
7288
7582
  metadata: thread.metadata
7289
- } }, { upsert: true });
7583
+ } }, {
7584
+ upsert: true,
7585
+ session
7586
+ }));
7290
7587
  return thread;
7291
7588
  } catch (error) {
7589
+ if (isRunFenceConflictError(error)) throw error;
7292
7590
  throw new MastraError({
7293
7591
  id: createStorageErrorId("MONGODB", "SAVE_THREAD", "FAILED"),
7294
7592
  domain: ErrorDomain.STORAGE,
@@ -7297,7 +7595,8 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7297
7595
  }, error);
7298
7596
  }
7299
7597
  }
7300
- async updateThread({ id, title, metadata }) {
7598
+ async updateThread({ id, title, metadata, fence }) {
7599
+ const check = this.#runFenceCheck(fence, "updateThread");
7301
7600
  const thread = await this.getThreadById({ threadId: id });
7302
7601
  if (!thread) throw new MastraError({
7303
7602
  id: createStorageErrorId("MONGODB", "UPDATE_THREAD", "NOT_FOUND"),
@@ -7320,12 +7619,14 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7320
7619
  updatedAt: now
7321
7620
  };
7322
7621
  try {
7323
- 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: {
7324
7624
  title: updatedThread.title,
7325
7625
  metadata: updatedThread.metadata,
7326
7626
  updatedAt: now
7327
- } });
7627
+ } }, { session }));
7328
7628
  } catch (error) {
7629
+ if (isRunFenceConflictError(error)) throw error;
7329
7630
  throw new MastraError({
7330
7631
  id: createStorageErrorId("MONGODB", "UPDATE_THREAD", "FAILED"),
7331
7632
  domain: ErrorDomain.STORAGE,
@@ -7348,16 +7649,19 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7348
7649
  }, error);
7349
7650
  }
7350
7651
  }
7351
- async deleteMessages(messageIds) {
7652
+ async deleteMessages(messageIds, options) {
7352
7653
  if (messageIds.length === 0) return;
7353
7654
  try {
7354
7655
  const messagesCollection = await this.getCollection(TABLE_MESSAGES);
7355
7656
  const threadsCollection = await this.getCollection(TABLE_THREADS);
7356
- const messagesToDelete = await messagesCollection.find({ id: { $in: messageIds } }).toArray();
7357
- const threadIds = [...new Set(messagesToDelete.map((m) => m.thread_id))];
7358
- await messagesCollection.deleteMany({ id: { $in: messageIds } });
7359
- 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
+ });
7360
7663
  } catch (error) {
7664
+ if (isRunFenceConflictError(error)) throw error;
7361
7665
  throw new MastraError({
7362
7666
  id: createStorageErrorId("MONGODB", "DELETE_MESSAGES", "FAILED"),
7363
7667
  domain: ErrorDomain.STORAGE,
@@ -7525,15 +7829,15 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7525
7829
  config: doc.config || {},
7526
7830
  metadata: doc.metadata || void 0,
7527
7831
  observedMessageIds: doc.observedMessageIds || void 0,
7528
- observedTimezone: doc.observedTimezone || void 0
7832
+ observedTimezone: doc.observedTimezone || void 0,
7833
+ supersededBy: doc.supersededBy ?? null
7529
7834
  };
7530
7835
  }
7531
7836
  async getObservationalMemory(threadId, resourceId) {
7532
7837
  try {
7533
- const lookupKey = this.getOMKey(threadId, resourceId);
7534
- const doc = await (await this.getCollection(OM_TABLE)).findOne({ lookupKey }, { sort: { generationCount: -1 } });
7535
- if (!doc) return null;
7536
- return this.parseOMDocument(doc);
7838
+ const collection = await this.getCollection(OM_TABLE);
7839
+ const doc = await this.#getHeadDoc(collection, this.getOMKey(threadId, resourceId));
7840
+ return doc ? this.parseOMDocument(doc) : null;
7537
7841
  } catch (error) {
7538
7842
  throw new MastraError({
7539
7843
  id: createStorageErrorId("MONGODB", "GET_OBSERVATIONAL_MEMORY", "FAILED"),
@@ -7596,60 +7900,55 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7596
7900
  }
7597
7901
  async initializeObservationalMemory(input) {
7598
7902
  try {
7903
+ const lookupKey = this.getOMKey(input.threadId, input.resourceId);
7904
+ const collection = await this.getCollection(OM_TABLE);
7905
+ const existing = await this.#getHeadDoc(collection, lookupKey);
7906
+ if (existing) return this.parseOMDocument(existing);
7599
7907
  const id = globalThis.crypto.randomUUID();
7600
7908
  const now = /* @__PURE__ */ new Date();
7601
- const lookupKey = this.getOMKey(input.threadId, input.resourceId);
7602
- const record = {
7603
- id,
7604
- scope: input.scope,
7605
- threadId: input.threadId,
7606
- resourceId: input.resourceId,
7607
- createdAt: now,
7608
- updatedAt: now,
7609
- lastObservedAt: void 0,
7610
- originType: "initial",
7611
- generationCount: 0,
7612
- activeObservations: "",
7613
- totalTokensObserved: 0,
7614
- observationTokenCount: 0,
7615
- pendingMessageTokens: 0,
7616
- isReflecting: false,
7617
- isObserving: false,
7618
- isBufferingObservation: false,
7619
- isBufferingReflection: false,
7620
- lastBufferedAtTokens: 0,
7621
- lastBufferedAtTime: null,
7622
- config: input.config,
7623
- observedTimezone: input.observedTimezone
7624
- };
7625
- await (await this.getCollection(OM_TABLE)).insertOne({
7626
- id,
7627
- lookupKey,
7628
- scope: input.scope,
7629
- resourceId: input.resourceId,
7630
- threadId: input.threadId || null,
7631
- activeObservations: "",
7632
- activeObservationsPendingUpdate: null,
7633
- originType: "initial",
7634
- config: input.config,
7635
- generationCount: 0,
7636
- lastObservedAt: null,
7637
- lastReflectionAt: null,
7638
- pendingMessageTokens: 0,
7639
- totalTokensObserved: 0,
7640
- observationTokenCount: 0,
7641
- isObserving: false,
7642
- isReflecting: false,
7643
- isBufferingObservation: false,
7644
- isBufferingReflection: false,
7645
- lastBufferedAtTokens: 0,
7646
- lastBufferedAtTime: null,
7647
- observedTimezone: input.observedTimezone || null,
7648
- createdAt: now,
7649
- updatedAt: now
7909
+ try {
7910
+ await collection.insertOne({
7911
+ _id: getGeneration0DocumentId(lookupKey),
7912
+ id,
7913
+ lookupKey,
7914
+ scope: input.scope,
7915
+ resourceId: input.resourceId,
7916
+ threadId: input.threadId || null,
7917
+ activeObservations: "",
7918
+ activeObservationsPendingUpdate: null,
7919
+ originType: "initial",
7920
+ config: input.config,
7921
+ generationCount: 0,
7922
+ lastObservedAt: null,
7923
+ lastReflectionAt: null,
7924
+ pendingMessageTokens: 0,
7925
+ totalTokensObserved: 0,
7926
+ observationTokenCount: 0,
7927
+ isObserving: false,
7928
+ isReflecting: false,
7929
+ isBufferingObservation: false,
7930
+ isBufferingReflection: false,
7931
+ lastBufferedAtTokens: 0,
7932
+ lastBufferedAtTime: null,
7933
+ observedTimezone: input.observedTimezone || null,
7934
+ supersededBy: null,
7935
+ createdAt: now,
7936
+ updatedAt: now
7937
+ });
7938
+ } catch (error) {
7939
+ if (!isDuplicateKeyError(error)) throw error;
7940
+ }
7941
+ const head = await this.#getHeadDoc(collection, lookupKey);
7942
+ if (!head) throw new MastraError({
7943
+ id: createStorageErrorId("MONGODB", "INITIALIZE_OBSERVATIONAL_MEMORY", "NOT_FOUND"),
7944
+ text: `Observational memory record not found after initialization: ${id}`,
7945
+ domain: ErrorDomain.STORAGE,
7946
+ category: ErrorCategory.THIRD_PARTY,
7947
+ details: { id }
7650
7948
  });
7651
- return record;
7949
+ return this.parseOMDocument(head);
7652
7950
  } catch (error) {
7951
+ if (error instanceof MastraError) throw error;
7653
7952
  throw new MastraError({
7654
7953
  id: createStorageErrorId("MONGODB", "INITIALIZE_OBSERVATIONAL_MEMORY", "FAILED"),
7655
7954
  domain: ErrorDomain.STORAGE,
@@ -7694,6 +7993,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7694
7993
  lastBufferedAtTime: record.lastBufferedAtTime || null,
7695
7994
  observedTimezone: record.observedTimezone || null,
7696
7995
  metadata: record.metadata || null,
7996
+ supersededBy: record.supersededBy ?? null,
7697
7997
  createdAt: record.createdAt,
7698
7998
  updatedAt: record.updatedAt
7699
7999
  });
@@ -7711,28 +8011,40 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7711
8011
  }
7712
8012
  }
7713
8013
  async updateActiveObservations(input) {
8014
+ assertActiveObservationsApplied(await this.commitActiveObservations(input), input.id);
8015
+ }
8016
+ async commitActiveObservations(input) {
7714
8017
  try {
7715
- const now = /* @__PURE__ */ new Date();
7716
8018
  const collection = await this.getCollection(OM_TABLE);
7717
8019
  const safeTokenCount = Number.isFinite(input.tokenCount) && input.tokenCount >= 0 ? input.tokenCount : 0;
7718
- const updateDoc = {
7719
- activeObservations: input.observations,
7720
- lastObservedAt: input.lastObservedAt,
7721
- pendingMessageTokens: 0,
7722
- observationTokenCount: safeTokenCount,
7723
- observedMessageIds: input.observedMessageIds ?? null,
7724
- updatedAt: now
7725
- };
7726
- if ((await collection.updateOne({ id: input.id }, {
7727
- $set: updateDoc,
7728
- $inc: { totalTokensObserved: safeTokenCount }
7729
- })).matchedCount === 0) throw new MastraError({
7730
- id: createStorageErrorId("MONGODB", "UPDATE_ACTIVE_OBSERVATIONS", "NOT_FOUND"),
7731
- text: `Observational memory record not found: ${input.id}`,
7732
- domain: ErrorDomain.STORAGE,
7733
- category: ErrorCategory.THIRD_PARTY,
7734
- details: { id: input.id }
7735
- });
8020
+ for (let attempt = 1; attempt <= OM_MAX_CONDITIONAL_ATTEMPTS; attempt++) {
8021
+ const doc = await collection.findOne({ id: input.id });
8022
+ if (!doc) throw omNotFound("UPDATE_ACTIVE_OBSERVATIONS", input.id);
8023
+ if (doc.supersededBy) return {
8024
+ applied: false,
8025
+ reason: "retired"
8026
+ };
8027
+ if (input.expectedActiveObservations !== void 0 && input.expectedActiveObservations !== (doc.activeObservations || "")) return {
8028
+ applied: false,
8029
+ reason: "conflict"
8030
+ };
8031
+ if ((await withRunFence(this.#connector, this.#runFenceCheck(void 0, "updateActiveObservations"), (session) => collection.updateOne({
8032
+ id: input.id,
8033
+ supersededBy: null,
8034
+ ...matchStoredFields(doc, ["activeObservations", "lastObservedAt"])
8035
+ }, {
8036
+ $set: {
8037
+ activeObservations: input.observations,
8038
+ lastObservedAt: maxObservationCursor(doc.lastObservedAt, input.lastObservedAt),
8039
+ pendingMessageTokens: 0,
8040
+ observationTokenCount: safeTokenCount,
8041
+ observedMessageIds: input.observedMessageIds ?? null,
8042
+ updatedAt: /* @__PURE__ */ new Date()
8043
+ },
8044
+ $inc: { totalTokensObserved: safeTokenCount }
8045
+ }, { session }))).matchedCount === 1) return { applied: true };
8046
+ }
8047
+ throw omConflict("UPDATE_ACTIVE_OBSERVATIONS", input.id);
7736
8048
  } catch (error) {
7737
8049
  if (error instanceof MastraError) throw error;
7738
8050
  throw new MastraError({
@@ -7745,62 +8057,31 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7745
8057
  }
7746
8058
  async createReflectionGeneration(input) {
7747
8059
  try {
7748
- const id = globalThis.crypto.randomUUID();
7749
- const now = /* @__PURE__ */ new Date();
7750
- const lookupKey = this.getOMKey(input.currentRecord.threadId, input.currentRecord.resourceId);
7751
- const record = {
7752
- id,
7753
- scope: input.currentRecord.scope,
7754
- threadId: input.currentRecord.threadId,
7755
- resourceId: input.currentRecord.resourceId,
7756
- createdAt: now,
7757
- updatedAt: now,
7758
- lastObservedAt: input.currentRecord.lastObservedAt,
7759
- originType: "reflection",
7760
- generationCount: input.currentRecord.generationCount + 1,
7761
- activeObservations: input.reflection,
7762
- totalTokensObserved: input.currentRecord.totalTokensObserved,
7763
- observationTokenCount: input.tokenCount,
7764
- pendingMessageTokens: 0,
7765
- isReflecting: false,
7766
- isObserving: false,
7767
- isBufferingObservation: false,
7768
- isBufferingReflection: false,
7769
- lastBufferedAtTokens: 0,
7770
- lastBufferedAtTime: null,
7771
- config: input.currentRecord.config,
7772
- metadata: input.currentRecord.metadata,
7773
- observedTimezone: input.currentRecord.observedTimezone
7774
- };
7775
- await (await this.getCollection(OM_TABLE)).insertOne({
7776
- id,
7777
- lookupKey,
7778
- scope: record.scope,
7779
- resourceId: record.resourceId,
7780
- threadId: record.threadId || null,
7781
- activeObservations: input.reflection,
7782
- activeObservationsPendingUpdate: null,
7783
- originType: "reflection",
7784
- config: record.config,
7785
- generationCount: input.currentRecord.generationCount + 1,
7786
- lastObservedAt: record.lastObservedAt || null,
7787
- lastReflectionAt: now,
7788
- pendingMessageTokens: record.pendingMessageTokens,
7789
- totalTokensObserved: record.totalTokensObserved,
7790
- observationTokenCount: record.observationTokenCount,
7791
- isObserving: false,
7792
- isReflecting: false,
7793
- isBufferingObservation: false,
7794
- isBufferingReflection: false,
7795
- lastBufferedAtTokens: 0,
7796
- lastBufferedAtTime: null,
7797
- observedTimezone: record.observedTimezone || null,
7798
- createdAt: now,
7799
- updatedAt: now,
7800
- metadata: record.metadata || null
8060
+ const { currentRecord } = input;
8061
+ return await this.#rollOver(currentRecord, "createReflectionGeneration", (doc) => {
8062
+ const stored = this.parseOMDocument(doc);
8063
+ const plan = planReflectionGenerationText({
8064
+ storedObservations: stored.activeObservations,
8065
+ storedObservationTokenCount: stored.observationTokenCount,
8066
+ snapshotObservations: currentRecord.activeObservations,
8067
+ snapshotObservationTokenCount: currentRecord.observationTokenCount,
8068
+ reflection: input.reflection,
8069
+ tokenCount: input.tokenCount
8070
+ });
8071
+ if (!plan) return null;
8072
+ return {
8073
+ newId: input.newRecordId ?? globalThis.crypto.randomUUID(),
8074
+ mode: plan.mode,
8075
+ reflection: input.reflection,
8076
+ tokenCount: input.tokenCount,
8077
+ snapTextLength: (currentRecord.activeObservations ?? "").length,
8078
+ snapObservationTokenCount: currentRecord.observationTokenCount ?? 0,
8079
+ clearBufferedReflection: false,
8080
+ createdAt: /* @__PURE__ */ new Date()
8081
+ };
7801
8082
  });
7802
- return record;
7803
8083
  } catch (error) {
8084
+ if (error instanceof MastraError) throw error;
7804
8085
  throw new MastraError({
7805
8086
  id: createStorageErrorId("MONGODB", "CREATE_REFLECTION_GENERATION", "FAILED"),
7806
8087
  domain: ErrorDomain.STORAGE,
@@ -7867,23 +8148,12 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7867
8148
  }
7868
8149
  async setBufferingObservationFlag(id, isBuffering, lastBufferedAtTokens) {
7869
8150
  try {
7870
- const collection = await this.getCollection(OM_TABLE);
7871
8151
  const updateDoc = {
7872
8152
  isBufferingObservation: isBuffering,
7873
8153
  updatedAt: /* @__PURE__ */ new Date()
7874
8154
  };
7875
8155
  if (lastBufferedAtTokens !== void 0) updateDoc.lastBufferedAtTokens = lastBufferedAtTokens;
7876
- if ((await collection.updateOne({ id }, { $set: updateDoc })).matchedCount === 0) throw new MastraError({
7877
- id: createStorageErrorId("MONGODB", "SET_BUFFERING_OBSERVATION_FLAG", "NOT_FOUND"),
7878
- text: `Observational memory record not found: ${id}`,
7879
- domain: ErrorDomain.STORAGE,
7880
- category: ErrorCategory.THIRD_PARTY,
7881
- details: {
7882
- id,
7883
- isBuffering,
7884
- lastBufferedAtTokens: lastBufferedAtTokens ?? null
7885
- }
7886
- });
8156
+ await this.#updateLiveDoc(id, "SET_BUFFERING_OBSERVATION_FLAG", { $set: updateDoc });
7887
8157
  } catch (error) {
7888
8158
  if (error instanceof MastraError) throw error;
7889
8159
  throw new MastraError({
@@ -7954,19 +8224,10 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
7954
8224
  }
7955
8225
  });
7956
8226
  try {
7957
- if ((await (await this.getCollection(OM_TABLE)).updateOne({ id }, { $set: {
8227
+ await this.#updateLiveDoc(id, "SET_PENDING_MESSAGE_TOKENS", { $set: {
7958
8228
  pendingMessageTokens: tokenCount,
7959
8229
  updatedAt: /* @__PURE__ */ new Date()
7960
- } })).matchedCount === 0) throw new MastraError({
7961
- id: createStorageErrorId("MONGODB", "SET_PENDING_MESSAGE_TOKENS", "NOT_FOUND"),
7962
- text: `Observational memory record not found: ${id}`,
7963
- domain: ErrorDomain.STORAGE,
7964
- category: ErrorCategory.THIRD_PARTY,
7965
- details: {
7966
- id,
7967
- tokenCount
7968
- }
7969
- });
8230
+ } });
7970
8231
  } catch (error) {
7971
8232
  if (error instanceof MastraError) throw error;
7972
8233
  throw new MastraError({
@@ -8008,6 +8269,9 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8008
8269
  }
8009
8270
  }
8010
8271
  async updateBufferedObservations(input) {
8272
+ await this.appendBufferedObservations(input);
8273
+ }
8274
+ async appendBufferedObservations(input) {
8011
8275
  try {
8012
8276
  const collection = await this.getCollection(OM_TABLE);
8013
8277
  const newChunk = {
@@ -8025,18 +8289,65 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8025
8289
  extractedValues: input.chunk.extractedValues,
8026
8290
  extractionFailures: input.chunk.extractionFailures
8027
8291
  };
8028
- const setStage = {
8029
- updatedAt: /* @__PURE__ */ new Date(),
8030
- bufferedObservationChunks: { $concatArrays: [{ $ifNull: ["$bufferedObservationChunks", []] }, [newChunk]] }
8031
- };
8032
- if (input.lastBufferedAtTime) setStage.lastBufferedAtTime = input.lastBufferedAtTime;
8033
- if ((await collection.updateOne({ id: input.id }, [{ $set: setStage }])).matchedCount === 0) throw new MastraError({
8034
- id: createStorageErrorId("MONGODB", "UPDATE_BUFFERED_OBSERVATIONS", "NOT_FOUND"),
8035
- text: `Observational memory record not found: ${input.id}`,
8036
- domain: ErrorDomain.STORAGE,
8037
- category: ErrorCategory.THIRD_PARTY,
8038
- details: { id: input.id }
8039
- });
8292
+ const notCovered = { $or: [{ lastObservedAt: null }, { lastObservedAt: { $lt: /* @__PURE__ */ new Date(new Date(input.chunk.lastObservedAt).getTime() - 1) } }] };
8293
+ const lastBufferedAtTime = input.lastBufferedAtTime ? new Date(input.lastBufferedAtTime) : void 0;
8294
+ let targetId = input.id;
8295
+ let hops = 0;
8296
+ let retries = 0;
8297
+ for (;;) {
8298
+ if ((await withRunFence(this.#connector, this.#runFenceCheck(void 0, "updateBufferedObservations"), (session) => collection.updateOne({
8299
+ id: targetId,
8300
+ supersededBy: null,
8301
+ "bufferedObservationChunks.cycleId": { $ne: input.chunk.cycleId },
8302
+ ...notCovered,
8303
+ $and: [{ $or: [
8304
+ { bufferedObservationChunks: { $exists: false } },
8305
+ { bufferedObservationChunks: null },
8306
+ { bufferedObservationChunks: { $type: "array" } }
8307
+ ] }]
8308
+ }, [{ $set: {
8309
+ bufferedObservationChunks: { $concatArrays: [{ $ifNull: ["$bufferedObservationChunks", []] }, { $literal: [newChunk] }] },
8310
+ ...lastBufferedAtTime ? { lastBufferedAtTime: { $max: ["$lastBufferedAtTime", { $literal: lastBufferedAtTime }] } } : {},
8311
+ updatedAt: { $literal: /* @__PURE__ */ new Date() }
8312
+ } }], { session }))).matchedCount === 1) return {
8313
+ persisted: true,
8314
+ recordId: targetId
8315
+ };
8316
+ const doc = await collection.findOne({ id: targetId });
8317
+ if (!doc) throw omNotFound("UPDATE_BUFFERED_OBSERVATIONS", targetId);
8318
+ if (doc.supersededBy) {
8319
+ const head = hops < OM_MAX_HEAD_HOPS ? await this.#getHeadDoc(collection, doc.lookupKey) : null;
8320
+ if (!head || head.supersededBy || head.id === targetId) throw omNoLiveHead(input.id);
8321
+ targetId = head.id;
8322
+ hops++;
8323
+ continue;
8324
+ }
8325
+ const stored = doc.bufferedObservationChunks;
8326
+ if (Array.isArray(stored) && stored.some((c) => c?.cycleId === input.chunk.cycleId)) return {
8327
+ persisted: false,
8328
+ recordId: targetId
8329
+ };
8330
+ if (isBufferedChunkCoveredByCursor(input.chunk.lastObservedAt, doc.lastObservedAt)) return {
8331
+ persisted: false,
8332
+ recordId: targetId
8333
+ };
8334
+ if (stored !== void 0 && stored !== null && !Array.isArray(stored)) {
8335
+ if ((await withRunFence(this.#connector, this.#runFenceCheck(void 0, "updateBufferedObservations"), (session) => collection.updateOne({
8336
+ id: targetId,
8337
+ supersededBy: null,
8338
+ bufferedObservationChunks: stored,
8339
+ ...notCovered
8340
+ }, { $set: {
8341
+ bufferedObservationChunks: [newChunk],
8342
+ ...lastBufferedAtTime ? { lastBufferedAtTime: maxObservationCursor(doc.lastBufferedAtTime, lastBufferedAtTime) } : {},
8343
+ updatedAt: /* @__PURE__ */ new Date()
8344
+ } }, { session }))).matchedCount === 1) return {
8345
+ persisted: true,
8346
+ recordId: targetId
8347
+ };
8348
+ }
8349
+ if (++retries >= OM_MAX_CONDITIONAL_ATTEMPTS) throw omConflict("UPDATE_BUFFERED_OBSERVATIONS", targetId);
8350
+ }
8040
8351
  } catch (error) {
8041
8352
  if (error instanceof MastraError) throw error;
8042
8353
  throw new MastraError({
@@ -8050,16 +8361,7 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8050
8361
  async swapBufferedToActive(input) {
8051
8362
  try {
8052
8363
  const collection = await this.getCollection(OM_TABLE);
8053
- const doc = await collection.findOne({ id: input.id });
8054
- if (!doc) throw new MastraError({
8055
- id: createStorageErrorId("MONGODB", "SWAP_BUFFERED_TO_ACTIVE", "NOT_FOUND"),
8056
- text: `Observational memory record not found: ${input.id}`,
8057
- domain: ErrorDomain.STORAGE,
8058
- category: ErrorCategory.THIRD_PARTY,
8059
- details: { id: input.id }
8060
- });
8061
- const chunks = Array.isArray(doc.bufferedObservationChunks) ? doc.bufferedObservationChunks : [];
8062
- if (chunks.length === 0) return {
8364
+ const emptyResult = {
8063
8365
  chunksActivated: 0,
8064
8366
  messageTokensActivated: 0,
8065
8367
  observationTokensActivated: 0,
@@ -8067,95 +8369,107 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8067
8369
  activatedCycleIds: [],
8068
8370
  activatedMessageIds: []
8069
8371
  };
8070
- const retentionFloor = input.messageTokensThreshold * (1 - input.activationRatio);
8071
- const targetMessageTokens = Math.max(0, input.currentPendingTokens - retentionFloor);
8072
- let cumulativeMessageTokens = 0;
8073
- let bestOverBoundary = 0;
8074
- let bestOverTokens = 0;
8075
- let bestUnderBoundary = 0;
8076
- let bestUnderTokens = 0;
8077
- for (let i = 0; i < chunks.length; i++) {
8078
- cumulativeMessageTokens += chunks[i].messageTokens ?? 0;
8079
- const boundary = i + 1;
8080
- if (cumulativeMessageTokens >= targetMessageTokens) {
8081
- if (bestOverBoundary === 0 || cumulativeMessageTokens < bestOverTokens) {
8082
- bestOverBoundary = boundary;
8083
- bestOverTokens = cumulativeMessageTokens;
8372
+ for (let attempt = 1; attempt <= OM_MAX_CONDITIONAL_ATTEMPTS; attempt++) {
8373
+ const doc = await collection.findOne({ id: input.id });
8374
+ if (!doc) throw omNotFound("SWAP_BUFFERED_TO_ACTIVE", input.id);
8375
+ if (doc.supersededBy) return {
8376
+ ...emptyResult,
8377
+ retired: true
8378
+ };
8379
+ const persistedChunks = Array.isArray(doc.bufferedObservationChunks) ? doc.bufferedObservationChunks : [];
8380
+ const refreshedWeights = new Map((Array.isArray(input.bufferedChunks) ? input.bufferedChunks : []).map((c) => [c.id, c.messageTokens]));
8381
+ const chunks = persistedChunks.map((c) => refreshedWeights.has(c.id) ? {
8382
+ ...c,
8383
+ messageTokens: refreshedWeights.get(c.id)
8384
+ } : c);
8385
+ if (chunks.length === 0) return emptyResult;
8386
+ const retentionFloor = input.messageTokensThreshold * (1 - input.activationRatio);
8387
+ const targetMessageTokens = Math.max(0, input.currentPendingTokens - retentionFloor);
8388
+ let cumulativeMessageTokens = 0;
8389
+ let bestOverBoundary = 0;
8390
+ let bestOverTokens = 0;
8391
+ let bestUnderBoundary = 0;
8392
+ let bestUnderTokens = 0;
8393
+ for (let i = 0; i < chunks.length; i++) {
8394
+ cumulativeMessageTokens += chunks[i].messageTokens ?? 0;
8395
+ const boundary = i + 1;
8396
+ if (cumulativeMessageTokens >= targetMessageTokens) {
8397
+ if (bestOverBoundary === 0 || cumulativeMessageTokens < bestOverTokens) {
8398
+ bestOverBoundary = boundary;
8399
+ bestOverTokens = cumulativeMessageTokens;
8400
+ }
8401
+ } else if (cumulativeMessageTokens > bestUnderTokens) {
8402
+ bestUnderBoundary = boundary;
8403
+ bestUnderTokens = cumulativeMessageTokens;
8084
8404
  }
8085
- } else if (cumulativeMessageTokens > bestUnderTokens) {
8086
- bestUnderBoundary = boundary;
8087
- bestUnderTokens = cumulativeMessageTokens;
8088
8405
  }
8406
+ const maxOvershoot = retentionFloor * .95;
8407
+ const overshoot = bestOverTokens - targetMessageTokens;
8408
+ const remainingAfterOver = input.currentPendingTokens - bestOverTokens;
8409
+ const remainingAfterUnder = input.currentPendingTokens - bestUnderTokens;
8410
+ const minRemaining = Math.min(1e3, retentionFloor);
8411
+ let chunksToActivate;
8412
+ if (input.forceMaxActivation && bestOverBoundary > 0 && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
8413
+ else if (bestOverBoundary > 0 && overshoot <= maxOvershoot && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
8414
+ else if (bestUnderBoundary > 0 && remainingAfterUnder >= minRemaining) chunksToActivate = bestUnderBoundary;
8415
+ else if (bestOverBoundary > 0) chunksToActivate = bestOverBoundary;
8416
+ else chunksToActivate = 1;
8417
+ const activatedChunks = chunks.slice(0, chunksToActivate);
8418
+ const remainingChunks = persistedChunks.slice(chunksToActivate);
8419
+ const activatedContent = activatedChunks.map((c) => c.observations).join("\n\n");
8420
+ const activatedTokens = activatedChunks.reduce((sum, c) => sum + c.tokenCount, 0);
8421
+ const activatedMessageTokens = activatedChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
8422
+ const activatedMessageCount = activatedChunks.reduce((sum, c) => sum + c.messageIds.length, 0);
8423
+ const activatedCycleIds = activatedChunks.map((c) => c.cycleId).filter((id) => !!id);
8424
+ const activatedMessageIds = activatedChunks.flatMap((c) => c.messageIds ?? []);
8425
+ const latestChunk = activatedChunks[activatedChunks.length - 1];
8426
+ const lastObservedAt = input.lastObservedAt ?? (latestChunk?.lastObservedAt ? new Date(latestChunk.lastObservedAt) : /* @__PURE__ */ new Date());
8427
+ const existingActive = doc.activeObservations || "";
8428
+ const existingTokenCount = Number(doc.observationTokenCount || 0);
8429
+ const boundary = `\n\n--- message boundary (${lastObservedAt.toISOString()}) ---\n\n`;
8430
+ const newActive = existingActive ? `${existingActive}${boundary}${activatedContent}` : activatedContent;
8431
+ const newTokenCount = existingTokenCount + activatedTokens;
8432
+ const existingPending = Number(doc.pendingMessageTokens || 0);
8433
+ const newPending = Math.max(0, existingPending - activatedMessageTokens);
8434
+ if ((await withRunFence(this.#connector, this.#runFenceCheck(void 0, "swapBufferedToActive"), (session) => collection.updateOne({
8435
+ id: input.id,
8436
+ supersededBy: null,
8437
+ ...matchStoredFields(doc, [
8438
+ "bufferedObservationChunks",
8439
+ "activeObservations",
8440
+ "observationTokenCount",
8441
+ "pendingMessageTokens",
8442
+ "lastObservedAt"
8443
+ ])
8444
+ }, { $set: {
8445
+ activeObservations: newActive,
8446
+ observationTokenCount: newTokenCount,
8447
+ pendingMessageTokens: newPending,
8448
+ bufferedObservationChunks: remainingChunks,
8449
+ lastObservedAt: maxObservationCursor(doc.lastObservedAt, lastObservedAt),
8450
+ updatedAt: /* @__PURE__ */ new Date()
8451
+ } }, { session }))).matchedCount === 0) continue;
8452
+ const latestChunkHints = activatedChunks[activatedChunks.length - 1];
8453
+ return {
8454
+ chunksActivated: activatedChunks.length,
8455
+ messageTokensActivated: activatedMessageTokens,
8456
+ observationTokensActivated: activatedTokens,
8457
+ messagesActivated: activatedMessageCount,
8458
+ activatedCycleIds,
8459
+ activatedMessageIds,
8460
+ observations: activatedContent,
8461
+ perChunk: activatedChunks.map((c) => ({
8462
+ cycleId: c.cycleId ?? "",
8463
+ messageTokens: c.messageTokens ?? 0,
8464
+ observationTokens: c.tokenCount,
8465
+ messageCount: c.messageIds.length,
8466
+ observations: c.observations
8467
+ })),
8468
+ suggestedContinuation: latestChunkHints?.suggestedContinuation ?? void 0,
8469
+ currentTask: latestChunkHints?.currentTask ?? void 0
8470
+ };
8089
8471
  }
8090
- const maxOvershoot = retentionFloor * .95;
8091
- const overshoot = bestOverTokens - targetMessageTokens;
8092
- const remainingAfterOver = input.currentPendingTokens - bestOverTokens;
8093
- const remainingAfterUnder = input.currentPendingTokens - bestUnderTokens;
8094
- const minRemaining = Math.min(1e3, retentionFloor);
8095
- let chunksToActivate;
8096
- if (input.forceMaxActivation && bestOverBoundary > 0 && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
8097
- else if (bestOverBoundary > 0 && overshoot <= maxOvershoot && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
8098
- else if (bestUnderBoundary > 0 && remainingAfterUnder >= minRemaining) chunksToActivate = bestUnderBoundary;
8099
- else if (bestOverBoundary > 0) chunksToActivate = bestOverBoundary;
8100
- else chunksToActivate = 1;
8101
- const activatedChunks = chunks.slice(0, chunksToActivate);
8102
- const remainingChunks = chunks.slice(chunksToActivate);
8103
- const activatedContent = activatedChunks.map((c) => c.observations).join("\n\n");
8104
- const activatedTokens = activatedChunks.reduce((sum, c) => sum + c.tokenCount, 0);
8105
- const activatedMessageTokens = activatedChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
8106
- const activatedMessageCount = activatedChunks.reduce((sum, c) => sum + c.messageIds.length, 0);
8107
- const activatedCycleIds = activatedChunks.map((c) => c.cycleId).filter((id) => !!id);
8108
- const activatedMessageIds = activatedChunks.flatMap((c) => c.messageIds ?? []);
8109
- const latestChunk = activatedChunks[activatedChunks.length - 1];
8110
- const lastObservedAt = input.lastObservedAt ?? (latestChunk?.lastObservedAt ? new Date(latestChunk.lastObservedAt) : /* @__PURE__ */ new Date());
8111
- const existingActive = doc.activeObservations || "";
8112
- const existingTokenCount = Number(doc.observationTokenCount || 0);
8113
- const boundary = `\n\n--- message boundary (${lastObservedAt.toISOString()}) ---\n\n`;
8114
- const newActive = existingActive ? `${existingActive}${boundary}${activatedContent}` : activatedContent;
8115
- const newTokenCount = existingTokenCount + activatedTokens;
8116
- const existingPending = Number(doc.pendingMessageTokens || 0);
8117
- const newPending = Math.max(0, existingPending - activatedMessageTokens);
8118
- if ((await collection.updateOne({
8119
- id: input.id,
8120
- bufferedObservationChunks: {
8121
- $exists: true,
8122
- $ne: null,
8123
- $not: { $size: 0 }
8124
- }
8125
- }, { $set: {
8126
- activeObservations: newActive,
8127
- observationTokenCount: newTokenCount,
8128
- pendingMessageTokens: newPending,
8129
- bufferedObservationChunks: remainingChunks,
8130
- lastObservedAt,
8131
- updatedAt: /* @__PURE__ */ new Date()
8132
- } })).modifiedCount === 0) return {
8133
- chunksActivated: 0,
8134
- messageTokensActivated: 0,
8135
- observationTokensActivated: 0,
8136
- messagesActivated: 0,
8137
- activatedCycleIds: [],
8138
- activatedMessageIds: []
8139
- };
8140
- const latestChunkHints = activatedChunks[activatedChunks.length - 1];
8141
- return {
8142
- chunksActivated: activatedChunks.length,
8143
- messageTokensActivated: activatedMessageTokens,
8144
- observationTokensActivated: activatedTokens,
8145
- messagesActivated: activatedMessageCount,
8146
- activatedCycleIds,
8147
- activatedMessageIds,
8148
- observations: activatedContent,
8149
- perChunk: activatedChunks.map((c) => ({
8150
- cycleId: c.cycleId ?? "",
8151
- messageTokens: c.messageTokens ?? 0,
8152
- observationTokens: c.tokenCount,
8153
- messageCount: c.messageIds.length,
8154
- observations: c.observations
8155
- })),
8156
- suggestedContinuation: latestChunkHints?.suggestedContinuation ?? void 0,
8157
- currentTask: latestChunkHints?.currentTask ?? void 0
8158
- };
8472
+ throw omConflict("SWAP_BUFFERED_TO_ACTIVE", input.id);
8159
8473
  } catch (error) {
8160
8474
  if (error instanceof MastraError) throw error;
8161
8475
  throw new MastraError({
@@ -8183,13 +8497,13 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8183
8497
  const newContent = existingContent ? `${existingContent}\n\n${input.reflection}` : input.reflection;
8184
8498
  const newTokens = existingTokens + input.tokenCount;
8185
8499
  const newInputTokens = existingInputTokens + input.inputTokenCount;
8186
- 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: {
8187
8501
  bufferedReflection: newContent,
8188
8502
  bufferedReflectionTokens: newTokens,
8189
8503
  bufferedReflectionInputTokens: newInputTokens,
8190
8504
  reflectedObservationLineCount: input.reflectedObservationLineCount,
8191
8505
  updatedAt: /* @__PURE__ */ new Date()
8192
- } })).matchedCount === 0) throw new MastraError({
8506
+ } }, { session }))).matchedCount === 0) throw new MastraError({
8193
8507
  id: createStorageErrorId("MONGODB", "UPDATE_BUFFERED_REFLECTION", "NOT_FOUND"),
8194
8508
  text: `Observational memory record not found: ${input.id}`,
8195
8509
  domain: ErrorDomain.STORAGE,
@@ -8208,39 +8522,33 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8208
8522
  }
8209
8523
  async swapBufferedReflectionToActive(input) {
8210
8524
  try {
8211
- const collection = await this.getCollection(OM_TABLE);
8212
- const doc = await collection.findOne({ id: input.currentRecord.id });
8213
- if (!doc) throw new MastraError({
8214
- id: createStorageErrorId("MONGODB", "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NOT_FOUND"),
8215
- text: `Observational memory record not found: ${input.currentRecord.id}`,
8216
- domain: ErrorDomain.STORAGE,
8217
- category: ErrorCategory.THIRD_PARTY,
8218
- details: { id: input.currentRecord.id }
8219
- });
8220
- const bufferedReflection = doc.bufferedReflection || "";
8221
- const reflectedLineCount = Number(doc.reflectedObservationLineCount || 0);
8222
- if (!bufferedReflection) throw new MastraError({
8223
- id: createStorageErrorId("MONGODB", "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NO_CONTENT"),
8224
- text: "No buffered reflection to swap",
8225
- domain: ErrorDomain.STORAGE,
8226
- category: ErrorCategory.USER,
8227
- details: { id: input.currentRecord.id }
8228
- });
8229
- const unreflectedContent = (doc.activeObservations || "").split("\n").slice(reflectedLineCount).join("\n").trim();
8230
- const newObservations = unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
8231
- const newRecord = await this.createReflectionGeneration({
8232
- currentRecord: input.currentRecord,
8233
- reflection: newObservations,
8234
- tokenCount: input.tokenCount
8525
+ const { currentRecord } = input;
8526
+ return await this.#rollOver(currentRecord, "swapBufferedReflectionToActive", (doc) => {
8527
+ const stored = this.parseOMDocument(doc);
8528
+ const bufferedReflection = stored.bufferedReflection || "";
8529
+ if (!bufferedReflection) throw new MastraError({
8530
+ id: createStorageErrorId("MONGODB", "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NO_CONTENT"),
8531
+ text: "No buffered reflection to swap",
8532
+ domain: ErrorDomain.STORAGE,
8533
+ category: ErrorCategory.USER,
8534
+ details: { id: currentRecord.id }
8535
+ });
8536
+ if (!isAppendOnlySince(stored.activeObservations, currentRecord.activeObservations)) return null;
8537
+ const reflectedLineCount = stored.reflectedObservationLineCount ?? 0;
8538
+ const unreflectedContent = (stored.activeObservations || "").split("\n").slice(reflectedLineCount).join("\n").trim();
8539
+ const newObservations = unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
8540
+ const tokenCount = input.tokenCount + Math.max(0, (stored.observationTokenCount ?? 0) - (currentRecord.observationTokenCount ?? 0));
8541
+ return {
8542
+ newId: input.newRecordId ?? globalThis.crypto.randomUUID(),
8543
+ mode: "equal",
8544
+ reflection: newObservations,
8545
+ tokenCount,
8546
+ snapTextLength: (stored.activeObservations ?? "").length,
8547
+ snapObservationTokenCount: stored.observationTokenCount ?? 0,
8548
+ clearBufferedReflection: true,
8549
+ createdAt: /* @__PURE__ */ new Date()
8550
+ };
8235
8551
  });
8236
- await collection.updateOne({ id: input.currentRecord.id }, { $set: {
8237
- bufferedReflection: null,
8238
- bufferedReflectionTokens: null,
8239
- bufferedReflectionInputTokens: null,
8240
- reflectedObservationLineCount: null,
8241
- updatedAt: /* @__PURE__ */ new Date()
8242
- } });
8243
- return newRecord;
8244
8552
  } catch (error) {
8245
8553
  if (error instanceof MastraError) throw error;
8246
8554
  throw new MastraError({
@@ -8251,6 +8559,171 @@ var MemoryStorageMongoDB = class MemoryStorageMongoDB extends MemoryStorage {
8251
8559
  }, error);
8252
8560
  }
8253
8561
  }
8562
+ /**
8563
+ * The head of a lookup key by the canonical order (`generationCount DESC, createdAt ASC, id ASC`).
8564
+ * A head that was fenced for rollover but whose successor insert never ran (crash, or still in
8565
+ * flight in another process) is rolled forward here, so every head read reaches the successor.
8566
+ */
8567
+ async #getHeadDoc(collection, lookupKey) {
8568
+ for (let hop = 0; hop <= OM_MAX_HEAD_HOPS; hop++) {
8569
+ const head = await collection.findOne({ lookupKey }, { sort: OM_HEAD_SORT });
8570
+ if (!head || !head.supersededBy || !head.pendingSuccessor) return head;
8571
+ await this.#completeRollover(collection, head, head.pendingSuccessor);
8572
+ }
8573
+ throw omNoLiveHead(lookupKey);
8574
+ }
8575
+ /**
8576
+ * A single-document update aimed at a live record. A retired (or concurrently fenced) id is
8577
+ * redirected to the head, at most `OM_MAX_HEAD_HOPS` times.
8578
+ */
8579
+ async #updateLiveDoc(id, operation, update) {
8580
+ const collection = await this.getCollection(OM_TABLE);
8581
+ let targetId = id;
8582
+ for (let hop = 0; hop <= OM_MAX_HEAD_HOPS; hop++) {
8583
+ if ((await collection.updateOne({
8584
+ id: targetId,
8585
+ supersededBy: null
8586
+ }, update)).matchedCount === 1) return targetId;
8587
+ const doc = await collection.findOne({ id: targetId }, { projection: {
8588
+ lookupKey: 1,
8589
+ supersededBy: 1
8590
+ } });
8591
+ if (!doc) throw omNotFound(operation, targetId);
8592
+ if (!doc.supersededBy) continue;
8593
+ const head = await this.#getHeadDoc(collection, doc.lookupKey);
8594
+ if (!head || head.supersededBy || head.id === targetId) throw omNoLiveHead(id);
8595
+ targetId = head.id;
8596
+ }
8597
+ throw omNoLiveHead(id);
8598
+ }
8599
+ /**
8600
+ * Roll the live record `currentRecord.id` over to a new generation.
8601
+ *
8602
+ * 1. Read the stored document and decide (`decide` returns the successor inputs, or `null`
8603
+ * when the snapshot is stale).
8604
+ * 2. Fence: one conditional update sets `supersededBy` and the small `pendingSuccessor`
8605
+ * payload, matching only if the fields the decision read are unchanged. From here the old
8606
+ * document is frozen — no lifecycle write matches it — so its pre-image is its final state.
8607
+ * 3. Insert the successor built purely from that pre-image, then clear the old document's
8608
+ * chunks (they moved) and `pendingSuccessor`.
8609
+ *
8610
+ * A crash between 2 and 3 is finished by the next head read (`#getHeadDoc`) or startup.
8611
+ */
8612
+ async #rollOver(currentRecord, operation, decide) {
8613
+ const collection = await this.getCollection(OM_TABLE);
8614
+ for (let attempt = 1; attempt <= OM_MAX_CONDITIONAL_ATTEMPTS; attempt++) {
8615
+ const doc = await collection.findOne({ id: currentRecord.id });
8616
+ if (!doc) return currentRecord;
8617
+ if (doc.supersededBy) {
8618
+ const head = await this.#getHeadDoc(collection, doc.lookupKey);
8619
+ return head ? this.parseOMDocument(head) : currentRecord;
8620
+ }
8621
+ const pending = decide(doc);
8622
+ if (!pending) return this.parseOMDocument(doc);
8623
+ const frozen = await withRunFence(this.#connector, this.#runFenceCheck(void 0, operation), (session) => collection.findOneAndUpdate({
8624
+ id: doc.id,
8625
+ supersededBy: null,
8626
+ ...matchStoredFields(doc, [
8627
+ "activeObservations",
8628
+ "observationTokenCount",
8629
+ "bufferedReflection",
8630
+ "reflectedObservationLineCount"
8631
+ ])
8632
+ }, { $set: {
8633
+ supersededBy: pending.newId,
8634
+ pendingSuccessor: pending
8635
+ } }, {
8636
+ returnDocument: "before",
8637
+ session
8638
+ }));
8639
+ if (!frozen) continue;
8640
+ return this.parseOMDocument(await this.#completeRollover(collection, frozen, pending));
8641
+ }
8642
+ throw omConflict("CREATE_REFLECTION_GENERATION", currentRecord.id);
8643
+ }
8644
+ /** Insert the successor of a fenced document (idempotent) and clean up the old document. */
8645
+ async #completeRollover(collection, frozen, pending) {
8646
+ try {
8647
+ await collection.insertOne(buildSuccessorDocument(frozen, pending));
8648
+ } catch (error) {
8649
+ if (!isDuplicateKeyError(error)) throw error;
8650
+ }
8651
+ await collection.updateOne({
8652
+ id: frozen.id,
8653
+ supersededBy: pending.newId
8654
+ }, {
8655
+ $set: {
8656
+ bufferedObservationChunks: [],
8657
+ ...pending.clearBufferedReflection ? {
8658
+ bufferedReflection: null,
8659
+ bufferedReflectionTokens: null,
8660
+ bufferedReflectionInputTokens: null,
8661
+ reflectedObservationLineCount: null
8662
+ } : {},
8663
+ updatedAt: /* @__PURE__ */ new Date()
8664
+ },
8665
+ $unset: { pendingSuccessor: "" }
8666
+ });
8667
+ const successor = await collection.findOne({ id: pending.newId });
8668
+ if (!successor) throw omNotFound("CREATE_REFLECTION_GENERATION", pending.newId);
8669
+ return successor;
8670
+ }
8671
+ /**
8672
+ * Startup maintenance for the `supersededBy` marker:
8673
+ * - finish rollovers interrupted after the fence (insert the successor if missing, clean up);
8674
+ * - for keys with more than one live document (left by older adapter versions, which never set
8675
+ * `supersededBy`), retire every live document that sorts after the canonical head. The head's
8676
+ * ordering key is embedded in the filter, so a document created by a concurrent rollover
8677
+ * (a newer generation) never matches.
8678
+ */
8679
+ async #maintainSupersededBy() {
8680
+ const collection = await this.getCollection(OM_TABLE);
8681
+ const interrupted = await collection.find({
8682
+ supersededBy: { $nin: [null] },
8683
+ pendingSuccessor: { $exists: true }
8684
+ }).toArray();
8685
+ for (const doc of interrupted) await this.#completeRollover(collection, doc, doc.pendingSuccessor);
8686
+ const keys = await collection.aggregate([
8687
+ { $match: { supersededBy: null } },
8688
+ { $group: {
8689
+ _id: "$lookupKey",
8690
+ live: { $sum: 1 }
8691
+ } },
8692
+ { $match: { live: { $gt: 1 } } }
8693
+ ]).toArray();
8694
+ for (const { _id: lookupKey } of keys) {
8695
+ const head = await this.#getHeadDoc(collection, lookupKey);
8696
+ if (!head || head.supersededBy) continue;
8697
+ await collection.updateMany({
8698
+ lookupKey,
8699
+ supersededBy: null,
8700
+ $or: [
8701
+ { generationCount: { $lt: head.generationCount } },
8702
+ {
8703
+ generationCount: head.generationCount,
8704
+ createdAt: { $gt: head.createdAt }
8705
+ },
8706
+ {
8707
+ generationCount: head.generationCount,
8708
+ createdAt: head.createdAt,
8709
+ id: { $gt: head.id }
8710
+ }
8711
+ ]
8712
+ }, { $set: { supersededBy: head.id } });
8713
+ }
8714
+ }
8715
+ /**
8716
+ * Cross-process rollover and initialization rely on the unique `id` index (a duplicate insert
8717
+ * of the same successor or generation-0 id must fail). Warn once if it is missing, e.g. under
8718
+ * `skipDefaultIndexes`.
8719
+ */
8720
+ async #warnIfOMIdIndexMissing() {
8721
+ try {
8722
+ if (!(await (await this.getCollection(OM_TABLE)).listIndexes().toArray().catch((error) => error?.code === 26 ? [] : Promise.reject(error))).some((index) => index.unique && Object.keys(index.key ?? {}).length === 1 && index.key.id === 1)) this.logger?.warn?.(`MongoDB collection "${OM_TABLE}" has no unique index on { id: 1 }. Observational memory needs it to stay consistent when several processes write the same thread or resource. Create it with db.${OM_TABLE}.createIndex({ id: 1 }, { unique: true }).`);
8723
+ } catch (error) {
8724
+ this.logger?.warn?.(`Could not list indexes on MongoDB collection "${OM_TABLE}":`, error);
8725
+ }
8726
+ }
8254
8727
  };
8255
8728
  //#endregion
8256
8729
  //#region src/storage/domains/notifications/index.ts
@@ -11477,33 +11950,58 @@ var MongoDBWorkflowDefinitionsStore = class MongoDBWorkflowDefinitionsStore exte
11477
11950
  };
11478
11951
  //#endregion
11479
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
+ }
11480
11969
  var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsStorage {
11481
11970
  #connector;
11482
11971
  #skipDefaultIndexes;
11483
11972
  #indexes;
11484
11973
  /** Collections managed by this domain */
11485
- static MANAGED_COLLECTIONS = [TABLE_WORKFLOW_SNAPSHOT];
11974
+ static MANAGED_COLLECTIONS = [TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKFLOW_RUN_OWNERS];
11486
11975
  /**
11487
- * Anchor is `updatedAt` (BSON date), so the policy reads as inactivity:
11488
- * 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`.
11489
11980
  */
11490
- static retentionTables = { workflowSnapshot: {
11491
- table: TABLE_WORKFLOW_SNAPSHOT,
11492
- column: "updatedAt",
11493
- indexed: true
11494
- } };
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
+ };
11495
11993
  constructor(config) {
11496
11994
  super();
11497
11995
  this.#connector = resolveMongoDBConfig(config);
11498
11996
  this.#skipDefaultIndexes = config.skipDefaultIndexes;
11499
11997
  this.#indexes = config.indexes?.filter((idx) => WorkflowsStorageMongoDB.MANAGED_COLLECTIONS.includes(idx.collection));
11500
11998
  }
11501
- /** 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. */
11502
12000
  async prune(policies, options) {
11503
12001
  const targets = resolveTargets({
11504
12002
  policies,
11505
12003
  descriptor: WorkflowsStorageMongoDB.retentionTables,
11506
- order: ["workflowSnapshot"]
12004
+ order: ["workflowSnapshot", "runOwnership"]
11507
12005
  });
11508
12006
  return runPrune({
11509
12007
  connector: this.#connector,
@@ -11516,6 +12014,13 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11516
12014
  supportsConcurrentUpdates() {
11517
12015
  return true;
11518
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
+ }
11519
12024
  async getCollection(name) {
11520
12025
  return this.#connector.getCollection(name);
11521
12026
  }
@@ -11523,6 +12028,124 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11523
12028
  await this.createDefaultIndexes();
11524
12029
  await this.createCustomIndexes();
11525
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
+ }
11526
12149
  /**
11527
12150
  * Returns default index definitions for the workflows domain collections.
11528
12151
  */
@@ -11582,8 +12205,10 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11582
12205
  }
11583
12206
  async dangerouslyClearAll() {
11584
12207
  await (await this.getCollection(TABLE_WORKFLOW_SNAPSHOT)).deleteMany({});
12208
+ await (await this.getCollection(TABLE_WORKFLOW_RUN_OWNERS)).deleteMany({});
11585
12209
  }
11586
- 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");
11587
12212
  try {
11588
12213
  const collection = await this.getCollection(TABLE_WORKFLOW_SNAPSHOT);
11589
12214
  const now = /* @__PURE__ */ new Date();
@@ -11603,7 +12228,7 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11603
12228
  runId,
11604
12229
  requestContext: {}
11605
12230
  };
11606
- const updatedDoc = await collection.findOneAndUpdate({
12231
+ const updatedDoc = await withRunFence(this.#connector, check, (session) => collection.findOneAndUpdate({
11607
12232
  workflow_name: workflowName,
11608
12233
  run_id: runId
11609
12234
  }, [{ $set: {
@@ -11618,10 +12243,12 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11618
12243
  createdAt: { $ifNull: ["$createdAt", now] }
11619
12244
  } }], {
11620
12245
  upsert: true,
11621
- returnDocument: "after"
11622
- });
12246
+ returnDocument: "after",
12247
+ session
12248
+ }));
11623
12249
  return (typeof updatedDoc?.snapshot === "string" ? JSON.parse(updatedDoc.snapshot) : updatedDoc?.snapshot)?.context || {};
11624
12250
  } catch (error) {
12251
+ if (isRunFenceConflictError(error)) throw error;
11625
12252
  throw new MastraError({
11626
12253
  id: createStorageErrorId("MONGODB", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
11627
12254
  domain: ErrorDomain.STORAGE,
@@ -11634,7 +12261,8 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11634
12261
  }, error);
11635
12262
  }
11636
12263
  }
11637
- async updateWorkflowState({ workflowName, runId, opts }) {
12264
+ async updateWorkflowState({ workflowName, runId, opts, fence }) {
12265
+ const check = this.#runFenceCheck(fence, runId, "updateWorkflowState");
11638
12266
  try {
11639
12267
  const collection = await this.getCollection(TABLE_WORKFLOW_SNAPSHOT);
11640
12268
  const { expectedStatus, ...state } = opts;
@@ -11643,10 +12271,13 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11643
12271
  run_id: runId
11644
12272
  };
11645
12273
  if (expectedStatus !== void 0) filter["snapshot.status"] = { $in: Array.isArray(expectedStatus) ? expectedStatus : [expectedStatus] };
11646
- const updatedDoc = await collection.findOneAndUpdate(filter, [{ $set: {
12274
+ const updatedDoc = await withRunFence(this.#connector, check, (session) => collection.findOneAndUpdate(filter, [{ $set: {
11647
12275
  snapshot: { $mergeObjects: ["$snapshot", state] },
11648
12276
  updatedAt: /* @__PURE__ */ new Date()
11649
- } }], { returnDocument: "after" });
12277
+ } }], {
12278
+ returnDocument: "after",
12279
+ session
12280
+ }));
11650
12281
  if (!updatedDoc) return;
11651
12282
  const snapshot = typeof updatedDoc.snapshot === "string" ? JSON.parse(updatedDoc.snapshot) : updatedDoc.snapshot;
11652
12283
  if (!snapshot?.context) throw new MastraError({
@@ -11661,7 +12292,7 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11661
12292
  });
11662
12293
  return snapshot;
11663
12294
  } catch (error) {
11664
- if (error instanceof MastraError) throw error;
12295
+ if (error instanceof MastraError || isRunFenceConflictError(error)) throw error;
11665
12296
  throw new MastraError({
11666
12297
  id: createStorageErrorId("MONGODB", "UPDATE_WORKFLOW_STATE", "FAILED"),
11667
12298
  domain: ErrorDomain.STORAGE,
@@ -11673,10 +12304,12 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11673
12304
  }, error);
11674
12305
  }
11675
12306
  }
11676
- 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");
11677
12309
  try {
11678
12310
  const now = /* @__PURE__ */ new Date();
11679
- 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({
11680
12313
  workflow_name: workflowName,
11681
12314
  run_id: runId
11682
12315
  }, {
@@ -11688,8 +12321,12 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11688
12321
  updatedAt: updatedAt ?? now
11689
12322
  },
11690
12323
  $setOnInsert: { createdAt: createdAt ?? now }
11691
- }, { upsert: true });
12324
+ }, {
12325
+ upsert: true,
12326
+ session
12327
+ }));
11692
12328
  } catch (error) {
12329
+ if (isRunFenceConflictError(error)) throw error;
11693
12330
  throw new MastraError({
11694
12331
  id: createStorageErrorId("MONGODB", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
11695
12332
  domain: ErrorDomain.STORAGE,
@@ -11782,13 +12419,16 @@ var WorkflowsStorageMongoDB = class WorkflowsStorageMongoDB extends WorkflowsSto
11782
12419
  }, error);
11783
12420
  }
11784
12421
  }
11785
- async deleteWorkflowRunById({ runId, workflowName }) {
12422
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
12423
+ const check = this.#runFenceCheck(fence, runId, "deleteWorkflowRunById");
11786
12424
  try {
11787
- 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({
11788
12427
  workflow_name: workflowName,
11789
12428
  run_id: runId
11790
- });
12429
+ }, { session }));
11791
12430
  } catch (error) {
12431
+ if (isRunFenceConflictError(error)) throw error;
11792
12432
  throw new MastraError({
11793
12433
  id: createStorageErrorId("MONGODB", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
11794
12434
  domain: ErrorDomain.STORAGE,