@mastra/oracledb 0.5.1 → 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.
Files changed (31) hide show
  1. package/dist/_types/@internal_core/dist/memory/index.d.ts +102 -0
  2. package/dist/_types/@internal_core/package.json +1 -0
  3. package/dist/docs/SKILL.md +1 -1
  4. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  5. package/dist/docs/references/docs-memory-observational-memory.md +17 -15
  6. package/dist/index.cjs +677 -170
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +678 -171
  9. package/dist/index.js.map +1 -1
  10. package/dist/storage/db/run-fencing.d.ts +32 -0
  11. package/dist/storage/db/run-fencing.d.ts.map +1 -0
  12. package/dist/storage/domains/memory/index.d.ts +28 -8
  13. package/dist/storage/domains/memory/index.d.ts.map +1 -1
  14. package/dist/storage/domains/memory/messages.d.ts +6 -4
  15. package/dist/storage/domains/memory/messages.d.ts.map +1 -1
  16. package/dist/storage/domains/memory/observational-buffering.d.ts +6 -5
  17. package/dist/storage/domains/memory/observational-buffering.d.ts.map +1 -1
  18. package/dist/storage/domains/memory/observational.d.ts +25 -3
  19. package/dist/storage/domains/memory/observational.d.ts.map +1 -1
  20. package/dist/storage/domains/memory/resources.d.ts +3 -2
  21. package/dist/storage/domains/memory/resources.d.ts.map +1 -1
  22. package/dist/storage/domains/memory/schema.d.ts +11 -0
  23. package/dist/storage/domains/memory/schema.d.ts.map +1 -1
  24. package/dist/storage/domains/memory/threads.d.ts +5 -3
  25. package/dist/storage/domains/memory/threads.d.ts.map +1 -1
  26. package/dist/storage/domains/memory/utils.d.ts +15 -2
  27. package/dist/storage/domains/memory/utils.d.ts.map +1 -1
  28. package/dist/storage/domains/workflows/index.d.ts +19 -6
  29. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  30. package/dist/storage/index.d.ts.map +1 -1
  31. package/package.json +5 -4
package/dist/index.cjs CHANGED
@@ -3369,6 +3369,124 @@ function jsonComparableValue$1(value) {
3369
3369
  return value;
3370
3370
  }
3371
3371
  //#endregion
3372
+ //#region ../../packages/_internal-core/dist/memory/index.js
3373
+ /**
3374
+ * A buffered chunk stores `lastObservedAt = max message time + 1ms`. It is wholly covered
3375
+ * (every message it observed is already at or before the cursor) iff
3376
+ * `cursor >= chunk.lastObservedAt - 1ms`.
3377
+ */
3378
+ function isBufferedChunkCoveredByCursor(chunkLastObservedAt, cursor) {
3379
+ if (!cursor) return false;
3380
+ return new Date(cursor).getTime() >= new Date(chunkLastObservedAt).getTime() - 1;
3381
+ }
3382
+ /** The later of two cursors; `undefined` only when both are absent. */
3383
+ function maxObservationCursor(a, b) {
3384
+ if (!a) return b ? new Date(b) : void 0;
3385
+ if (!b) return new Date(a);
3386
+ return new Date(Math.max(new Date(a).getTime(), new Date(b).getTime()));
3387
+ }
3388
+ /**
3389
+ * Decide the new generation's text when a reflection built from `snapshot` commits against
3390
+ * the stored record.
3391
+ *
3392
+ * - Stored text equals the snapshot: the reflection replaces it.
3393
+ * - Stored text extends the snapshot (activation only appends): the appended tail is kept
3394
+ * after the reflection, and its tokens are added.
3395
+ * - Anything else (a non-append rewrite): `null` — the reflection must not be applied.
3396
+ */
3397
+ function planReflectionGenerationText(input) {
3398
+ const stored = input.storedObservations ?? "";
3399
+ const snapshot = input.snapshotObservations ?? "";
3400
+ if (stored === snapshot) return {
3401
+ mode: "equal",
3402
+ observations: input.reflection,
3403
+ tokenCount: input.tokenCount
3404
+ };
3405
+ if (!stored.startsWith(snapshot)) return null;
3406
+ const tail = stored.slice(snapshot.length).trimStart();
3407
+ if (!tail.trim()) return {
3408
+ mode: "equal",
3409
+ observations: input.reflection,
3410
+ tokenCount: input.tokenCount
3411
+ };
3412
+ return {
3413
+ mode: "append",
3414
+ observations: input.reflection ? `${input.reflection}\n\n${tail}` : tail,
3415
+ tokenCount: input.tokenCount + Math.max(0, (input.storedObservationTokenCount ?? 0) - (input.snapshotObservationTokenCount ?? 0))
3416
+ };
3417
+ }
3418
+ /**
3419
+ * Backs `updateActiveObservations` in adapters that implement `commitActiveObservations`.
3420
+ * Callers of `updateActiveObservations` (which resolves `void`) can't see a commit result, so a
3421
+ * write that was not applied throws instead of resolving as if it had committed.
3422
+ */
3423
+ function assertActiveObservationsApplied(result, id) {
3424
+ if (result.applied) return;
3425
+ 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`);
3426
+ }
3427
+ /**
3428
+ * Whether the stored text can still accept a reflection built from `snapshot`
3429
+ * (equal, or only appended to since).
3430
+ */
3431
+ function isAppendOnlySince(storedObservations, snapshotObservations) {
3432
+ const stored = storedObservations ?? "";
3433
+ const snapshot = snapshotObservations ?? "";
3434
+ return stored === snapshot || stored.startsWith(snapshot);
3435
+ }
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
3372
3490
  //#region src/storage/domains/memory/utils.ts
3373
3491
  const STORE_NAME$5 = "ORACLEDB";
3374
3492
  const EMPTY_STRING_SENTINEL = "__MASTRA_ORACLE_EMPTY_STRING__";
@@ -3376,6 +3494,14 @@ const STRING_SENTINEL_ESCAPE_PREFIX = "__MASTRA_ORACLE_ESCAPED__";
3376
3494
  function table(ctx, tableName) {
3377
3495
  return qualifyName(tableName, ctx.schemaName);
3378
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
+ }
3379
3505
  function storageError$1(operation, reason, details, cause, category = _mastra_core_error.ErrorCategory.THIRD_PARTY) {
3380
3506
  return createOracleStorageError({
3381
3507
  storeName: STORE_NAME$5,
@@ -3406,6 +3532,17 @@ function chunkValues(values, size = 900) {
3406
3532
  for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
3407
3533
  return chunks;
3408
3534
  }
3535
+ /**
3536
+ * Bind an instant as TIMESTAMP WITH TIME ZONE. A bare `Date` binds as TIMESTAMP WITH LOCAL
3537
+ * TIME ZONE, which the server converts with the session's fixed offset (captured at connect
3538
+ * time); an instant on the other side of a DST change is then stored shifted by an hour.
3539
+ */
3540
+ function timestampBind(value) {
3541
+ return value === void 0 || value === null ? null : {
3542
+ type: oracledb.default.DB_TYPE_TIMESTAMP_TZ,
3543
+ val: toDate(value)
3544
+ };
3545
+ }
3409
3546
  function boolToNumber(value) {
3410
3547
  return value ? 1 : 0;
3411
3548
  }
@@ -3554,12 +3691,48 @@ const OM_LAST_BUFFERED_AT_TOKENS = "\"lastBufferedAtTokens\"";
3554
3691
  const OM_LAST_BUFFERED_AT_TIME = "\"lastBufferedAtTime\"";
3555
3692
  const OM_CREATED_AT = "\"createdAt\"";
3556
3693
  const OM_UPDATED_AT = "\"updatedAt\"";
3694
+ const OM_SUPERSEDED_BY = "\"supersededBy\"";
3557
3695
  async function initMemorySchema(ctx) {
3558
3696
  await ctx.db.withConnection(async (connection) => {
3559
3697
  await createTables(ctx, connection);
3560
3698
  await createIndexes(ctx, connection);
3699
+ await backfillObservationalMemorySupersededBy(ctx, connection);
3561
3700
  });
3562
3701
  }
3702
+ /**
3703
+ * Retire every live observational memory row that sorts after its key's canonical head
3704
+ * (`generationCount DESC, createdAt ASC, id ASC`), marking it superseded by the head. Rows
3705
+ * retired by older adapter versions (which never set `supersededBy`) become frozen. Idempotent.
3706
+ * One statement: the head's ordering key is part of the condition, so a row created by a
3707
+ * concurrent rollover (higher generation) never matches, and a row locked by one is
3708
+ * re-evaluated after it commits.
3709
+ */
3710
+ async function backfillObservationalMemorySupersededBy(ctx, connection) {
3711
+ const om = table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY);
3712
+ const heads = `
3713
+ SELECT ${OM_LOOKUP_KEY} AS head_key, id AS head_id, ${OM_GENERATION_COUNT} AS head_gen, ${OM_CREATED_AT} AS head_created
3714
+ FROM (
3715
+ SELECT ${OM_LOOKUP_KEY}, id, ${OM_GENERATION_COUNT}, ${OM_CREATED_AT},
3716
+ ROW_NUMBER() OVER (PARTITION BY ${OM_LOOKUP_KEY} ORDER BY ${OM_GENERATION_COUNT} DESC, ${OM_CREATED_AT} ASC, id ASC) AS rn
3717
+ FROM ${om}
3718
+ WHERE ${OM_LOOKUP_KEY} IN (
3719
+ SELECT ${OM_LOOKUP_KEY} FROM ${om} WHERE ${OM_SUPERSEDED_BY} IS NULL GROUP BY ${OM_LOOKUP_KEY} HAVING COUNT(*) > 1
3720
+ )
3721
+ )
3722
+ WHERE rn = 1`;
3723
+ await connection.execute(`UPDATE ${om} r
3724
+ SET r.${OM_SUPERSEDED_BY} = (SELECT h.head_id FROM (${heads}) h WHERE h.head_key = r.${OM_LOOKUP_KEY})
3725
+ WHERE r.${OM_SUPERSEDED_BY} IS NULL
3726
+ AND EXISTS (
3727
+ SELECT 1 FROM (${heads}) h
3728
+ WHERE h.head_key = r.${OM_LOOKUP_KEY}
3729
+ AND (
3730
+ r.${OM_GENERATION_COUNT} < h.head_gen
3731
+ OR (r.${OM_GENERATION_COUNT} = h.head_gen AND r.${OM_CREATED_AT} > h.head_created)
3732
+ OR (r.${OM_GENERATION_COUNT} = h.head_gen AND r.${OM_CREATED_AT} = h.head_created AND r.id > h.head_id)
3733
+ )
3734
+ )`, {}, { autoCommit: true });
3735
+ }
3563
3736
  async function clearAllMemoryTables(ctx) {
3564
3737
  await ctx.db.tx(async (client) => {
3565
3738
  await client.none(`DELETE FROM ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}`);
@@ -3633,7 +3806,8 @@ async function createTables(ctx, connection) {
3633
3806
  ${OM_LAST_BUFFERED_AT_TIME} TIMESTAMP WITH TIME ZONE,
3634
3807
  metadata JSON,
3635
3808
  ${OM_CREATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL,
3636
- ${OM_UPDATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL
3809
+ ${OM_UPDATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL,
3810
+ ${OM_SUPERSEDED_BY} VARCHAR2(512)
3637
3811
  )`, [-955]);
