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