@mastra/mysql 0.13.0 → 0.13.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.cjs CHANGED
@@ -3,6 +3,7 @@ let _mastra_core_error = require("@mastra/core/error");
3
3
  let _mastra_core_storage = require("@mastra/core/storage");
4
4
  let mysql2_promise = require("mysql2/promise");
5
5
  let _mastra_core_utils = require("@mastra/core/utils");
6
+ let crypto$1 = require("crypto");
6
7
  let _mastra_core_agent = require("@mastra/core/agent");
7
8
  let _mastra_core_storage_domains_skills = require("@mastra/core/storage/domains/skills");
8
9
  //#region src/storage/db/schema-snapshot.ts
@@ -6022,9 +6023,180 @@ var MCPServersMySQL = class MCPServersMySQL extends _mastra_core_storage.MCPServ
6022
6023
  }
6023
6024
  };
6024
6025
  //#endregion
6026
+ //#region ../../packages/_internal-core/dist/memory/index.js
6027
+ /**
6028
+ * Shared rules for observational memory lifecycle writes. Every storage adapter applies
6029
+ * these inside its own atomic section (lock, transaction, or conditional update), so the
6030
+ * decisions are identical across adapters.
6031
+ */
6032
+ /**
6033
+ * Observational memory columns that lifecycle writes depend on. Storage adapters merge these
6034
+ * into the table schema they get from `@mastra/core`, so the columns exist even when the
6035
+ * installed core predates them.
6036
+ */
6037
+ const OBSERVATIONAL_MEMORY_LIFECYCLE_COLUMNS = { supersededBy: {
6038
+ type: "text",
6039
+ nullable: true
6040
+ } };
6041
+ /** `schema` plus {@link OBSERVATIONAL_MEMORY_LIFECYCLE_COLUMNS}; `undefined` when `schema` is. */
6042
+ function withObservationalMemoryLifecycleColumns(schema) {
6043
+ return schema ? {
6044
+ ...schema,
6045
+ ...OBSERVATIONAL_MEMORY_LIFECYCLE_COLUMNS
6046
+ } : void 0;
6047
+ }
6048
+ /**
6049
+ * A buffered chunk stores `lastObservedAt = max message time + 1ms`. It is wholly covered
6050
+ * (every message it observed is already at or before the cursor) iff
6051
+ * `cursor >= chunk.lastObservedAt - 1ms`.
6052
+ */
6053
+ function isBufferedChunkCoveredByCursor(chunkLastObservedAt, cursor) {
6054
+ if (!cursor) return false;
6055
+ return new Date(cursor).getTime() >= new Date(chunkLastObservedAt).getTime() - 1;
6056
+ }
6057
+ /** The later of two cursors; `undefined` only when both are absent. */
6058
+ function maxObservationCursor(a, b) {
6059
+ if (!a) return b ? new Date(b) : void 0;
6060
+ if (!b) return new Date(a);
6061
+ return new Date(Math.max(new Date(a).getTime(), new Date(b).getTime()));
6062
+ }
6063
+ /**
6064
+ * Decide the new generation's text when a reflection built from `snapshot` commits against
6065
+ * the stored record.
6066
+ *
6067
+ * - Stored text equals the snapshot: the reflection replaces it.
6068
+ * - Stored text extends the snapshot (activation only appends): the appended tail is kept
6069
+ * after the reflection, and its tokens are added.
6070
+ * - Anything else (a non-append rewrite): `null` — the reflection must not be applied.
6071
+ */
6072
+ function planReflectionGenerationText(input) {
6073
+ const stored = input.storedObservations ?? "";
6074
+ const snapshot = input.snapshotObservations ?? "";
6075
+ if (stored === snapshot) return {
6076
+ mode: "equal",
6077
+ observations: input.reflection,
6078
+ tokenCount: input.tokenCount
6079
+ };
6080
+ if (!stored.startsWith(snapshot)) return null;
6081
+ const tail = stored.slice(snapshot.length).trimStart();
6082
+ if (!tail.trim()) return {
6083
+ mode: "equal",
6084
+ observations: input.reflection,
6085
+ tokenCount: input.tokenCount
6086
+ };
6087
+ return {
6088
+ mode: "append",
6089
+ observations: input.reflection ? `${input.reflection}\n\n${tail}` : tail,
6090
+ tokenCount: input.tokenCount + Math.max(0, (input.storedObservationTokenCount ?? 0) - (input.snapshotObservationTokenCount ?? 0))
6091
+ };
6092
+ }
6093
+ /**
6094
+ * Backs `updateActiveObservations` in adapters that implement `commitActiveObservations`.
6095
+ * Callers of `updateActiveObservations` (which resolves `void`) can't see a commit result, so a
6096
+ * write that was not applied throws instead of resolving as if it had committed.
6097
+ */
6098
+ function assertActiveObservationsApplied(result, id) {
6099
+ if (result.applied) return;
6100
+ 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`);
6101
+ }
6102
+ /**
6103
+ * Whether the stored text can still accept a reflection built from `snapshot`
6104
+ * (equal, or only appended to since).
6105
+ */
6106
+ function isAppendOnlySince(storedObservations, snapshotObservations) {
6107
+ const stored = storedObservations ?? "";
6108
+ const snapshot = snapshotObservations ?? "";
6109
+ return stored === snapshot || stored.startsWith(snapshot);
6110
+ }
6111
+ //#endregion
6112
+ //#region src/storage/db/run-fencing.ts
6113
+ /**
6114
+ * Epoch milliseconds on the database clock. `UTC_TIMESTAMP` is the time the
6115
+ * statement started and doesn't depend on the session time zone. Every lease
6116
+ * comparison runs in a statement that already holds the run's owner row, so
6117
+ * no lock wait sits between that start time and the comparison.
6118
+ */
6119
+ const DB_NOW_MS = `(TIMESTAMPDIFF(MICROSECOND, '1970-01-01 00:00:00', UTC_TIMESTAMP(3)) DIV 1000)`;
6120
+ const ER_DUP_ENTRY = 1062;
6121
+ const ER_LOCK_DEADLOCK = 1213;
6122
+ const MAX_CLAIM_ATTEMPTS = 5;
6123
+ /**
6124
+ * The check half of a fenced write; call it first inside the write's
6125
+ * transaction. The shared lock holds the claim row until commit, so a claim
6126
+ * that updates the row waits for the write to finish, and every later write
6127
+ * sees the new claim.
6128
+ */
6129
+ async function assertRunFence(q, claimsTable, fence, operation) {
6130
+ const [rows] = await q.execute(`SELECT generation, ${quoteIdentifier("ownerId", "column name")} AS ownerId FROM ${claimsTable}
6131
+ WHERE ${quoteIdentifier("runId", "column name")} = ? LOCK IN SHARE MODE`, [fence.runId]);
6132
+ const row = rows[0];
6133
+ if (!(0, _mastra_core_storage.matchesRunFence)(row ? {
6134
+ generation: Number(row.generation),
6135
+ ownerId: String(row.ownerId)
6136
+ } : null, fence)) throw new _mastra_core_storage.RunFenceConflictError(fence, operation);
6137
+ }
6138
+ /** Runs `fn` in a transaction on its own pooled connection. */
6139
+ async function inTransaction(pool, fn) {
6140
+ const connection = await pool.getConnection();
6141
+ try {
6142
+ await connection.beginTransaction();
6143
+ const result = await fn(connection);
6144
+ await connection.commit();
6145
+ return result;
6146
+ } catch (error) {
6147
+ await connection.rollback().catch(() => {});
6148
+ throw error;
6149
+ } finally {
6150
+ connection.release();
6151
+ }
6152
+ }
6153
+ /**
6154
+ * Runs `write` directly when it carries no fence, or in a transaction behind
6155
+ * the fence check when it does.
6156
+ */
6157
+ function withRunFence(pool, claimsTable, fence, operation, write) {
6158
+ if (!fence) return write(pool);
6159
+ return inTransaction(pool, async (connection) => {
6160
+ await assertRunFence(connection, claimsTable, fence, operation);
6161
+ return write(connection);
6162
+ });
6163
+ }
6164
+ /**
6165
+ * Runs a transaction that creates or updates a run's claim row, retrying when
6166
+ * two first claims race to insert it. Under REPEATABLE READ both lock the gap
6167
+ * where the row would go and one is chosen as a deadlock victim; under READ
6168
+ * COMMITTED the later insert hits the duplicate key. On retry the row exists
6169
+ * and the claim serializes behind its row lock.
6170
+ */
6171
+ async function claimTransaction(pool, fn) {
6172
+ for (let attempt = 1;; attempt++) try {
6173
+ return await inTransaction(pool, fn);
6174
+ } catch (error) {
6175
+ const errno = error.errno;
6176
+ if (attempt >= MAX_CLAIM_ATTEMPTS || errno !== ER_DUP_ENTRY && errno !== ER_LOCK_DEADLOCK) throw error;
6177
+ }
6178
+ }
6179
+ //#endregion
6025
6180
  //#region src/storage/domains/memory/index.ts
6026
6181
  const OM_TABLE = "mastra_observational_memory";
6027
6182
  const OM_TABLE_QUOTED = quoteIdentifier(OM_TABLE, "table name");
6183
+ const RUN_FENCES_TABLE = formatTableName(_mastra_core_storage.TABLE_MEMORY_RUN_FENCES);
6184
+ const RUN_ID$1 = quoteIdentifier("runId", "column name");
6185
+ const OWNER_ID$1 = quoteIdentifier("ownerId", "column name");
6186
+ const RETIRED_AT = quoteIdentifier("retiredAt", "column name");
6187
+ /** Canonical head order: newest generation, then earliest createdAt, then lowest id. */
6188
+ const OM_HEAD_ORDER = `${quoteIdentifier("generationCount", "column name")} DESC, ${quoteIdentifier("createdAt", "column name")} ASC, ${quoteIdentifier("id", "column name")} ASC`;
6189
+ /** Max `supersededBy` hops when redirecting a write aimed at a retired record. */
6190
+ const OM_MAX_HEAD_HOPS = 3;
6191
+ /** Attempts for an OM transaction that hits a deadlock or lock-wait timeout. */
6192
+ const OM_MAX_TRANSACTION_ATTEMPTS = 3;
6193
+ /** How long initialization waits for the per-key `GET_LOCK` before failing. */
6194
+ const OM_INIT_LOCK_TIMEOUT_SECONDS = 30;
6195
+ /** ER_LOCK_DEADLOCK (1213) or ER_LOCK_WAIT_TIMEOUT (1205): the transaction was rolled back and can be retried. */
6196
+ function isRetryableLockError(error) {
6197
+ const errno = error?.errno;
6198
+ return errno === 1213 || errno === 1205;
6199
+ }
6028
6200
  function emitValidationError(message) {
6029
6201
  return new _mastra_core_error.MastraError({
6030
6202
  id: "MYSQL_MEMORY_VALIDATION_ERROR",
@@ -6118,6 +6290,69 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6118
6290
  this.#skipDefaultIndexes = skipDefaultIndexes;
6119
6291
  this.#indexes = indexes?.filter((idx) => MemoryMySQL.MANAGED_TABLES.includes(idx.table));
6120
6292
  }
6293
+ supportsRunFencing() {
6294
+ return true;
6295
+ }
6296
+ async raiseRunFence(fence) {
6297
+ try {
6298
+ return await claimTransaction(this.pool, async (connection) => {
6299
+ const [rows] = await connection.execute(`SELECT generation, ${OWNER_ID$1} AS ownerId FROM ${RUN_FENCES_TABLE} WHERE ${RUN_ID$1} = ? FOR UPDATE`, [fence.runId]);
6300
+ const row = rows[0];
6301
+ if (!row) {
6302
+ await connection.execute(`INSERT INTO ${RUN_FENCES_TABLE} (${RUN_ID$1}, generation, ${OWNER_ID$1}) VALUES (?, ?, ?)`, [
6303
+ fence.runId,
6304
+ fence.generation,
6305
+ fence.ownerId
6306
+ ]);
6307
+ return true;
6308
+ }
6309
+ const generation = Number(row.generation);
6310
+ const raises = generation < fence.generation;
6311
+ const reaffirms = generation === fence.generation && String(row.ownerId) === fence.ownerId;
6312
+ if (!raises && !reaffirms) return false;
6313
+ await connection.execute(`UPDATE ${RUN_FENCES_TABLE} SET generation = ?, ${OWNER_ID$1} = ?, ${RETIRED_AT} = NULL WHERE ${RUN_ID$1} = ?`, [
6314
+ fence.generation,
6315
+ fence.ownerId,
6316
+ fence.runId
6317
+ ]);
6318
+ return true;
6319
+ });
6320
+ } catch (error) {
6321
+ throw new _mastra_core_error.MastraError({
6322
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "RAISE_RUN_FENCE", "FAILED"),
6323
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
6324
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
6325
+ details: { runId: fence.runId }
6326
+ }, error);
6327
+ }
6328
+ }
6329
+ async retireRunFence(fence) {
6330
+ try {
6331
+ return await inTransaction(this.pool, async (connection) => {
6332
+ const [rows] = await connection.execute(`SELECT generation, ${OWNER_ID$1} AS ownerId FROM ${RUN_FENCES_TABLE} WHERE ${RUN_ID$1} = ? FOR UPDATE`, [fence.runId]);
6333
+ const row = rows[0];
6334
+ if (!row || Number(row.generation) !== fence.generation || String(row.ownerId) !== fence.ownerId) return false;
6335
+ await connection.execute(`UPDATE ${RUN_FENCES_TABLE} SET ${RETIRED_AT} = ${DB_NOW_MS} WHERE ${RUN_ID$1} = ?`, [fence.runId]);
6336
+ return true;
6337
+ });
6338
+ } catch (error) {
6339
+ throw new _mastra_core_error.MastraError({
6340
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "RETIRE_RUN_FENCE", "FAILED"),
6341
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
6342
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
6343
+ details: { runId: fence.runId }
6344
+ }, error);
6345
+ }
6346
+ }
6347
+ /** Runs `write` behind its own fence, otherwise the one in scope for this store. */
6348
+ #fenced(fence, operation, write) {
6349
+ return withRunFence(this.pool, RUN_FENCES_TABLE, (0, _mastra_core_storage.resolveRunFence)(this, fence), operation, write);
6350
+ }
6351
+ /** The fence check for a write that already runs in its own transaction. */
6352
+ async #assertFence(connection, fence, operation) {
6353
+ const resolved = (0, _mastra_core_storage.resolveRunFence)(this, fence);
6354
+ if (resolved) await assertRunFence(connection, RUN_FENCES_TABLE, resolved, operation);
6355
+ }
6121
6356
  async init() {
6122
6357
  await this.operations.createTable({
6123
6358
  tableName: _mastra_core_storage.TABLE_THREADS,
@@ -6131,7 +6366,11 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6131
6366
  tableName: _mastra_core_storage.TABLE_RESOURCES,
6132
6367
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_RESOURCES]
6133
6368
  });
6134
- const omSchema = _mastra_core_storage.OBSERVATIONAL_MEMORY_TABLE_SCHEMA?.[OM_TABLE];
6369
+ await this.operations.createTable({
6370
+ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
6371
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_MEMORY_RUN_FENCES]
6372
+ });
6373
+ const omSchema = withObservationalMemoryLifecycleColumns(_mastra_core_storage.OBSERVATIONAL_MEMORY_TABLE_SCHEMA?.[OM_TABLE]);
6135
6374
  if (omSchema) {
6136
6375
  await this.operations.createTable({
6137
6376
  tableName: OM_TABLE,
@@ -6154,7 +6393,8 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6154
6393
  "isBufferingObservation",
6155
6394
  "isBufferingReflection",
6156
6395
  "lastBufferedAtTokens",
6157
- "lastBufferedAtTime"
6396
+ "lastBufferedAtTime",
6397
+ "supersededBy"
6158
6398
  ]
6159
6399
  });
6160
6400
  }
@@ -6172,6 +6412,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6172
6412
  if (err?.errno !== 1061) throw err;
6173
6413
  }
6174
6414
  }
6415
+ if (omSchema) await this.backfillSupersededBy();
6175
6416
  await this.createDefaultIndexes();
6176
6417
  await this.createCustomIndexes();
6177
6418
  }
@@ -6196,6 +6437,10 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6196
6437
  tableName,
6197
6438
  schema: _mastra_core_storage.TABLE_SCHEMAS[tableName]
6198
6439
  }));
6440
+ statements.push(generateTableSQL({
6441
+ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
6442
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_MEMORY_RUN_FENCES]
6443
+ }));
6199
6444
  for (const idx of MemoryMySQL.getDefaultIndexDefs()) statements.push(generateIndexSQL(idx));
6200
6445
  return statements;
6201
6446
  }
@@ -6214,6 +6459,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6214
6459
  await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)}`);