3638
3812
  await ensureObservationalMemoryColumns(ctx, connection);
3639
3813
  await relaxObservationalMemoryNullability(ctx, connection);
@@ -3771,6 +3945,10 @@ async function ensureObservationalMemoryColumns(ctx, connection) {
3771
3945
  {
3772
3946
  name: OM_UPDATED_AT,
3773
3947
  type: "TIMESTAMP WITH TIME ZONE"
3948
+ },
3949
+ {
3950
+ name: OM_SUPERSEDED_BY,
3951
+ type: "VARCHAR2(512)"
3774
3952
  }
3775
3953
  ];
3776
3954
  for (const column of columns) await executeDdl(connection, `ALTER TABLE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)} ADD (${column.name} ${column.type})`, [-1430]);
@@ -3881,7 +4059,7 @@ async function listMessagesByResourceId(ctx, args) {
3881
4059
  orderBy
3882
4060
  });
3883
4061
  }
3884
- async function saveMessages(ctx, { messages }) {
4062
+ async function saveMessages(ctx, { messages, fence }) {
3885
4063
  if (messages.length === 0) return { messages: [] };
3886
4064
  const threadIds = /* @__PURE__ */ new Set();
3887
4065
  try {
@@ -3891,6 +4069,7 @@ async function saveMessages(ctx, { messages }) {
3891
4069
  }
3892
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);
3893
4071
  await ctx.db.tx(async (client) => {
4072
+ await assertMemoryFence(ctx, client, fence, "saveMessages");
3894
4073
  await insertMessageBatch(ctx, client, messages);
3895
4074
  });
3896
4075
  return { messages: new _mastra_core_agent.MessageList().add(messages, "memory").get.all.db() };
@@ -3968,7 +4147,7 @@ async function insertMessageBatch(ctx, client, messages) {
3968
4147
  threadId: messageThreadId
3969
4148
  })));
3970
4149
  }
