@mastra/mysql 0.13.0-alpha.1 → 0.13.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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,108 @@ 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
6025
6112
  //#region src/storage/domains/memory/index.ts
6026
6113
  const OM_TABLE = "mastra_observational_memory";
6027
6114
  const OM_TABLE_QUOTED = quoteIdentifier(OM_TABLE, "table name");
6115
+ /** Canonical head order: newest generation, then earliest createdAt, then lowest id. */
6116
+ const OM_HEAD_ORDER = `${quoteIdentifier("generationCount", "column name")} DESC, ${quoteIdentifier("createdAt", "column name")} ASC, ${quoteIdentifier("id", "column name")} ASC`;
6117
+ /** Max `supersededBy` hops when redirecting a write aimed at a retired record. */
6118
+ const OM_MAX_HEAD_HOPS = 3;
6119
+ /** Attempts for an OM transaction that hits a deadlock or lock-wait timeout. */
6120
+ const OM_MAX_TRANSACTION_ATTEMPTS = 3;
6121
+ /** How long initialization waits for the per-key `GET_LOCK` before failing. */
6122
+ const OM_INIT_LOCK_TIMEOUT_SECONDS = 30;
6123
+ /** ER_LOCK_DEADLOCK (1213) or ER_LOCK_WAIT_TIMEOUT (1205): the transaction was rolled back and can be retried. */
6124
+ function isRetryableLockError(error) {
6125
+ const errno = error?.errno;
6126
+ return errno === 1213 || errno === 1205;
6127
+ }
6028
6128
  function emitValidationError(message) {
6029
6129
  return new _mastra_core_error.MastraError({
6030
6130
  id: "MYSQL_MEMORY_VALIDATION_ERROR",
@@ -6131,7 +6231,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6131
6231
  tableName: _mastra_core_storage.TABLE_RESOURCES,
6132
6232
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_RESOURCES]
6133
6233
  });
6134
- const omSchema = _mastra_core_storage.OBSERVATIONAL_MEMORY_TABLE_SCHEMA?.[OM_TABLE];
6234
+ const omSchema = withObservationalMemoryLifecycleColumns(_mastra_core_storage.OBSERVATIONAL_MEMORY_TABLE_SCHEMA?.[OM_TABLE]);
6135
6235
  if (omSchema) {
6136
6236
  await this.operations.createTable({
6137
6237
  tableName: OM_TABLE,
@@ -6154,7 +6254,8 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6154
6254
  "isBufferingObservation",
6155
6255
  "isBufferingReflection",
6156
6256
  "lastBufferedAtTokens",
6157
- "lastBufferedAtTime"
6257
+ "lastBufferedAtTime",
6258
+ "supersededBy"
6158
6259
  ]
6159
6260
  });
6160
6261
  }
@@ -6172,6 +6273,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6172
6273
  if (err?.errno !== 1061) throw err;
6173
6274
  }
6174
6275
  }
6276
+ if (omSchema) await this.backfillSupersededBy();
6175
6277
  await this.createDefaultIndexes();
6176
6278
  await this.createCustomIndexes();
6177
6279
  }
@@ -7288,7 +7390,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7288
7390
  async getObservationalMemory(threadId, resourceId) {
7289
7391
  try {
7290
7392
  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]);
7393
+ const [rows] = await this.pool.execute(`SELECT * FROM ${OM_TABLE_QUOTED} WHERE ${omCol("lookupKey")} = ? ORDER BY ${OM_HEAD_ORDER} LIMIT 1`, [lookupKey]);
7292
7394
  if (!rows || rows.length === 0) return null;
7293
7395
  return this.parseOMRow(rows[0]);
7294
7396
  } catch (error) {
@@ -7350,32 +7452,11 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7350
7452
  }