6215
6460
  await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_THREADS)}`);
6216
6461
  await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_RESOURCES)}`);
6462
+ await this.pool.execute(`DELETE FROM ${RUN_FENCES_TABLE}`);
6217
6463
  try {
6218
6464
  await this.pool.execute(`DELETE FROM ${OM_TABLE_QUOTED}`);
6219
6465
  } catch (err) {
@@ -6394,8 +6640,8 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6394
6640
  }
6395
6641
  return includeMessages;
6396
6642
  }
6397
- async upsertThread(thread) {
6398
- await this.operations.insert({
6643
+ async upsertThread(q, thread) {
6644
+ const statement = prepareStatement({
6399
6645
  tableName: _mastra_core_storage.TABLE_THREADS,
6400
6646
  record: {
6401
6647
  id: thread.id,
@@ -6406,18 +6652,22 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6406
6652
  updatedAt: thread.updatedAt
6407
6653
  }
6408
6654
  });
6655
+ await q.execute(statement.sql, statement.args);
6656
+ }
6657
+ async #readThread(q, threadId, resourceId) {
6658
+ let sql = `SELECT * FROM ${formatTableName(_mastra_core_storage.TABLE_THREADS)} WHERE ${quoteIdentifier("id", "column name")} = ?`;
6659
+ const params = [threadId];
6660
+ if (resourceId !== void 0) {
6661
+ sql += ` AND ${quoteIdentifier("resourceId", "column name")} = ?`;
6662
+ params.push(resourceId);
6663
+ }
6664
+ const [rows] = await q.execute(sql, params);
6665
+ const row = rows[0];
6666
+ return row ? this.mapThread(row) : null;
6409
6667
  }
6410
6668
  async getThreadById({ threadId, resourceId }) {
6411
6669
  try {
6412
- let sql = `SELECT * FROM ${formatTableName(_mastra_core_storage.TABLE_THREADS)} WHERE ${quoteIdentifier("id", "column name")} = ?`;
6413
- const params = [threadId];
6414
- if (resourceId !== void 0) {
6415
- sql += ` AND ${quoteIdentifier("resourceId", "column name")} = ?`;
6416
- params.push(resourceId);
6417
- }
6418
- const [rows] = await this.pool.execute(sql, params);
6419
- const row = rows[0];
6420
- return row ? this.mapThread(row) : null;
6670
+ return await this.#readThread(this.pool, threadId, resourceId);
6421
6671
  } catch (error) {
6422
6672
  throw new _mastra_core_error.MastraError({
6423
6673
  id: "MYSQL_MEMORY_GET_THREAD_BY_ID_FAILED",
@@ -6570,11 +6820,12 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6570
6820
  } : void 0
6571
6821
  });
6572
6822
  }
6573
- async saveThread({ thread }) {
6823
+ async saveThread({ thread, fence }) {
6574
6824
  try {
6575
- await this.upsertThread(thread);
6825
+ await this.#fenced(fence, "saveThread", (q) => this.upsertThread(q, thread));
6576
6826
  return thread;
6577
6827
  } catch (error) {
6828
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
6578
6829
  throw new _mastra_core_error.MastraError({
6579
6830
  id: "MYSQL_MEMORY_SAVE_THREAD_FAILED",
6580
6831
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -6583,36 +6834,39 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6583
6834
  }, error);
6584
6835
  }
6585
6836
  }
6586
- async updateThread({ id, title, metadata }) {
6837
+ async updateThread({ id, title, metadata, fence }) {
6587
6838
  try {
6588
- const existing = await this.getThreadById({ threadId: id });
6589
- if (!existing) throw new _mastra_core_error.MastraError({
6590
- id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "UPDATE_THREAD", "NOT_FOUND"),
6591
- domain: _mastra_core_error.ErrorDomain.STORAGE,
6592
- category: _mastra_core_error.ErrorCategory.USER,
6593
- text: `Thread ${id} not found`,
6594
- details: { threadId: id }
6595
- });
6596
- const mergedMetadata = {
6597
- ...existing.metadata ?? {},
6598
- ...metadata ?? {}
6599
- };
6600
- const updatedAt = /* @__PURE__ */ new Date();
6601
- await this.operations.update({
6602
- tableName: _mastra_core_storage.TABLE_THREADS,
6603
- keys: { id },
6604
- data: {
6839
+ return await this.#fenced(fence, "updateThread", async (q) => {
6840
+ const existing = await this.#readThread(q, id);
6841
+ if (!existing) throw new _mastra_core_error.MastraError({
6842
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "UPDATE_THREAD", "NOT_FOUND"),
6843
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
6844
+ category: _mastra_core_error.ErrorCategory.USER,
6845
+ text: `Thread ${id} not found`,
6846
+ details: { threadId: id }
6847
+ });
6848
+ const mergedMetadata = {
6849
+ ...existing.metadata ?? {},
6850
+ ...metadata ?? {}
6851
+ };
6852
+ const updatedAt = /* @__PURE__ */ new Date();
6853
+ const statement = prepareUpdateStatement({
6854
+ tableName: _mastra_core_storage.TABLE_THREADS,
6855
+ keys: { id },
6856
+ updates: {
6857
+ title: title ?? existing.title,
6858
+ metadata: JSON.stringify(mergedMetadata),
6859
+ updatedAt
6860
+ }
6861
+ });
6862
+ await q.execute(statement.sql, statement.args);
6863
+ return {
6864
+ ...existing,
6605
6865
  title: title ?? existing.title,
6606
- metadata: JSON.stringify(mergedMetadata),
6866
+ metadata: mergedMetadata,
6607
6867
  updatedAt
6608
- }
6868
+ };
6609
6869
  });
6610
- return {
6611
- ...existing,
6612
- title: title ?? existing.title,
6613
- metadata: mergedMetadata,
6614
- updatedAt
6615
- };
6616
6870
  } catch (error) {
6617
6871
  if (error instanceof _mastra_core_error.MastraError) throw error;
6618
6872
  throw new _mastra_core_error.MastraError({
@@ -6693,6 +6947,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6693
6947
  const insertedIds = [];
6694
6948
  try {
6695
6949
  await connection.beginTransaction();
6950
+ await this.#assertFence(connection, args.fence, "saveMessages");
6696
6951
  for (const message of messages) {
6697
6952
  if (!message.threadId) throw new _mastra_core_error.MastraError({
6698
6953
  id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "SAVE_MESSAGES", "INVALID_INPUT"),
@@ -6786,18 +7041,29 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6786
7041
  if (update.threadId && update.threadId !== current.threadId && current.threadId) affectedThreads.set(current.threadId, now);
6787
7042
  }
6788
7043
  if (!updates.length) return existing;
6789
- await this.operations.batchUpdate({
6790
- tableName: _mastra_core_storage.TABLE_MESSAGES,
6791
- items: updates
6792
- });
6793
- for (const [threadId, timestamp] of affectedThreads.entries()) await this.operations.update({
6794
- tableName: _mastra_core_storage.TABLE_THREADS,
6795
- keys: { id: threadId },
6796
- data: { updatedAt: timestamp }
7044
+ await inTransaction(this.pool, async (connection) => {
7045
+ await this.#assertFence(connection, args.fence, "updateMessages");
7046
+ for (const { keys, data } of updates) {
7047
+ const statement = prepareUpdateStatement({
7048
+ tableName: _mastra_core_storage.TABLE_MESSAGES,
7049
+ updates: data,
7050
+ keys
7051
+ });
7052
+ await connection.execute(statement.sql, statement.args);
7053
+ }
7054
+ for (const [threadId, timestamp] of affectedThreads.entries()) {
7055
+ const statement = prepareUpdateStatement({
7056
+ tableName: _mastra_core_storage.TABLE_THREADS,
7057
+ updates: { updatedAt: timestamp },
7058
+ keys: { id: threadId }
7059
+ });
7060
+ await connection.execute(statement.sql, statement.args);
7061
+ }
6797
7062
  });
6798
7063
  const { messages: updated } = await this.listMessagesById({ messageIds: messages.map((m) => m.id) });
6799
7064
  return updated;
6800
7065
  } catch (error) {
7066
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
6801
7067
  throw new _mastra_core_error.MastraError({
6802
7068
  id: "MYSQL_MEMORY_UPDATE_MESSAGES_FAILED",
6803
7069
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -6805,19 +7071,22 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6805
7071
  }, error);
6806
7072
  }
6807
7073
  }
6808
- async deleteMessages(messageIds) {
7074
+ async deleteMessages(messageIds, options) {
6809
7075
  if (!messageIds.length) return;
6810
7076
  try {
6811
- const placeholders = messageIds.map(() => "?").join(", ");
6812
- const [rows] = await this.pool.execute(`SELECT DISTINCT thread_id FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)} WHERE id IN (${placeholders})`, messageIds);
6813
- const threadIds = rows.map((row) => row.thread_id).filter((threadId) => Boolean(threadId));
6814
- await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)} WHERE id IN (${placeholders})`, messageIds);
6815
- if (threadIds.length) {
6816
- const threadPlaceholders = threadIds.map(() => "?").join(", ");
6817
- const timestamp = transformToSqlValue(/* @__PURE__ */ new Date());
6818
- await this.pool.execute(`UPDATE ${formatTableName(_mastra_core_storage.TABLE_THREADS)} SET ${quoteIdentifier("updatedAt", "column name")} = ? WHERE id IN (${threadPlaceholders})`, [timestamp, ...threadIds]);
6819
- }
7077
+ await this.#fenced(options?.fence, "deleteMessages", async (q) => {
7078
+ const placeholders = messageIds.map(() => "?").join(", ");
7079
+ const [rows] = await q.execute(`SELECT DISTINCT thread_id FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)} WHERE id IN (${placeholders})`, messageIds);
7080
+ const threadIds = rows.map((row) => row.thread_id).filter((threadId) => Boolean(threadId));
7081
+ await q.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)} WHERE id IN (${placeholders})`, messageIds);
7082
+ if (threadIds.length) {
7083
+ const threadPlaceholders = threadIds.map(() => "?").join(", ");
7084
+ const timestamp = transformToSqlValue(/* @__PURE__ */ new Date());
7085
+ await q.execute(`UPDATE ${formatTableName(_mastra_core_storage.TABLE_THREADS)} SET ${quoteIdentifier("updatedAt", "column name")} = ? WHERE id IN (${threadPlaceholders})`, [timestamp, ...threadIds]);
7086
+ }
7087
+ });
6820
7088
  } catch (error) {
7089
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
6821
7090
  throw new _mastra_core_error.MastraError({
6822
7091
  id: "MYSQL_MEMORY_DELETE_MESSAGES_FAILED",
6823
7092
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -7216,19 +7485,23 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7216
7485
  }, error);
7217
7486
  }
7218
7487
  }
7488
+ async #insertResource(q, resource) {
7489
+ const metadataValue = resource.metadata === void 0 || resource.metadata === null ? null : JSON.stringify(resource.metadata);
7490
+ const statement = prepareStatement({
7491
+ tableName: _mastra_core_storage.TABLE_RESOURCES,
7492
+ record: {
7493
+ id: resource.id,
7494
+ workingMemory: resource.workingMemory ?? null,
7495
+ metadata: metadataValue,
7496
+ createdAt: resource.createdAt ?? /* @__PURE__ */ new Date(),
7497
+ updatedAt: resource.updatedAt ?? /* @__PURE__ */ new Date()
7498
+ }
7499
+ });
7500
+ await q.execute(statement.sql, statement.args);
7501
+ }
7219
7502
  async saveResource({ resource }) {
7220
7503
  try {
7221
- const metadataValue = resource.metadata === void 0 || resource.metadata === null ? null : JSON.stringify(resource.metadata);
7222
- await this.operations.insert({
7223
- tableName: _mastra_core_storage.TABLE_RESOURCES,
7224
- record: {
7225
- id: resource.id,
7226
- workingMemory: resource.workingMemory ?? null,
7227
- metadata: metadataValue,
7228
- createdAt: resource.createdAt ?? /* @__PURE__ */ new Date(),
7229
- updatedAt: resource.updatedAt ?? /* @__PURE__ */ new Date()
7230
- }
7231
- });
7504
+ await this.#insertResource(this.pool, resource);
7232
7505
  return resource;
7233
7506
  } catch (error) {
7234
7507
  throw new _mastra_core_error.MastraError({
@@ -7239,7 +7512,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7239
7512
  }, error);
7240
7513
  }
7241
7514
  }
7242
- async updateResource({ resourceId, workingMemory, metadata }) {
7515
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
7243
7516
  try {
7244
7517
  const existing = await this.getResourceById({ resourceId });
7245
7518
  if (!existing) {
@@ -7250,22 +7523,24 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7250
7523
  createdAt: /* @__PURE__ */ new Date(),
7251
7524
  updatedAt: /* @__PURE__ */ new Date()
7252
7525
  };
7253
- return await this.saveResource({ resource: newResource });
7526
+ await this.#fenced(fence, "updateResource", (q) => this.#insertResource(q, newResource));
7527
+ return newResource;
7254
7528
  }
7255
7529
  const mergedMetadata = metadata !== void 0 ? {
7256
7530
  ...existing.metadata ?? {},
7257
7531
  ...metadata ?? {}
7258
7532
  } : existing.metadata;
7259
7533
  const metadataValue = metadata !== void 0 ? metadata === null ? null : JSON.stringify(mergedMetadata ?? {}) : void 0;
7260
- await this.operations.update({
7534
+ const statement = prepareUpdateStatement({
7261
7535
  tableName: _mastra_core_storage.TABLE_RESOURCES,
7262
7536
  keys: { id: resourceId },
7263
- data: {
7537
+ updates: {
7264
7538
  ...workingMemory !== void 0 ? { workingMemory } : {},
7265
7539
  ...metadataValue !== void 0 ? { metadata: metadataValue } : {},
7266
7540
  updatedAt: /* @__PURE__ */ new Date()
7267
7541
  }
7268
7542
  });
7543
+ await this.#fenced(fence, "updateResource", (q) => q.execute(statement.sql, statement.args));
7269
7544
  const updated = await this.getResourceById({ resourceId });
7270
7545
  if (!updated) throw new _mastra_core_error.MastraError({
7271
7546
  id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "UPDATE_RESOURCE", "NOT_FOUND_AFTER_UPDATE"),
@@ -7288,7 +7563,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7288
7563
  async getObservationalMemory(threadId, resourceId) {
7289
7564
  try {
7290
7565
  const lookupKey = this.getOMKey(threadId, resourceId);
7291
- const [rows] = await this.pool.execute(`SELECT * FROM ${OM_TABLE_QUOTED} WHERE ${omCol("lookupKey")} = ? ORDER BY ${omCol("generationCount")} DESC LIMIT 1`, [lookupKey]);
7566
+ const [rows] = await this.pool.execute(`SELECT * FROM ${OM_TABLE_QUOTED} WHERE ${omCol("lookupKey")} = ? ORDER BY ${OM_HEAD_ORDER} LIMIT 1`, [lookupKey]);
7292
7567
  if (!rows || rows.length === 0) return null;
7293
7568
  return this.parseOMRow(rows[0]);
7294
7569
  } catch (error) {
@@ -7350,32 +7625,11 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7350
7625
  }
7351
7626
  async initializeObservationalMemory(input) {
7352
7627
  try {
7628
+ const lookupKey = this.getOMKey(input.threadId, input.resourceId);
7353
7629
  const id = globalThis.crypto.randomUUID();
7354
7630
  const now = /* @__PURE__ */ new Date();
7355
- const lookupKey = this.getOMKey(input.threadId, input.resourceId);
7356
- const record = {
7357
- id,
7358
- scope: input.scope,
7359
- threadId: input.threadId,
7360
- resourceId: input.resourceId,
7361
- createdAt: now,
7362
- updatedAt: now,
7363
- lastObservedAt: void 0,
7364
- originType: "initial",
7365
- generationCount: 0,
7366
- activeObservations: "",
7367
- totalTokensObserved: 0,
7368
- observationTokenCount: 0,
7369
- pendingMessageTokens: 0,
7370
- isReflecting: false,
7371
- isObserving: false,
7372
- isBufferingObservation: false,
7373
- isBufferingReflection: false,
7374
- lastBufferedAtTokens: 0,
7375
- lastBufferedAtTime: null,
7376
- config: input.config,
7377
- observedTimezone: input.observedTimezone
7378
- };
7631
+ const existing = await this.getObservationalMemory(input.threadId, input.resourceId);
7632
+ if (existing) return existing;
7379
7633
  const nowSql = transformToSqlValue(now);
7380
7634
  const cols = [
7381
7635
  "id",
@@ -7401,36 +7655,53 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7401
7655
  "lastBufferedAtTime",
7402
7656
  "observedTimezone",
7403
7657
  "createdAt",
7404
- "updatedAt"
7658
+ "updatedAt",
7659
+ "supersededBy"
7405
7660
  ].map(omCol).join(", ");
7406
- const placeholders = Array.from({ length: 24 }, () => "?").join(", ");
7407
- await this.pool.execute(`INSERT INTO ${OM_TABLE_QUOTED} (${cols}) VALUES (${placeholders})`, [
7408
- id,
7409
- lookupKey,
7410
- input.scope,
7411
- input.resourceId,
7412
- input.threadId || null,
7413
- "",
7414
- null,
7415
- "initial",
7416
- JSON.stringify(input.config),
7417
- 0,
7418
- null,
7419
- null,
7420
- 0,
7421
- 0,
7422
- 0,
7423
- false,
7424
- false,
7425
- false,
7426
- false,
7427
- 0,
7428
- null,
7429
- input.observedTimezone || null,
7430
- nowSql,
7431
- nowSql
7432
- ]);
7433
- return record;
7661
+ const placeholders = Array.from({ length: 25 }, () => "?").join(", ");
7662
+ const lockName = `mastra_om_init:${(0, crypto$1.createHash)("sha256").update(lookupKey).digest("hex").slice(0, 40)}`;
7663
+ const connection = await this.pool.getConnection();
7664
+ try {
7665
+ const [lockRows] = await connection.query("SELECT GET_LOCK(?, ?) AS acquired", [lockName, OM_INIT_LOCK_TIMEOUT_SECONDS]);
7666
+ if (Number(lockRows[0]?.acquired) !== 1) throw new Error(`Timed out waiting for the observational memory initialization lock for ${lookupKey}`);
7667
+ try {
7668
+ const [liveRows] = await connection.query(`SELECT 1 FROM ${OM_TABLE_QUOTED} WHERE ${omCol("lookupKey")} = ? AND ${omCol("supersededBy")} IS NULL LIMIT 1`, [lookupKey]);
7669
+ if (liveRows.length === 0) await connection.execute(`INSERT INTO ${OM_TABLE_QUOTED} (${cols}) VALUES (${placeholders})`, [
7670
+ id,
7671
+ lookupKey,
7672
+ input.scope,
7673
+ input.resourceId,
7674
+ input.threadId || null,
7675
+ "",
7676
+ null,
7677
+ "initial",
7678
+ JSON.stringify(input.config),
7679
+ 0,
7680
+ null,
7681
+ null,
7682
+ 0,
7683
+ 0,
7684
+ 0,
7685
+ false,
7686
+ false,
7687
+ false,
7688
+ false,
7689
+ 0,
7690
+ null,
7691
+ input.observedTimezone || null,
7692
+ nowSql,
7693
+ nowSql,
7694
+ null
7695
+ ]);
7696
+ } finally {
7697
+ await connection.query("SELECT RELEASE_LOCK(?)", [lockName]);
7698
+ }
7699
+ } finally {
7700
+ connection.release();
7701
+ }
7702
+ const head = await this.getObservationalMemory(input.threadId, input.resourceId);
7703
+ if (!head) throwOMNotFound(id, "INITIALIZE_OBSERVATIONAL_MEMORY");
7704
+ return head;
7434
7705
  } catch (error) {
7435
7706
  rethrowOrWrapOM(error, "", "INITIALIZE_OBSERVATIONAL_MEMORY", {
7436
7707
  threadId: input.threadId,
@@ -7439,119 +7710,153 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7439
7710
  }
7440
7711
  }
7441
7712
  async updateActiveObservations(input) {
7713
+ assertActiveObservationsApplied(await this.commitActiveObservations(input), input.id);
7714
+ }
7715
+ async commitActiveObservations(input) {
7442
7716
  try {
7443
- const now = /* @__PURE__ */ new Date();
7444
7717
  const observedMessageIdsJson = input.observedMessageIds ? JSON.stringify(input.observedMessageIds) : null;
7445
- const [result] = await this.pool.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7446
- ${omCol("activeObservations")} = ?,
7447
- ${omCol("lastObservedAt")} = ?,
7448
- ${omCol("pendingMessageTokens")} = 0,
7449
- ${omCol("observationTokenCount")} = ?,
7450
- ${omCol("totalTokensObserved")} = ${omCol("totalTokensObserved")} + ?,
7451
- ${omCol("observedMessageIds")} = ?,
7452
- ${omCol("updatedAt")} = ?
7453
- WHERE ${omCol("id")} = ?`, [
7454
- input.observations,
7455
- transformToSqlValue(input.lastObservedAt),
7456
- input.tokenCount,
7457
- input.tokenCount,
7458
- observedMessageIdsJson,
7459
- transformToSqlValue(now),
7460
- input.id
7461
- ]);
7462
- if (result.affectedRows === 0) throwOMNotFound(input.id, "UPDATE_ACTIVE_OBSERVATIONS");
7718
+ return await this.withOMTransaction(async (connection) => {
7719
+ const row = await this.lockOMRow(connection, input.id);
7720
+ if (!row) throwOMNotFound(input.id, "UPDATE_ACTIVE_OBSERVATIONS");
7721
+ if (row.supersededBy) return {
7722
+ applied: false,
7723
+ reason: "retired"
7724
+ };
7725
+ if (input.expectedActiveObservations !== void 0 && input.expectedActiveObservations !== (row.activeObservations || "")) return {
7726
+ applied: false,
7727
+ reason: "conflict"
7728
+ };
7729
+ const lastObservedAt = maxObservationCursor(parseDateTime(row.lastObservedAt), input.lastObservedAt);
7730
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7731
+ ${omCol("activeObservations")} = ?,
7732
+ ${omCol("lastObservedAt")} = ?,
7733
+ ${omCol("pendingMessageTokens")} = 0,
7734
+ ${omCol("observationTokenCount")} = ?,
7735
+ ${omCol("totalTokensObserved")} = ${omCol("totalTokensObserved")} + ?,
7736
+ ${omCol("observedMessageIds")} = ?,
7737
+ ${omCol("updatedAt")} = ?
7738
+ WHERE ${omCol("id")} = ?`, [
7739
+ input.observations,
7740
+ transformToSqlValue(lastObservedAt),
7741
+ input.tokenCount,
7742
+ input.tokenCount,
7743
+ observedMessageIdsJson,
7744
+ transformToSqlValue(/* @__PURE__ */ new Date()),
7745
+ input.id
7746
+ ]);
7747
+ return { applied: true };
7748
+ }, "commitActiveObservations");
7463
7749
  } catch (error) {
7464
7750
  rethrowOrWrapOM(error, input.id, "UPDATE_ACTIVE_OBSERVATIONS");
7465
7751
  }
7466
7752
  }
7467
7753
  async createReflectionGeneration(input) {
7468
7754
  try {
7469
- const id = globalThis.crypto.randomUUID();
7470
- const now = /* @__PURE__ */ new Date();
7471
- const lookupKey = this.getOMKey(input.currentRecord.threadId, input.currentRecord.resourceId);
7472
- const record = {
7473
- id,
7474
- scope: input.currentRecord.scope,
7475
- threadId: input.currentRecord.threadId,
7476
- resourceId: input.currentRecord.resourceId,
7477
- createdAt: now,
7478
- updatedAt: now,
7479
- lastObservedAt: input.currentRecord.lastObservedAt,
7480
- originType: "reflection",
7481
- generationCount: input.currentRecord.generationCount + 1,
7482
- activeObservations: input.reflection,
7483
- totalTokensObserved: input.currentRecord.totalTokensObserved,
7484
- observationTokenCount: input.tokenCount,
7485
- pendingMessageTokens: 0,
7486
- isReflecting: false,
7487
- isObserving: false,
7488
- isBufferingObservation: false,
7489
- isBufferingReflection: false,
7490
- lastBufferedAtTokens: 0,
7491
- lastBufferedAtTime: null,
7492
- config: input.currentRecord.config,
7493
- metadata: input.currentRecord.metadata,
7494
- observedTimezone: input.currentRecord.observedTimezone
7495
- };
7496
- const nowSql = transformToSqlValue(now);
7497
- const cols = [
7498
- "id",
7499
- "lookupKey",
7500
- "scope",
7501
- "resourceId",
7502
- "threadId",
7503
- "activeObservations",
7504
- "activeObservationsPendingUpdate",
7505
- "originType",
7506
- "config",
7507
- "generationCount",
7508
- "lastObservedAt",
7509
- "lastReflectionAt",
7510
- "pendingMessageTokens",
7511
- "totalTokensObserved",
7512
- "observationTokenCount",
7513
- "isObserving",
7514
- "isReflecting",
7515
- "isBufferingObservation",
7516
- "isBufferingReflection",
7517
- "lastBufferedAtTokens",
7518
- "lastBufferedAtTime",
7519
- "observedTimezone",
7520
- "createdAt",
7521
- "updatedAt"
7522
- ].map(omCol).join(", ");
7523
- const placeholders = Array.from({ length: 24 }, () => "?").join(", ");
7524
- await this.pool.execute(`INSERT INTO ${OM_TABLE_QUOTED} (${cols}) VALUES (${placeholders})`, [
7525
- id,
7526
- lookupKey,
7527
- record.scope,
7528
- record.resourceId,
7529
- record.threadId || null,
7530
- input.reflection,
7531
- null,
7532
- "reflection",
7533
- JSON.stringify(record.config),
7534
- input.currentRecord.generationCount + 1,
7535
- record.lastObservedAt ? transformToSqlValue(record.lastObservedAt) : null,
7536
- nowSql,
7537
- record.pendingMessageTokens,
7538
- record.totalTokensObserved,
7539
- record.observationTokenCount,
7540
- false,
7541
- false,
7542
- false,
7543
- false,
7544
- 0,
7545
- null,
7546
- record.observedTimezone || null,
7547
- nowSql,
7548
- nowSql
7549
- ]);
7550
- return record;
7755
+ const { currentRecord } = input;
7756
+ return await this.withOMTransaction(async (connection) => {
7757
+ const row = await this.lockOMRow(connection, currentRecord.id);
7758
+ if (!row) return currentRecord;
7759
+ if (row.supersededBy) return this.parseOMRow(await this.resolveLiveOMRow(connection, row));
7760
+ const stored = this.parseOMRow(row);
7761
+ const plan = planReflectionGenerationText({
7762
+ storedObservations: stored.activeObservations,
7763
+ storedObservationTokenCount: stored.observationTokenCount,
7764
+ snapshotObservations: currentRecord.activeObservations,
7765
+ snapshotObservationTokenCount: currentRecord.observationTokenCount,
7766
+ reflection: input.reflection,
7767
+ tokenCount: input.tokenCount
7768
+ });
7769
+ if (!plan) return stored;
7770
+ return this.rollOverOMRow(connection, stored.id, plan.observations, plan.tokenCount, input.newRecordId, false);
7771
+ }, "createReflectionGeneration");
7551
7772
  } catch (error) {
7552
7773
  rethrowOrWrapOM(error, input.currentRecord.id, "CREATE_REFLECTION_GENERATION", { currentRecordId: input.currentRecord.id });
7553
7774
  }
7554
7775
  }
7776
+ async #insertReflectionGeneration(q, input) {
7777
+ const id = globalThis.crypto.randomUUID();
7778
+ const now = /* @__PURE__ */ new Date();
7779
+ const lookupKey = this.getOMKey(input.currentRecord.threadId, input.currentRecord.resourceId);
7780
+ const record = {
7781
+ id,
7782
+ scope: input.currentRecord.scope,
7783
+ threadId: input.currentRecord.threadId,
7784
+ resourceId: input.currentRecord.resourceId,
7785
+ createdAt: now,
7786
+ updatedAt: now,
7787
+ lastObservedAt: input.currentRecord.lastObservedAt,
7788
+ originType: "reflection",
7789
+ generationCount: input.currentRecord.generationCount + 1,
7790
+ activeObservations: input.reflection,
7791
+ totalTokensObserved: input.currentRecord.totalTokensObserved,
7792
+ observationTokenCount: input.tokenCount,
7793
+ pendingMessageTokens: 0,
7794
+ isReflecting: false,
7795
+ isObserving: false,
7796
+ isBufferingObservation: false,
7797
+ isBufferingReflection: false,
7798
+ lastBufferedAtTokens: 0,
7799
+ lastBufferedAtTime: null,
7800
+ config: input.currentRecord.config,
7801
+ metadata: input.currentRecord.metadata,
7802
+ observedTimezone: input.currentRecord.observedTimezone
7803
+ };
7804
+ const nowSql = transformToSqlValue(now);
7805
+ const cols = [
7806
+ "id",
7807
+ "lookupKey",
7808
+ "scope",
7809
+ "resourceId",
7810
+ "threadId",
7811
+ "activeObservations",
7812
+ "activeObservationsPendingUpdate",
7813
+ "originType",
7814
+ "config",
7815
+ "generationCount",
7816
+ "lastObservedAt",
7817
+ "lastReflectionAt",
7818
+ "pendingMessageTokens",
7819
+ "totalTokensObserved",
7820
+ "observationTokenCount",
7821
+ "isObserving",
7822
+ "isReflecting",
7823
+ "isBufferingObservation",
7824
+ "isBufferingReflection",
7825
+ "lastBufferedAtTokens",
7826
+ "lastBufferedAtTime",
7827
+ "observedTimezone",
7828
+ "createdAt",
7829
+ "updatedAt"
7830
+ ].map(omCol).join(", ");
7831
+ const placeholders = Array.from({ length: 24 }, () => "?").join(", ");
7832
+ await q.execute(`INSERT INTO ${OM_TABLE_QUOTED} (${cols}) VALUES (${placeholders})`, [
7833
+ id,
7834
+ lookupKey,
7835
+ record.scope,
7836
+ record.resourceId,
7837
+ record.threadId || null,
7838
+ input.reflection,
7839
+ null,
7840
+ "reflection",
7841
+ JSON.stringify(record.config),
7842
+ input.currentRecord.generationCount + 1,
7843
+ record.lastObservedAt ? transformToSqlValue(record.lastObservedAt) : null,
7844
+ nowSql,
7845
+ record.pendingMessageTokens,
7846
+ record.totalTokensObserved,
7847
+ record.observationTokenCount,
7848
+ false,
7849
+ false,
7850
+ false,
7851
+ false,
7852
+ 0,
7853
+ null,
7854
+ record.observedTimezone || null,
7855
+ nowSql,
7856
+ nowSql
7857
+ ]);
7858
+ return record;
7859
+ }
7555
7860
  async setReflectingFlag(id, isReflecting) {
7556
7861
  await this.updateOMFlag(id, "isReflecting", isReflecting, "SET_REFLECTING_FLAG");
7557
7862
  }
@@ -7560,20 +7865,22 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7560
7865
  }
7561
7866
  async setBufferingObservationFlag(id, isBuffering, lastBufferedAtTokens) {
7562
7867
  try {
7563
- const nowSql = transformToSqlValue(/* @__PURE__ */ new Date());
7564
- const sets = [
7565
- `${omCol("isBufferingObservation")} = ?`,
7566
- ...lastBufferedAtTokens !== void 0 ? [`${omCol("lastBufferedAtTokens")} = ?`] : [],
7567
- `${omCol("updatedAt")} = ?`
7568
- ].join(", ");
7569
- const params = [
7570
- isBuffering,
7571
- ...lastBufferedAtTokens !== void 0 ? [lastBufferedAtTokens] : [],
7572
- nowSql,
7573
- id
7574
- ];
7575
- const [result] = await this.pool.execute(`UPDATE ${OM_TABLE_QUOTED} SET ${sets} WHERE ${omCol("id")} = ?`, params);
7576
- if (result.affectedRows === 0) throwOMNotFound(id, "SET_BUFFERING_OBSERVATION_FLAG");
7868
+ await this.withOMTransaction(async (connection) => {
7869
+ const row = await this.lockOMRow(connection, id);
7870
+ if (!row) throwOMNotFound(id, "SET_BUFFERING_OBSERVATION_FLAG");
7871
+ const target = await this.resolveLiveOMRow(connection, row);
7872
+ const sets = [
7873
+ `${omCol("isBufferingObservation")} = ?`,
7874
+ ...lastBufferedAtTokens !== void 0 ? [`${omCol("lastBufferedAtTokens")} = ?`] : [],
7875
+ `${omCol("updatedAt")} = ?`
7876
+ ].join(", ");
7877
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET ${sets} WHERE ${omCol("id")} = ?`, [
7878
+ isBuffering,
7879
+ ...lastBufferedAtTokens !== void 0 ? [lastBufferedAtTokens] : [],
7880
+ transformToSqlValue(/* @__PURE__ */ new Date()),
7881
+ target.id
7882
+ ]);
7883
+ });
7577
7884
  } catch (error) {
7578
7885
  rethrowOrWrapOM(error, id, "SET_BUFFERING_OBSERVATION_FLAG");
7579
7886
  }
@@ -7594,83 +7901,78 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7594
7901
  }
7595
7902
  async setPendingMessageTokens(id, tokenCount) {
7596
7903
  try {
7597
- const [result] = await this.pool.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7598
- ${omCol("pendingMessageTokens")} = ?,
7599
- ${omCol("updatedAt")} = ?
7600
- WHERE ${omCol("id")} = ?`, [
7601
- tokenCount,
7602
- transformToSqlValue(/* @__PURE__ */ new Date()),
7603
- id
7604
- ]);
7605
- if (result.affectedRows === 0) throwOMNotFound(id, "SET_PENDING_MESSAGE_TOKENS");
7904
+ await this.withOMTransaction(async (connection) => {
7905
+ const row = await this.lockOMRow(connection, id);
7906
+ if (!row) throwOMNotFound(id, "SET_PENDING_MESSAGE_TOKENS");
7907
+ const target = await this.resolveLiveOMRow(connection, row);
7908
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7909
+ ${omCol("pendingMessageTokens")} = ?,
7910
+ ${omCol("updatedAt")} = ?
7911
+ WHERE ${omCol("id")} = ?`, [
7912
+ tokenCount,
7913
+ transformToSqlValue(/* @__PURE__ */ new Date()),
7914
+ target.id
7915
+ ]);
7916
+ });
7606
7917
  } catch (error) {
7607
7918
  rethrowOrWrapOM(error, id, "SET_PENDING_MESSAGE_TOKENS");
7608
7919
  }
7609
7920
  }
7610
7921
  async updateBufferedObservations(input) {
7611
- try {
7612
- const connection = await this.pool.getConnection();
7613
- try {
7614
- await connection.beginTransaction();
7615
- const nowSql = transformToSqlValue(/* @__PURE__ */ new Date());
7616
- const [currentRows] = await connection.execute(`SELECT ${omCol("bufferedObservationChunks")} FROM ${OM_TABLE_QUOTED} WHERE ${omCol("id")} = ? FOR UPDATE`, [input.id]);
7617
- if (!currentRows || currentRows.length === 0) throwOMNotFound(input.id, "UPDATE_BUFFERED_OBSERVATIONS");
7618
- const existingChunks = parseBufferedChunks(currentRows[0].bufferedObservationChunks);
7619
- if (existingChunks.some((existing) => existing.cycleId === input.chunk.cycleId)) {
7620
- await connection.commit();
7621
- return;
7622
- }
7623
- const newChunk = {
7624
- id: `ombuf-${globalThis.crypto.randomUUID()}`,
7625
- cycleId: input.chunk.cycleId,
7626
- observations: input.chunk.observations,
7627
- tokenCount: input.chunk.tokenCount,
7628
- messageIds: input.chunk.messageIds,
7629
- messageTokens: input.chunk.messageTokens,
7630
- lastObservedAt: input.chunk.lastObservedAt,
7631
- createdAt: /* @__PURE__ */ new Date(),
7632
- suggestedContinuation: input.chunk.suggestedContinuation,
7633
- currentTask: input.chunk.currentTask,
7634
- threadTitle: input.chunk.threadTitle,
7635
- extractedValues: input.chunk.extractedValues,
7636
- extractionFailures: input.chunk.extractionFailures
7922
+ await this.appendBufferedObservations(input);
7923
+ }
7924
+ async appendBufferedObservations(input) {
7925
+ try {
7926
+ const newChunk = {
7927
+ id: `ombuf-${globalThis.crypto.randomUUID()}`,
7928
+ cycleId: input.chunk.cycleId,
7929
+ observations: input.chunk.observations,
7930
+ tokenCount: input.chunk.tokenCount,
7931
+ messageIds: input.chunk.messageIds,
7932
+ messageTokens: input.chunk.messageTokens,
7933
+ lastObservedAt: input.chunk.lastObservedAt,
7934
+ createdAt: /* @__PURE__ */ new Date(),
7935
+ suggestedContinuation: input.chunk.suggestedContinuation,
7936
+ currentTask: input.chunk.currentTask,
7937
+ threadTitle: input.chunk.threadTitle,
7938
+ extractedValues: input.chunk.extractedValues,
7939
+ extractionFailures: input.chunk.extractionFailures
7940
+ };
7941
+ return await this.withOMTransaction(async (connection) => {
7942
+ const row = await this.lockOMRow(connection, input.id);
7943
+ if (!row) throwOMNotFound(input.id, "UPDATE_BUFFERED_OBSERVATIONS");
7944
+ const target = await this.resolveLiveOMRow(connection, row);
7945
+ const existingChunks = parseBufferedChunks(target.bufferedObservationChunks);
7946
+ if (existingChunks.some((existing) => existing.cycleId === input.chunk.cycleId) || isBufferedChunkCoveredByCursor(input.chunk.lastObservedAt, parseDateTime(target.lastObservedAt))) return {
7947
+ persisted: false,
7948
+ recordId: target.id
7637
7949
  };
7638
- const newChunks = [...existingChunks, newChunk];
7639
- const lastBufferedAtTime = input.lastBufferedAtTime ? transformToSqlValue(input.lastBufferedAtTime) : null;
7640
- const [result] = await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7950
+ const lastBufferedAtTime = maxObservationCursor(parseDateTime(target.lastBufferedAtTime), input.lastBufferedAtTime ?? void 0);
7951
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7641
7952
  ${omCol("bufferedObservationChunks")} = ?,
7642
- ${omCol("lastBufferedAtTime")} = COALESCE(?, ${omCol("lastBufferedAtTime")}),
7953
+ ${omCol("lastBufferedAtTime")} = ?,
7643
7954
  ${omCol("updatedAt")} = ?
7644
7955
  WHERE ${omCol("id")} = ?`, [
7645
- JSON.stringify(newChunks),
7646
- lastBufferedAtTime,
7647
- nowSql,
7648
- input.id
7956
+ JSON.stringify([...existingChunks, newChunk]),
7957
+ transformToSqlValue(lastBufferedAtTime),
7958
+ transformToSqlValue(/* @__PURE__ */ new Date()),
7959
+ target.id
7649
7960
  ]);
7650
- if (result.affectedRows === 0) throwOMNotFound(input.id, "UPDATE_BUFFERED_OBSERVATIONS");
7651
- await connection.commit();
7652
- } catch (error) {
7653
- await connection.rollback();
7654
- throw error;
7655
- } finally {
7656
- connection.release();
7657
- }
7961
+ return {
7962
+ persisted: true,
7963
+ recordId: target.id
7964
+ };
7965
+ }, "appendBufferedObservations");
7658
7966
  } catch (error) {
7659
7967
  rethrowOrWrapOM(error, input.id, "UPDATE_BUFFERED_OBSERVATIONS");
7660
7968
  }
7661
7969
  }
7662
7970
  async swapBufferedToActive(input) {
7663
- const connection = await this.pool.getConnection();
7664
7971
  try {
7665
- await connection.beginTransaction();
7666
- const nowSql = transformToSqlValue(/* @__PURE__ */ new Date());
7667
- const [currentRows] = await connection.execute(`SELECT * FROM ${OM_TABLE_QUOTED} WHERE ${omCol("id")} = ? FOR UPDATE`, [input.id]);
7668
- if (!currentRows || currentRows.length === 0) throwOMNotFound(input.id, "SWAP_BUFFERED_TO_ACTIVE");
7669
- const row = currentRows[0];
7670
- const chunks = parseBufferedChunks(row.bufferedObservationChunks);
7671
- if (chunks.length === 0) {
7672
- await connection.commit();
7673
- return {
7972
+ return await this.withOMTransaction(async (connection) => {
7973
+ const row = await this.lockOMRow(connection, input.id);
7974
+ if (!row) throwOMNotFound(input.id, "SWAP_BUFFERED_TO_ACTIVE");
7975
+ const emptyResult = {
7674
7976
  chunksActivated: 0,
7675
7977
  messageTokensActivated: 0,
7676
7978
  observationTokensActivated: 0,
@@ -7678,102 +7980,110 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7678
7980
  activatedCycleIds: [],
7679
7981
  activatedMessageIds: []
7680
7982
  };
7681
- }
7682
- const retentionFloor = input.messageTokensThreshold * (1 - input.activationRatio);
7683
- const targetMessageTokens = Math.max(0, input.currentPendingTokens - retentionFloor);
7684
- let cumulativeMessageTokens = 0;
7685
- let bestOverBoundary = 0;
7686
- let bestOverTokens = 0;
7687
- let bestUnderBoundary = 0;
7688
- let bestUnderTokens = 0;
7689
- for (let i = 0; i < chunks.length; i++) {
7690
- cumulativeMessageTokens += chunks[i].messageTokens ?? 0;
7691
- const boundary = i + 1;
7692
- if (cumulativeMessageTokens >= targetMessageTokens) {
7693
- if (bestOverBoundary === 0 || cumulativeMessageTokens < bestOverTokens) {
7694
- bestOverBoundary = boundary;
7695
- bestOverTokens = cumulativeMessageTokens;
7983
+ if (row.supersededBy) return {
7984
+ ...emptyResult,
7985
+ retired: true
7986
+ };
7987
+ const persistedChunks = parseBufferedChunks(row.bufferedObservationChunks);
7988
+ const refreshedWeights = new Map((Array.isArray(input.bufferedChunks) ? input.bufferedChunks : []).map((c) => [c.id, c.messageTokens]));
7989
+ const chunks = persistedChunks.map((c) => refreshedWeights.has(c.id) ? {
7990
+ ...c,
7991
+ messageTokens: refreshedWeights.get(c.id)
7992
+ } : c);
7993
+ if (chunks.length === 0) return emptyResult;
7994
+ const retentionFloor = input.messageTokensThreshold * (1 - input.activationRatio);
7995
+ const targetMessageTokens = Math.max(0, input.currentPendingTokens - retentionFloor);
7996
+ let cumulativeMessageTokens = 0;
7997
+ let bestOverBoundary = 0;
7998
+ let bestOverTokens = 0;
7999
+ let bestUnderBoundary = 0;
8000
+ let bestUnderTokens = 0;
8001
+ for (let i = 0; i < chunks.length; i++) {
8002
+ cumulativeMessageTokens += chunks[i].messageTokens ?? 0;
8003
+ const boundary = i + 1;
8004
+ if (cumulativeMessageTokens >= targetMessageTokens) {
8005
+ if (bestOverBoundary === 0 || cumulativeMessageTokens < bestOverTokens) {
8006
+ bestOverBoundary = boundary;
8007
+ bestOverTokens = cumulativeMessageTokens;
8008
+ }
8009
+ } else if (cumulativeMessageTokens > bestUnderTokens) {
8010
+ bestUnderBoundary = boundary;
8011
+ bestUnderTokens = cumulativeMessageTokens;
7696
8012
  }
7697
- } else if (cumulativeMessageTokens > bestUnderTokens) {
7698
- bestUnderBoundary = boundary;
7699
- bestUnderTokens = cumulativeMessageTokens;
7700
- }
7701
- }
7702
- const maxOvershoot = retentionFloor * .95;
7703
- const overshoot = bestOverTokens - targetMessageTokens;
7704
- const remainingAfterOver = input.currentPendingTokens - bestOverTokens;
7705
- const remainingAfterUnder = input.currentPendingTokens - bestUnderTokens;
7706
- const minRemaining = Math.min(1e3, retentionFloor);
7707
- let chunksToActivate;
7708
- if (input.forceMaxActivation && bestOverBoundary > 0 && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
7709
- else if (bestOverBoundary > 0 && overshoot <= maxOvershoot && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
7710
- else if (bestUnderBoundary > 0 && remainingAfterUnder >= minRemaining) chunksToActivate = bestUnderBoundary;
7711
- else if (bestOverBoundary > 0) chunksToActivate = bestOverBoundary;
7712
- else chunksToActivate = 1;
7713
- const activatedChunks = chunks.slice(0, chunksToActivate);
7714
- const remainingChunks = chunks.slice(chunksToActivate);
7715
- const activatedContent = activatedChunks.map((c) => c.observations).join("\n\n");
7716
- const activatedTokens = activatedChunks.reduce((sum, c) => sum + c.tokenCount, 0);
7717
- const activatedMessageTokens = activatedChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
7718
- const activatedMessageCount = activatedChunks.reduce((sum, c) => sum + c.messageIds.length, 0);
7719
- const activatedCycleIds = activatedChunks.map((c) => c.cycleId).filter((cid) => !!cid);
7720
- const activatedMessageIds = activatedChunks.flatMap((c) => c.messageIds ?? []);
7721
- const latestChunk = activatedChunks[activatedChunks.length - 1];
7722
- const lastObservedAtSql = transformToSqlValue(input.lastObservedAt ?? (latestChunk?.lastObservedAt ? new Date(latestChunk.lastObservedAt) : /* @__PURE__ */ new Date()));
7723
- const existingActive = row.activeObservations || "";
7724
- const existingTokenCount = Number(row.observationTokenCount || 0);
7725
- const newActive = existingActive ? `${existingActive}\n\n${activatedContent}` : activatedContent;
7726
- const newTokenCount = existingTokenCount + activatedTokens;
7727
- const existingPending = Number(row.pendingMessageTokens || 0);
7728
- const newPending = Math.max(0, existingPending - activatedMessageTokens);
7729
- await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7730
- ${omCol("activeObservations")} = ?,
7731
- ${omCol("observationTokenCount")} = ?,
7732
- ${omCol("pendingMessageTokens")} = ?,
7733
- ${omCol("bufferedObservationChunks")} = ?,
7734
- ${omCol("lastObservedAt")} = ?,
7735
- ${omCol("updatedAt")} = ?
7736
- WHERE ${omCol("id")} = ?`, [
7737
- newActive,
7738
- newTokenCount,
7739
- newPending,
7740
- remainingChunks.length > 0 ? JSON.stringify(remainingChunks) : null,
7741
- lastObservedAtSql,
7742
- nowSql,
7743
- input.id
7744
- ]);
7745
- await connection.commit();
7746
- const latestChunkHints = activatedChunks[activatedChunks.length - 1];
7747
- return {
7748
- chunksActivated: activatedChunks.length,
7749
- messageTokensActivated: activatedMessageTokens,
7750
- observationTokensActivated: activatedTokens,
7751
- messagesActivated: activatedMessageCount,
7752
- activatedCycleIds,
7753
- activatedMessageIds,
7754
- observations: activatedContent,
7755
- perChunk: activatedChunks.map((c) => ({
7756
- cycleId: c.cycleId ?? "",
7757
- messageTokens: c.messageTokens ?? 0,
7758
- observationTokens: c.tokenCount,
7759
- messageCount: c.messageIds.length,
7760
- observations: c.observations
7761
- })),
7762
- suggestedContinuation: latestChunkHints?.suggestedContinuation ?? void 0,
7763
- currentTask: latestChunkHints?.currentTask ?? void 0
7764
- };
8013
+ }
8014
+ const maxOvershoot = retentionFloor * .95;
8015
+ const overshoot = bestOverTokens - targetMessageTokens;
8016
+ const remainingAfterOver = input.currentPendingTokens - bestOverTokens;
8017
+ const remainingAfterUnder = input.currentPendingTokens - bestUnderTokens;
8018
+ const minRemaining = Math.min(1e3, retentionFloor);
8019
+ let chunksToActivate;
8020
+ if (input.forceMaxActivation && bestOverBoundary > 0 && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
8021
+ else if (bestOverBoundary > 0 && overshoot <= maxOvershoot && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
8022
+ else if (bestUnderBoundary > 0 && remainingAfterUnder >= minRemaining) chunksToActivate = bestUnderBoundary;
8023
+ else if (bestOverBoundary > 0) chunksToActivate = bestOverBoundary;
8024
+ else chunksToActivate = 1;
8025
+ const activatedChunks = chunks.slice(0, chunksToActivate);
8026
+ const remainingChunks = persistedChunks.slice(chunksToActivate);
8027
+ const activatedContent = activatedChunks.map((c) => c.observations).join("\n\n");
8028
+ const activatedTokens = activatedChunks.reduce((sum, c) => sum + c.tokenCount, 0);
8029
+ const activatedMessageTokens = activatedChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
8030
+ const activatedMessageCount = activatedChunks.reduce((sum, c) => sum + c.messageIds.length, 0);
8031
+ const activatedCycleIds = activatedChunks.map((c) => c.cycleId).filter((cid) => !!cid);
8032
+ const activatedMessageIds = activatedChunks.flatMap((c) => c.messageIds ?? []);
8033
+ const latestChunk = activatedChunks[activatedChunks.length - 1];
8034
+ const derivedLastObservedAt = input.lastObservedAt ?? (latestChunk?.lastObservedAt ? new Date(latestChunk.lastObservedAt) : /* @__PURE__ */ new Date());
8035
+ const lastObservedAt = maxObservationCursor(parseDateTime(row.lastObservedAt), derivedLastObservedAt);
8036
+ const existingActive = row.activeObservations || "";
8037
+ const existingTokenCount = Number(row.observationTokenCount || 0);
8038
+ const newActive = existingActive ? `${existingActive}\n\n${activatedContent}` : activatedContent;
8039
+ const newTokenCount = existingTokenCount + activatedTokens;
8040
+ const existingPending = Number(row.pendingMessageTokens || 0);
8041
+ const newPending = Math.max(0, existingPending - activatedMessageTokens);
8042
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
8043
+ ${omCol("activeObservations")} = ?,
8044
+ ${omCol("observationTokenCount")} = ?,
8045
+ ${omCol("pendingMessageTokens")} = ?,
8046
+ ${omCol("bufferedObservationChunks")} = ?,
8047
+ ${omCol("lastObservedAt")} = ?,
8048
+ ${omCol("updatedAt")} = ?
8049
+ WHERE ${omCol("id")} = ?`, [
8050
+ newActive,
8051
+ newTokenCount,
8052
+ newPending,
8053
+ remainingChunks.length > 0 ? JSON.stringify(remainingChunks) : null,
8054
+ transformToSqlValue(lastObservedAt),
8055
+ transformToSqlValue(/* @__PURE__ */ new Date()),
8056
+ input.id
8057
+ ]);
8058
+ const latestChunkHints = activatedChunks[activatedChunks.length - 1];
8059
+ return {
8060
+ chunksActivated: activatedChunks.length,
8061
+ messageTokensActivated: activatedMessageTokens,
8062
+ observationTokensActivated: activatedTokens,
8063
+ messagesActivated: activatedMessageCount,
8064
+ activatedCycleIds,
8065
+ activatedMessageIds,
8066
+ observations: activatedContent,
8067
+ perChunk: activatedChunks.map((c) => ({
8068
+ cycleId: c.cycleId ?? "",
8069
+ messageTokens: c.messageTokens ?? 0,
8070
+ observationTokens: c.tokenCount,
8071
+ messageCount: c.messageIds.length,
8072
+ observations: c.observations
8073
+ })),
8074
+ suggestedContinuation: latestChunkHints?.suggestedContinuation ?? void 0,
8075
+ currentTask: latestChunkHints?.currentTask ?? void 0
8076
+ };
8077
+ }, "swapBufferedToActive");
7765
8078
  } catch (error) {
7766
- await connection.rollback();
7767
8079
  rethrowOrWrapOM(error, input.id, "SWAP_BUFFERED_TO_ACTIVE");
7768
- } finally {
7769
- connection.release();
7770
8080
  }
7771
8081
  }
7772
8082
  async updateBufferedReflection(input) {
7773
8083
  try {
7774
8084
  const nowSql = transformToSqlValue(/* @__PURE__ */ new Date());
7775
8085
  const br = omCol("bufferedReflection");
7776
- const [result] = await this.pool.execute(`UPDATE ${OM_TABLE_QUOTED} SET
8086
+ const [result] = await this.#fenced(void 0, "updateBufferedReflection", (q) => q.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7777
8087
  ${br} = CASE
7778
8088
  WHEN ${br} IS NOT NULL AND ${br} != ''
7779
8089
  THEN CONCAT(${br}, CHAR(10), CHAR(10), ?)
@@ -7791,7 +8101,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7791
8101
  input.reflectedObservationLineCount,
7792
8102
  nowSql,
7793
8103
  input.id
7794
- ]);
8104
+ ]));
7795
8105
  if (result.affectedRows === 0) throwOMNotFound(input.id, "UPDATE_BUFFERED_REFLECTION");
7796
8106
  } catch (error) {
7797
8107
  rethrowOrWrapOM(error, input.id, "UPDATE_BUFFERED_REFLECTION");
@@ -7799,38 +8109,183 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7799
8109
  }
7800
8110
  async swapBufferedReflectionToActive(input) {
7801
8111
  try {
7802
- const [currentRows] = await this.pool.execute(`SELECT * FROM ${OM_TABLE_QUOTED} WHERE ${omCol("id")} = ?`, [input.currentRecord.id]);
7803
- if (!currentRows || currentRows.length === 0) throwOMNotFound(input.currentRecord.id, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE");
7804
- const row = currentRows[0];
7805
- const bufferedReflection = row.bufferedReflection || "";
7806
- const reflectedLineCount = Number(row.reflectedObservationLineCount || 0);
7807
- if (!bufferedReflection) throw new _mastra_core_error.MastraError({
7808
- id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NO_CONTENT"),
7809
- text: "No buffered reflection to swap",
7810
- domain: _mastra_core_error.ErrorDomain.STORAGE,
7811
- category: _mastra_core_error.ErrorCategory.USER,
7812
- details: { id: input.currentRecord.id }
7813
- });
7814
- const unreflectedContent = (row.activeObservations || "").split("\n").slice(reflectedLineCount).join("\n").trim();
7815
- const newObservations = unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
7816
- const newRecord = await this.createReflectionGeneration({
7817
- currentRecord: input.currentRecord,
7818
- reflection: newObservations,
7819
- tokenCount: input.tokenCount
7820
- });
7821
- const nowSql = transformToSqlValue(/* @__PURE__ */ new Date());
7822
- await this.pool.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7823
- ${omCol("bufferedReflection")} = NULL,
7824
- ${omCol("bufferedReflectionTokens")} = NULL,
7825
- ${omCol("bufferedReflectionInputTokens")} = NULL,
7826
- ${omCol("reflectedObservationLineCount")} = NULL,
7827
- ${omCol("updatedAt")} = ?
7828
- WHERE ${omCol("id")} = ?`, [nowSql, input.currentRecord.id]);
7829
- return newRecord;
8112
+ const { currentRecord } = input;
8113
+ return await this.withOMTransaction(async (connection) => {
8114
+ const row = await this.lockOMRow(connection, currentRecord.id);
8115
+ if (!row) return currentRecord;
8116
+ if (row.supersededBy) return this.parseOMRow(await this.resolveLiveOMRow(connection, row));
8117
+ const stored = this.parseOMRow(row);
8118
+ const bufferedReflection = stored.bufferedReflection || "";
8119
+ if (!bufferedReflection) throw new _mastra_core_error.MastraError({
8120
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NO_CONTENT"),
8121
+ text: "No buffered reflection to swap",
8122
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
8123
+ category: _mastra_core_error.ErrorCategory.USER,
8124
+ details: { id: currentRecord.id }
8125
+ });
8126
+ if (!isAppendOnlySince(stored.activeObservations, currentRecord.activeObservations)) return stored;
8127
+ const reflectedLineCount = stored.reflectedObservationLineCount ?? 0;
8128
+ const unreflectedContent = (stored.activeObservations || "").split("\n").slice(reflectedLineCount).join("\n").trim();
8129
+ const newObservations = unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
8130
+ const tokenCount = input.tokenCount + Math.max(0, (stored.observationTokenCount ?? 0) - (currentRecord.observationTokenCount ?? 0));
8131
+ return this.rollOverOMRow(connection, stored.id, newObservations, tokenCount, input.newRecordId, true);
8132
+ }, "swapBufferedReflectionToActive");
7830
8133
  } catch (error) {
7831
8134
  rethrowOrWrapOM(error, input.currentRecord.id, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE");
7832
8135
  }
7833
8136
  }
8137
+ /**
8138
+ * Runs `fn` in one transaction on a dedicated connection. Every OM lifecycle write locks its
8139
+ * target row by primary key (`SELECT … FOR UPDATE`, a record lock — never a range), so the
8140
+ * only expected lock conflict is a wait; a deadlock or lock-wait timeout (e.g. against the
8141
+ * startup backfill's scan) is retried a bounded number of times.
8142
+ */
8143
+ async withOMTransaction(fn, fencedOperation) {
8144
+ for (let attempt = 1;; attempt++) {
8145
+ const connection = await this.pool.getConnection();
8146
+ try {
8147
+ await connection.beginTransaction();
8148
+ if (fencedOperation) await this.#assertFence(connection, void 0, fencedOperation);
8149
+ const result = await fn(connection);
8150
+ await connection.commit();
8151
+ return result;
8152
+ } catch (error) {
8153
+ await connection.rollback().catch(() => {});
8154
+ if (attempt < OM_MAX_TRANSACTION_ATTEMPTS && isRetryableLockError(error)) continue;
8155
+ throw error;
8156
+ } finally {
8157
+ connection.release();
8158
+ }
8159
+ }
8160
+ }
8161
+ /** Row-locks one OM record by primary key for the rest of the transaction. */
8162
+ async lockOMRow(connection, id) {
8163
+ const [rows] = await connection.execute(`SELECT * FROM ${OM_TABLE_QUOTED} WHERE ${omCol("id")} = ? FOR UPDATE`, [id]);
8164
+ return rows?.[0] ?? null;
8165
+ }
8166
+ /**
8167
+ * The live, row-locked record a lifecycle write aimed at `row` lands on: `row` itself while
8168
+ * live, otherwise the head. The head is found with a plain (non-locking, so no range locks)
8169
+ * canonical head read, then locked by primary key. Under REPEATABLE READ that read can predate
8170
+ * a rollover committed after this transaction started; the locking read sees the latest
8171
+ * committed row, so a head retired since is followed through `supersededBy` (bounded).
8172
+ */
8173
+ async resolveLiveOMRow(connection, row) {
8174
+ if (!row.supersededBy) return row;
8175
+ const [heads] = await connection.execute(`SELECT id FROM ${OM_TABLE_QUOTED} WHERE ${omCol("lookupKey")} = ? ORDER BY ${OM_HEAD_ORDER} LIMIT 1`, [row.lookupKey]);
8176
+ const headId = heads?.[0]?.id ?? row.supersededBy;
8177
+ let current = await this.lockOMRow(connection, headId) ?? row;
8178
+ for (let hop = 0; current.supersededBy; hop++) {
8179
+ const next = hop < OM_MAX_HEAD_HOPS ? await this.lockOMRow(connection, current.supersededBy) : null;
8180
+ if (!next) throw new _mastra_core_error.MastraError({
8181
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "RESOLVE_OBSERVATIONAL_MEMORY_HEAD", "FAILED"),
8182
+ text: `Observational memory record ${row.id} is superseded but no live head was found`,
8183
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
8184
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
8185
+ details: { id: row.id }
8186
+ });
8187
+ current = next;
8188
+ }
8189
+ return current;
8190
+ }
8191
+ /**
8192
+ * Create the next generation from the stored (live, row-locked) record and retire the stored
8193
+ * record in the same transaction. Carried columns are copied in SQL from the stored row, so
8194
+ * nothing round-trips through a client-side date conversion. Buffered chunks move to the new
8195
+ * generation; the cursor, buffering markers, flags, and counters carry over; buffered
8196
+ * reflection state does not (and is cleared on the retired row after a buffered swap).
8197
+ */
8198
+ async rollOverOMRow(connection, storedId, observations, tokenCount, newRecordId, clearBufferedReflection) {
8199
+ const id = newRecordId ?? globalThis.crypto.randomUUID();
8200
+ const nowSql = transformToSqlValue(/* @__PURE__ */ new Date());
8201
+ const c = omCol;
8202
+ await connection.execute(`INSERT INTO ${OM_TABLE_QUOTED} (
8203
+ ${c("id")}, ${c("lookupKey")}, ${c("scope")}, ${c("resourceId")}, ${c("threadId")},
8204
+ ${c("activeObservations")}, ${c("activeObservationsPendingUpdate")},
8205
+ ${c("originType")}, ${c("config")}, ${c("generationCount")}, ${c("lastObservedAt")}, ${c("lastReflectionAt")},
8206
+ ${c("pendingMessageTokens")}, ${c("totalTokensObserved")}, ${c("observationTokenCount")}, ${c("bufferedObservationChunks")},
8207
+ ${c("isObserving")}, ${c("isReflecting")}, ${c("isBufferingObservation")}, ${c("isBufferingReflection")},
8208
+ ${c("lastBufferedAtTokens")}, ${c("lastBufferedAtTime")},
8209
+ ${c("observedTimezone")}, ${c("metadata")}, ${c("supersededBy")}, ${c("createdAt")}, ${c("updatedAt")}
8210
+ )
8211
+ SELECT
8212
+ ?, ${c("lookupKey")}, ${c("scope")}, ${c("resourceId")}, ${c("threadId")},
8213
+ ?, NULL,
8214
+ 'reflection', ${c("config")}, ${c("generationCount")} + 1, ${c("lastObservedAt")}, ?,
8215
+ ${c("pendingMessageTokens")}, ${c("totalTokensObserved")}, ?, ${c("bufferedObservationChunks")},
8216
+ false, false, ${c("isBufferingObservation")}, false,
8217
+ ${c("lastBufferedAtTokens")}, ${c("lastBufferedAtTime")},
8218
+ ${c("observedTimezone")}, ${c("metadata")}, NULL, ?, ?
8219
+ FROM ${OM_TABLE_QUOTED} WHERE ${c("id")} = ?`, [
8220
+ id,
8221
+ observations,
8222
+ nowSql,
8223
+ Math.round(tokenCount),
8224
+ nowSql,
8225
+ nowSql,
8226
+ storedId
8227
+ ]);
8228
+ const [retired] = await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
8229
+ ${c("supersededBy")} = ?,
8230
+ ${c("bufferedObservationChunks")} = NULL,
8231
+ ${clearBufferedReflection ? `${c("bufferedReflection")} = NULL, ${c("bufferedReflectionTokens")} = NULL,
8232
+ ${c("bufferedReflectionInputTokens")} = NULL, ${c("reflectedObservationLineCount")} = NULL,` : ""}
8233
+ ${c("updatedAt")} = ?
8234
+ WHERE ${c("id")} = ? AND ${c("supersededBy")} IS NULL`, [
8235
+ id,
8236
+ nowSql,
8237
+ storedId
8238
+ ]);
8239
+ if (retired.affectedRows !== 1) throw new _mastra_core_error.MastraError({
8240
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "CREATE_REFLECTION_GENERATION", "RETIRE_FAILED"),
8241
+ text: `Failed to retire observational memory record ${storedId}`,
8242
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
8243
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
8244
+ details: { id: storedId }
8245
+ });
8246
+ const inserted = await this.lockOMRow(connection, id);
8247
+ if (!inserted) throwOMNotFound(id, "CREATE_REFLECTION_GENERATION");
8248
+ return this.parseOMRow(inserted);
8249
+ }
8250
+ /**
8251
+ * Retire every live row that sorts after its key's canonical head, marking it superseded by
8252
+ * the head. Rows retired by older adapter versions (which never set `supersededBy`) become
8253
+ * frozen. Only keys with more than one live row are touched; each update embeds the head's
8254
+ * ordering key, so a row created by a concurrent rollover (a newer generation) never matches.
8255
+ * Runs at READ COMMITTED so the scan takes no gap locks.
8256
+ */
8257
+ async backfillSupersededBy() {
8258
+ const [keys] = await this.pool.execute(`SELECT ${omCol("lookupKey")} AS lookupKey FROM ${OM_TABLE_QUOTED}
8259
+ WHERE ${omCol("supersededBy")} IS NULL GROUP BY ${omCol("lookupKey")} HAVING COUNT(*) > 1`);
8260
+ for (const { lookupKey } of keys ?? []) for (let attempt = 1;; attempt++) {
8261
+ const connection = await this.pool.getConnection();
8262
+ try {
8263
+ await connection.query("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
8264
+ await connection.beginTransaction();
8265
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} AS r
8266
+ JOIN (
8267
+ SELECT ${omCol("id")} AS id, ${omCol("generationCount")} AS generationCount, ${omCol("createdAt")} AS createdAt
8268
+ FROM ${OM_TABLE_QUOTED} WHERE ${omCol("lookupKey")} = ? ORDER BY ${OM_HEAD_ORDER} LIMIT 1
8269
+ ) AS h
8270
+ SET r.${omCol("supersededBy")} = h.id
8271
+ WHERE r.${omCol("lookupKey")} = ?
8272
+ AND r.${omCol("supersededBy")} IS NULL
8273
+ AND (
8274
+ r.${omCol("generationCount")} < h.generationCount
8275
+ OR (r.${omCol("generationCount")} = h.generationCount AND r.${omCol("createdAt")} > h.createdAt)
8276
+ OR (r.${omCol("generationCount")} = h.generationCount AND r.${omCol("createdAt")} = h.createdAt AND r.${omCol("id")} > h.id)
8277
+ )`, [lookupKey, lookupKey]);
8278
+ await connection.commit();
8279
+ break;
8280
+ } catch (error) {
8281
+ await connection.rollback().catch(() => {});
8282
+ if (attempt < OM_MAX_TRANSACTION_ATTEMPTS && isRetryableLockError(error)) continue;
8283
+ throw error;
8284
+ } finally {
8285
+ connection.release();
8286
+ }
8287
+ }
8288
+ }
7834
8289
  async updateOMFlag(id, column, value, operation) {
7835
8290
  try {
7836
8291
  const [result] = await this.pool.execute(`UPDATE ${OM_TABLE_QUOTED} SET ${omCol(column)} = ?, ${omCol("updatedAt")} = ? WHERE ${omCol("id")} = ?`, [
@@ -7878,7 +8333,8 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7878
8333
  config: parseJSONColumn(row.config) ?? {},
7879
8334
  metadata: parseJSONColumn(row.metadata),
7880
8335
  observedMessageIds: parseJSONColumn(row.observedMessageIds),
7881
- observedTimezone: row.observedTimezone || void 0
8336
+ observedTimezone: row.observedTimezone || void 0,
8337
+ supersededBy: row.supersededBy ?? null
7882
8338
  };
7883
8339
  }
7884
8340
  };
@@ -10866,6 +11322,26 @@ function parseSnapshot(snapshot) {
10866
11322
  }
10867
11323
  return snapshot;
10868
11324
  }
11325
+ const RUN_OWNERS_TABLE = formatTableName(_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS);
11326
+ const RUN_ID = quoteIdentifier("runId", "column name");
11327
+ const OWNER_ID = quoteIdentifier("ownerId", "column name");
11328
+ const LEASE_EXPIRES_AT = quoteIdentifier("leaseExpiresAt", "column name");
11329
+ const UPDATED_AT = quoteIdentifier("updatedAt", "column name");
11330
+ const RUN_OWNER_COLUMNS = `generation, ${OWNER_ID} AS ownerId, ${LEASE_EXPIRES_AT} AS leaseExpiresAt, ${DB_NOW_MS} AS nowMs`;
11331
+ function toRunOwnershipRecord(runId, row) {
11332
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
11333
+ return {
11334
+ runId,
11335
+ generation: Number(row.generation),
11336
+ ownerId: String(row.ownerId),
11337
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
11338
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
11339
+ };
11340
+ }
11341
+ async function readRunOwner(q, runId) {
11342
+ const [rows] = await q.execute(`SELECT ${RUN_OWNER_COLUMNS} FROM ${RUN_OWNERS_TABLE} WHERE ${RUN_ID} = ?`, [runId]);
11343
+ return rows[0] ? toRunOwnershipRecord(runId, rows[0]) : null;
11344
+ }
10869
11345
  function mapWorkflowRow(row) {
10870
11346
  return {
10871
11347
  workflowName: row.workflow_name,
@@ -10898,6 +11374,9 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
10898
11374
  tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
10899
11375
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT],
10900
11376
  compositePrimaryKey: ["workflow_name", "run_id"]
11377
+ }), generateTableSQL({
11378
+ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
11379
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]
10901
11380
  })];
10902
11381
  }