3971
- async function updateMessages(ctx, { messages }) {
4150
+ async function updateMessages(ctx, { messages, fence }) {
3972
4151
  if (messages.length === 0) return [];
3973
4152
  const messageIds = messages.map((message) => message.id);
3974
4153
  const existingMessages = (await ctx.listMessagesById({ messageIds })).messages;
@@ -3977,6 +4156,7 @@ async function updateMessages(ctx, { messages }) {
3977
4156
  const threadIdsToUpdate = /* @__PURE__ */ new Set();
3978
4157
  try {
3979
4158
  await ctx.db.tx(async (client) => {
4159
+ await assertMemoryFence(ctx, client, fence, "updateMessages");
3980
4160
  const semanticRecallMessageIds = [];
3981
4161
  for (const updatePayload of messages) {
3982
4162
  const existing = existingById.get(updatePayload.id);
@@ -4023,13 +4203,15 @@ async function updateMessages(ctx, { messages }) {
4023
4203
  });
4024
4204
  return (await ctx.listMessagesById({ messageIds })).messages;
4025
4205
  } catch (error) {
4206
+ if (error instanceof _mastra_core_error.MastraError) throw error;
4026
4207
  throw storageError$1("UPDATE_MESSAGES", "FAILED", { messageIds: messageIds.join(",") }, error);
4027
4208
  }
4028
4209
  }
4029
- async function deleteMessages(ctx, messageIds) {
4210
+ async function deleteMessages(ctx, messageIds, fence) {
4030
4211
  if (!messageIds.length) return;
4031
4212
  try {
4032
4213
  await ctx.db.tx(async (client) => {
4214
+ await assertMemoryFence(ctx, client, fence, "deleteMessages");
4033
4215
  const threadIds = /* @__PURE__ */ new Set();
4034
4216
  for (const [chunkIndex, chunk] of chunkValues(messageIds).entries()) {
4035
4217
  const { sql, binds } = inClause(`messageId${chunkIndex}`, chunk);
@@ -4045,6 +4227,7 @@ async function deleteMessages(ctx, messageIds) {
4045
4227
  })));
4046
4228
  });
4047
4229
  } catch (error) {
4230
+ if (error instanceof _mastra_core_error.MastraError) throw error;
4048
4231
  throw storageError$1("DELETE_MESSAGES", "FAILED", { messageIds: messageIds.join(",") }, error);
4049
4232
  }
4050
4233
  }
@@ -4286,14 +4469,15 @@ function mergeMessageContent(existingContent, updateContent) {
4286
4469
  }
4287
4470
  //#endregion
4288
4471
  //#region src/storage/domains/memory/observational.ts
4472
+ /** Hops a write aimed at a retired record may follow to reach the head before giving up. */
4473
+ const OM_MAX_HEAD_HOPS = 3;
4474
+ /** Canonical head order for a lookup key. */
4475
+ const OM_HEAD_ORDER = `${OM_GENERATION_COUNT} DESC, ${OM_CREATED_AT} ASC, id ASC`;
4289
4476
  async function getObservationalMemory(ctx, threadId, resourceId) {
4290
4477
  try {
4291
4478
  const lookupKey = getOMKey(threadId, resourceId);
4292
4479
  return await ctx.db.withConnection(async (connection) => {
4293
- const row = rows(await connection.execute(`${omSelect()} FROM ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4294
- WHERE ${OM_LOOKUP_KEY} = :lookupKey
4295
- ORDER BY ${OM_GENERATION_COUNT} DESC
4296
- FETCH FIRST 1 ROWS ONLY`, asBindParameters({ lookupKey }), executeOptions()))[0];
4480
+ const row = await getHeadOMRow(ctx, connection, lookupKey);
4297
4481
  return row ? parseOMRow(row) : null;
4298
4482
  });
4299
4483
  } catch (error) {
@@ -4325,11 +4509,11 @@ async function getObservationalMemoryHistory(ctx, threadId, resourceId, limit =
4325
4509
  }
4326
4510
  if (options?.from) {
4327
4511
  conditions.push(`${OM_CREATED_AT} >= :fromDate`);
4328
- binds.fromDate = options.from;
4512
+ binds.fromDate = timestampBind(options.from);
4329
4513
  }
4330
4514
  if (options?.to) {
4331
4515
  conditions.push(`${OM_CREATED_AT} <= :toDate`);
4332
- binds.toDate = options.to;
4516
+ binds.toDate = timestampBind(options.to);
4333
4517
  }
4334
4518
  if (options?.groupId !== void 0) {
4335
4519
  conditions.push(`(DBMS_LOB.INSTR(${OM_ACTIVE_OBSERVATIONS}, :groupPrefix) > 0 OR EXISTS (
@@ -4368,6 +4552,7 @@ async function getObservationalMemoryHistory(ctx, threadId, resourceId, limit =
4368
4552
  }
4369
4553
  async function initializeObservationalMemory(ctx, input) {
4370
4554
  const now = /* @__PURE__ */ new Date();
4555
+ const lookupKey = getOMKey(input.threadId, input.resourceId);
4371
4556
  const record = {
4372
4557
  id: globalThis.crypto.randomUUID(),
4373
4558
  scope: input.scope,
@@ -4389,13 +4574,22 @@ async function initializeObservationalMemory(ctx, input) {
4389
4574
  lastBufferedAtTokens: 0,
4390
4575
  lastBufferedAtTime: null,
4391
4576
  config: input.config,
4392
- observedTimezone: input.observedTimezone
4577
+ observedTimezone: input.observedTimezone,
4578
+ supersededBy: null
4393
4579
  };
4394
4580
  try {
4395
- await ctx.db.tx(async (_client, connection) => {
4581
+ const existing = await getObservationalMemory(ctx, input.threadId, input.resourceId);
4582
+ if (existing) return existing;
4583
+ if (await ctx.db.tx(async (_client, connection) => {
4584
+ const omTable = table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY);
4585
+ await connection.execute(`LOCK TABLE ${omTable} IN SHARE ROW EXCLUSIVE MODE`);
4586
+ if ((await connection.execute(`SELECT 1 FROM ${omTable} WHERE "lookupKey" = :lookupKey AND "supersededBy" IS NULL FETCH FIRST 1 ROWS ONLY`, { lookupKey })).rows?.length) return false;
4396
4587
  await insertOMRecord(ctx, connection, record);
4397
- });
4398
- return record;
4588
+ return true;
4589
+ })) return record;
4590
+ const head = await getObservationalMemory(ctx, input.threadId, input.resourceId);
4591
+ if (!head) throw new Error(`Observational memory record not found after initialization: ${lookupKey}`);
4592
+ return head;
4399
4593
  } catch (error) {
4400
4594
  throw storageError$1("INITIALIZE_OBSERVATIONAL_MEMORY", "FAILED", {
4401
4595
  threadId: input.threadId ?? "",
@@ -4415,9 +4609,20 @@ async function insertObservationalMemoryRecord(ctx, record) {
4415
4609
  }, error);
4416
4610
  }
4417
4611
  }
4418
- async function updateActiveObservations(ctx, input) {
4612
+ async function updateActiveObservations(ctx, input, fence) {
4419
4613
  try {
4420
- await ctx.db.tx(async (_client, connection) => {
4614
+ return await ctx.db.tx(async (client, connection) => {
4615
+ await assertMemoryFence(ctx, client, fence, "commitActiveObservations");
4616
+ const row = await lockOMRow(ctx, connection, input.id, "UPDATE_ACTIVE_OBSERVATIONS");
4617
+ if (row.supersededBy) return {
4618
+ applied: false,
4619
+ reason: "retired"
4620
+ };
4621
+ if (input.expectedActiveObservations !== void 0 && input.expectedActiveObservations !== stringOrEmpty(row.activeObservations)) return {
4622
+ applied: false,
4623
+ reason: "conflict"
4624
+ };
4625
+ const lastObservedAt = maxObservationCursor(row.lastObservedAt, input.lastObservedAt);
4421
4626
  assertRowsAffected((await connection.execute(`
4422
4627
  UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4423
4628
  SET ${OM_ACTIVE_OBSERVATIONS} = :activeObservations,
@@ -4431,52 +4636,88 @@ async function updateActiveObservations(ctx, input) {
4431
4636
  WHERE id = :id`, {
4432
4637
  id: input.id,
4433
4638
  activeObservations: nullableClobBind(input.observations),
4434
- lastObservedAt: input.lastObservedAt,
4639
+ lastObservedAt: timestampBind(lastObservedAt),
4435
4640
  tokenCount: Math.round(input.tokenCount),
4436
4641
  observedMessageIds: nullableJsonBind(input.observedMessageIds),
4437
4642
  observedTimezone: input.observedTimezone ?? null,
4438
- updatedAt: /* @__PURE__ */ new Date()
4643
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4439
4644
  })).rowsAffected, "UPDATE_ACTIVE_OBSERVATIONS", input.id);
4645
+ return { applied: true };
4440
4646
  });
4441
4647
  } catch (error) {
4442
4648
  if (error instanceof _mastra_core_error.MastraError) throw error;
4443
4649
  throw storageError$1("UPDATE_ACTIVE_OBSERVATIONS", "FAILED", { id: input.id }, error);
4444
4650
  }
4445
4651
  }
4446
- async function createReflectionGeneration(ctx, input) {
4652
+ async function createReflectionGeneration(ctx, input, fence) {
4653
+ const { currentRecord } = input;
4654
+ try {
4655
+ return await ctx.db.tx(async (client, connection) => {
4656
+ await assertMemoryFence(ctx, client, fence, "createReflectionGeneration");
4657
+ const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
4658
+ if (!row) return currentRecord;
4659
+ if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
4660
+ const stored = parseOMRow(row);
4661
+ const plan = planReflectionGenerationText({
4662
+ storedObservations: stored.activeObservations,
4663
+ storedObservationTokenCount: stored.observationTokenCount,
4664
+ snapshotObservations: currentRecord.activeObservations,
4665
+ snapshotObservationTokenCount: currentRecord.observationTokenCount,
4666
+ reflection: input.reflection,
4667
+ tokenCount: input.tokenCount
4668
+ });
4669
+ if (!plan) return stored;
4670
+ return rollOverOMRow(ctx, connection, stored, plan.observations, plan.tokenCount, input.newRecordId);
4671
+ });
4672
+ } catch (error) {
4673
+ if (error instanceof _mastra_core_error.MastraError) throw error;
4674
+ throw storageError$1("CREATE_REFLECTION_GENERATION", "FAILED", { id: currentRecord.id }, error);
4675
+ }
4676
+ }
4677
+ /**
4678
+ * Create the next generation from the stored (live, row-locked) record and retire it in the
4679
+ * same transaction. Buffered chunks move to the new generation; the cursor, buffering markers,
4680
+ * flags, and counters carry over; buffered reflection state does not.
4681
+ */
4682
+ async function rollOverOMRow(ctx, connection, stored, observations, tokenCount, newRecordId) {
4447
4683
  const now = /* @__PURE__ */ new Date();
4448
4684
  const record = {
4449
- id: globalThis.crypto.randomUUID(),
4450
- scope: input.currentRecord.scope,
4451
- threadId: input.currentRecord.threadId,
4452
- resourceId: input.currentRecord.resourceId,
4685
+ id: newRecordId ?? globalThis.crypto.randomUUID(),
4686
+ scope: stored.scope,
4687
+ threadId: stored.threadId,
4688
+ resourceId: stored.resourceId,
4453
4689
  createdAt: now,
4454
4690
  updatedAt: now,
4455
- lastObservedAt: input.currentRecord.lastObservedAt,
4691
+ lastObservedAt: stored.lastObservedAt,
4456
4692
  originType: "reflection",
4457
- generationCount: input.currentRecord.generationCount + 1,
4458
- activeObservations: input.reflection,
4459
- totalTokensObserved: input.currentRecord.totalTokensObserved,
4460
- observationTokenCount: Math.round(input.tokenCount),
4461
- pendingMessageTokens: 0,
4693
+ generationCount: stored.generationCount + 1,
4694
+ activeObservations: observations,
4695
+ totalTokensObserved: stored.totalTokensObserved,
4696
+ observationTokenCount: Math.round(tokenCount),
4697
+ pendingMessageTokens: stored.pendingMessageTokens,
4462
4698
  isReflecting: false,
4463
4699
  isObserving: false,
4464
- isBufferingObservation: false,
4700
+ isBufferingObservation: stored.isBufferingObservation,
4465
4701
  isBufferingReflection: false,
4466
- lastBufferedAtTokens: 0,
4467
- lastBufferedAtTime: null,
4468
- config: input.currentRecord.config,
4469
- metadata: input.currentRecord.metadata,
4470
- observedTimezone: input.currentRecord.observedTimezone
4702
+ lastBufferedAtTokens: stored.lastBufferedAtTokens,
4703
+ lastBufferedAtTime: stored.lastBufferedAtTime ?? null,
4704
+ bufferedObservationChunks: stored.bufferedObservationChunks?.length ? stored.bufferedObservationChunks : void 0,
4705
+ config: stored.config,
4706
+ metadata: stored.metadata,
4707
+ observedTimezone: stored.observedTimezone,
4708
+ supersededBy: null
4471
4709
  };
4472
- try {
4473
- await ctx.db.tx(async (_client, connection) => {
4474
- await insertOMRecord(ctx, connection, record, now);
4475
- });
4476
- return record;
4477
- } catch (error) {
4478
- throw storageError$1("CREATE_REFLECTION_GENERATION", "FAILED", { id: input.currentRecord.id }, error);
4479
- }
4710
+ await insertOMRecord(ctx, connection, record, now);
4711
+ if ((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4712
+ SET "supersededBy" = :newId,
4713
+ "bufferedObservationChunks" = NULL,
4714
+ "updatedAt" = :updatedAt
4715
+ WHERE id = :id AND "supersededBy" IS NULL`, {
4716
+ newId: record.id,
4717
+ updatedAt: timestampBind(now),
4718
+ id: stored.id
4719
+ })).rowsAffected !== 1) throw storageError$1("CREATE_REFLECTION_GENERATION", "RETIRE_FAILED", { id: stored.id }, /* @__PURE__ */ new Error(`Failed to retire observational memory record ${stored.id}`));
4720
+ return record;
4480
4721
  }
4481
4722
  async function setReflectingFlag(ctx, id, isReflecting) {
4482
4723
  await updateOMFlag(ctx, id, OM_IS_REFLECTING, isReflecting, "SET_REFLECTING_FLAG");
@@ -4491,9 +4732,10 @@ async function setBufferingObservationFlag(ctx, id, isBuffering, lastBufferedAtT
4491
4732
  const binds = {
4492
4733
  id,
4493
4734
  isBuffering: boolToNumber(isBuffering),
4494
- updatedAt: /* @__PURE__ */ new Date()
4735
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4495
4736
  };
4496
4737
  if (lastBufferedAtTokens !== void 0) binds.lastBufferedAtTokens = Math.round(lastBufferedAtTokens);
4738
+ binds.id = (await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, id, "SET_BUFFERING_OBSERVATION_FLAG"))).id;
4497
4739
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4498
4740
  SET ${OM_IS_BUFFERING_OBSERVATION} = :isBuffering,
4499
4741
  ${OM_UPDATED_AT} = :updatedAt
@@ -4524,7 +4766,17 @@ async function clearObservationalMemory(ctx, threadId, resourceId) {
4524
4766
  }
4525
4767
  async function setPendingMessageTokens(ctx, id, tokenCount) {
4526
4768
  try {
4527
- await updateOMColumns(ctx, id, "SET_PENDING_MESSAGE_TOKENS", { [OM_PENDING_MESSAGE_TOKENS]: Math.round(tokenCount) });
4769
+ await ctx.db.tx(async (_client, connection) => {
4770
+ const target = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, id, "SET_PENDING_MESSAGE_TOKENS"));
4771
+ assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4772
+ SET ${OM_PENDING_MESSAGE_TOKENS} = :tokenCount,
4773
+ ${OM_UPDATED_AT} = :updatedAt
4774
+ WHERE id = :id`, {
4775
+ tokenCount: Math.round(tokenCount),
4776
+ updatedAt: timestampBind(/* @__PURE__ */ new Date()),
4777
+ id: target.id
4778
+ })).rowsAffected, "SET_PENDING_MESSAGE_TOKENS", id);
4779
+ });
4528
4780
  } catch (error) {
4529
4781
  if (error instanceof _mastra_core_error.MastraError) throw error;
4530
4782
  throw storageError$1("SET_PENDING_MESSAGE_TOKENS", "FAILED", {
@@ -4546,7 +4798,7 @@ async function updateObservationalMemoryConfig(ctx, input) {
4546
4798
  WHERE id = :id`, {
4547
4799
  id: input.id,
4548
4800
  config: jsonBind(merged),
4549
- updatedAt: /* @__PURE__ */ new Date()
4801
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4550
4802
  })).rowsAffected, "UPDATE_OM_CONFIG", input.id);
4551
4803
  });
4552
4804
  } catch (error) {
@@ -4592,6 +4844,7 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4592
4844
  ${OM_LAST_BUFFERED_AT_TOKENS},
4593
4845
  ${OM_LAST_BUFFERED_AT_TIME},
4594
4846
  metadata,
4847
+ ${OM_SUPERSEDED_BY},
4595
4848
  ${OM_CREATED_AT},
4596
4849
  ${OM_UPDATED_AT}
4597
4850
  ) VALUES (
@@ -4627,6 +4880,7 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4627
4880
  :lastBufferedAtTokens,
4628
4881
  :lastBufferedAtTime,
4629
4882
  :metadata,
4883
+ :supersededBy,
4630
4884
  :createdAt,
4631
4885
  :updatedAt
4632
4886
  )`, {
@@ -4640,8 +4894,8 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4640
4894
  originType: record.originType ?? "initial",
4641
4895
  config: jsonBind(record.config ?? {}),
4642
4896
  generationCount: record.generationCount ?? 0,
4643
- lastObservedAt: record.lastObservedAt ?? null,
4644
- lastReflectionAt: record.originType === "reflection" ? timestamp : null,
4897
+ lastObservedAt: timestampBind(record.lastObservedAt),
4898
+ lastReflectionAt: record.originType === "reflection" ? timestampBind(timestamp) : null,
4645
4899
  pendingMessageTokens: Math.round(record.pendingMessageTokens ?? 0),
4646
4900
  totalTokensObserved: Math.round(record.totalTokensObserved ?? 0),
4647
4901
  observationTokenCount: Math.round(record.observationTokenCount ?? 0),
@@ -4660,10 +4914,11 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4660
4914
  isBufferingObservation: boolToNumber(record.isBufferingObservation),
4661
4915
  isBufferingReflection: boolToNumber(record.isBufferingReflection),
4662
4916
  lastBufferedAtTokens: Math.round(record.lastBufferedAtTokens ?? 0),
4663
- lastBufferedAtTime: record.lastBufferedAtTime ?? null,
4917
+ lastBufferedAtTime: timestampBind(record.lastBufferedAtTime),
4664
4918
  metadata: nullableJsonBind(record.metadata),
4665
- createdAt: record.createdAt,
4666
- updatedAt: record.updatedAt
4919
+ supersededBy: record.supersededBy ?? null,
4920
+ createdAt: timestampBind(record.createdAt),
4921
+ updatedAt: timestampBind(record.updatedAt)
4667
4922
  });
4668
4923
  }
4669
4924
  async function updateOMFlag(ctx, id, column, value, operation) {
@@ -4687,7 +4942,7 @@ async function updateOMColumns(ctx, id, operation, columns) {
4687
4942
  WHERE id = :id`, {
4688
4943
  ...binds,
4689
4944
  id,
4690
- updatedAt: /* @__PURE__ */ new Date()
4945
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4691
4946
  })).rowsAffected, operation, id);