7351
7453
  async initializeObservationalMemory(input) {
7352
7454
  try {
7455
+ const lookupKey = this.getOMKey(input.threadId, input.resourceId);
7353
7456
  const id = globalThis.crypto.randomUUID();
7354
7457
  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
- };
7458
+ const existing = await this.getObservationalMemory(input.threadId, input.resourceId);
7459
+ if (existing) return existing;
7379
7460
  const nowSql = transformToSqlValue(now);
7380
7461
  const cols = [
7381
7462
  "id",
@@ -7401,36 +7482,53 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7401
7482
  "lastBufferedAtTime",
7402
7483
  "observedTimezone",
7403
7484
  "createdAt",
7404
- "updatedAt"
7485
+ "updatedAt",
7486
+ "supersededBy"
7405
7487
  ].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;
7488
+ const placeholders = Array.from({ length: 25 }, () => "?").join(", ");
7489
+ const lockName = `mastra_om_init:${(0, crypto$1.createHash)("sha256").update(lookupKey).digest("hex").slice(0, 40)}`;
7490
+ const connection = await this.pool.getConnection();
7491
+ try {
7492
+ const [lockRows] = await connection.query("SELECT GET_LOCK(?, ?) AS acquired", [lockName, OM_INIT_LOCK_TIMEOUT_SECONDS]);
7493
+ if (Number(lockRows[0]?.acquired) !== 1) throw new Error(`Timed out waiting for the observational memory initialization lock for ${lookupKey}`);
7494
+ try {
7495
+ const [liveRows] = await connection.query(`SELECT 1 FROM ${OM_TABLE_QUOTED} WHERE ${omCol("lookupKey")} = ? AND ${omCol("supersededBy")} IS NULL LIMIT 1`, [lookupKey]);
7496
+ if (liveRows.length === 0) await connection.execute(`INSERT INTO ${OM_TABLE_QUOTED} (${cols}) VALUES (${placeholders})`, [
7497
+ id,
7498
+ lookupKey,
7499
+ input.scope,
7500
+ input.resourceId,
7501
+ input.threadId || null,
7502
+ "",
7503
+ null,
7504
+ "initial",
7505
+ JSON.stringify(input.config),
7506
+ 0,
7507
+ null,
7508
+ null,
7509
+ 0,
7510
+ 0,
7511
+ 0,
7512
+ false,
7513
+ false,
7514
+ false,
7515
+ false,
7516
+ 0,
7517
+ null,
7518
+ input.observedTimezone || null,
7519
+ nowSql,
7520
+ nowSql,
7521
+ null
7522
+ ]);
7523
+ } finally {
7524
+ await connection.query("SELECT RELEASE_LOCK(?)", [lockName]);
7525
+ }
7526
+ } finally {
7527
+ connection.release();
7528
+ }
7529
+ const head = await this.getObservationalMemory(input.threadId, input.resourceId);
7530
+ if (!head) throwOMNotFound(id, "INITIALIZE_OBSERVATIONAL_MEMORY");
7531
+ return head;
7434
7532
  } catch (error) {
7435
7533
  rethrowOrWrapOM(error, "", "INITIALIZE_OBSERVATIONAL_MEMORY", {
7436
7534
  threadId: input.threadId,
@@ -7439,115 +7537,65 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7439
7537
  }
7440
7538
  }
7441
7539
  async updateActiveObservations(input) {
7540
+ assertActiveObservationsApplied(await this.commitActiveObservations(input), input.id);
7541
+ }
7542
+ async commitActiveObservations(input) {
7442
7543
  try {
7443
- const now = /* @__PURE__ */ new Date();
7444
7544
  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");
7545
+ return await this.withOMTransaction(async (connection) => {
7546
+ const row = await this.lockOMRow(connection, input.id);
7547
+ if (!row) throwOMNotFound(input.id, "UPDATE_ACTIVE_OBSERVATIONS");
7548
+ if (row.supersededBy) return {
7549
+ applied: false,
7550
+ reason: "retired"
7551
+ };
7552
+ if (input.expectedActiveObservations !== void 0 && input.expectedActiveObservations !== (row.activeObservations || "")) return {
7553
+ applied: false,
7554
+ reason: "conflict"
7555
+ };
7556
+ const lastObservedAt = maxObservationCursor(parseDateTime(row.lastObservedAt), input.lastObservedAt);
7557
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7558
+ ${omCol("activeObservations")} = ?,
7559
+ ${omCol("lastObservedAt")} = ?,
7560
+ ${omCol("pendingMessageTokens")} = 0,
7561
+ ${omCol("observationTokenCount")} = ?,
7562
+ ${omCol("totalTokensObserved")} = ${omCol("totalTokensObserved")} + ?,
7563
+ ${omCol("observedMessageIds")} = ?,
7564
+ ${omCol("updatedAt")} = ?
7565
+ WHERE ${omCol("id")} = ?`, [
7566
+ input.observations,
7567
+ transformToSqlValue(lastObservedAt),
7568
+ input.tokenCount,
7569
+ input.tokenCount,
7570
+ observedMessageIdsJson,
7571
+ transformToSqlValue(/* @__PURE__ */ new Date()),
7572
+ input.id
7573
+ ]);
7574
+ return { applied: true };
7575
+ });
7463
7576
  } catch (error) {
7464
7577
  rethrowOrWrapOM(error, input.id, "UPDATE_ACTIVE_OBSERVATIONS");
7465
7578
  }
7466
7579
  }
7467
7580
  async createReflectionGeneration(input) {
7468
7581
  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;
7582
+ const { currentRecord } = input;
7583
+ return await this.withOMTransaction(async (connection) => {
7584
+ const row = await this.lockOMRow(connection, currentRecord.id);
7585
+ if (!row) return currentRecord;
7586
+ if (row.supersededBy) return this.parseOMRow(await this.resolveLiveOMRow(connection, row));
7587
+ const stored = this.parseOMRow(row);
7588
+ const plan = planReflectionGenerationText({
7589
+ storedObservations: stored.activeObservations,
7590
+ storedObservationTokenCount: stored.observationTokenCount,
7591
+ snapshotObservations: currentRecord.activeObservations,
7592
+ snapshotObservationTokenCount: currentRecord.observationTokenCount,
7593
+ reflection: input.reflection,
7594
+ tokenCount: input.tokenCount
7595
+ });
7596
+ if (!plan) return stored;
7597
+ return this.rollOverOMRow(connection, stored.id, plan.observations, plan.tokenCount, input.newRecordId, false);
7598
+ });
7551
7599
  } catch (error) {
7552
7600
  rethrowOrWrapOM(error, input.currentRecord.id, "CREATE_REFLECTION_GENERATION", { currentRecordId: input.currentRecord.id });
7553
7601
  }
@@ -7560,20 +7608,22 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7560
7608
  }
7561
7609
  async setBufferingObservationFlag(id, isBuffering, lastBufferedAtTokens) {
7562
7610
  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");
7611
+ await this.withOMTransaction(async (connection) => {
7612
+ const row = await this.lockOMRow(connection, id);
7613
+ if (!row) throwOMNotFound(id, "SET_BUFFERING_OBSERVATION_FLAG");
7614
+ const target = await this.resolveLiveOMRow(connection, row);
7615
+ const sets = [
7616
+ `${omCol("isBufferingObservation")} = ?`,
7617
+ ...lastBufferedAtTokens !== void 0 ? [`${omCol("lastBufferedAtTokens")} = ?`] : [],
7618
+ `${omCol("updatedAt")} = ?`
7619
+ ].join(", ");
7620
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET ${sets} WHERE ${omCol("id")} = ?`, [
7621
+ isBuffering,
7622
+ ...lastBufferedAtTokens !== void 0 ? [lastBufferedAtTokens] : [],
7623
+ transformToSqlValue(/* @__PURE__ */ new Date()),
7624
+ target.id
7625
+ ]);
7626
+ });
7577
7627
  } catch (error) {
7578
7628
  rethrowOrWrapOM(error, id, "SET_BUFFERING_OBSERVATION_FLAG");
7579
7629
  }
@@ -7594,83 +7644,78 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7594
7644
  }
