@mastra/oracledb 0.5.2-alpha.0 → 0.5.2-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3434,6 +3434,59 @@ function isAppendOnlySince(storedObservations, snapshotObservations) {
3434
3434
  return stored === snapshot || stored.startsWith(snapshot);
3435
3435
  }
3436
3436
  //#endregion
3437
+ //#region src/storage/db/run-fencing.ts
3438
+ const SINCE_EPOCH = `(SYS_EXTRACT_UTC(SYSTIMESTAMP) - TIMESTAMP '1970-01-01 00:00:00')`;
3439
+ /**
3440
+ * Epoch milliseconds on the database clock. Oracle evaluates `SYSTIMESTAMP`
3441
+ * once per statement, so every reference here reads the same instant, and a
3442
+ * statement that waits on a lock compares against the time it started. That
3443
+ * error only makes an existing lease look longer and a new lease shorter.
3444
+ */
3445
+ const DB_NOW_MS = `(EXTRACT(DAY FROM ${SINCE_EPOCH}) * 86400000 + EXTRACT(HOUR FROM ${SINCE_EPOCH}) * 3600000 + EXTRACT(MINUTE FROM ${SINCE_EPOCH}) * 60000 + FLOOR(EXTRACT(SECOND FROM ${SINCE_EPOCH}) * 1000))`;
3446
+ const ORA_UNIQUE_CONSTRAINT = -1;
3447
+ const ORA_DEADLOCK = -60;
3448
+ const MAX_CLAIM_ATTEMPTS = 5;
3449
+ /**
3450
+ * The check half of a fenced write; call it first inside the write's
3451
+ * transaction. Oracle has no shared row lock, so this takes the claim row
3452
+ * `FOR UPDATE`: a claim that updates the row waits for the write to commit,
3453
+ * and a write queued behind a claim re-reads the row once the claim commits
3454
+ * and sees the new generation. Writes from one run serialize on its row,
3455
+ * which they mostly do anyway.
3456
+ */
3457
+ async function assertRunFence(client, claimsTable, fence, operation) {
3458
+ const row = await client.oneOrNone(`SELECT generation AS "generation", "ownerId" FROM ${claimsTable} WHERE "runId" = :runId FOR UPDATE`, { runId: fence.runId });
3459
+ if (!(0, _mastra_core_storage.matchesRunFence)(row ? {
3460
+ generation: Number(row.generation),
3461
+ ownerId: String(row.ownerId)
3462
+ } : null, fence)) throw new _mastra_core_storage.RunFenceConflictError(fence, operation);
3463
+ }
3464
+ /**
3465
+ * Runs `write` in a transaction, behind the fence check when it carries a
3466
+ * fence.
3467
+ */
3468
+ function withRunFence(db, claimsTable, fence, operation, write) {
3469
+ return db.tx(async (client) => {
3470
+ if (fence) await assertRunFence(client, claimsTable, fence, operation);
3471
+ return write(client);
3472
+ });
3473
+ }
3474
+ /**
3475
+ * Runs a transaction that creates or updates a run's claim row, retrying when
3476
+ * two first claims race to insert it. `FOR UPDATE` locks nothing while the row
3477
+ * is missing, so racing first claims both insert; the second waits on the
3478
+ * primary key and fails once the first commits. On retry the row exists and
3479
+ * the claim serializes behind its row lock. A deadlock is retried too.
3480
+ */
3481
+ async function claimTransaction(db, fn) {
3482
+ for (let attempt = 1;; attempt++) try {
3483
+ return await db.tx((client) => fn(client));
3484
+ } catch (error) {
3485
+ const retryable = isOracleErrorCode(error, [ORA_UNIQUE_CONSTRAINT, ORA_DEADLOCK]);
3486
+ if (attempt >= MAX_CLAIM_ATTEMPTS || !retryable) throw error;
3487
+ }
3488
+ }
3489
+ //#endregion
3437
3490
  //#region src/storage/domains/memory/utils.ts
3438
3491
  const STORE_NAME$5 = "ORACLEDB";
3439
3492
  const EMPTY_STRING_SENTINEL = "__MASTRA_ORACLE_EMPTY_STRING__";
@@ -3441,6 +3494,14 @@ const STRING_SENTINEL_ESCAPE_PREFIX = "__MASTRA_ORACLE_ESCAPED__";
3441
3494
  function table(ctx, tableName) {
3442
3495
  return qualifyName(tableName, ctx.schemaName);
3443
3496
  }