4692
4947
  });
4693
4948
  }
@@ -4725,6 +4980,7 @@ function omSelect() {
4725
4980
  ${OM_LAST_BUFFERED_AT_TOKENS} AS "lastBufferedAtTokens",
4726
4981
  ${OM_LAST_BUFFERED_AT_TIME} AS "lastBufferedAtTime",
4727
4982
  metadata AS "metadata",
4983
+ ${OM_SUPERSEDED_BY} AS "supersededBy",
4728
4984
  ${OM_CREATED_AT} AS "createdAt",
4729
4985
  ${OM_UPDATED_AT} AS "updatedAt"`;
4730
4986
  }
@@ -4760,15 +5016,61 @@ function parseOMRow(row) {
4760
5016
  config: parseJson(row.config),
4761
5017
  metadata: parseOptionalJsonObject(row.metadata, { emptyObjectAsUndefined: true }),
4762
5018
  observedMessageIds: parseOptionalStringArray(row.observedMessageIds),
4763
- observedTimezone: row.observedTimezone ? String(row.observedTimezone) : void 0
5019
+ observedTimezone: row.observedTimezone ? String(row.observedTimezone) : void 0,
5020
+ supersededBy: row.supersededBy ? String(row.supersededBy) : null
4764
5021
  };
4765
5022
  }
5023
+ /** The canonical head row of a lookup key (non-locking read). */
5024
+ async function getHeadOMRow(ctx, connection, lookupKey) {
5025
+ return rows(await connection.execute(`${omSelect()} FROM ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
5026
+ WHERE ${OM_LOOKUP_KEY} = :lookupKey
5027
+ ORDER BY ${OM_HEAD_ORDER}
5028
+ FETCH FIRST 1 ROWS ONLY`, asBindParameters({ lookupKey }), executeOptions()))[0];
5029
+ }
5030
+ /** Row-locks one record by primary key for the rest of the transaction; `undefined` when missing. */
5031
+ async function findOMRowForUpdate(ctx, connection, id) {
5032
+ return rows(await connection.execute(`${omSelect()} FROM ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)} WHERE id = :id FOR UPDATE`, { id }, executeOptions()))[0];
5033
+ }
5034
+ /**
5035
+ * Row-locks one record by primary key. Observational memory updates are incremental and
5036
+ * order-sensitive, so mutating paths derive their next state from a locked row.
5037
+ * Throws "record not found" when it is missing.
5038
+ */
5039
+ async function lockOMRow(ctx, connection, id, operation) {
5040
+ const row = await findOMRowForUpdate(ctx, connection, id);
5041
+ if (!row) {
5042
+ assertRowsAffected(0, operation, id);
5043
+ throw new Error(`Observational memory record not found: ${id}`);
5044
+ }
5045
+ return row;
5046
+ }
5047
+ /**
5048
+ * The live, row-locked record a lifecycle write aimed at `row` lands on: `row` itself while
5049
+ * live, otherwise the head of its lookup key (locked, then re-checked — a rollover may retire
5050
+ * it between the head read and the lock).
5051
+ */
5052
+ async function resolveLiveOMRow(ctx, connection, row) {
5053
+ let current = row;
5054
+ for (let hop = 0; current.supersededBy; hop++) {
5055
+ const head = hop < OM_MAX_HEAD_HOPS ? await getHeadOMRow(ctx, connection, current.lookupKey) : void 0;
5056
+ const locked = head && head.id !== current.id ? await findOMRowForUpdate(ctx, connection, head.id) : void 0;
5057
+ if (!locked) throw storageError$1("RESOLVE_OBSERVATIONAL_MEMORY_HEAD", "FAILED", { id: row.id }, /* @__PURE__ */ new Error(`Observational memory record ${row.id} is superseded but no live head was found`));
5058
+ current = locked;
5059
+ }
5060
+ return current;
5061
+ }
4766
5062
  //#endregion
4767
5063
  //#region src/storage/domains/memory/observational-buffering.ts
4768
- async function updateBufferedObservations(ctx, input) {
5064
+ async function updateBufferedObservations(ctx, input, fence) {
4769
5065
  try {
4770
- await ctx.db.tx(async (_client, connection) => {
4771
- const existingChunks = parseBufferedChunks((await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS")).bufferedObservationChunks);
5066
+ return await ctx.db.tx(async (client, connection) => {
5067
+ await assertMemoryFence(ctx, client, fence, "appendBufferedObservations");
5068
+ const row = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS"));
5069
+ const existingChunks = parseBufferedChunks(row.bufferedObservationChunks);
5070
+ if (existingChunks.some((existing) => existing.cycleId === input.chunk.cycleId) || isBufferedChunkCoveredByCursor(input.chunk.lastObservedAt, row.lastObservedAt)) return {
5071
+ persisted: false,
5072
+ recordId: row.id
5073
+ };
4772
5074
  const newChunk = {
4773
5075
  id: `ombuf-${globalThis.crypto.randomUUID()}`,
4774
5076
  cycleId: input.chunk.cycleId,
@@ -4784,32 +5086,48 @@ async function updateBufferedObservations(ctx, input) {
4784
5086
  extractedValues: input.chunk.extractedValues,
4785
5087
  extractionFailures: input.chunk.extractionFailures
4786
5088
  };
4787
- const updatedChunks = [...existingChunks, newChunk];
4788
- const lastBufferedAtTimeSql = input.lastBufferedAtTime === void 0 || input.lastBufferedAtTime === null ? "" : `,\n ${OM_LAST_BUFFERED_AT_TIME} = :lastBufferedAtTime`;
4789
- const binds = {
4790
- id: input.id,
4791
- bufferedObservationChunks: nullableJsonBind(updatedChunks),
4792
- updatedAt: /* @__PURE__ */ new Date()
4793
- };
4794
- if (input.lastBufferedAtTime !== void 0 && input.lastBufferedAtTime !== null) binds.lastBufferedAtTime = toDate(input.lastBufferedAtTime);
5089
+ const lastBufferedAtTime = maxObservationCursor(row.lastBufferedAtTime, input.lastBufferedAtTime);
4795
5090
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4796
5091
  SET ${OM_BUFFERED_OBSERVATION_CHUNKS} = :bufferedObservationChunks,
4797
- ${OM_UPDATED_AT} = :updatedAt${lastBufferedAtTimeSql}
4798
- WHERE id = :id`, asBindParameters(binds))).rowsAffected, "UPDATE_BUFFERED_OBSERVATIONS", input.id);
5092
+ ${OM_LAST_BUFFERED_AT_TIME} = :lastBufferedAtTime,
5093
+ ${OM_UPDATED_AT} = :updatedAt
5094
+ WHERE id = :id`, asBindParameters({
5095
+ id: row.id,
5096
+ bufferedObservationChunks: nullableJsonBind([...existingChunks, newChunk]),
5097
+ lastBufferedAtTime: timestampBind(lastBufferedAtTime),
5098
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
5099
+ }))).rowsAffected, "UPDATE_BUFFERED_OBSERVATIONS", row.id);
5100
+ return {
5101
+ persisted: true,
5102
+ recordId: row.id
5103
+ };
4799
5104
  });
4800
5105
  } catch (error) {
4801
5106
  if (error instanceof _mastra_core_error.MastraError) throw error;
4802
5107
  throw storageError$1("UPDATE_BUFFERED_OBSERVATIONS", "FAILED", { id: input.id }, error);
4803
5108
  }
4804
5109
  }
4805
- async function swapBufferedToActive(ctx, input) {
5110
+ async function swapBufferedToActive(ctx, input, fence) {
4806
5111
  try {
4807
- return await ctx.db.tx(async (_client, connection) => {
5112
+ return await ctx.db.tx(async (client, connection) => {
5113
+ await assertMemoryFence(ctx, client, fence, "swapBufferedToActive");
4808
5114
  const row = await lockOMRow(ctx, connection, input.id, "SWAP_BUFFERED_TO_ACTIVE");
4809
- const chunks = input.bufferedChunks?.length ? input.bufferedChunks : parseBufferedChunks(row.bufferedObservationChunks);
5115
+ if (row.supersededBy) return {
5116
+ ...emptySwapResult(),
5117
+ retired: true
5118
+ };
5119
+ const refreshedWeights = new Map((input.bufferedChunks ?? []).map((chunk) => [chunk.id, chunk.messageTokens]));
5120
+ const chunks = parseBufferedChunks(row.bufferedObservationChunks).map((chunk) => {
5121
+ const weight = refreshedWeights.get(chunk.id);
5122
+ return weight === void 0 ? chunk : {
5123
+ ...chunk,
5124
+ messageTokens: weight
5125
+ };
5126
+ });
4810
5127
  if (chunks.length === 0) return emptySwapResult();
4811
5128
  const activation = calculateBufferedActivation(chunks, input);
4812
5129
  const lastObservedAt = input.lastObservedAt ?? (activation.activatedChunks.at(-1)?.lastObservedAt ? toDate(activation.activatedChunks.at(-1).lastObservedAt) : /* @__PURE__ */ new Date());
5130
+ const storedCursor = maxObservationCursor(row.lastObservedAt, lastObservedAt);
4813
5131
  const boundary = `\n\n--- message boundary (${lastObservedAt.toISOString()}) ---\n\n`;
4814
5132
  const existingActive = stringOrEmpty(row.activeObservations);
4815
5133
  const newActive = existingActive ? `${existingActive}${boundary}${activation.activatedContent}` : activation.activatedContent;
@@ -4827,8 +5145,8 @@ async function swapBufferedToActive(ctx, input) {
4827
5145
  observationTokens: activation.activatedTokens,
4828
5146
  pendingMessageTokens: pendingTokens,
4829
5147
  bufferedObservationChunks: nullableJsonBind(activation.remainingChunks.length > 0 ? activation.remainingChunks : null),
4830
- lastObservedAt,
4831
- updatedAt: /* @__PURE__ */ new Date()
5148
+ lastObservedAt: timestampBind(storedCursor),
5149
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4832
5150
  })).rowsAffected, "SWAP_BUFFERED_TO_ACTIVE", input.id);
4833
5151
  return activation.result;
4834
5152
  });
@@ -4837,9 +5155,10 @@ async function swapBufferedToActive(ctx, input) {
4837
5155
  throw storageError$1("SWAP_BUFFERED_TO_ACTIVE", "FAILED", { id: input.id }, error);
4838
5156
  }
4839
5157
  }
4840
- async function updateBufferedReflection(ctx, input) {
5158
+ async function updateBufferedReflection(ctx, input, fence) {
4841
5159
  try {
4842
- await ctx.db.tx(async (_client, connection) => {
5160
+ await ctx.db.tx(async (client, connection) => {
5161
+ await assertMemoryFence(ctx, client, fence, "updateBufferedReflection");
4843
5162
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4844
5163
  SET ${OM_BUFFERED_REFLECTION} = CASE
4845
5164
  WHEN ${OM_BUFFERED_REFLECTION} IS NOT NULL AND DBMS_LOB.GETLENGTH(${OM_BUFFERED_REFLECTION}) > 0
@@ -4856,7 +5175,7 @@ async function updateBufferedReflection(ctx, input) {
4856
5175
  tokenCount: Math.round(input.tokenCount),
4857
5176
  inputTokenCount: Math.round(input.inputTokenCount),
4858
5177
  reflectedObservationLineCount: Math.round(input.reflectedObservationLineCount),
4859
- updatedAt: /* @__PURE__ */ new Date()
5178
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4860
5179
  })).rowsAffected, "UPDATE_BUFFERED_REFLECTION", input.id);
4861
5180
  });
4862
5181
  } catch (error) {
@@ -4864,67 +5183,34 @@ async function updateBufferedReflection(ctx, input) {
4864
5183
  throw storageError$1("UPDATE_BUFFERED_REFLECTION", "FAILED", { id: input.id }, error);
4865
5184
  }
4866
5185
  }
4867
- async function swapBufferedReflectionToActive(ctx, input) {
5186
+ async function swapBufferedReflectionToActive(ctx, input, fence) {
5187
+ const { currentRecord } = input;
4868
5188
  try {
4869
- return await ctx.db.tx(async (_client, connection) => {
4870
- const row = await lockOMRow(ctx, connection, input.currentRecord.id, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE");
4871
- const bufferedReflection = stringOrEmpty(row.bufferedReflection);
4872
- if (!bufferedReflection) throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NO_CONTENT", { id: input.currentRecord.id }, /* @__PURE__ */ new Error("No buffered reflection to swap"), _mastra_core_error.ErrorCategory.USER);
4873
- const reflectedLineCount = numberOrZero(row.reflectedObservationLineCount);
4874
- const unreflectedContent = stringOrEmpty(row.activeObservations).split("\n").slice(reflectedLineCount).join("\n").trim();
4875
- const newObservations = unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
4876
- const now = /* @__PURE__ */ new Date();
4877
- const lockedRecord = parseOMRow(row);
4878
- const newRecord = {
4879
- id: globalThis.crypto.randomUUID(),
4880
- scope: lockedRecord.scope,
4881
- threadId: lockedRecord.threadId,
4882
- resourceId: lockedRecord.resourceId,
4883
- createdAt: now,
4884
- updatedAt: now,
4885
- lastObservedAt: lockedRecord.lastObservedAt,
4886
- originType: "reflection",
4887
- generationCount: lockedRecord.generationCount + 1,
4888
- activeObservations: newObservations,
4889
- totalTokensObserved: lockedRecord.totalTokensObserved,
4890
- observationTokenCount: Math.round(input.tokenCount),
4891
- pendingMessageTokens: 0,
4892
- isReflecting: false,
4893
- isObserving: false,
4894
- isBufferingObservation: false,
4895
- isBufferingReflection: false,
4896
- lastBufferedAtTokens: 0,
4897
- lastBufferedAtTime: null,
4898
- config: lockedRecord.config,
4899
- metadata: lockedRecord.metadata,
4900
- observedTimezone: lockedRecord.observedTimezone
4901
- };
4902
- await insertOMRecord(ctx, connection, newRecord, now);
5189
+ return await ctx.db.tx(async (client, connection) => {
5190
+ await assertMemoryFence(ctx, client, fence, "swapBufferedReflectionToActive");
5191
+ const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
5192
+ if (!row) return currentRecord;
5193
+ if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
5194
+ const stored = parseOMRow(row);
5195
+ const bufferedReflection = stored.bufferedReflection ?? "";
5196
+ if (!bufferedReflection) throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NO_CONTENT", { id: currentRecord.id }, /* @__PURE__ */ new Error("No buffered reflection to swap"), _mastra_core_error.ErrorCategory.USER);
5197
+ if (!isAppendOnlySince(stored.activeObservations, currentRecord.activeObservations)) return stored;
5198
+ const reflectedLineCount = stored.reflectedObservationLineCount ?? 0;
5199
+ const unreflectedContent = stored.activeObservations.split("\n").slice(reflectedLineCount).join("\n").trim();
5200
+ const newRecord = await rollOverOMRow(ctx, connection, stored, unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection, input.tokenCount + Math.max(0, stored.observationTokenCount - (currentRecord.observationTokenCount ?? 0)), input.newRecordId);
4903
5201
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4904
5202
  SET ${OM_BUFFERED_REFLECTION} = NULL,
4905
5203
  ${OM_BUFFERED_REFLECTION_TOKENS} = NULL,
4906
5204
  ${OM_BUFFERED_REFLECTION_INPUT_TOKENS} = NULL,
4907
- ${OM_REFLECTED_OBSERVATION_LINE_COUNT} = NULL,
4908
- ${OM_UPDATED_AT} = :updatedAt
4909
- WHERE id = :id`, {
4910
- id: input.currentRecord.id,
4911
- updatedAt: now
4912
- })).rowsAffected, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", input.currentRecord.id);
5205
+ ${OM_REFLECTED_OBSERVATION_LINE_COUNT} = NULL
5206
+ WHERE id = :id`, { id: stored.id })).rowsAffected, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", stored.id);
4913
5207
  return newRecord;
4914
5208
  });
4915
5209
  } catch (error) {
4916
5210
  if (error instanceof _mastra_core_error.MastraError) throw error;
4917
- throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "FAILED", { id: input.currentRecord.id }, error);
5211
+ throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "FAILED", { id: currentRecord.id }, error);
4918
5212
  }
4919
5213
  }
4920
- async function lockOMRow(ctx, connection, id, operation) {
4921
- const row = rows(await connection.execute(`${omSelect()} FROM ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)} WHERE id = :id FOR UPDATE`, { id }, executeOptions()))[0];
4922
- if (!row) {
4923
- assertRowsAffected(0, operation, id);
4924
- throw new Error(`Observational memory record not found: ${id}`);
4925
- }
4926
- return row;
4927
- }
4928
5214
  function emptySwapResult() {
4929
5215
  return {
4930
5216
  chunksActivated: 0,
@@ -5054,17 +5340,24 @@ async function saveResource(ctx, { resource }) {
5054
5340
  throw storageError$1("SAVE_RESOURCE", "FAILED", { resourceId: resource.id }, error);
5055
5341
  }
5056
5342
  }
5057
- async function updateResource(ctx, { resourceId, workingMemory, metadata }) {
5343
+ async function updateResource(ctx, { resourceId, workingMemory, metadata, fence }) {
5058
5344
  const existing = await ctx.getResourceById({ resourceId });
5059
- if (!existing) return ctx.saveResource({ resource: {
5345
+ const newResource = () => ({
5060
5346
  id: resourceId,
5061
5347
  workingMemory,
5062
5348
  metadata: metadata ?? {},
5063
5349
  createdAt: /* @__PURE__ */ new Date(),
5064
5350
  updatedAt: /* @__PURE__ */ new Date()
5065
- } });
5351
+ });
5352
+ if (!existing && !fence) return ctx.saveResource({ resource: newResource() });
5066
5353
  try {
5067
- 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
+ }
5068
5361
  const row = rows(await connection.execute(`${resourceSelect()} FROM ${table(ctx, _mastra_core_storage.TABLE_RESOURCES)} WHERE id = :resourceId FOR UPDATE`, { resourceId }, executeOptions()))[0];
5069
5362
  const locked = row ? parseResource(row) : existing;
5070
5363
  const updated = {
@@ -5158,11 +5451,15 @@ async function getThreadById(ctx, { threadId, resourceId }) {
5158
5451
  throw storageError$1("GET_THREAD_BY_ID", "FAILED", { threadId }, error);
5159
5452
  }
5160
5453
  }
5161
- async function saveThread(ctx, { thread }) {
5454
+ async function saveThread(ctx, { thread, fence }) {
5162
5455
  try {
5163
- 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
+ });
5164
5460
  return thread;
5165
5461
  } catch (error) {
5462
+ if (error instanceof _mastra_core_error.MastraError) throw error;
5166
5463
  throw storageError$1("SAVE_THREAD", "FAILED", { threadId: thread.id }, error);
5167
5464
  }
5168
5465
  }
@@ -5247,7 +5544,7 @@ async function insertThreadRow(ctx, client, thread) {
5247
5544
  updatedAt: thread.updatedAt ?? /* @__PURE__ */ new Date()
5248
5545
  });
5249
5546
  }
5250
- async function updateThread(ctx, { id, title, metadata }) {
5547
+ async function updateThread(ctx, { id, title, metadata, fence }) {
5251
5548
  const existingThread = await ctx.getThreadById({ threadId: id });
5252
5549
  if (!existingThread) throw storageError$1("UPDATE_THREAD", "FAILED", {
5253
5550
  threadId: id,
@@ -5259,16 +5556,19 @@ async function updateThread(ctx, { id, title, metadata }) {
5259
5556
  };
5260
5557
  const updatedAt = /* @__PURE__ */ new Date();
5261
5558
  try {
5262
- await ctx.db.none(`
5263
- UPDATE ${table(ctx, _mastra_core_storage.TABLE_THREADS)}
5264
- SET title = COALESCE(:title, title),
5265
- metadata = :metadata,
5266
- ${THREAD_UPDATED_AT} = :updatedAt
5267
- WHERE id = :id`, {
5268
- id,
5269
- title: optionalStringBind(title),
5270
- metadata: jsonBind(mergedMetadata),
5271
- 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
+ });
5272
5572
  });
5273
5573
  const updatedThread = await ctx.getThreadById({ threadId: id });
5274
5574
  if (!updatedThread) throw storageError$1("UPDATE_THREAD", "FAILED", {
@@ -5420,7 +5720,8 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5420
5720
  _mastra_core_storage.TABLE_THREADS,
5421
5721
  _mastra_core_storage.TABLE_MESSAGES,
5422
5722
  _mastra_core_storage.TABLE_RESOURCES,
5423
- _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY
5723
+ _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY,
5724
+ _mastra_core_storage.TABLE_MEMORY_RUN_FENCES
5424
5725
  ];
5425
5726
  db;
5426
5727
  schemaName;
@@ -5458,18 +5759,77 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5458
5759
  }
5459
5760
  async init() {
5460
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
+ });
5766
+ }
5767
+ /**
5768
+ * Mark observational memory rows that are not their key's canonical head as superseded.
5769
+ * Runs as part of `init()`; `OracleStore` also runs it on every start when the memory schema
5770
+ * migration is unchanged and skipped.
5771
+ * @internal
5772
+ */
5773
+ async backfillObservationalMemorySupersededBy() {
5774
+ await this.ctx.db.withConnection((connection) => backfillObservationalMemorySupersededBy(this.ctx, connection));
5461
5775
  }
5462
5776
  async dangerouslyClearAll() {
5463
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
+ }
5464
5818
  }
5465
5819
  async getThreadById(args) {
5466
5820
  return getThreadById(this.ctx, args);
5467
5821
  }
5468
- async saveThread(args) {
5469
- 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
+ });
5470
5827
  }
5471
- async updateThread(args) {
5472
- 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
+ });
5473
5833
  }
5474
5834
  async deleteThread(args) {
5475
5835
  return deleteThread(this.ctx, args);
@@ -5489,14 +5849,20 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5489
5849
  async listMessagesByResourceId(args) {
5490
5850
  return listMessagesByResourceId(this.ctx, args);
5491
5851
  }
5492
- async saveMessages(args) {
5493
- 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
+ });
5494
5857
  }
5495
- async updateMessages(args) {
5496
- 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
+ });
5497
5863
  }
5498
- async deleteMessages(messageIds) {
5499
- return deleteMessages(this.ctx, messageIds);
5864
+ async deleteMessages(messageIds, options) {
5865
+ return deleteMessages(this.ctx, messageIds, (0, _mastra_core_storage.resolveRunFence)(this, options?.fence));
5500
5866
  }
5501
5867
  async getResourceById(args) {
5502
5868
  return getResourceById(this.ctx, args);
@@ -5504,8 +5870,11 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5504
5870
  async saveResource(args) {
5505
5871
  return saveResource(this.ctx, args);
5506
5872
  }
5507
- async updateResource(args) {
5508
- 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
+ });
5509
5878
  }
5510
5879
  async cloneThread(args) {
5511
5880
  const sourceThread = await this.getThreadById({ threadId: args.sourceThreadId });
@@ -5595,10 +5964,13 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5595
5964
  return insertObservationalMemoryRecord(this.ctx, record);
5596
5965
  }
5597
5966
  async updateActiveObservations(input) {
5598
- return updateActiveObservations(this.ctx, input);
5967
+ assertActiveObservationsApplied(await this.commitActiveObservations(input), input.id);
5968
+ }
5969
+ async commitActiveObservations(input) {
5970
+ return updateActiveObservations(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5599
5971
  }
5600
5972
  async createReflectionGeneration(input) {
5601
- return createReflectionGeneration(this.ctx, input);
5973
+ return createReflectionGeneration(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5602
5974
  }
5603
5975
  async setReflectingFlag(id, isReflecting) {
5604
5976
  return setReflectingFlag(this.ctx, id, isReflecting);
@@ -5622,16 +5994,19 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5622
5994
  return updateObservationalMemoryConfig(this.ctx, input);
5623
5995
  }
5624
5996
  async updateBufferedObservations(input) {
5625
- return updateBufferedObservations(this.ctx, input);
5997
+ await this.appendBufferedObservations(input);
5998
+ }
5999
+ async appendBufferedObservations(input) {
6000
+ return updateBufferedObservations(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5626
6001
  }
5627
6002
  async swapBufferedToActive(input) {
5628
- return swapBufferedToActive(this.ctx, input);
6003
+ return swapBufferedToActive(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5629
6004
  }
5630
6005
  async updateBufferedReflection(input) {
5631
- return updateBufferedReflection(this.ctx, input);
6006
+ return updateBufferedReflection(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5632
6007
  }
5633
6008
  async swapBufferedReflectionToActive(input) {
5634
- return swapBufferedReflectionToActive(this.ctx, input);
6009
+ return swapBufferedReflectionToActive(this.ctx, input, (0, _mastra_core_storage.resolveRunFence)(this, void 0));
5635
6010
  }
5636
6011
  };
5637
6012
  //#endregion
@@ -7976,8 +8351,19 @@ const STORE_NAME$1 = "ORACLEDB";
7976
8351
  const WORKFLOW_CREATED_AT = "\"createdAt\"";
7977
8352
  const WORKFLOW_UPDATED_AT = "\"updatedAt\"";
7978
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
+ }
7979
8365
  var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.WorkflowsStorage {
7980
- 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];
7981
8367
  db;
7982
8368
  schemaName;
7983
8369
  skipDefaultIndexes;
@@ -7998,10 +8384,114 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
7998
8384
  }
7999
8385
  async dangerouslyClearAll() {
8000
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
+ }
8462
+ }
8463
+ async releaseRunOwnership(fence) {
8464
+ try {
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
+ }
8001
8478
  }
8002
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
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);
8003
8492
  try {
8004
8493
  return await this.db.tx(async (client) => {
8494
+ if (fence) await assertRunFence(client, this.runOwnersTable(), fence, "updateWorkflowResults");
8005
8495
  await this.ensureWorkflowRunRow(client, workflowName, runId);
8006
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`, {
8007
8497
  workflowName,
@@ -8025,6 +8515,7 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8025
8515
  return snapshot.context;
8026
8516
  });