7595
7645
  async setPendingMessageTokens(id, tokenCount) {
7596
7646
  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");
7647
+ await this.withOMTransaction(async (connection) => {
7648
+ const row = await this.lockOMRow(connection, id);
7649
+ if (!row) throwOMNotFound(id, "SET_PENDING_MESSAGE_TOKENS");
7650
+ const target = await this.resolveLiveOMRow(connection, row);
7651
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7652
+ ${omCol("pendingMessageTokens")} = ?,
7653
+ ${omCol("updatedAt")} = ?
7654
+ WHERE ${omCol("id")} = ?`, [
7655
+ tokenCount,
7656
+ transformToSqlValue(/* @__PURE__ */ new Date()),
7657
+ target.id
7658
+ ]);
7659
+ });
7606
7660
  } catch (error) {
7607
7661
  rethrowOrWrapOM(error, id, "SET_PENDING_MESSAGE_TOKENS");
7608
7662
  }
7609
7663
  }
7610
7664
  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
7665
+ await this.appendBufferedObservations(input);
7666
+ }
7667
+ async appendBufferedObservations(input) {
7668
+ try {
7669
+ const newChunk = {
7670
+ id: `ombuf-${globalThis.crypto.randomUUID()}`,
7671
+ cycleId: input.chunk.cycleId,
7672
+ observations: input.chunk.observations,
7673
+ tokenCount: input.chunk.tokenCount,
7674
+ messageIds: input.chunk.messageIds,
7675
+ messageTokens: input.chunk.messageTokens,
7676
+ lastObservedAt: input.chunk.lastObservedAt,
7677
+ createdAt: /* @__PURE__ */ new Date(),
7678
+ suggestedContinuation: input.chunk.suggestedContinuation,
7679
+ currentTask: input.chunk.currentTask,
7680
+ threadTitle: input.chunk.threadTitle,
7681
+ extractedValues: input.chunk.extractedValues,
7682
+ extractionFailures: input.chunk.extractionFailures
7683
+ };
7684
+ return await this.withOMTransaction(async (connection) => {
7685
+ const row = await this.lockOMRow(connection, input.id);
7686
+ if (!row) throwOMNotFound(input.id, "UPDATE_BUFFERED_OBSERVATIONS");
7687
+ const target = await this.resolveLiveOMRow(connection, row);
7688
+ const existingChunks = parseBufferedChunks(target.bufferedObservationChunks);
7689
+ if (existingChunks.some((existing) => existing.cycleId === input.chunk.cycleId) || isBufferedChunkCoveredByCursor(input.chunk.lastObservedAt, parseDateTime(target.lastObservedAt))) return {
7690
+ persisted: false,
7691
+ recordId: target.id
7637
7692
  };
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
7693
+ const lastBufferedAtTime = maxObservationCursor(parseDateTime(target.lastBufferedAtTime), input.lastBufferedAtTime ?? void 0);
7694
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7641
7695
  ${omCol("bufferedObservationChunks")} = ?,
7642
- ${omCol("lastBufferedAtTime")} = COALESCE(?, ${omCol("lastBufferedAtTime")}),
7696
+ ${omCol("lastBufferedAtTime")} = ?,
7643
7697
  ${omCol("updatedAt")} = ?
7644
7698
  WHERE ${omCol("id")} = ?`, [
7645
- JSON.stringify(newChunks),
7646
- lastBufferedAtTime,
7647
- nowSql,
7648
- input.id
7699
+ JSON.stringify([...existingChunks, newChunk]),
7700
+ transformToSqlValue(lastBufferedAtTime),
7701
+ transformToSqlValue(/* @__PURE__ */ new Date()),
7702
+ target.id
7649
7703
  ]);
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
- }
7704
+ return {
7705
+ persisted: true,
7706
+ recordId: target.id
7707
+ };
7708
+ });
7658
7709
  } catch (error) {
7659
7710
  rethrowOrWrapOM(error, input.id, "UPDATE_BUFFERED_OBSERVATIONS");
7660
7711
  }
