@mastra/oracledb 0.5.2-alpha.0 → 0.5.2

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
@@ -2,7 +2,7 @@ import { MastraVector, validateTopK, validateUpsertInput } from "@mastra/core/ve
2
2
  import oracledb from "oracledb";
3
3
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
4
4
  import { createHash } from "crypto";
5
- import { AgentsStorage, MCPClientsStorage, MastraCompositeStore, MemoryStorage, ObservabilityStorage, ScorerDefinitionsStorage, ScoresStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MESSAGES, TABLE_OBSERVATIONAL_MEMORY, TABLE_RESOURCES, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_SNAPSHOT, TraceStatus, WorkflowsStorage, calculatePagination, createStorageErrorId, createVectorErrorId, executeRetentionPrune, listLogsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, normalizePerPage, resolveRetentionTargets, retentionCutoffMs, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
5
+ import { AgentsStorage, MCPClientsStorage, MastraCompositeStore, MemoryStorage, ObservabilityStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, ScorerDefinitionsStorage, ScoresStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_OBSERVATIONAL_MEMORY, TABLE_RESOURCES, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TraceStatus, WorkflowsStorage, calculatePagination, createStorageErrorId, createVectorErrorId, executeRetentionPrune, isRunFenceConflictError, listLogsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, normalizePerPage, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
6
6
  import { MessageList } from "@mastra/core/agent";
7
7
  import { saveScorePayloadSchema } from "@mastra/core/evals";
8
8
  //#region src/shared/connection.ts
@@ -3410,6 +3410,59 @@ function isAppendOnlySince(storedObservations, snapshotObservations) {
3410
3410
  return stored === snapshot || stored.startsWith(snapshot);
3411
3411
  }
3412
3412
  //#endregion
3413
+ //#region src/storage/db/run-fencing.ts
3414
+ const SINCE_EPOCH = `(SYS_EXTRACT_UTC(SYSTIMESTAMP) - TIMESTAMP '1970-01-01 00:00:00')`;
3415
+ /**
3416
+ * Epoch milliseconds on the database clock. Oracle evaluates `SYSTIMESTAMP`
3417
+ * once per statement, so every reference here reads the same instant, and a
3418
+ * statement that waits on a lock compares against the time it started. That
3419
+ * error only makes an existing lease look longer and a new lease shorter.
3420
+ */
3421
+ 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))`;
3422
+ const ORA_UNIQUE_CONSTRAINT = -1;
3423
+ const ORA_DEADLOCK = -60;
3424
+ const MAX_CLAIM_ATTEMPTS = 5;
3425
+ /**
3426
+ * The check half of a fenced write; call it first inside the write's
3427
+ * transaction. Oracle has no shared row lock, so this takes the claim row
3428
+ * `FOR UPDATE`: a claim that updates the row waits for the write to commit,
3429
+ * and a write queued behind a claim re-reads the row once the claim commits
3430
+ * and sees the new generation. Writes from one run serialize on its row,
3431
+ * which they mostly do anyway.
3432
+ */
3433
+ async function assertRunFence(client, claimsTable, fence, operation) {
3434
+ const row = await client.oneOrNone(`SELECT generation AS "generation", "ownerId" FROM ${claimsTable} WHERE "runId" = :runId FOR UPDATE`, { runId: fence.runId });
3435
+ if (!matchesRunFence(row ? {
3436
+ generation: Number(row.generation),
3437
+ ownerId: String(row.ownerId)
3438
+ } : null, fence)) throw new RunFenceConflictError(fence, operation);
3439
+ }
3440
+ /**
3441
+ * Runs `write` in a transaction, behind the fence check when it carries a
3442
+ * fence.
3443
+ */
3444
+ function withRunFence(db, claimsTable, fence, operation, write) {
3445
+ return db.tx(async (client) => {
3446
+ if (fence) await assertRunFence(client, claimsTable, fence, operation);
3447
+ return write(client);
3448
+ });
3449
+ }
3450
+ /**
3451
+ * Runs a transaction that creates or updates a run's claim row, retrying when
3452
+ * two first claims race to insert it. `FOR UPDATE` locks nothing while the row
3453
+ * is missing, so racing first claims both insert; the second waits on the
3454
+ * primary key and fails once the first commits. On retry the row exists and
3455
+ * the claim serializes behind its row lock. A deadlock is retried too.
3456
+ */
3457
+ async function claimTransaction(db, fn) {
3458
+ for (let attempt = 1;; attempt++) try {
3459
+ return await db.tx((client) => fn(client));
3460
+ } catch (error) {
3461
+ const retryable = isOracleErrorCode(error, [ORA_UNIQUE_CONSTRAINT, ORA_DEADLOCK]);
3462
+ if (attempt >= MAX_CLAIM_ATTEMPTS || !retryable) throw error;
3463
+ }
3464
+ }
3465
+ //#endregion
3413
3466
  //#region src/storage/domains/memory/utils.ts
3414
3467
  const STORE_NAME$5 = "ORACLEDB";
3415
3468
  const EMPTY_STRING_SENTINEL = "__MASTRA_ORACLE_EMPTY_STRING__";
@@ -3417,6 +3470,14 @@ const STRING_SENTINEL_ESCAPE_PREFIX = "__MASTRA_ORACLE_ESCAPED__";
3417
3470
  function table(ctx, tableName) {
3418
3471
  return qualifyName(tableName, ctx.schemaName);
3419
3472
  }
3473
+ /**
3474
+ * The fence check for a memory write; call it first inside the write's
3475
+ * transaction. `fence` is the one `MemoryOracle` already resolved against the
3476
+ * run fence scope, so an unfenced write skips the check.
3477
+ */
3478
+ async function assertMemoryFence(ctx, client, fence, operation) {
3479
+ if (fence) await assertRunFence(client, table(ctx, TABLE_MEMORY_RUN_FENCES), fence, operation);
3480
+ }
3420
3481
  function storageError$1(operation, reason, details, cause, category = ErrorCategory.THIRD_PARTY) {
3421
3482
  return createOracleStorageError({
3422
3483
  storeName: STORE_NAME$5,
@@ -3974,7 +4035,7 @@ async function listMessagesByResourceId(ctx, args) {
3974
4035
  orderBy
3975
4036
  });
3976
4037
  }
3977
- async function saveMessages(ctx, { messages }) {
4038
+ async function saveMessages(ctx, { messages, fence }) {
3978
4039
  if (messages.length === 0) return { messages: [] };
3979
4040
  const threadIds = /* @__PURE__ */ new Set();
3980
4041
  try {
@@ -3984,6 +4045,7 @@ async function saveMessages(ctx, { messages }) {
3984
4045
  }
3985
4046
  for (const threadId of threadIds) if (!await ctx.getThreadById({ threadId })) throw storageError$1("SAVE_MESSAGES", "FAILED", { threadId }, /* @__PURE__ */ new Error(`Thread ${threadId} not found`), ErrorCategory.USER);
3986
4047
  await ctx.db.tx(async (client) => {
4048
+ await assertMemoryFence(ctx, client, fence, "saveMessages");
3987
4049
  await insertMessageBatch(ctx, client, messages);
3988
4050
  });
3989
4051
  return { messages: new MessageList().add(messages, "memory").get.all.db() };
@@ -4061,7 +4123,7 @@ async function insertMessageBatch(ctx, client, messages) {
4061
4123
  threadId: messageThreadId
4062
4124
  })));
4063
4125
  }
4064
- async function updateMessages(ctx, { messages }) {
4126
+ async function updateMessages(ctx, { messages, fence }) {
4065
4127
  if (messages.length === 0) return [];
4066
4128
  const messageIds = messages.map((message) => message.id);
4067
4129
  const existingMessages = (await ctx.listMessagesById({ messageIds })).messages;
@@ -4070,6 +4132,7 @@ async function updateMessages(ctx, { messages }) {
4070
4132
  const threadIdsToUpdate = /* @__PURE__ */ new Set();
4071
4133
  try {
4072
4134
  await ctx.db.tx(async (client) => {
4135
+ await assertMemoryFence(ctx, client, fence, "updateMessages");
4073
4136
  const semanticRecallMessageIds = [];
4074
4137
  for (const updatePayload of messages) {
4075
4138
  const existing = existingById.get(updatePayload.id);
@@ -4116,13 +4179,15 @@ async function updateMessages(ctx, { messages }) {
4116
4179
  });
4117
4180
  return (await ctx.listMessagesById({ messageIds })).messages;
4118
4181
  } catch (error) {
4182
+ if (error instanceof MastraError) throw error;
4119
4183
  throw storageError$1("UPDATE_MESSAGES", "FAILED", { messageIds: messageIds.join(",") }, error);
4120
4184
  }
4121
4185
  }
4122
- async function deleteMessages(ctx, messageIds) {
4186
+ async function deleteMessages(ctx, messageIds, fence) {
4123
4187
  if (!messageIds.length) return;
4124
4188
  try {
4125
4189
  await ctx.db.tx(async (client) => {
4190
+ await assertMemoryFence(ctx, client, fence, "deleteMessages");
4126
4191
  const threadIds = /* @__PURE__ */ new Set();
4127
4192
  for (const [chunkIndex, chunk] of chunkValues(messageIds).entries()) {
4128
4193
  const { sql, binds } = inClause(`messageId${chunkIndex}`, chunk);
@@ -4138,6 +4203,7 @@ async function deleteMessages(ctx, messageIds) {
4138
4203
  })));
4139
4204
  });
4140
4205
  } catch (error) {
4206
+ if (error instanceof MastraError) throw error;
4141
4207
  throw storageError$1("DELETE_MESSAGES", "FAILED", { messageIds: messageIds.join(",") }, error);
4142
4208
  }
4143
4209
  }
@@ -4519,9 +4585,10 @@ async function insertObservationalMemoryRecord(ctx, record) {
4519
4585
  }, error);
4520
4586
  }
4521
4587
  }
4522
- async function updateActiveObservations(ctx, input) {
4588
+ async function updateActiveObservations(ctx, input, fence) {
4523
4589
  try {
4524
- return await ctx.db.tx(async (_client, connection) => {
4590
+ return await ctx.db.tx(async (client, connection) => {
4591
+ await assertMemoryFence(ctx, client, fence, "commitActiveObservations");
4525
4592
  const row = await lockOMRow(ctx, connection, input.id, "UPDATE_ACTIVE_OBSERVATIONS");
4526
4593
  if (row.supersededBy) return {
4527
4594
  applied: false,
@@ -4558,10 +4625,11 @@ async function updateActiveObservations(ctx, input) {
4558
4625
  throw storageError$1("UPDATE_ACTIVE_OBSERVATIONS", "FAILED", { id: input.id }, error);
4559
4626
  }
4560
4627
  }
4561
- async function createReflectionGeneration(ctx, input) {
4628
+ async function createReflectionGeneration(ctx, input, fence) {
4562
4629
  const { currentRecord } = input;
4563
4630
  try {
4564
- return await ctx.db.tx(async (_client, connection) => {
4631
+ return await ctx.db.tx(async (client, connection) => {
4632
+ await assertMemoryFence(ctx, client, fence, "createReflectionGeneration");
4565
4633
  const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
4566
4634
  if (!row) return currentRecord;
4567
4635
  if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
@@ -4969,9 +5037,10 @@ async function resolveLiveOMRow(ctx, connection, row) {
4969
5037
  }
4970
5038
  //#endregion
4971
5039
  //#region src/storage/domains/memory/observational-buffering.ts
4972
- async function updateBufferedObservations(ctx, input) {
5040
+ async function updateBufferedObservations(ctx, input, fence) {
4973
5041
  try {
4974
- return await ctx.db.tx(async (_client, connection) => {
5042
+ return await ctx.db.tx(async (client, connection) => {
5043
+ await assertMemoryFence(ctx, client, fence, "appendBufferedObservations");
4975
5044
  const row = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS"));
4976
5045
  const existingChunks = parseBufferedChunks(row.bufferedObservationChunks);
4977
5046
  if (existingChunks.some((existing) => existing.cycleId === input.chunk.cycleId) || isBufferedChunkCoveredByCursor(input.chunk.lastObservedAt, row.lastObservedAt)) return {
@@ -5014,9 +5083,10 @@ async function updateBufferedObservations(ctx, input) {
5014
5083
  throw storageError$1("UPDATE_BUFFERED_OBSERVATIONS", "FAILED", { id: input.id }, error);
5015
5084
  }
5016
5085
  }
5017
- async function swapBufferedToActive(ctx, input) {
5086
+ async function swapBufferedToActive(ctx, input, fence) {
5018
5087
  try {
5019
- return await ctx.db.tx(async (_client, connection) => {
5088
+ return await ctx.db.tx(async (client, connection) => {
5089
+ await assertMemoryFence(ctx, client, fence, "swapBufferedToActive");
5020
5090
  const row = await lockOMRow(ctx, connection, input.id, "SWAP_BUFFERED_TO_ACTIVE");
5021
5091
  if (row.supersededBy) return {
5022
5092
  ...emptySwapResult(),
@@ -5061,9 +5131,10 @@ async function swapBufferedToActive(ctx, input) {
5061
5131
  throw storageError$1("SWAP_BUFFERED_TO_ACTIVE", "FAILED", { id: input.id }, error);
5062
5132
  }
5063
5133
  }
5064
- async function updateBufferedReflection(ctx, input) {
5134
+ async function updateBufferedReflection(ctx, input, fence) {
5065
5135
  try {
5066
- await ctx.db.tx(async (_client, connection) => {
5136
+ await ctx.db.tx(async (client, connection) => {
5137
+ await assertMemoryFence(ctx, client, fence, "updateBufferedReflection");
5067
5138
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
5068
5139
  SET ${OM_BUFFERED_REFLECTION} = CASE
5069
5140
  WHEN ${OM_BUFFERED_REFLECTION} IS NOT NULL AND DBMS_LOB.GETLENGTH(${OM_BUFFERED_REFLECTION}) > 0
@@ -5088,10 +5159,11 @@ async function updateBufferedReflection(ctx, input) {
5088
5159
  throw storageError$1("UPDATE_BUFFERED_REFLECTION", "FAILED", { id: input.id }, error);
5089
5160
  }
5090
5161
  }
5091
- async function swapBufferedReflectionToActive(ctx, input) {
5162
+ async function swapBufferedReflectionToActive(ctx, input, fence) {
5092
5163
  const { currentRecord } = input;
5093
5164
  try {
5094
- return await ctx.db.tx(async (_client, connection) => {
5165
+ return await ctx.db.tx(async (client, connection) => {
5166
+ await assertMemoryFence(ctx, client, fence, "swapBufferedReflectionToActive");
5095
5167
  const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
5096
5168
  if (!row) return currentRecord;
5097
5169
  if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
@@ -5244,17 +5316,24 @@ async function saveResource(ctx, { resource }) {
5244
5316
  throw storageError$1("SAVE_RESOURCE", "FAILED", { resourceId: resource.id }, error);
5245
5317
  }
5246
5318
  }
5247
- async function updateResource(ctx, { resourceId, workingMemory, metadata }) {
5319
+ async function updateResource(ctx, { resourceId, workingMemory, metadata, fence }) {
5248
5320
  const existing = await ctx.getResourceById({ resourceId });
5249
- if (!existing) return ctx.saveResource({ resource: {
5321
+ const newResource = () => ({
5250
5322
  id: resourceId,
5251
5323
  workingMemory,
5252
5324
  metadata: metadata ?? {},
5253
5325
  createdAt: /* @__PURE__ */ new Date(),
5254
5326
  updatedAt: /* @__PURE__ */ new Date()
5255
- } });
5327
+ });
5328
+ if (!existing && !fence) return ctx.saveResource({ resource: newResource() });
5256
5329
  try {
5257
- return await ctx.db.tx(async (_client, connection) => {
5330
+ return await ctx.db.tx(async (client, connection) => {
5331
+ await assertMemoryFence(ctx, client, fence, "updateResource");
5332
+ if (!existing) {
5333
+ const resource = newResource();
5334
+ await mergeResourceRow(ctx, connection, resource);
5335
+ return resource;
5336
+ }
5258
5337
  const row = rows(await connection.execute(`${resourceSelect()} FROM ${table(ctx, TABLE_RESOURCES)} WHERE id = :resourceId FOR UPDATE`, { resourceId }, executeOptions()))[0];
5259
5338
  const locked = row ? parseResource(row) : existing;
5260
5339
  const updated = {
@@ -5348,11 +5427,15 @@ async function getThreadById(ctx, { threadId, resourceId }) {
5348
5427
  throw storageError$1("GET_THREAD_BY_ID", "FAILED", { threadId }, error);
5349
5428
  }
5350
5429
  }
5351
- async function saveThread(ctx, { thread }) {
5430
+ async function saveThread(ctx, { thread, fence }) {
5352
5431
  try {
5353
- await mergeThreadRow(ctx, ctx.db, thread);
5432
+ await ctx.db.tx(async (client) => {
5433
+ await assertMemoryFence(ctx, client, fence, "saveThread");
5434
+ await mergeThreadRow(ctx, client, thread);
5435
+ });
5354
5436
  return thread;
5355
5437
  } catch (error) {
5438
+ if (error instanceof MastraError) throw error;
5356
5439
  throw storageError$1("SAVE_THREAD", "FAILED", { threadId: thread.id }, error);
5357
5440
  }
5358
5441
  }
@@ -5437,7 +5520,7 @@ async function insertThreadRow(ctx, client, thread) {
5437
5520
  updatedAt: thread.updatedAt ?? /* @__PURE__ */ new Date()
5438
5521
  });
5439
5522
  }
5440
- async function updateThread(ctx, { id, title, metadata }) {
5523
+ async function updateThread(ctx, { id, title, metadata, fence }) {
5441
5524
  const existingThread = await ctx.getThreadById({ threadId: id });
5442
5525
  if (!existingThread) throw storageError$1("UPDATE_THREAD", "FAILED", {
5443
5526
  threadId: id,
@@ -5449,16 +5532,19 @@ async function updateThread(ctx, { id, title, metadata }) {
5449
5532
  };
5450
5533
  const updatedAt = /* @__PURE__ */ new Date();
5451
5534
  try {
5452
- await ctx.db.none(`
5453
- UPDATE ${table(ctx, TABLE_THREADS)}
5454
- SET title = COALESCE(:title, title),
5455
- metadata = :metadata,
5456
- ${THREAD_UPDATED_AT} = :updatedAt
5457
- WHERE id = :id`, {
5458
- id,
5459
- title: optionalStringBind(title),
5460
- metadata: jsonBind(mergedMetadata),
5461
- updatedAt
5535
+ await ctx.db.tx(async (client) => {
5536
+ await assertMemoryFence(ctx, client, fence, "updateThread");
5537
+ await client.none(`
5538
+ UPDATE ${table(ctx, TABLE_THREADS)}
5539
+ SET title = COALESCE(:title, title),
5540
+ metadata = :metadata,
5541
+ ${THREAD_UPDATED_AT} = :updatedAt
5542
+ WHERE id = :id`, {
5543
+ id,
5544
+ title: optionalStringBind(title),
5545
+ metadata: jsonBind(mergedMetadata),
5546
+ updatedAt
5547
+ });
5462
5548
  });
5463
5549
  const updatedThread = await ctx.getThreadById({ threadId: id });
5464
5550
  if (!updatedThread) throw storageError$1("UPDATE_THREAD", "FAILED", {
@@ -5610,7 +5696,8 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5610
5696
  TABLE_THREADS,
5611
5697
  TABLE_MESSAGES,
5612
5698
  TABLE_RESOURCES,
5613
- TABLE_OBSERVATIONAL_MEMORY
5699
+ TABLE_OBSERVATIONAL_MEMORY,
5700
+ TABLE_MEMORY_RUN_FENCES
5614
5701
  ];
5615
5702
  db;
5616
5703
  schemaName;
@@ -5648,6 +5735,10 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5648
5735
  }
5649
5736
  async init() {
5650
5737
  await initMemorySchema(this.ctx);
5738
+ await this.db.createTable({
5739
+ tableName: TABLE_MEMORY_RUN_FENCES,
5740
+ schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_MEMORY_RUN_FENCES]
5741
+ });
5651
5742
  }
5652
5743
  /**
5653
5744
  * Mark observational memory rows that are not their key's canonical head as superseded.
@@ -5660,15 +5751,61 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5660
5751
  }
5661
5752
  async dangerouslyClearAll() {
5662
5753
  await clearAllMemoryTables(this.ctx);
5754
+ await this.db.clearTable(TABLE_MEMORY_RUN_FENCES);
5755
+ }
5756
+ supportsRunFencing() {
5757
+ return true;
5758
+ }
5759
+ async raiseRunFence(fence) {
5760
+ const fences = table(this.ctx, TABLE_MEMORY_RUN_FENCES);
5761
+ try {
5762
+ return await claimTransaction(this.db, async (client) => {
5763
+ const row = await client.oneOrNone(`SELECT generation AS "generation", "ownerId" FROM ${fences} WHERE "runId" = :runId FOR UPDATE`, { runId: fence.runId });
5764
+ const binds = {
5765
+ runId: fence.runId,
5766
+ generation: fence.generation,
5767
+ ownerId: fence.ownerId
5768
+ };
5769
+ if (!row) {
5770
+ await client.none(`INSERT INTO ${fences} ("runId", generation, "ownerId") VALUES (:runId, :generation, :ownerId)`, binds);
5771
+ return true;
5772
+ }
5773
+ const generation = Number(row.generation);
5774
+ if (!(generation < fence.generation) && !(generation === fence.generation && String(row.ownerId) === fence.ownerId)) return false;
5775
+ await client.none(`UPDATE ${fences} SET generation = :generation, "ownerId" = :ownerId, "retiredAt" = NULL WHERE "runId" = :runId`, binds);
5776
+ return true;
5777
+ });
5778
+ } catch (error) {
5779
+ throw storageError$1("RAISE_RUN_FENCE", "FAILED", { runId: fence.runId }, error);
5780
+ }
5781
+ }
5782
+ async retireRunFence(fence) {
5783
+ const fences = table(this.ctx, TABLE_MEMORY_RUN_FENCES);
5784
+ try {
5785
+ return await this.db.tx(async (client) => {
5786
+ const row = await client.oneOrNone(`SELECT generation AS "generation", "ownerId" FROM ${fences} WHERE "runId" = :runId FOR UPDATE`, { runId: fence.runId });
5787
+ if (!row || Number(row.generation) !== fence.generation || String(row.ownerId) !== fence.ownerId) return false;
5788
+ await client.none(`UPDATE ${fences} SET "retiredAt" = ${DB_NOW_MS} WHERE "runId" = :runId`, { runId: fence.runId });
5789
+ return true;
5790
+ });
5791
+ } catch (error) {
5792
+ throw storageError$1("RETIRE_RUN_FENCE", "FAILED", { runId: fence.runId }, error);
5793
+ }
5663
5794
  }
5664
5795
  async getThreadById(args) {
5665
5796
  return getThreadById(this.ctx, args);
5666
5797
  }
5667
- async saveThread(args) {
5668
- return saveThread(this.ctx, args);
5798
+ async saveThread({ fence, ...args }) {
5799
+ return saveThread(this.ctx, {
5800
+ ...args,
5801
+ fence: resolveRunFence(this, fence)
5802
+ });
5669
5803
  }
5670
- async updateThread(args) {
5671
- return updateThread(this.ctx, args);
5804
+ async updateThread({ fence, ...args }) {
5805
+ return updateThread(this.ctx, {
5806
+ ...args,
5807
+ fence: resolveRunFence(this, fence)
5808
+ });
5672
5809
  }
5673
5810
  async deleteThread(args) {
5674
5811
  return deleteThread(this.ctx, args);
@@ -5688,14 +5825,20 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5688
5825
  async listMessagesByResourceId(args) {
5689
5826
  return listMessagesByResourceId(this.ctx, args);
5690
5827
  }
5691
- async saveMessages(args) {
5692
- return saveMessages(this.ctx, args);
5828
+ async saveMessages({ fence, ...args }) {
5829
+ return saveMessages(this.ctx, {
5830
+ ...args,
5831
+ fence: resolveRunFence(this, fence)
5832
+ });
5693
5833
  }
5694
- async updateMessages(args) {
5695
- return updateMessages(this.ctx, args);
5834
+ async updateMessages({ fence, ...args }) {
5835
+ return updateMessages(this.ctx, {
5836
+ ...args,
5837
+ fence: resolveRunFence(this, fence)
5838
+ });
5696
5839
  }
5697
- async deleteMessages(messageIds) {
5698
- return deleteMessages(this.ctx, messageIds);
5840
+ async deleteMessages(messageIds, options) {
5841
+ return deleteMessages(this.ctx, messageIds, resolveRunFence(this, options?.fence));
5699
5842
  }
5700
5843
  async getResourceById(args) {
5701
5844
  return getResourceById(this.ctx, args);
@@ -5703,8 +5846,11 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5703
5846
  async saveResource(args) {
5704
5847
  return saveResource(this.ctx, args);
5705
5848
  }
5706
- async updateResource(args) {
5707
- return updateResource(this.ctx, args);
5849
+ async updateResource({ fence, ...args }) {
5850
+ return updateResource(this.ctx, {
5851
+ ...args,
5852
+ fence: resolveRunFence(this, fence)
5853
+ });
5708
5854
  }
5709
5855
  async cloneThread(args) {
5710
5856
  const sourceThread = await this.getThreadById({ threadId: args.sourceThreadId });
@@ -5797,10 +5943,10 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5797
5943
  assertActiveObservationsApplied(await this.commitActiveObservations(input), input.id);
5798
5944
  }
5799
5945
  async commitActiveObservations(input) {
5800
- return updateActiveObservations(this.ctx, input);
5946
+ return updateActiveObservations(this.ctx, input, resolveRunFence(this, void 0));
5801
5947
  }
5802
5948
  async createReflectionGeneration(input) {
5803
- return createReflectionGeneration(this.ctx, input);
5949
+ return createReflectionGeneration(this.ctx, input, resolveRunFence(this, void 0));
5804
5950
  }
5805
5951
  async setReflectingFlag(id, isReflecting) {
5806
5952
  return setReflectingFlag(this.ctx, id, isReflecting);
@@ -5827,16 +5973,16 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5827
5973
  await this.appendBufferedObservations(input);
5828
5974
  }
5829
5975
  async appendBufferedObservations(input) {
5830
- return updateBufferedObservations(this.ctx, input);
5976
+ return updateBufferedObservations(this.ctx, input, resolveRunFence(this, void 0));
5831
5977
  }
5832
5978
  async swapBufferedToActive(input) {
5833
- return swapBufferedToActive(this.ctx, input);
5979
+ return swapBufferedToActive(this.ctx, input, resolveRunFence(this, void 0));
5834
5980
  }
5835
5981
  async updateBufferedReflection(input) {
5836
- return updateBufferedReflection(this.ctx, input);
5982
+ return updateBufferedReflection(this.ctx, input, resolveRunFence(this, void 0));
5837
5983
  }
5838
5984
  async swapBufferedReflectionToActive(input) {
5839
- return swapBufferedReflectionToActive(this.ctx, input);
5985
+ return swapBufferedReflectionToActive(this.ctx, input, resolveRunFence(this, void 0));
5840
5986
  }
5841
5987
  };
5842
5988
  //#endregion
@@ -8181,8 +8327,19 @@ const STORE_NAME$1 = "ORACLEDB";
8181
8327
  const WORKFLOW_CREATED_AT = "\"createdAt\"";
8182
8328
  const WORKFLOW_UPDATED_AT = "\"updatedAt\"";
8183
8329
  const WORKFLOW_RESOURCE_ID = "\"resourceId\"";
8330
+ const RUN_OWNER_COLUMNS = `generation AS "generation", "ownerId", "leaseExpiresAt", ${DB_NOW_MS} AS "nowMs"`;
8331
+ function toRunOwnershipRecord(runId, row) {
8332
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
8333
+ return {
8334
+ runId,
8335
+ generation: Number(row.generation),
8336
+ ownerId: String(row.ownerId),
8337
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
8338
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
8339
+ };
8340
+ }
8184
8341
  var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8185
- static MANAGED_TABLES = [TABLE_WORKFLOW_SNAPSHOT];
8342
+ static MANAGED_TABLES = [TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKFLOW_RUN_OWNERS];
8186
8343
  db;
8187
8344
  schemaName;
8188
8345
  skipDefaultIndexes;
@@ -8203,10 +8360,114 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8203
8360
  }
8204
8361
  async dangerouslyClearAll() {
8205
8362
  await this.db.clearTable(TABLE_WORKFLOW_SNAPSHOT);
8363
+ await this.db.clearTable(TABLE_WORKFLOW_RUN_OWNERS);
8364
+ }
8365
+ supportsRunFencing() {
8366
+ return true;
8367
+ }
8368
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
8369
+ const table = this.runOwnersTable();
8370
+ try {
8371
+ return await claimTransaction(this.db, async (client) => {
8372
+ const existing = await client.oneOrNone(`SELECT generation AS "generation" FROM ${table} WHERE "runId" = :runId FOR UPDATE`, { runId });
8373
+ if (!existing) {
8374
+ if (expectedGeneration !== void 0 && expectedGeneration !== 0) return {
8375
+ acquired: false,
8376
+ record: null
8377
+ };
8378
+ await client.none(`INSERT INTO ${table} ("runId", generation, "ownerId", "leaseExpiresAt", "updatedAt")
8379
+ VALUES (:runId, ${DB_NOW_MS}, :ownerId, ${DB_NOW_MS} + :leaseMs, ${DB_NOW_MS})`, {
8380
+ runId,
8381
+ ownerId,
8382
+ leaseMs
8383
+ });
8384
+ return {
8385
+ acquired: true,
8386
+ record: await this.readRunOwner(client, runId)
8387
+ };
8388
+ }
8389
+ await client.none(`UPDATE ${table}
8390
+ SET generation = generation + 1, "ownerId" = :ownerId, "leaseExpiresAt" = ${DB_NOW_MS} + :leaseMs,
8391
+ "updatedAt" = ${DB_NOW_MS}
8392
+ WHERE "runId" = :runId
8393
+ AND (:expectedGeneration IS NULL OR generation = :expectedGeneration)
8394
+ AND (:force = 1 OR "leaseExpiresAt" IS NULL OR "leaseExpiresAt" <= ${DB_NOW_MS})`, {
8395
+ runId,
8396
+ ownerId,
8397
+ leaseMs,
8398
+ expectedGeneration: expectedGeneration ?? null,
8399
+ force: force === true ? 1 : 0
8400
+ });
8401
+ const record = await this.readRunOwner(client, runId);
8402
+ return record && record.generation === Number(existing.generation) + 1 ? {
8403
+ acquired: true,
8404
+ record
8405
+ } : {
8406
+ acquired: false,
8407
+ record
8408
+ };
8409
+ });
8410
+ } catch (error) {
8411
+ throw this.storageError("CLAIM_RUN_OWNERSHIP", "FAILED", { runId }, error);
8412
+ }
8413
+ }
8414
+ async renewRunOwnership({ leaseMs, ...fence }) {
8415
+ try {
8416
+ return await this.db.tx(async (client) => {
8417
+ await client.none(`UPDATE ${this.runOwnersTable()}
8418
+ SET "leaseExpiresAt" = ${DB_NOW_MS} + :leaseMs, "updatedAt" = ${DB_NOW_MS}
8419
+ WHERE "runId" = :runId AND generation = :generation AND "ownerId" = :ownerId
8420
+ AND "leaseExpiresAt" IS NOT NULL`, {
8421
+ runId: fence.runId,
8422
+ generation: fence.generation,
8423
+ ownerId: fence.ownerId,
8424
+ leaseMs
8425
+ });
8426
+ const record = await this.readRunOwner(client, fence.runId);
8427
+ return record !== null && record.generation === fence.generation && record.ownerId === fence.ownerId && record.leaseExpiresAt !== null ? {
8428
+ renewed: true,
8429
+ record
8430
+ } : {
8431
+ renewed: false,
8432
+ record
8433
+ };
8434
+ });
8435
+ } catch (error) {
8436
+ throw this.storageError("RENEW_RUN_OWNERSHIP", "FAILED", { runId: fence.runId }, error);
8437
+ }
8206
8438
  }
8207
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
8439
+ async releaseRunOwnership(fence) {
8208
8440
  try {
8209
8441
  return await this.db.tx(async (client) => {
8442
+ await client.none(`UPDATE ${this.runOwnersTable()} SET "leaseExpiresAt" = NULL, "updatedAt" = ${DB_NOW_MS}
8443
+ WHERE "runId" = :runId AND generation = :generation AND "ownerId" = :ownerId`, {
8444
+ runId: fence.runId,
8445
+ generation: fence.generation,
8446
+ ownerId: fence.ownerId
8447
+ });
8448
+ const record = await this.readRunOwner(client, fence.runId);
8449
+ return record !== null && record.generation === fence.generation && record.ownerId === fence.ownerId;
8450
+ });
8451
+ } catch (error) {
8452
+ throw this.storageError("RELEASE_RUN_OWNERSHIP", "FAILED", { runId: fence.runId }, error);
8453
+ }
8454
+ }
8455
+ async getRunOwnership({ runId }) {
8456
+ try {
8457
+ return await this.readRunOwner(this.db, runId);
8458
+ } catch (error) {
8459
+ throw this.storageError("GET_RUN_OWNERSHIP", "FAILED", { runId }, error);
8460
+ }
8461
+ }
8462
+ async readRunOwner(client, runId) {
8463
+ const row = await client.oneOrNone(`SELECT ${RUN_OWNER_COLUMNS} FROM ${this.runOwnersTable()} WHERE "runId" = :runId`, { runId });
8464
+ return row ? toRunOwnershipRecord(runId, row) : null;
8465
+ }
8466
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
8467
+ const fence = resolveRunFence(this, explicitFence, runId);
8468
+ try {
8469
+ return await this.db.tx(async (client) => {
8470
+ if (fence) await assertRunFence(client, this.runOwnersTable(), fence, "updateWorkflowResults");
8210
8471
  await this.ensureWorkflowRunRow(client, workflowName, runId);
8211
8472
  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`, {
8212
8473
  workflowName,
@@ -8230,6 +8491,7 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8230
8491
  return snapshot.context;
8231
8492
  });