3497
+ /**
3498
+ * The fence check for a memory write; call it first inside the write's
3499
+ * transaction. `fence` is the one `MemoryOracle` already resolved against the
3500
+ * run fence scope, so an unfenced write skips the check.
3501
+ */
3502
+ async function assertMemoryFence(ctx, client, fence, operation) {
3503
+ if (fence) await assertRunFence(client, table(ctx, _mastra_core_storage.TABLE_MEMORY_RUN_FENCES), fence, operation);
3504
+ }
3444
3505
  function storageError$1(operation, reason, details, cause, category = _mastra_core_error.ErrorCategory.THIRD_PARTY) {
3445
3506
  return createOracleStorageError({
3446
3507
  storeName: STORE_NAME$5,
@@ -3998,7 +4059,7 @@ async function listMessagesByResourceId(ctx, args) {
3998
4059
  orderBy
3999
4060
  });
4000
4061
  }
4001
- async function saveMessages(ctx, { messages }) {
4062
+ async function saveMessages(ctx, { messages, fence }) {
4002
4063
  if (messages.length === 0) return { messages: [] };
4003
4064
  const threadIds = /* @__PURE__ */ new Set();
4004
4065
  try {
@@ -4008,6 +4069,7 @@ async function saveMessages(ctx, { messages }) {
4008
4069
  }
4009
4070
  for (const threadId of threadIds) if (!await ctx.getThreadById({ threadId })) throw storageError$1("SAVE_MESSAGES", "FAILED", { threadId }, /* @__PURE__ */ new Error(`Thread ${threadId} not found`), _mastra_core_error.ErrorCategory.USER);
4010
4071
  await ctx.db.tx(async (client) => {
4072
+ await assertMemoryFence(ctx, client, fence, "saveMessages");
4011
4073
  await insertMessageBatch(ctx, client, messages);
4012
4074
  });
4013
4075
  return { messages: new _mastra_core_agent.MessageList().add(messages, "memory").get.all.db() };
@@ -4085,7 +4147,7 @@ async function insertMessageBatch(ctx, client, messages) {
4085
4147
  threadId: messageThreadId
4086
4148
  })));
4087
4149
  }
4088
- async function updateMessages(ctx, { messages }) {
4150
+ async function updateMessages(ctx, { messages, fence }) {
4089
4151
  if (messages.length === 0) return [];
4090
4152
  const messageIds = messages.map((message) => message.id);
4091
4153
  const existingMessages = (await ctx.listMessagesById({ messageIds })).messages;
@@ -4094,6 +4156,7 @@ async function updateMessages(ctx, { messages }) {
4094
4156
  const threadIdsToUpdate = /* @__PURE__ */ new Set();
4095
4157
  try {
4096
4158
  await ctx.db.tx(async (client) => {
4159
+ await assertMemoryFence(ctx, client, fence, "updateMessages");
4097
4160
  const semanticRecallMessageIds = [];
4098
4161
  for (const updatePayload of messages) {
4099
4162
  const existing = existingById.get(updatePayload.id);
@@ -4140,13 +4203,15 @@ async function updateMessages(ctx, { messages }) {
4140
4203
  });
4141
4204
  return (await ctx.listMessagesById({ messageIds })).messages;
4142
4205
  } catch (error) {
4206
+ if (error instanceof _mastra_core_error.MastraError) throw error;
4143
4207
  throw storageError$1("UPDATE_MESSAGES", "FAILED", { messageIds: messageIds.join(",") }, error);
4144
4208
  }
4145
4209
  }
4146
- async function deleteMessages(ctx, messageIds) {
4210
+ async function deleteMessages(ctx, messageIds, fence) {
4147
4211
  if (!messageIds.length) return;
4148
4212
  try {
4149
4213
  await ctx.db.tx(async (client) => {
4214
+ await assertMemoryFence(ctx, client, fence, "deleteMessages");
4150
4215
  const threadIds = /* @__PURE__ */ new Set();
4151
4216
  for (const [chunkIndex, chunk] of chunkValues(messageIds).entries()) {
4152
4217
  const { sql, binds } = inClause(`messageId${chunkIndex}`, chunk);
@@ -4162,6 +4227,7 @@ async function deleteMessages(ctx, messageIds) {
4162
4227
  })));
4163
4228
  });
4164
4229
  } catch (error) {
4230
+ if (error instanceof _mastra_core_error.MastraError) throw error;
4165
4231
  throw storageError$1("DELETE_MESSAGES", "FAILED", { messageIds: messageIds.join(",") }, error);
4166
4232
  }
4167
4233
  }
@@ -4543,9 +4609,10 @@ async function insertObservationalMemoryRecord(ctx, record) {
4543
4609
  }, error);
4544
4610
  }
4545
4611
  }