8027
8517
  } catch (error) {
8518
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
8028
8519
  throw this.storageError("UPDATE_WORKFLOW_RESULTS", "FAILED", {
8029
8520
  workflowName,
8030
8521
  runId,
@@ -8032,9 +8523,11 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8032
8523
  }, error);
8033
8524
  }
8034
8525
  }
8035
- async updateWorkflowState({ workflowName, runId, opts }) {
8526
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
8527
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
8036
8528
  try {
8037
8529
  return await this.db.tx(async (client) => {
8530
+ if (fence) await assertRunFence(client, this.runOwnersTable(), fence, "updateWorkflowState");
8038
8531
  const existing = await client.oneOrNone(`SELECT snapshot AS "snapshot" FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId FOR UPDATE`, {
8039
8532
  workflowName,
8040
8533
  runId
@@ -8057,24 +8550,26 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8057
8550
  return updatedSnapshot;
8058
8551
  });
8059
8552
  } catch (error) {
8553
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
8060
8554
  throw this.storageError("UPDATE_WORKFLOW_STATE", "FAILED", {
8061
8555
  workflowName,
8062
8556
  runId
8063
8557
  }, error);
8064
8558
  }
8065
8559
  }
8066
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
8560
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
8067
8561
  try {
8068
8562
  const now = /* @__PURE__ */ new Date();
8069
- 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(), {
8070
8564
  workflowName,
8071
8565
  runId,
8072
8566
  resourceId: resourceId ?? null,
8073
8567
  snapshot: jsonBind(snapshot),
8074
8568
  createdAt: createdAt ?? now,
8075
8569
  updatedAt: updatedAt ?? now
8076
- });
8570
+ }));
8077
8571
  } catch (error) {
8572
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
8078
8573
  throw this.storageError("PERSIST_WORKFLOW_SNAPSHOT", "FAILED", {
8079
8574
  workflowName,
8080
8575
  runId
@@ -8138,13 +8633,14 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8138
8633
  }, error);
8139
8634
  }