8232
8493
  } catch (error) {
8494
+ if (isRunFenceConflictError(error)) throw error;
8233
8495
  throw this.storageError("UPDATE_WORKFLOW_RESULTS", "FAILED", {
8234
8496
  workflowName,
8235
8497
  runId,
@@ -8237,9 +8499,11 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8237
8499
  }, error);
8238
8500
  }
8239
8501
  }
8240
- async updateWorkflowState({ workflowName, runId, opts }) {
8502
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
8503
+ const fence = resolveRunFence(this, explicitFence, runId);
8241
8504
  try {
8242
8505
  return await this.db.tx(async (client) => {
8506
+ if (fence) await assertRunFence(client, this.runOwnersTable(), fence, "updateWorkflowState");
8243
8507
  const existing = await client.oneOrNone(`SELECT snapshot AS "snapshot" FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId FOR UPDATE`, {
8244
8508
  workflowName,
8245
8509
  runId
@@ -8262,24 +8526,26 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8262
8526
  return updatedSnapshot;
8263
8527
  });
8264
8528
  } catch (error) {
8529
+ if (isRunFenceConflictError(error)) throw error;
8265
8530
  throw this.storageError("UPDATE_WORKFLOW_STATE", "FAILED", {
8266
8531
  workflowName,
8267
8532
  runId
8268
8533
  }, error);
8269
8534
  }
8270
8535
  }
8271
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
8536
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
8272
8537
  try {
8273
8538
  const now = /* @__PURE__ */ new Date();
8274
- await this.db.none(this.workflowMergeSql(), {
8539
+ await withRunFence(this.db, this.runOwnersTable(), resolveRunFence(this, fence, runId), "persistWorkflowSnapshot", (client) => client.none(this.workflowMergeSql(), {
8275
8540
  workflowName,
8276
8541
  runId,
8277
8542
  resourceId: resourceId ?? null,
8278
8543
  snapshot: jsonBind(snapshot),
8279
8544
  createdAt: createdAt ?? now,
8280
8545
  updatedAt: updatedAt ?? now
8281
- });
8546
+ }));
8282
8547
  } catch (error) {
8548
+ if (isRunFenceConflictError(error)) throw error;
8283
8549
  throw this.storageError("PERSIST_WORKFLOW_SNAPSHOT", "FAILED", {
8284
8550
  workflowName,
8285
8551
  runId
@@ -8343,13 +8609,14 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8343
8609
  }, error);
8344
8610
  }
8345
8611
  }
8346
- async deleteWorkflowRunById({ runId, workflowName }) {
8612
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
8347
8613
  try {
8348
- await this.db.none(`DELETE FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId`, {
8614
+ await withRunFence(this.db, this.runOwnersTable(), resolveRunFence(this, fence, runId), "deleteWorkflowRunById", (client) => client.none(`DELETE FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId`, {
8349
8615
  workflowName,
8350
8616
  runId
8351
- });
8617
+ }));
8352
8618
  } catch (error) {
8619
+ if (isRunFenceConflictError(error)) throw error;
8353
8620
  throw this.storageError("DELETE_WORKFLOW_RUN_BY_ID", "FAILED", {
8354
8621
  runId,
8355
8622
  workflowName
@@ -8368,6 +8635,10 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8368
8635
  CONSTRAINT ${this.constraint("MASTRA_WORKFLOW_SNAPSHOT_PK")} PRIMARY KEY (workflow_name, run_id)
8369
8636
  )`, [-955]);
8370
8637
  await this.db.executeDdl(`ALTER TABLE ${this.table()} ADD (${WORKFLOW_RESOURCE_ID} VARCHAR2(512))`, [-1430]);
8638
+ await this.db.createTable({
8639
+ tableName: TABLE_WORKFLOW_RUN_OWNERS,
8640
+ schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS]
8641
+ });
8371
8642
  }
8372
8643
  async createIndexes() {
8373
8644
  await this.db.withConnection(async (connection) => {
@@ -8505,6 +8776,9 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8505
8776
  table() {
8506
8777
  return qualifyName(TABLE_WORKFLOW_SNAPSHOT, this.schemaName);
8507
8778
  }
8779
+ runOwnersTable() {
8780
+ return qualifyName(TABLE_WORKFLOW_RUN_OWNERS, this.schemaName);
8781
+ }
8508
8782
  indexName(indexName) {
8509
8783
  return indexNameForTable(indexName, "IDX");
8510
8784
  }