10903
11382
  constructor({ operations, pool, skipDefaultIndexes, indexes }) {
@@ -10941,16 +11420,125 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
10941
11420
  schema,
10942
11421
  ifNotExists: ["resourceId"]
10943
11422
  });
11423
+ await this.operations.createTable({
11424
+ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
11425
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]
11426
+ });
10944
11427
  await this.createDefaultIndexes();
10945
11428
  await this.createCustomIndexes();
10946
11429
  }
10947
11430
  async dangerouslyClearAll() {
10948
11431
  await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)}`);
11432
+ await this.pool.execute(`DELETE FROM ${RUN_OWNERS_TABLE}`);
10949
11433
  }
10950
- async deleteWorkflowRunById({ runId, workflowName }) {
11434
+ supportsRunFencing() {
11435
+ return true;
11436
+ }
11437
+ #ownershipError(operation, runId, error) {
11438
+ return new _mastra_core_error.MastraError({
11439
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", operation, "FAILED"),
11440
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
11441
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
11442
+ details: { runId }
11443
+ }, error);
11444
+ }
11445
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
11446
+ try {
11447
+ return await claimTransaction(this.pool, async (connection) => {
11448
+ const [existing] = await connection.execute(`SELECT generation FROM ${RUN_OWNERS_TABLE} WHERE ${RUN_ID} = ? FOR UPDATE`, [runId]);
11449
+ if (existing.length === 0) {
11450
+ if (expectedGeneration !== void 0 && expectedGeneration !== 0) return {
11451
+ acquired: false,
11452
+ record: null
11453
+ };
11454
+ await connection.execute(`INSERT INTO ${RUN_OWNERS_TABLE} (${RUN_ID}, generation, ${OWNER_ID}, ${LEASE_EXPIRES_AT}, ${UPDATED_AT})
11455
+ VALUES (?, GREATEST(1, ${DB_NOW_MS}), ?, ${DB_NOW_MS} + ?, ${DB_NOW_MS})`, [
11456
+ runId,
11457
+ ownerId,
11458
+ leaseMs
11459
+ ]);
11460
+ return {
11461
+ acquired: true,
11462
+ record: await readRunOwner(connection, runId)
11463
+ };
11464
+ }
11465
+ await connection.execute(`UPDATE ${RUN_OWNERS_TABLE}
11466
+ SET generation = generation + 1, ${OWNER_ID} = ?, ${LEASE_EXPIRES_AT} = ${DB_NOW_MS} + ?,
11467
+ ${UPDATED_AT} = ${DB_NOW_MS}
11468
+ WHERE ${RUN_ID} = ?
11469
+ AND (? IS NULL OR generation = ?)
11470
+ AND (? OR ${LEASE_EXPIRES_AT} IS NULL OR ${LEASE_EXPIRES_AT} <= ${DB_NOW_MS})`, [
11471
+ ownerId,
11472
+ leaseMs,
11473
+ runId,
11474
+ expectedGeneration ?? null,
11475
+ expectedGeneration ?? null,
11476
+ force === true
11477
+ ]);
11478
+ const record = await readRunOwner(connection, runId);
11479
+ return record && record.generation === Number(existing[0].generation) + 1 ? {
11480
+ acquired: true,
11481
+ record
11482
+ } : {
11483
+ acquired: false,
11484
+ record
11485
+ };
11486
+ });
11487
+ } catch (error) {
11488
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", runId, error);
11489
+ }
11490
+ }
11491
+ async renewRunOwnership({ leaseMs, ...fence }) {
10951
11492
  try {
10952
- await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)} WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ?`, [workflowName, runId]);
11493
+ return await inTransaction(this.pool, async (connection) => {
11494
+ await connection.execute(`UPDATE ${RUN_OWNERS_TABLE}
11495
+ SET ${LEASE_EXPIRES_AT} = ${DB_NOW_MS} + ?, ${UPDATED_AT} = ${DB_NOW_MS}
11496
+ WHERE ${RUN_ID} = ? AND generation = ? AND ${OWNER_ID} = ? AND ${LEASE_EXPIRES_AT} IS NOT NULL`, [
11497
+ leaseMs,
11498
+ fence.runId,
11499
+ fence.generation,
11500
+ fence.ownerId
11501
+ ]);
11502
+ const record = await readRunOwner(connection, fence.runId);
11503
+ return record && (0, _mastra_core_storage.matchesRunFence)(record, fence) && record.leaseExpiresAt !== null ? {
11504
+ renewed: true,
11505
+ record
11506
+ } : {
11507
+ renewed: false,
11508
+ record
11509
+ };
11510
+ });
10953
11511
  } catch (error) {
11512
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, error);
11513
+ }
11514
+ }
11515
+ async releaseRunOwnership(fence) {
11516
+ try {
11517
+ return await inTransaction(this.pool, async (connection) => {
11518
+ await connection.execute(`UPDATE ${RUN_OWNERS_TABLE} SET ${LEASE_EXPIRES_AT} = NULL, ${UPDATED_AT} = ${DB_NOW_MS}
11519
+ WHERE ${RUN_ID} = ? AND generation = ? AND ${OWNER_ID} = ?`, [
11520
+ fence.runId,
11521
+ fence.generation,
11522
+ fence.ownerId
11523
+ ]);
11524
+ return (0, _mastra_core_storage.matchesRunFence)(await readRunOwner(connection, fence.runId), fence);
11525
+ });
11526
+ } catch (error) {
11527
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
11528
+ }
11529
+ }
11530
+ async getRunOwnership({ runId }) {
11531
+ try {
11532
+ return await readRunOwner(this.pool, runId);
11533
+ } catch (error) {
11534
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
11535
+ }
11536
+ }
11537
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
11538
+ try {
11539
+ await withRunFence(this.pool, RUN_OWNERS_TABLE, (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "deleteWorkflowRunById", (q) => q.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)} WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ?`, [workflowName, runId]));
11540
+ } catch (error) {
11541
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
10954
11542
  throw new _mastra_core_error.MastraError({
10955
11543
  id: "MYSQL_WORKFLOWS_DELETE_RUN_BY_ID_FAILED",
10956
11544
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -10962,10 +11550,12 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
10962
11550
  }, error);
10963
11551
  }
10964
11552
  }
10965
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
11553
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
11554
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
10966
11555
  const connection = await this.pool.getConnection();
10967
11556
  try {
10968
11557
  await connection.beginTransaction();
11558
+ if (fence) await assertRunFence(connection, RUN_OWNERS_TABLE, fence, "updateWorkflowResults");
10969
11559
  const [rows] = await connection.execute(`SELECT snapshot FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)} WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ? FOR UPDATE`, [workflowName, runId]);
10970
11560
  if (!Array.isArray(rows) || rows.length === 0) {
10971
11561
  await connection.rollback();
@@ -11016,10 +11606,12 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
11016
11606
  connection.release();
11017
11607
  }
11018
11608
  }
11019
- async updateWorkflowState({ workflowName, runId, opts }) {
11609
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
11610
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
11020
11611
  const connection = await this.pool.getConnection();
11021
11612
  try {
11022
11613
  await connection.beginTransaction();
11614
+ if (fence) await assertRunFence(connection, RUN_OWNERS_TABLE, fence, "updateWorkflowState");
11023
11615
  const [rows] = await connection.execute(`SELECT snapshot FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)} WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ? FOR UPDATE`, [workflowName, runId]);
11024
11616
  if (!Array.isArray(rows) || rows.length === 0) {
11025
11617
  await connection.rollback();
@@ -11045,6 +11637,7 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
11045
11637
  return updatedSnapshot;
11046
11638
  } catch (error) {
11047
11639
  await connection.rollback();
11640
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
11048
11641
  throw new _mastra_core_error.MastraError({
11049
11642
  id: "MYSQL_WORKFLOWS_UPDATE_STATE_FAILED",
11050
11643
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -11058,35 +11651,38 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
11058
11651
  connection.release();
11059
11652
  }
11060
11653
  }
11061
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
11654
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
11062
11655
  const now = /* @__PURE__ */ new Date();
11063
11656
  const createdAtValue = createdAt ?? now;
11064
11657
  const updatedAtValue = updatedAt ?? now;
11658
+ const tableName = formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT);
11065
11659
  try {
11066
- const tableName = formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT);
11067
- const [updateResult] = await this.pool.execute(`UPDATE ${tableName}
11068
- SET ${quoteIdentifier("resourceId", "column name")} = ?,
11069
- ${quoteIdentifier("snapshot", "column name")} = ?,
11070
- ${quoteIdentifier("updatedAt", "column name")} = ?
11071
- WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ?`, [
11072
- resourceId ?? null,
11073
- JSON.stringify(snapshot),
11074
- transformToSqlValue(updatedAtValue),
11075
- workflowName,
11076
- runId
11077
- ]);
11078
- if (updateResult.affectedRows > 0) return;
11079
- await this.pool.execute(`INSERT INTO ${tableName} (${quoteIdentifier("workflow_name", "column name")}, ${quoteIdentifier("run_id", "column name")}, ${quoteIdentifier("resourceId", "column name")}, ${quoteIdentifier("snapshot", "column name")}, ${quoteIdentifier("createdAt", "column name")}, ${quoteIdentifier("updatedAt", "column name")})
11080
- VALUES (?, ?, ?, ?, ?, ?)
11081
- ON DUPLICATE KEY UPDATE ${quoteIdentifier("resourceId", "column name")} = VALUES(${quoteIdentifier("resourceId", "column name")}), ${quoteIdentifier("snapshot", "column name")} = VALUES(${quoteIdentifier("snapshot", "column name")}), ${quoteIdentifier("updatedAt", "column name")} = VALUES(${quoteIdentifier("updatedAt", "column name")})`, [
11082
- workflowName,
11083
- runId,
11084
- resourceId ?? null,
11085
- JSON.stringify(snapshot),
11086
- transformToSqlValue(createdAtValue),
11087
- transformToSqlValue(updatedAtValue)
11088
- ]);
11660
+ await withRunFence(this.pool, RUN_OWNERS_TABLE, (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "persistWorkflowSnapshot", async (q) => {
11661
+ const [updateResult] = await q.execute(`UPDATE ${tableName}
11662
+ SET ${quoteIdentifier("resourceId", "column name")} = ?,
11663
+ ${quoteIdentifier("snapshot", "column name")} = ?,
11664
+ ${quoteIdentifier("updatedAt", "column name")} = ?
11665
+ WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ?`, [
11666
+ resourceId ?? null,
11667
+ JSON.stringify(snapshot),
11668
+ transformToSqlValue(updatedAtValue),
11669
+ workflowName,
11670
+ runId
11671
+ ]);
11672
+ if (updateResult.affectedRows > 0) return;
11673
+ await q.execute(`INSERT INTO ${tableName} (${quoteIdentifier("workflow_name", "column name")}, ${quoteIdentifier("run_id", "column name")}, ${quoteIdentifier("resourceId", "column name")}, ${quoteIdentifier("snapshot", "column name")}, ${quoteIdentifier("createdAt", "column name")}, ${quoteIdentifier("updatedAt", "column name")})
11674
+ VALUES (?, ?, ?, ?, ?, ?)
11675
+ ON DUPLICATE KEY UPDATE ${quoteIdentifier("resourceId", "column name")} = VALUES(${quoteIdentifier("resourceId", "column name")}), ${quoteIdentifier("snapshot", "column name")} = VALUES(${quoteIdentifier("snapshot", "column name")}), ${quoteIdentifier("updatedAt", "column name")} = VALUES(${quoteIdentifier("updatedAt", "column name")})`, [
11676
+ workflowName,
11677
+ runId,
11678
+ resourceId ?? null,
11679
+ JSON.stringify(snapshot),
11680
+ transformToSqlValue(createdAtValue),
11681
+ transformToSqlValue(updatedAtValue)
11682
+ ]);
11683
+ });
11089
11684
  } catch (error) {
11685
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
11090
11686
  throw new _mastra_core_error.MastraError({
11091
11687
  id: "MYSQL_WORKFLOWS_PERSIST_SNAPSHOT_FAILED",
11092
11688
  domain: _mastra_core_error.ErrorDomain.STORAGE,