8140
8635
  }
8141
- async deleteWorkflowRunById({ runId, workflowName }) {
8636
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
8142
8637
  try {
8143
- 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`, {
8144
8639
  workflowName,
8145
8640
  runId
8146
- });
8641
+ }));
8147
8642
  } catch (error) {
8643
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
8148
8644
  throw this.storageError("DELETE_WORKFLOW_RUN_BY_ID", "FAILED", {
8149
8645
  runId,
8150
8646
  workflowName
@@ -8163,6 +8659,10 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8163
8659
  CONSTRAINT ${this.constraint("MASTRA_WORKFLOW_SNAPSHOT_PK")} PRIMARY KEY (workflow_name, run_id)
8164
8660
  )`, [-955]);
8165
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
+ });
8166
8666
  }
8167
8667
  async createIndexes() {
8168
8668
  await this.db.withConnection(async (connection) => {
@@ -8300,6 +8800,9 @@ var WorkflowsOracle = class WorkflowsOracle extends _mastra_core_storage.Workflo
8300
8800
  table() {
8301
8801
  return qualifyName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, this.schemaName);
8302
8802
  }
8803
+ runOwnersTable() {
8804
+ return qualifyName(_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, this.schemaName);
8805
+ }
8303
8806
  indexName(indexName) {
8304
8807
  return indexNameForTable(indexName, "IDX");
8305
8808
  }
@@ -8501,7 +9004,7 @@ function checksumMigration(input) {
8501
9004
  //#region src/storage/index.ts
8502
9005
  const STORE_NAME = "ORACLEDB";
8503
9006
  const DOMAIN_SCHEMA_VERSIONS = {
8504
- R001_MEMORY_SCHEMA: 1,
9007
+ R001_MEMORY_SCHEMA: 2,
8505
9008
  R002_WORKFLOWS_SCHEMA: 1,
8506
9009
  R003_OBSERVABILITY_SCHEMA: 1,
8507
9010
  R004_SCORES_SCHEMA: 1,
@@ -8589,6 +9092,10 @@ var OracleStore = class extends _mastra_core_storage.MastraCompositeStore {
8589
9092
  async runMigrations(forceRepeatable) {
8590
9093
  try {
8591
9094
  const results = await this.migrationRegistry.run(this.storageMigrations(), { forceRepeatable });
9095
+ if (results.find((result) => result.id === "R001_MEMORY_SCHEMA")?.status === "skipped") {
9096
+ const memory = this.stores.memory;
9097
+ if (memory instanceof MemoryOracle) await memory.backfillObservationalMemorySupersededBy();
9098
+ }
8592
9099
  this.isInitialized = true;
8593
9100
  return results;
8594
9101
  } catch (error) {