7661
7712
  }
7662
7713
  async swapBufferedToActive(input) {
7663
- const connection = await this.pool.getConnection();
7664
7714
  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 {
7715
+ return await this.withOMTransaction(async (connection) => {
7716
+ const row = await this.lockOMRow(connection, input.id);
7717
+ if (!row) throwOMNotFound(input.id, "SWAP_BUFFERED_TO_ACTIVE");
7718
+ const emptyResult = {
7674
7719
  chunksActivated: 0,
7675
7720
  messageTokensActivated: 0,
7676
7721
  observationTokensActivated: 0,
@@ -7678,95 +7723,103 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7678
7723
  activatedCycleIds: [],
7679
7724
  activatedMessageIds: []
7680
7725
  };
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;
7726
+ if (row.supersededBy) return {
7727
+ ...emptyResult,
7728
+ retired: true
7729
+ };
7730
+ const persistedChunks = parseBufferedChunks(row.bufferedObservationChunks);
7731
+ const refreshedWeights = new Map((Array.isArray(input.bufferedChunks) ? input.bufferedChunks : []).map((c) => [c.id, c.messageTokens]));
7732
+ const chunks = persistedChunks.map((c) => refreshedWeights.has(c.id) ? {
7733
+ ...c,
7734
+ messageTokens: refreshedWeights.get(c.id)
7735
+ } : c);
7736
+ if (chunks.length === 0) return emptyResult;
7737
+ const retentionFloor = input.messageTokensThreshold * (1 - input.activationRatio);
7738
+ const targetMessageTokens = Math.max(0, input.currentPendingTokens - retentionFloor);
7739
+ let cumulativeMessageTokens = 0;
7740
+ let bestOverBoundary = 0;
7741
+ let bestOverTokens = 0;
7742
+ let bestUnderBoundary = 0;
7743
+ let bestUnderTokens = 0;
7744
+ for (let i = 0; i < chunks.length; i++) {
7745
+ cumulativeMessageTokens += chunks[i].messageTokens ?? 0;
7746
+ const boundary = i + 1;
7747
+ if (cumulativeMessageTokens >= targetMessageTokens) {
7748
+ if (bestOverBoundary === 0 || cumulativeMessageTokens < bestOverTokens) {
7749
+ bestOverBoundary = boundary;
7750
+ bestOverTokens = cumulativeMessageTokens;
7751
+ }
7752
+ } else if (cumulativeMessageTokens > bestUnderTokens) {
7753
+ bestUnderBoundary = boundary;
7754
+ bestUnderTokens = cumulativeMessageTokens;
7696
7755
  }
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
- };
7756
+ }
7757
+ const maxOvershoot = retentionFloor * .95;
7758
+ const overshoot = bestOverTokens - targetMessageTokens;
7759
+ const remainingAfterOver = input.currentPendingTokens - bestOverTokens;
7760
+ const remainingAfterUnder = input.currentPendingTokens - bestUnderTokens;
7761
+ const minRemaining = Math.min(1e3, retentionFloor);
7762
+ let chunksToActivate;
7763
+ if (input.forceMaxActivation && bestOverBoundary > 0 && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
7764
+ else if (bestOverBoundary > 0 && overshoot <= maxOvershoot && remainingAfterOver >= minRemaining) chunksToActivate = bestOverBoundary;
7765
+ else if (bestUnderBoundary > 0 && remainingAfterUnder >= minRemaining) chunksToActivate = bestUnderBoundary;
7766
+ else if (bestOverBoundary > 0) chunksToActivate = bestOverBoundary;
7767
+ else chunksToActivate = 1;
7768
+ const activatedChunks = chunks.slice(0, chunksToActivate);
7769
+ const remainingChunks = persistedChunks.slice(chunksToActivate);
7770
+ const activatedContent = activatedChunks.map((c) => c.observations).join("\n\n");
7771
+ const activatedTokens = activatedChunks.reduce((sum, c) => sum + c.tokenCount, 0);
7772
+ const activatedMessageTokens = activatedChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
7773
+ const activatedMessageCount = activatedChunks.reduce((sum, c) => sum + c.messageIds.length, 0);
7774
+ const activatedCycleIds = activatedChunks.map((c) => c.cycleId).filter((cid) => !!cid);
7775
+ const activatedMessageIds = activatedChunks.flatMap((c) => c.messageIds ?? []);
7776
+ const latestChunk = activatedChunks[activatedChunks.length - 1];
7777
+ const derivedLastObservedAt = input.lastObservedAt ?? (latestChunk?.lastObservedAt ? new Date(latestChunk.lastObservedAt) : /* @__PURE__ */ new Date());
7778
+ const lastObservedAt = maxObservationCursor(parseDateTime(row.lastObservedAt), derivedLastObservedAt);
7779
+ const existingActive = row.activeObservations || "";
7780
+ const existingTokenCount = Number(row.observationTokenCount || 0);
7781
+ const newActive = existingActive ? `${existingActive}\n\n${activatedContent}` : activatedContent;
7782
+ const newTokenCount = existingTokenCount + activatedTokens;
7783
+ const existingPending = Number(row.pendingMessageTokens || 0);
7784
+ const newPending = Math.max(0, existingPending - activatedMessageTokens);
7785
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7786
+ ${omCol("activeObservations")} = ?,
7787
+ ${omCol("observationTokenCount")} = ?,
7788
+ ${omCol("pendingMessageTokens")} = ?,
7789
+ ${omCol("bufferedObservationChunks")} = ?,
7790
+ ${omCol("lastObservedAt")} = ?,
7791
+ ${omCol("updatedAt")} = ?
7792
+ WHERE ${omCol("id")} = ?`, [
7793
+ newActive,
7794
+ newTokenCount,
7795
+ newPending,
7796
+ remainingChunks.length > 0 ? JSON.stringify(remainingChunks) : null,
7797
+ transformToSqlValue(lastObservedAt),
7798
+ transformToSqlValue(/* @__PURE__ */ new Date()),
7799
+ input.id
7800
+ ]);
7801
+ const latestChunkHints = activatedChunks[activatedChunks.length - 1];
7802
+ return {
7803
+ chunksActivated: activatedChunks.length,
7804
+ messageTokensActivated: activatedMessageTokens,
7805
+ observationTokensActivated: activatedTokens,
7806
+ messagesActivated: activatedMessageCount,
7807
+ activatedCycleIds,
7808
+ activatedMessageIds,
7809
+ observations: activatedContent,
7810
+ perChunk: activatedChunks.map((c) => ({
7811
+ cycleId: c.cycleId ?? "",
7812
+ messageTokens: c.messageTokens ?? 0,
7813
+ observationTokens: c.tokenCount,
7814
+ messageCount: c.messageIds.length,
7815
+ observations: c.observations
7816
+ })),
7817
+ suggestedContinuation: latestChunkHints?.suggestedContinuation ?? void 0,
7818
+ currentTask: latestChunkHints?.currentTask ?? void 0
7819
+ };
7820
+ });
7765
7821
  } catch (error) {
7766
- await connection.rollback();
7767
7822
  rethrowOrWrapOM(error, input.id, "SWAP_BUFFERED_TO_ACTIVE");
7768
- } finally {
7769
- connection.release();
7770
7823
  }
7771
7824
  }
7772
7825
  async updateBufferedReflection(input) {
@@ -7799,38 +7852,182 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7799
7852
  }
7800
7853
  async swapBufferedReflectionToActive(input) {
7801
7854
  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
7855
+ const { currentRecord } = input;
7856
+ return await this.withOMTransaction(async (connection) => {
7857
+ const row = await this.lockOMRow(connection, currentRecord.id);
7858
+ if (!row) return currentRecord;
7859
+ if (row.supersededBy) return this.parseOMRow(await this.resolveLiveOMRow(connection, row));
7860
+ const stored = this.parseOMRow(row);
7861
+ const bufferedReflection = stored.bufferedReflection || "";
7862
+ if (!bufferedReflection) throw new _mastra_core_error.MastraError({
7863
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NO_CONTENT"),
7864
+ text: "No buffered reflection to swap",
7865
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
7866
+ category: _mastra_core_error.ErrorCategory.USER,
7867
+ details: { id: currentRecord.id }
7868
+ });
7869
+ if (!isAppendOnlySince(stored.activeObservations, currentRecord.activeObservations)) return stored;
7870
+ const reflectedLineCount = stored.reflectedObservationLineCount ?? 0;
7871
+ const unreflectedContent = (stored.activeObservations || "").split("\n").slice(reflectedLineCount).join("\n").trim();
7872
+ const newObservations = unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
7873
+ const tokenCount = input.tokenCount + Math.max(0, (stored.observationTokenCount ?? 0) - (currentRecord.observationTokenCount ?? 0));
7874
+ return this.rollOverOMRow(connection, stored.id, newObservations, tokenCount, input.newRecordId, true);
7820
7875
  });
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;
7830
7876
  } catch (error) {
7831
7877
  rethrowOrWrapOM(error, input.currentRecord.id, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE");
7832
7878
  }
7833
7879
  }
7880
+ /**
7881
+ * Runs `fn` in one transaction on a dedicated connection. Every OM lifecycle write locks its
7882
+ * target row by primary key (`SELECT … FOR UPDATE`, a record lock — never a range), so the
7883
+ * only expected lock conflict is a wait; a deadlock or lock-wait timeout (e.g. against the
7884
+ * startup backfill's scan) is retried a bounded number of times.
7885
+ */
7886
+ async withOMTransaction(fn) {
7887
+ for (let attempt = 1;; attempt++) {
7888
+ const connection = await this.pool.getConnection();
7889
+ try {
7890
+ await connection.beginTransaction();
7891
+ const result = await fn(connection);
7892
+ await connection.commit();
7893
+ return result;
7894
+ } catch (error) {
7895
+ await connection.rollback().catch(() => {});
7896
+ if (attempt < OM_MAX_TRANSACTION_ATTEMPTS && isRetryableLockError(error)) continue;
7897
+ throw error;
7898
+ } finally {
7899
+ connection.release();
7900
+ }
7901
+ }
7902
+ }
7903
+ /** Row-locks one OM record by primary key for the rest of the transaction. */
7904
+ async lockOMRow(connection, id) {
7905
+ const [rows] = await connection.execute(`SELECT * FROM ${OM_TABLE_QUOTED} WHERE ${omCol("id")} = ? FOR UPDATE`, [id]);
7906
+ return rows?.[0] ?? null;
7907
+ }
7908
+ /**
7909
+ * The live, row-locked record a lifecycle write aimed at `row` lands on: `row` itself while
7910
+ * live, otherwise the head. The head is found with a plain (non-locking, so no range locks)
7911
+ * canonical head read, then locked by primary key. Under REPEATABLE READ that read can predate
7912
+ * a rollover committed after this transaction started; the locking read sees the latest
7913
+ * committed row, so a head retired since is followed through `supersededBy` (bounded).
7914
+ */
7915
+ async resolveLiveOMRow(connection, row) {
7916
+ if (!row.supersededBy) return row;
7917
+ const [heads] = await connection.execute(`SELECT id FROM ${OM_TABLE_QUOTED} WHERE ${omCol("lookupKey")} = ? ORDER BY ${OM_HEAD_ORDER} LIMIT 1`, [row.lookupKey]);
7918
+ const headId = heads?.[0]?.id ?? row.supersededBy;
7919
+ let current = await this.lockOMRow(connection, headId) ?? row;
7920
+ for (let hop = 0; current.supersededBy; hop++) {
7921
+ const next = hop < OM_MAX_HEAD_HOPS ? await this.lockOMRow(connection, current.supersededBy) : null;
7922
+ if (!next) throw new _mastra_core_error.MastraError({
7923
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "RESOLVE_OBSERVATIONAL_MEMORY_HEAD", "FAILED"),
7924
+ text: `Observational memory record ${row.id} is superseded but no live head was found`,
7925
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
7926
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
7927
+ details: { id: row.id }
7928
+ });
7929
+ current = next;
7930
+ }
7931
+ return current;
7932
+ }
7933
+ /**
7934
+ * Create the next generation from the stored (live, row-locked) record and retire the stored
7935
+ * record in the same transaction. Carried columns are copied in SQL from the stored row, so
7936
+ * nothing round-trips through a client-side date conversion. Buffered chunks move to the new
7937
+ * generation; the cursor, buffering markers, flags, and counters carry over; buffered
7938
+ * reflection state does not (and is cleared on the retired row after a buffered swap).
7939
+ */
7940
+ async rollOverOMRow(connection, storedId, observations, tokenCount, newRecordId, clearBufferedReflection) {
7941
+ const id = newRecordId ?? globalThis.crypto.randomUUID();
7942
+ const nowSql = transformToSqlValue(/* @__PURE__ */ new Date());
7943
+ const c = omCol;
7944
+ await connection.execute(`INSERT INTO ${OM_TABLE_QUOTED} (
7945
+ ${c("id")}, ${c("lookupKey")}, ${c("scope")}, ${c("resourceId")}, ${c("threadId")},
7946
+ ${c("activeObservations")}, ${c("activeObservationsPendingUpdate")},
7947
+ ${c("originType")}, ${c("config")}, ${c("generationCount")}, ${c("lastObservedAt")}, ${c("lastReflectionAt")},
7948
+ ${c("pendingMessageTokens")}, ${c("totalTokensObserved")}, ${c("observationTokenCount")}, ${c("bufferedObservationChunks")},
7949
+ ${c("isObserving")}, ${c("isReflecting")}, ${c("isBufferingObservation")}, ${c("isBufferingReflection")},
7950
+ ${c("lastBufferedAtTokens")}, ${c("lastBufferedAtTime")},
7951
+ ${c("observedTimezone")}, ${c("metadata")}, ${c("supersededBy")}, ${c("createdAt")}, ${c("updatedAt")}
7952
+ )
7953
+ SELECT
7954
+ ?, ${c("lookupKey")}, ${c("scope")}, ${c("resourceId")}, ${c("threadId")},
7955
+ ?, NULL,
7956
+ 'reflection', ${c("config")}, ${c("generationCount")} + 1, ${c("lastObservedAt")}, ?,
7957
+ ${c("pendingMessageTokens")}, ${c("totalTokensObserved")}, ?, ${c("bufferedObservationChunks")},
7958
+ false, false, ${c("isBufferingObservation")}, false,
7959
+ ${c("lastBufferedAtTokens")}, ${c("lastBufferedAtTime")},
7960
+ ${c("observedTimezone")}, ${c("metadata")}, NULL, ?, ?
7961
+ FROM ${OM_TABLE_QUOTED} WHERE ${c("id")} = ?`, [
7962
+ id,
7963
+ observations,
7964
+ nowSql,
7965
+ Math.round(tokenCount),
7966
+ nowSql,
7967
+ nowSql,
7968
+ storedId
7969
+ ]);
7970
+ const [retired] = await connection.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7971
+ ${c("supersededBy")} = ?,
7972
+ ${c("bufferedObservationChunks")} = NULL,
7973
+ ${clearBufferedReflection ? `${c("bufferedReflection")} = NULL, ${c("bufferedReflectionTokens")} = NULL,
7974
+ ${c("bufferedReflectionInputTokens")} = NULL, ${c("reflectedObservationLineCount")} = NULL,` : ""}
7975
+ ${c("updatedAt")} = ?
7976
+ WHERE ${c("id")} = ? AND ${c("supersededBy")} IS NULL`, [
7977
+ id,
7978
+ nowSql,
7979
+ storedId
7980
+ ]);
7981
+ if (retired.affectedRows !== 1) throw new _mastra_core_error.MastraError({
7982
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "CREATE_REFLECTION_GENERATION", "RETIRE_FAILED"),
7983
+ text: `Failed to retire observational memory record ${storedId}`,
7984
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
7985
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
7986
+ details: { id: storedId }
7987
+ });
7988
+ const inserted = await this.lockOMRow(connection, id);
7989
+ if (!inserted) throwOMNotFound(id, "CREATE_REFLECTION_GENERATION");
7990
+ return this.parseOMRow(inserted);
7991
+ }
7992
+ /**
7993
+ * Retire every live row that sorts after its key's canonical head, marking it superseded by
7994
+ * the head. Rows retired by older adapter versions (which never set `supersededBy`) become
7995
+ * frozen. Only keys with more than one live row are touched; each update embeds the head's
7996
+ * ordering key, so a row created by a concurrent rollover (a newer generation) never matches.
7997
+ * Runs at READ COMMITTED so the scan takes no gap locks.
7998
+ */
7999
+ async backfillSupersededBy() {
8000
+ const [keys] = await this.pool.execute(`SELECT ${omCol("lookupKey")} AS lookupKey FROM ${OM_TABLE_QUOTED}
8001
+ WHERE ${omCol("supersededBy")} IS NULL GROUP BY ${omCol("lookupKey")} HAVING COUNT(*) > 1`);
8002
+ for (const { lookupKey } of keys ?? []) for (let attempt = 1;; attempt++) {
8003
+ const connection = await this.pool.getConnection();
8004
+ try {
8005
+ await connection.query("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
8006
+ await connection.beginTransaction();
8007
+ await connection.execute(`UPDATE ${OM_TABLE_QUOTED} AS r
8008
+ JOIN (
8009
+ SELECT ${omCol("id")} AS id, ${omCol("generationCount")} AS generationCount, ${omCol("createdAt")} AS createdAt
8010
+ FROM ${OM_TABLE_QUOTED} WHERE ${omCol("lookupKey")} = ? ORDER BY ${OM_HEAD_ORDER} LIMIT 1
8011
+ ) AS h
8012
+ SET r.${omCol("supersededBy")} = h.id
8013
+ WHERE r.${omCol("lookupKey")} = ?
8014
+ AND r.${omCol("supersededBy")} IS NULL
8015
+ AND (
8016
+ r.${omCol("generationCount")} < h.generationCount
8017
+ OR (r.${omCol("generationCount")} = h.generationCount AND r.${omCol("createdAt")} > h.createdAt)
8018
+ OR (r.${omCol("generationCount")} = h.generationCount AND r.${omCol("createdAt")} = h.createdAt AND r.${omCol("id")} > h.id)
8019
+ )`, [lookupKey, lookupKey]);
8020
+ await connection.commit();
8021
+ break;
8022
+ } catch (error) {
8023
+ await connection.rollback().catch(() => {});
8024
+ if (attempt < OM_MAX_TRANSACTION_ATTEMPTS && isRetryableLockError(error)) continue;
8025
+ throw error;
8026
+ } finally {
8027
+ connection.release();
8028
+ }
8029
+ }
8030
+ }
7834
8031
  async updateOMFlag(id, column, value, operation) {
7835
8032
  try {
7836
8033
  const [result] = await this.pool.execute(`UPDATE ${OM_TABLE_QUOTED} SET ${omCol(column)} = ?, ${omCol("updatedAt")} = ? WHERE ${omCol("id")} = ?`, [
@@ -7878,7 +8075,8 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7878
8075
  config: parseJSONColumn(row.config) ?? {},
7879
8076
  metadata: parseJSONColumn(row.metadata),
7880
8077
  observedMessageIds: parseJSONColumn(row.observedMessageIds),
7881
- observedTimezone: row.observedTimezone || void 0
8078
+ observedTimezone: row.observedTimezone || void 0,
8079
+ supersededBy: row.supersededBy ?? null
7882
8080
  };
7883
8081
  }
7884
8082
  };