4546
- async function updateActiveObservations(ctx, input) {
4612
+ async function updateActiveObservations(ctx, input, fence) {
4547
4613
  try {
4548
- return await ctx.db.tx(async (_client, connection) => {
4614
+ return await ctx.db.tx(async (client, connection) => {
4615
+ await assertMemoryFence(ctx, client, fence, "commitActiveObservations");
4549
4616
  const row = await lockOMRow(ctx, connection, input.id, "UPDATE_ACTIVE_OBSERVATIONS");
4550
4617
  if (row.supersededBy) return {
4551
4618
  applied: false,
@@ -4582,10 +4649,11 @@ async function updateActiveObservations(ctx, input) {
4582
4649
  throw storageError$1("UPDATE_ACTIVE_OBSERVATIONS", "FAILED", { id: input.id }, error);
4583
4650
  }
4584
4651
  }
4585
- async function createReflectionGeneration(ctx, input) {
4652
+ async function createReflectionGeneration(ctx, input, fence) {
4586
4653
  const { currentRecord } = input;
4587
4654
  try {
4588
- return await ctx.db.tx(async (_client, connection) => {
4655
+ return await ctx.db.tx(async (client, connection) => {
4656
+ await assertMemoryFence(ctx, client, fence, "createReflectionGeneration");
4589
4657
  const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
4590
4658
  if (!row) return currentRecord;
4591
4659
  if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
@@ -4993,9 +5061,10 @@ async function resolveLiveOMRow(ctx, connection, row) {
4993
5061
  }
4994
5062
  //#endregion
4995
5063
  //#region src/storage/domains/memory/observational-buffering.ts
4996
- async function updateBufferedObservations(ctx, input) {
5064
+ async function updateBufferedObservations(ctx, input, fence) {
4997
5065
  try {
4998
- return await ctx.db.tx(async (_client, connection) => {
5066
+ return await ctx.db.tx(async (client, connection) => {
5067
+ await assertMemoryFence(ctx, client, fence, "appendBufferedObservations");
4999
5068
  const row = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS"));
5000
5069
  const existingChunks = parseBufferedChunks(row.bufferedObservationChunks);
5001
5070
  if (existingChunks.some((existing) => existing.cycleId === input.chunk.cycleId) || isBufferedChunkCoveredByCursor(input.chunk.lastObservedAt, row.lastObservedAt)) return {
@@ -5038,9 +5107,10 @@ async function updateBufferedObservations(ctx, input) {
5038
5107
  throw storageError$1("UPDATE_BUFFERED_OBSERVATIONS", "FAILED", { id: input.id }, error);
5039
5108
  }
5040
5109
  }
5041
- async function swapBufferedToActive(ctx, input) {
5110
+ async function swapBufferedToActive(ctx, input, fence) {
5042
5111
  try {
5043
- return await ctx.db.tx(async (_client, connection) => {
5112
+ return await ctx.db.tx(async (client, connection) => {
5113
+ await assertMemoryFence(ctx, client, fence, "swapBufferedToActive");
5044
5114
  const row = await lockOMRow(ctx, connection, input.id, "SWAP_BUFFERED_TO_ACTIVE");
5045
5115
  if (row.supersededBy) return {
5046
5116
  ...emptySwapResult(),
@@ -5085,9 +5155,10 @@ async function swapBufferedToActive(ctx, input) {
5085
5155
  throw storageError$1("SWAP_BUFFERED_TO_ACTIVE", "FAILED", { id: input.id }, error);
5086
5156
  }
5087
5157
  }
5088
- async function updateBufferedReflection(ctx, input) {
5158
+ async function updateBufferedReflection(ctx, input, fence) {
5089
5159
  try {
5090
- await ctx.db.tx(async (_client, connection) => {
5160
+ await ctx.db.tx(async (client, connection) => {
5161
+ await assertMemoryFence(ctx, client, fence, "updateBufferedReflection");
5091
5162
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
5092
5163
  SET ${OM_BUFFERED_REFLECTION} = CASE
5093
5164
  WHEN ${OM_BUFFERED_REFLECTION} IS NOT NULL AND DBMS_LOB.GETLENGTH(${OM_BUFFERED_REFLECTION}) > 0
@@ -5112,10 +5183,11 @@ async function updateBufferedReflection(ctx, input) {
5112
5183
  throw storageError$1("UPDATE_BUFFERED_REFLECTION", "FAILED", { id: input.id }, error);
5113
5184
  }
5114
5185
  }
5115
- async function swapBufferedReflectionToActive(ctx, input) {
5186
+ async function swapBufferedReflectionToActive(ctx, input, fence) {
5116
5187
  const { currentRecord } = input;
5117
5188
  try {
5118
- return await ctx.db.tx(async (_client, connection) => {
5189
+ return await ctx.db.tx(async (client, connection) => {
5190
+ await assertMemoryFence(ctx, client, fence, "swapBufferedReflectionToActive");
5119
5191
  const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
5120
5192
  if (!row) return currentRecord;
5121
5193
  if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
@@ -5268,17 +5340,24 @@ async function saveResource(ctx, { resource }) {
5268
5340
  throw storageError$1("SAVE_RESOURCE", "FAILED", { resourceId: resource.id }, error);
5269
5341
  }
5270
5342
  }
5271
- async function updateResource(ctx, { resourceId, workingMemory, metadata }) {
5343
+ async function updateResource(ctx, { resourceId, workingMemory, metadata, fence }) {
5272
5344
  const existing = await ctx.getResourceById({ resourceId });
5273
- if (!existing) return ctx.saveResource({ resource: {
5345
+ const newResource = () => ({
5274
5346
  id: resourceId,
5275
5347
  workingMemory,
5276
5348
  metadata: metadata ?? {},
5277
5349
  createdAt: /* @__PURE__ */ new Date(),
5278
5350
  updatedAt: /* @__PURE__ */ new Date()
5279
- } });
5351
+ });
5352
+ if (!existing && !fence) return ctx.saveResource({ resource: newResource() });
5280
5353
  try {
5281
- return await ctx.db.tx(async (_client, connection) => {
5354
+ return await ctx.db.tx(async (client, connection) => {
5355
+ await assertMemoryFence(ctx, client, fence, "updateResource");
5356
+ if (!existing) {
5357
+ const resource = newResource();
5358
+ await mergeResourceRow(ctx, connection, resource);
5359
+ return resource;
5360
+ }
5282
5361
  const row = rows(await connection.execute(`${resourceSelect()} FROM ${table(ctx, _mastra_core_storage.TABLE_RESOURCES)} WHERE id = :resourceId FOR UPDATE`, { resourceId }, executeOptions()))[0];
5283
5362
  const locked = row ? parseResource(row) : existing;
5284
5363
  const updated = {
@@ -5372,11 +5451,15 @@ async function getThreadById(ctx, { threadId, resourceId }) {
5372
5451
  throw storageError$1("GET_THREAD_BY_ID", "FAILED", { threadId }, error);
5373
5452
  }
5374
5453
  }
5375
- async function saveThread(ctx, { thread }) {
5454
+ async function saveThread(ctx, { thread, fence }) {
5376
5455
  try {
5377
- await mergeThreadRow(ctx, ctx.db, thread);
5456
+ await ctx.db.tx(async (client) => {
5457
+ await assertMemoryFence(ctx, client, fence, "saveThread");
5458
+ await mergeThreadRow(ctx, client, thread);
5459
+ });
5378
5460
  return thread;
5379
5461
  } catch (error) {
5462
+ if (error instanceof _mastra_core_error.MastraError) throw error;
5380
5463
  throw storageError$1("SAVE_THREAD", "FAILED", { threadId: thread.id }, error);
5381
5464
  }
5382
5465
  }
@@ -5461,7 +5544,7 @@ async function insertThreadRow(ctx, client, thread) {
5461
5544
  updatedAt: thread.updatedAt ?? /* @__PURE__ */ new Date()
5462
5545
  });
5463
5546
  }
5464
- async function updateThread(ctx, { id, title, metadata }) {
5547
+ async function updateThread(ctx, { id, title, metadata, fence }) {
5465
5548
  const existingThread = await ctx.getThreadById({ threadId: id });
5466
5549
  if (!existingThread) throw storageError$1("UPDATE_THREAD", "FAILED", {
5467
5550
  threadId: id,
@@ -5473,16 +5556,19 @@ async function updateThread(ctx, { id, title, metadata }) {
5473
5556
  };
5474
5557
  const updatedAt = /* @__PURE__ */ new Date();
5475
5558
  try {
5476
- await ctx.db.none(`
5477
- UPDATE ${table(ctx, _mastra_core_storage.TABLE_THREADS)}
5478
- SET title = COALESCE(:title, title),
5479
- metadata = :metadata,
5480
- ${THREAD_UPDATED_AT} = :updatedAt
5481
- WHERE id = :id`, {
5482
- id,
5483
- title: optionalStringBind(title),
5484
- metadata: jsonBind(mergedMetadata),
5485
- updatedAt
5559
+ await ctx.db.tx(async (client) => {
5560
+ await assertMemoryFence(ctx, client, fence, "updateThread");
5561
+ await client.none(`
5562
+ UPDATE ${table(ctx, _mastra_core_storage.TABLE_THREADS)}
5563
+ SET title = COALESCE(:title, title),
5564
+ metadata = :metadata,
5565
+ ${THREAD_UPDATED_AT} = :updatedAt
5566
+ WHERE id = :id`, {
5567
+ id,
5568
+ title: optionalStringBind(title),
5569
+ metadata: jsonBind(mergedMetadata),
5570
+ updatedAt
5571
+ });
5486
5572
  });
5487
5573
  const updatedThread = await ctx.getThreadById({ threadId: id });
5488
5574
  if (!updatedThread) throw storageError$1("UPDATE_THREAD", "FAILED", {
@@ -5634,7 +5720,8 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5634
5720
  _mastra_core_storage.TABLE_THREADS,
5635
5721
  _mastra_core_storage.TABLE_MESSAGES,
5636
5722
  _mastra_core_storage.TABLE_RESOURCES,
5637
- _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY
5723
+ _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY,
5724
+ _mastra_core_storage.TABLE_MEMORY_RUN_FENCES
5638
5725
  ];
5639
5726
  db;
5640
5727
  schemaName;
@@ -5672,6 +5759,10 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5672
5759
  }
5673
5760
  async init() {
5674
5761
  await initMemorySchema(this.ctx);
5762
+ await this.db.createTable({
5763
+ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
5764
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_MEMORY_RUN_FENCES]
5765
+ });
5675
5766
  }
5676
5767
  /**
5677
5768
  * Mark observational memory rows that are not their key's canonical head as superseded.
@@ -5684,15 +5775,61 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5684
5775
  }
5685
5776
  async dangerouslyClearAll() {
5686
5777
  await clearAllMemoryTables(this.ctx);
5778
+ await this.db.clearTable(_mastra_core_storage.TABLE_MEMORY_RUN_FENCES);
5779
+ }
5780
+ supportsRunFencing() {
5781
+ return true;
5782
+ }
5783
+ async raiseRunFence(fence) {
5784
+ const fences = table(this.ctx, _mastra_core_storage.TABLE_MEMORY_RUN_FENCES);
5785
+ try {
5786
+ return await claimTransaction(this.db, async (client) => {
5787
+ const row = await client.oneOrNone(`SELECT generation AS "generation", "ownerId" FROM ${fences} WHERE "runId" = :runId FOR UPDATE`, { runId: fence.runId });
5788
+ const binds = {
5789
+ runId: fence.runId,
5790
+ generation: fence.generation,
5791
+ ownerId: fence.ownerId
5792
+ };
5793
+ if (!row) {
5794
+ await client.none(`INSERT INTO ${fences} ("runId", generation, "ownerId") VALUES (:runId, :generation, :ownerId)`, binds);
5795
+ return true;
5796
+ }
5797
+ const generation = Number(row.generation);
5798
+ if (!(generation < fence.generation) && !(generation === fence.generation && String(row.ownerId) === fence.ownerId)) return false;
5799
+ await client.none(`UPDATE ${fences} SET generation = :generation, "ownerId" = :ownerId, "retiredAt" = NULL WHERE "runId" = :runId`, binds);
5800
+ return true;
5801
+ });
5802
+ } catch (error) {
5803
+ throw storageError$1("RAISE_RUN_FENCE", "FAILED", { runId: fence.runId }, error);
5804
+ }
5805
+ }
5806
+ async retireRunFence(fence) {
5807
+ const fences = table(this.ctx, _mastra_core_storage.TABLE_MEMORY_RUN_FENCES);
5808
+ try {
5809
+ return await this.db.tx(async (client) => {
5810
+ const row = await client.oneOrNone(`SELECT generation AS "generation", "ownerId" FROM ${fences} WHERE "runId" = :runId FOR UPDATE`, { runId: fence.runId });
5811
+ if (!row || Number(row.generation) !== fence.generation || String(row.ownerId) !== fence.ownerId) return false;
5812
+ await client.none(`UPDATE ${fences} SET "retiredAt" = ${DB_NOW_MS} WHERE "runId" = :runId`, { runId: fence.runId });
5813
+ return true;
5814
+ });
5815
+ } catch (error) {
5816
+ throw storageError$1("RETIRE_RUN_FENCE", "FAILED", { runId: fence.runId }, error);
5817
+ }
5687
5818
  }
5688
5819
  async getThreadById(args) {
5689
5820
  return getThreadById(this.ctx, args);
5690
5821
  }
5691
- async saveThread(args) {
5692
- return saveThread(this.ctx, args);
5822
+ async saveThread({ fence, ...args }) {
5823
+ return saveThread(this.ctx, {
5824
+ ...args,
5825
+ fence: (0, _mastra_core_storage.resolveRunFence)(this, fence)
5826
+ });
5693
5827
  }
5694
- async updateThread(args) {
5695
- return updateThread(this.ctx, args);
5828
+ async updateThread({ fence, ...args }) {
5829
+ return updateThread(this.ctx, {
5830
+ ...args,
5831
+ fence: (0, _mastra_core_storage.resolveRunFence)(this, fence)
5832
+ });
5696
5833
  }
5697
5834
  async deleteThread(args) {
5698
5835
  return deleteThread(this.ctx, args);
@@ -5712,14 +5849,20 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5712
5849
  async listMessagesByResourceId(args) {
5713
5850
  return listMessagesByResourceId(this.ctx, args);
5714
5851
  }
5715
- async saveMessages(args) {
5716
- return saveMessages(this.ctx, args);
5852
+ async saveMessages({ fence, ...args }) {
5853
+ return saveMessages(this.ctx, {
5854
+ ...args,
5855
+ fence: (0, _mastra_core_storage.resolveRunFence)(this, fence)
5856
+ });
5717
5857
  }
5718
- async updateMessages(args) {
5719
- return updateMessages(this.ctx, args);
5858
+ async updateMessages({ fence, ...args }) {
5859
+ return updateMessages(this.ctx, {
5860
+ ...args,
5861
+ fence: (0, _mastra_core_storage.resolveRunFence)(this, fence)
5862
+ });
5720
5863
  }
5721
- async deleteMessages(messageIds) {
5722
- return deleteMessages(this.ctx, messageIds);
5864
+ async deleteMessages(messageIds, options) {
5865
+ return deleteMessages(this.ctx, messageIds, (0, _mastra_core_storage.resolveRunFence)(this, options?.fence));
5723
5866
  }
5724
5867
  async getResourceById(args) {
5725
5868
  return getResourceById(this.ctx, args);
@@ -5727,8 +5870,11 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5727
5870
  async saveResource(args) {
5728
5871
  return saveResource(this.ctx, args);
5729
5872
  }
5730
- async updateResource(args) {
5731
- return updateResource(this.ctx, args);
5873
+ async updateResource({ fence, ...args }) {
5874
+ return updateResource(this.ctx, {
5875
+ ...args,
5876
+ fence: (0, _mastra_core_storage.resolveRunFence)(this, fence)
5877
+ });
5732
5878
  }
5733
5879
  async cloneThread(args) {
5734
5880
  const sourceThread = await this.getThreadById({ threadId: args.sourceThreadId });
@@ -5821,10 +5967,10 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5821
5967
  assertActiveObservationsApplied(await this.commitActiveObservations(input), input.id);
5822
5968
  }
5823
5969
  async commitActiveObservations(input) {
5824
- return updateActiveObservations(this.ctx, input);
5970
+ return updateActiveObservations(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5825
5971
  }
5826
5972
  async createReflectionGeneration(input) {
5827
- return createReflectionGeneration(this.ctx, input);
5973
+ return createReflectionGeneration(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5828
5974
  }
5829
5975
  async setReflectingFlag(id, isReflecting) {
5830
5976
  return setReflectingFlag(this.ctx, id, isReflecting);
@@ -5851,16 +5997,16 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5851
5997
  await this.appendBufferedObservations(input);
5852
5998
  }
5853
5999
  async appendBufferedObservations(input) {
5854
- return updateBufferedObservations(this.ctx, input);
6000
+ return updateBufferedObservations(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5855
6001
  }
5856
6002
  async swapBufferedToActive(input) {
5857
- return swapBufferedToActive(this.ctx, input);
6003
+ return swapBufferedToActive(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5858
6004
  }
5859
6005
  async updateBufferedReflection(input) {
5860
- return updateBufferedReflection(this.ctx, input);
6006
+ return updateBufferedReflection(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5861
6007
  }
5862
6008
  async swapBufferedReflectionToActive(input) {
5863
- return swapBufferedReflectionToActive(this.ctx, input);
6009
+ return swapBufferedReflectionToActive(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5864
6010
  }
5865
6011
  };
5866
6012
  //#endregion
@@ -8205,8 +8351,19 @@ const STORE_NAME$1 = "ORACLEDB";
8205
8351
  const WORKFLOW_CREATED_AT = "\"createdAt\"";
8206
8352
  const WORKFLOW_UPDATED_AT = "\"updatedAt\"";
8207
8353
  const WORKFLOW_RESOURCE_ID = "\"resourceId\"";
8354
+ const RUN_OWNER_COLUMNS = `generation AS "generation", "ownerId", "leaseExpiresAt", ${DB_NOW_MS} AS "nowMs"`;
8355
+ function toRunOwnershipRecord(runId, row) {
8356
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
8357
+ return {
8358
+ runId,
8359
+ generation: Number(row.generation),
8360
+ ownerId: String(row.ownerId),
8361
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
8362
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
8363
+ };
8364
+ }
8208
8365
  var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.WorkflowsStorage {
8209
- static MANAGED_TABLES = [_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT];
8366
+ static MANAGED_TABLES = [_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS];
8210
8367
  db;
8211
8368
  schemaName;
8212
8369
  skipDefaultIndexes;
@@ -8227,10 +8384,114 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8227
8384
  }
8228
8385
  async dangerouslyClearAll() {
8229
8386
  await this.db.clearTable(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT);
8387
+ await this.db.clearTable(_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS);
8388
+ }
8389
+ supportsRunFencing() {
8390
+ return true;
8391
+ }
8392
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
8393
+ const table = this.runOwnersTable();
8394
+ try {
8395
+ return await claimTransaction(this.db, async (client) => {
8396
+ const existing = await client.oneOrNone(`SELECT generation AS "generation" FROM ${table} WHERE "runId" = :runId FOR UPDATE`, { runId });
8397
+ if (!existing) {
8398
+ if (expectedGeneration !== void 0 && expectedGeneration !== 0) return {
8399
+ acquired: false,
8400
+ record: null
8401
+ };
8402
+ await client.none(`INSERT INTO ${table} ("runId", generation, "ownerId", "leaseExpiresAt", "updatedAt")
8403
+ VALUES (:runId, ${DB_NOW_MS}, :ownerId, ${DB_NOW_MS} + :leaseMs, ${DB_NOW_MS})`, {
8404
+ runId,
8405
+ ownerId,
8406
+ leaseMs
8407
+ });
8408
+ return {
8409
+ acquired: true,
8410
+ record: await this.readRunOwner(client, runId)
8411
+ };
8412
+ }
8413
+ await client.none(`UPDATE ${table}
8414
+ SET generation = generation + 1, "ownerId" = :ownerId, "leaseExpiresAt" = ${DB_NOW_MS} + :leaseMs,
8415
+ "updatedAt" = ${DB_NOW_MS}
8416
+ WHERE "runId" = :runId
8417
+ AND (:expectedGeneration IS NULL OR generation = :expectedGeneration)
8418
+ AND (:force = 1 OR "leaseExpiresAt" IS NULL OR "leaseExpiresAt" <= ${DB_NOW_MS})`, {
8419
+ runId,
8420
+ ownerId,
8421
+ leaseMs,
8422
+ expectedGeneration: expectedGeneration ?? null,
8423
+ force: force === true ? 1 : 0
8424
+ });
8425
+ const record = await this.readRunOwner(client, runId);
8426
+ return record && record.generation === Number(existing.generation) + 1 ? {
8427
+ acquired: true,
8428
+ record
8429
+ } : {
8430
+ acquired: false,
8431
+ record
8432
+ };
8433
+ });
8434
+ } catch (error) {
8435
+ throw this.storageError("CLAIM_RUN_OWNERSHIP", "FAILED", { runId }, error);
8436
+ }
8437
+ }
8438
+ async renewRunOwnership({ leaseMs, ...fence }) {
8439
+ try {
8440
+ return await this.db.tx(async (client) => {
8441
+ await client.none(`UPDATE ${this.runOwnersTable()}
8442
+ SET "leaseExpiresAt" = ${DB_NOW_MS} + :leaseMs, "updatedAt" = ${DB_NOW_MS}
8443
+ WHERE "runId" = :runId AND generation = :generation AND "ownerId" = :ownerId
8444
+ AND "leaseExpiresAt" IS NOT NULL`, {
8445
+ runId: fence.runId,
8446
+ generation: fence.generation,
8447
+ ownerId: fence.ownerId,
8448
+ leaseMs
8449
+ });
8450
+ const record = await this.readRunOwner(client, fence.runId);
8451
+ return record !== null && record.generation === fence.generation && record.ownerId === fence.ownerId && record.leaseExpiresAt !== null ? {
8452
+ renewed: true,
8453
+ record
8454
+ } : {
8455
+ renewed: false,
8456
+ record
8457
+ };
8458
+ });
8459
+ } catch (error) {
8460
+ throw this.storageError("RENEW_RUN_OWNERSHIP", "FAILED", { runId: fence.runId }, error);
8461
+ }
8230
8462
  }
8231
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
8463
+ async releaseRunOwnership(fence) {
8232
8464
  try {
8233
8465
  return await this.db.tx(async (client) => {
8466
+ await client.none(`UPDATE ${this.runOwnersTable()} SET "leaseExpiresAt" = NULL, "updatedAt" = ${DB_NOW_MS}
8467
+ WHERE "runId" = :runId AND generation = :generation AND "ownerId" = :ownerId`, {
8468
+ runId: fence.runId,
8469
+ generation: fence.generation,
8470
+ ownerId: fence.ownerId
8471
+ });
8472
+ const record = await this.readRunOwner(client, fence.runId);
8473
+ return record !== null && record.generation === fence.generation && record.ownerId === fence.ownerId;
8474
+ });
8475
+ } catch (error) {
8476
+ throw this.storageError("RELEASE_RUN_OWNERSHIP", "FAILED", { runId: fence.runId }, error);
8477
+ }
8478
+ }
8479
+ async getRunOwnership({ runId }) {
8480
+ try {
8481
+ return await this.readRunOwner(this.db, runId);
8482
+ } catch (error) {
8483
+ throw this.storageError("GET_RUN_OWNERSHIP", "FAILED", { runId }, error);
8484
+ }
8485
+ }
8486
+ async readRunOwner(client, runId) {
8487
+ const row = await client.oneOrNone(`SELECT ${RUN_OWNER_COLUMNS} FROM ${this.runOwnersTable()} WHERE "runId" = :runId`, { runId });
8488
+ return row ? toRunOwnershipRecord(runId, row) : null;
8489
+ }
8490
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
8491
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
8492
+ try {
8493
+ return await this.db.tx(async (client) => {
8494
+ if (fence) await assertRunFence(client, this.runOwnersTable(), fence, "updateWorkflowResults");
8234
8495
  await this.ensureWorkflowRunRow(client, workflowName, runId);
8235
8496
  const existing = await client.one(`SELECT snapshot AS "snapshot", ${WORKFLOW_RESOURCE_ID} AS "resourceId" FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId FOR UPDATE`, {
8236
8497
  workflowName,
@@ -8254,6 +8515,7 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8254
8515
  return snapshot.context;
8255
8516
  });
8256
8517
  } catch (error) {
8518
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
8257
8519
  throw this.storageError("UPDATE_WORKFLOW_RESULTS", "FAILED", {
8258
8520
  workflowName,
8259
8521
  runId,
@@ -8261,9 +8523,11 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8261
8523
  }, error);
8262
8524
  }
8263
8525
  }
8264
- async updateWorkflowState({ workflowName, runId, opts }) {
8526
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
8527
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
8265
8528
  try {
8266
8529
  return await this.db.tx(async (client) => {
8530
+ if (fence) await assertRunFence(client, this.runOwnersTable(), fence, "updateWorkflowState");
8267
8531
  const existing = await client.oneOrNone(`SELECT snapshot AS "snapshot" FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId FOR UPDATE`, {
8268
8532
  workflowName,
8269
8533
  runId
@@ -8286,24 +8550,26 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8286
8550
  return updatedSnapshot;
8287
8551
  });
8288
8552
  } catch (error) {
8553
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
8289
8554
  throw this.storageError("UPDATE_WORKFLOW_STATE", "FAILED", {
8290
8555
  workflowName,
8291
8556
  runId
8292
8557
  }, error);
8293
8558
  }
8294
8559
  }
8295
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
8560
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
8296
8561
  try {
8297
8562
  const now = /* @__PURE__ */ new Date();
8298
- await this.db.none(this.workflowMergeSql(), {
8563
+ await withRunFence(this.db, this.runOwnersTable(), (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "persistWorkflowSnapshot", (client) => client.none(this.workflowMergeSql(), {
8299
8564
  workflowName,
8300
8565
  runId,
8301
8566
  resourceId: resourceId ?? null,
8302
8567
  snapshot: jsonBind(snapshot),
8303
8568
  createdAt: createdAt ?? now,
8304
8569
  updatedAt: updatedAt ?? now
8305
- });
8570
+ }));
8306
8571
  } catch (error) {
8572
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
8307
8573
  throw this.storageError("PERSIST_WORKFLOW_SNAPSHOT", "FAILED", {
8308
8574
  workflowName,
8309
8575
  runId
@@ -8367,13 +8633,14 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8367
8633
  }, error);
8368
8634
  }
8369
8635
  }
8370
- async deleteWorkflowRunById({ runId, workflowName }) {
8636
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
8371
8637
  try {
8372
- await this.db.none(`DELETE FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId`, {
8638
+ await withRunFence(this.db, this.runOwnersTable(), (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "deleteWorkflowRunById", (client) => client.none(`DELETE FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId`, {
8373
8639
  workflowName,
8374
8640
  runId
8375
- });
8641
+ }));
8376
8642
  } catch (error) {
8643
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
8377
8644
  throw this.storageError("DELETE_WORKFLOW_RUN_BY_ID", "FAILED", {
8378
8645
  runId,
8379
8646
  workflowName
@@ -8392,6 +8659,10 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8392
8659
  CONSTRAINT ${this.constraint("MASTRA_WORKFLOW_SNAPSHOT_PK")} PRIMARY KEY (workflow_name, run_id)
8393
8660
  )`, [-955]);
8394
8661
  await this.db.executeDdl(`ALTER TABLE ${this.table()} ADD (${WORKFLOW_RESOURCE_ID} VARCHAR2(512))`, [-1430]);
8662
+ await this.db.createTable({
8663
+ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
8664
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]
8665
+ });
8395
8666
  }
8396
8667
  async createIndexes() {
8397
8668
  await this.db.withConnection(async (connection) => {
@@ -8529,6 +8800,9 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8529
8800
  table() {
8530
8801
  return qualifyName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, this.schemaName);
8531
8802
  }
8803
+ runOwnersTable() {
8804
+ return qualifyName(_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, this.schemaName);
8805
+ }
8532
8806
  indexName(indexName) {
8533
8807
  return indexNameForTable(indexName, "IDX");
8534
8808
  }