@mastra/oracledb 0.5.1 → 0.5.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3369,6 +3369,71 @@ 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
3372
3437
  //#region src/storage/domains/memory/utils.ts
3373
3438
  const STORE_NAME$5 = "ORACLEDB";
3374
3439
  const EMPTY_STRING_SENTINEL = "__MASTRA_ORACLE_EMPTY_STRING__";
@@ -3406,6 +3471,17 @@ function chunkValues(values, size = 900) {
3406
3471
  for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
3407
3472
  return chunks;
3408
3473
  }
3474
+ /**
3475
+ * Bind an instant as TIMESTAMP WITH TIME ZONE. A bare `Date` binds as TIMESTAMP WITH LOCAL
3476
+ * TIME ZONE, which the server converts with the session's fixed offset (captured at connect
3477
+ * time); an instant on the other side of a DST change is then stored shifted by an hour.
3478
+ */
3479
+ function timestampBind(value) {
3480
+ return value === void 0 || value === null ? null : {
3481
+ type: oracledb.default.DB_TYPE_TIMESTAMP_TZ,
3482
+ val: toDate(value)
3483
+ };
3484
+ }
3409
3485
  function boolToNumber(value) {
3410
3486
  return value ? 1 : 0;
3411
3487
  }
@@ -3554,12 +3630,48 @@ const OM_LAST_BUFFERED_AT_TOKENS = "\"lastBufferedAtTokens\"";
3554
3630
  const OM_LAST_BUFFERED_AT_TIME = "\"lastBufferedAtTime\"";
3555
3631
  const OM_CREATED_AT = "\"createdAt\"";
3556
3632
  const OM_UPDATED_AT = "\"updatedAt\"";
3633
+ const OM_SUPERSEDED_BY = "\"supersededBy\"";
3557
3634
  async function initMemorySchema(ctx) {
3558
3635
  await ctx.db.withConnection(async (connection) => {
3559
3636
  await createTables(ctx, connection);
3560
3637
  await createIndexes(ctx, connection);
3638
+ await backfillObservationalMemorySupersededBy(ctx, connection);
3561
3639
  });
3562
3640
  }
3641
+ /**
3642
+ * Retire every live observational memory row that sorts after its key's canonical head
3643
+ * (`generationCount DESC, createdAt ASC, id ASC`), marking it superseded by the head. Rows
3644
+ * retired by older adapter versions (which never set `supersededBy`) become frozen. Idempotent.
3645
+ * One statement: the head's ordering key is part of the condition, so a row created by a
3646
+ * concurrent rollover (higher generation) never matches, and a row locked by one is
3647
+ * re-evaluated after it commits.
3648
+ */
3649
+ async function backfillObservationalMemorySupersededBy(ctx, connection) {
3650
+ const om = table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY);
3651
+ const heads = `
3652
+ SELECT ${OM_LOOKUP_KEY} AS head_key, id AS head_id, ${OM_GENERATION_COUNT} AS head_gen, ${OM_CREATED_AT} AS head_created
3653
+ FROM (
3654
+ SELECT ${OM_LOOKUP_KEY}, id, ${OM_GENERATION_COUNT}, ${OM_CREATED_AT},
3655
+ ROW_NUMBER() OVER (PARTITION BY ${OM_LOOKUP_KEY} ORDER BY ${OM_GENERATION_COUNT} DESC, ${OM_CREATED_AT} ASC, id ASC) AS rn
3656
+ FROM ${om}
3657
+ WHERE ${OM_LOOKUP_KEY} IN (
3658
+ SELECT ${OM_LOOKUP_KEY} FROM ${om} WHERE ${OM_SUPERSEDED_BY} IS NULL GROUP BY ${OM_LOOKUP_KEY} HAVING COUNT(*) > 1
3659
+ )
3660
+ )
3661
+ WHERE rn = 1`;
3662
+ await connection.execute(`UPDATE ${om} r
3663
+ SET r.${OM_SUPERSEDED_BY} = (SELECT h.head_id FROM (${heads}) h WHERE h.head_key = r.${OM_LOOKUP_KEY})
3664
+ WHERE r.${OM_SUPERSEDED_BY} IS NULL
3665
+ AND EXISTS (
3666
+ SELECT 1 FROM (${heads}) h
3667
+ WHERE h.head_key = r.${OM_LOOKUP_KEY}
3668
+ AND (
3669
+ r.${OM_GENERATION_COUNT} < h.head_gen
3670
+ OR (r.${OM_GENERATION_COUNT} = h.head_gen AND r.${OM_CREATED_AT} > h.head_created)
3671
+ OR (r.${OM_GENERATION_COUNT} = h.head_gen AND r.${OM_CREATED_AT} = h.head_created AND r.id > h.head_id)
3672
+ )
3673
+ )`, {}, { autoCommit: true });
3674
+ }
3563
3675
  async function clearAllMemoryTables(ctx) {
3564
3676
  await ctx.db.tx(async (client) => {
3565
3677
  await client.none(`DELETE FROM ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}`);
@@ -3633,7 +3745,8 @@ async function createTables(ctx, connection) {
3633
3745
  ${OM_LAST_BUFFERED_AT_TIME} TIMESTAMP WITH TIME ZONE,
3634
3746
  metadata JSON,
3635
3747
  ${OM_CREATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL,
3636
- ${OM_UPDATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL
3748
+ ${OM_UPDATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL,
3749
+ ${OM_SUPERSEDED_BY} VARCHAR2(512)
3637
3750
  )`, [-955]);
3638
3751
  await ensureObservationalMemoryColumns(ctx, connection);
3639
3752
  await relaxObservationalMemoryNullability(ctx, connection);
@@ -3771,6 +3884,10 @@ async function ensureObservationalMemoryColumns(ctx, connection) {
3771
3884
  {
3772
3885
  name: OM_UPDATED_AT,
3773
3886
  type: "TIMESTAMP WITH TIME ZONE"
3887
+ },
3888
+ {
3889
+ name: OM_SUPERSEDED_BY,
3890
+ type: "VARCHAR2(512)"
3774
3891
  }
3775
3892
  ];
3776
3893
  for (const column of columns) await executeDdl(connection, `ALTER TABLE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)} ADD (${column.name} ${column.type})`, [-1430]);
@@ -4286,14 +4403,15 @@ function mergeMessageContent(existingContent, updateContent) {
4286
4403
  }
4287
4404
  //#endregion
4288
4405
  //#region src/storage/domains/memory/observational.ts
4406
+ /** Hops a write aimed at a retired record may follow to reach the head before giving up. */
4407
+ const OM_MAX_HEAD_HOPS = 3;
4408
+ /** Canonical head order for a lookup key. */
4409
+ const OM_HEAD_ORDER = `${OM_GENERATION_COUNT} DESC, ${OM_CREATED_AT} ASC, id ASC`;
4289
4410
  async function getObservationalMemory(ctx, threadId, resourceId) {
4290
4411
  try {
4291
4412
  const lookupKey = getOMKey(threadId, resourceId);
4292
4413
  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];
4414
+ const row = await getHeadOMRow(ctx, connection, lookupKey);
4297
4415
  return row ? parseOMRow(row) : null;
4298
4416
  });
4299
4417
  } catch (error) {
@@ -4325,11 +4443,11 @@ async function getObservationalMemoryHistory(ctx, threadId, resourceId, limit =
4325
4443
  }
4326
4444
  if (options?.from) {
4327
4445
  conditions.push(`${OM_CREATED_AT} >= :fromDate`);
4328
- binds.fromDate = options.from;
4446
+ binds.fromDate = timestampBind(options.from);
4329
4447
  }
4330
4448
  if (options?.to) {
4331
4449
  conditions.push(`${OM_CREATED_AT} <= :toDate`);
4332
- binds.toDate = options.to;
4450
+ binds.toDate = timestampBind(options.to);
4333
4451
  }
4334
4452
  if (options?.groupId !== void 0) {
4335
4453
  conditions.push(`(DBMS_LOB.INSTR(${OM_ACTIVE_OBSERVATIONS}, :groupPrefix) > 0 OR EXISTS (
@@ -4368,6 +4486,7 @@ async function getObservationalMemoryHistory(ctx, threadId, resourceId, limit =
4368
4486
  }
4369
4487
  async function initializeObservationalMemory(ctx, input) {
4370
4488
  const now = /* @__PURE__ */ new Date();
4489
+ const lookupKey = getOMKey(input.threadId, input.resourceId);
4371
4490
  const record = {
4372
4491
  id: globalThis.crypto.randomUUID(),
4373
4492
  scope: input.scope,
@@ -4389,13 +4508,22 @@ async function initializeObservationalMemory(ctx, input) {
4389
4508
  lastBufferedAtTokens: 0,
4390
4509
  lastBufferedAtTime: null,
4391
4510
  config: input.config,
4392
- observedTimezone: input.observedTimezone
4511
+ observedTimezone: input.observedTimezone,
4512
+ supersededBy: null
4393
4513
  };
4394
4514
  try {
4395
- await ctx.db.tx(async (_client, connection) => {
4515
+ const existing = await getObservationalMemory(ctx, input.threadId, input.resourceId);
4516
+ if (existing) return existing;
4517
+ if (await ctx.db.tx(async (_client, connection) => {
4518
+ const omTable = table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY);
4519
+ await connection.execute(`LOCK TABLE ${omTable} IN SHARE ROW EXCLUSIVE MODE`);
4520
+ 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
4521
  await insertOMRecord(ctx, connection, record);
4397
- });
4398
- return record;
4522
+ return true;
4523
+ })) return record;
4524
+ const head = await getObservationalMemory(ctx, input.threadId, input.resourceId);
4525
+ if (!head) throw new Error(`Observational memory record not found after initialization: ${lookupKey}`);
4526
+ return head;
4399
4527
  } catch (error) {
4400
4528
  throw storageError$1("INITIALIZE_OBSERVATIONAL_MEMORY", "FAILED", {
4401
4529
  threadId: input.threadId ?? "",
@@ -4417,7 +4545,17 @@ async function insertObservationalMemoryRecord(ctx, record) {
4417
4545
  }
4418
4546
  async function updateActiveObservations(ctx, input) {
4419
4547
  try {
4420
- await ctx.db.tx(async (_client, connection) => {
4548
+ return await ctx.db.tx(async (_client, connection) => {
4549
+ const row = await lockOMRow(ctx, connection, input.id, "UPDATE_ACTIVE_OBSERVATIONS");
4550
+ if (row.supersededBy) return {
4551
+ applied: false,
4552
+ reason: "retired"
4553
+ };
4554
+ if (input.expectedActiveObservations !== void 0 && input.expectedActiveObservations !== stringOrEmpty(row.activeObservations)) return {
4555
+ applied: false,
4556
+ reason: "conflict"
4557
+ };
4558
+ const lastObservedAt = maxObservationCursor(row.lastObservedAt, input.lastObservedAt);
4421
4559
  assertRowsAffected((await connection.execute(`
4422
4560
  UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4423
4561
  SET ${OM_ACTIVE_OBSERVATIONS} = :activeObservations,
@@ -4431,12 +4569,13 @@ async function updateActiveObservations(ctx, input) {
4431
4569
  WHERE id = :id`, {
4432
4570
  id: input.id,
4433
4571
  activeObservations: nullableClobBind(input.observations),
4434
- lastObservedAt: input.lastObservedAt,
4572
+ lastObservedAt: timestampBind(lastObservedAt),
4435
4573
  tokenCount: Math.round(input.tokenCount),
4436
4574
  observedMessageIds: nullableJsonBind(input.observedMessageIds),
4437
4575
  observedTimezone: input.observedTimezone ?? null,
4438
- updatedAt: /* @__PURE__ */ new Date()
4576
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4439
4577
  })).rowsAffected, "UPDATE_ACTIVE_OBSERVATIONS", input.id);
4578
+ return { applied: true };
4440
4579
  });
4441
4580
  } catch (error) {
4442
4581
  if (error instanceof _mastra_core_error.MastraError) throw error;
@@ -4444,39 +4583,73 @@ async function updateActiveObservations(ctx, input) {
4444
4583
  }
4445
4584
  }
4446
4585
  async function createReflectionGeneration(ctx, input) {
4586
+ const { currentRecord } = input;
4587
+ try {
4588
+ return await ctx.db.tx(async (_client, connection) => {
4589
+ const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
4590
+ if (!row) return currentRecord;
4591
+ if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
4592
+ const stored = parseOMRow(row);
4593
+ const plan = planReflectionGenerationText({
4594
+ storedObservations: stored.activeObservations,
4595
+ storedObservationTokenCount: stored.observationTokenCount,
4596
+ snapshotObservations: currentRecord.activeObservations,
4597
+ snapshotObservationTokenCount: currentRecord.observationTokenCount,
4598
+ reflection: input.reflection,
4599
+ tokenCount: input.tokenCount
4600
+ });
4601
+ if (!plan) return stored;
4602
+ return rollOverOMRow(ctx, connection, stored, plan.observations, plan.tokenCount, input.newRecordId);
4603
+ });
4604
+ } catch (error) {
4605
+ if (error instanceof _mastra_core_error.MastraError) throw error;
4606
+ throw storageError$1("CREATE_REFLECTION_GENERATION", "FAILED", { id: currentRecord.id }, error);
4607
+ }
4608
+ }
4609
+ /**
4610
+ * Create the next generation from the stored (live, row-locked) record and retire it in the
4611
+ * same transaction. Buffered chunks move to the new generation; the cursor, buffering markers,
4612
+ * flags, and counters carry over; buffered reflection state does not.
4613
+ */
4614
+ async function rollOverOMRow(ctx, connection, stored, observations, tokenCount, newRecordId) {
4447
4615
  const now = /* @__PURE__ */ new Date();
4448
4616
  const record = {
4449
- id: globalThis.crypto.randomUUID(),
4450
- scope: input.currentRecord.scope,
4451
- threadId: input.currentRecord.threadId,
4452
- resourceId: input.currentRecord.resourceId,
4617
+ id: newRecordId ?? globalThis.crypto.randomUUID(),
4618
+ scope: stored.scope,
4619
+ threadId: stored.threadId,
4620
+ resourceId: stored.resourceId,
4453
4621
  createdAt: now,
4454
4622
  updatedAt: now,
4455
- lastObservedAt: input.currentRecord.lastObservedAt,
4623
+ lastObservedAt: stored.lastObservedAt,
4456
4624
  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,
4625
+ generationCount: stored.generationCount + 1,
4626
+ activeObservations: observations,
4627
+ totalTokensObserved: stored.totalTokensObserved,
4628
+ observationTokenCount: Math.round(tokenCount),
4629
+ pendingMessageTokens: stored.pendingMessageTokens,
4462
4630
  isReflecting: false,
4463
4631
  isObserving: false,
4464
- isBufferingObservation: false,
4632
+ isBufferingObservation: stored.isBufferingObservation,
4465
4633
  isBufferingReflection: false,
4466
- lastBufferedAtTokens: 0,
4467
- lastBufferedAtTime: null,
4468
- config: input.currentRecord.config,
4469
- metadata: input.currentRecord.metadata,
4470
- observedTimezone: input.currentRecord.observedTimezone
4634
+ lastBufferedAtTokens: stored.lastBufferedAtTokens,
4635
+ lastBufferedAtTime: stored.lastBufferedAtTime ?? null,
4636
+ bufferedObservationChunks: stored.bufferedObservationChunks?.length ? stored.bufferedObservationChunks : void 0,
4637
+ config: stored.config,
4638
+ metadata: stored.metadata,
4639
+ observedTimezone: stored.observedTimezone,
4640
+ supersededBy: null
4471
4641
  };
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
- }
4642
+ await insertOMRecord(ctx, connection, record, now);
4643
+ if ((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4644
+ SET "supersededBy" = :newId,
4645
+ "bufferedObservationChunks" = NULL,
4646
+ "updatedAt" = :updatedAt
4647
+ WHERE id = :id AND "supersededBy" IS NULL`, {
4648
+ newId: record.id,
4649
+ updatedAt: timestampBind(now),
4650
+ id: stored.id
4651
+ })).rowsAffected !== 1) throw storageError$1("CREATE_REFLECTION_GENERATION", "RETIRE_FAILED", { id: stored.id }, /* @__PURE__ */ new Error(`Failed to retire observational memory record ${stored.id}`));
4652
+ return record;
4480
4653
  }
4481
4654
  async function setReflectingFlag(ctx, id, isReflecting) {
4482
4655
  await updateOMFlag(ctx, id, OM_IS_REFLECTING, isReflecting, "SET_REFLECTING_FLAG");
@@ -4491,9 +4664,10 @@ async function setBufferingObservationFlag(ctx, id, isBuffering, lastBufferedAtT
4491
4664
  const binds = {
4492
4665
  id,
4493
4666
  isBuffering: boolToNumber(isBuffering),
4494
- updatedAt: /* @__PURE__ */ new Date()
4667
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4495
4668
  };
4496
4669
  if (lastBufferedAtTokens !== void 0) binds.lastBufferedAtTokens = Math.round(lastBufferedAtTokens);
4670
+ binds.id = (await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, id, "SET_BUFFERING_OBSERVATION_FLAG"))).id;
4497
4671
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4498
4672
  SET ${OM_IS_BUFFERING_OBSERVATION} = :isBuffering,
4499
4673
  ${OM_UPDATED_AT} = :updatedAt
@@ -4524,7 +4698,17 @@ async function clearObservationalMemory(ctx, threadId, resourceId) {
4524
4698
  }
4525
4699
  async function setPendingMessageTokens(ctx, id, tokenCount) {
4526
4700
  try {
4527
- await updateOMColumns(ctx, id, "SET_PENDING_MESSAGE_TOKENS", { [OM_PENDING_MESSAGE_TOKENS]: Math.round(tokenCount) });
4701
+ await ctx.db.tx(async (_client, connection) => {
4702
+ const target = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, id, "SET_PENDING_MESSAGE_TOKENS"));
4703
+ assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4704
+ SET ${OM_PENDING_MESSAGE_TOKENS} = :tokenCount,
4705
+ ${OM_UPDATED_AT} = :updatedAt
4706
+ WHERE id = :id`, {
4707
+ tokenCount: Math.round(tokenCount),
4708
+ updatedAt: timestampBind(/* @__PURE__ */ new Date()),
4709
+ id: target.id
4710
+ })).rowsAffected, "SET_PENDING_MESSAGE_TOKENS", id);
4711
+ });
4528
4712
  } catch (error) {
4529
4713
  if (error instanceof _mastra_core_error.MastraError) throw error;
4530
4714
  throw storageError$1("SET_PENDING_MESSAGE_TOKENS", "FAILED", {
@@ -4546,7 +4730,7 @@ async function updateObservationalMemoryConfig(ctx, input) {
4546
4730
  WHERE id = :id`, {
4547
4731
  id: input.id,
4548
4732
  config: jsonBind(merged),
4549
- updatedAt: /* @__PURE__ */ new Date()
4733
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4550
4734
  })).rowsAffected, "UPDATE_OM_CONFIG", input.id);
4551
4735
  });
4552
4736
  } catch (error) {
@@ -4592,6 +4776,7 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4592
4776
  ${OM_LAST_BUFFERED_AT_TOKENS},
4593
4777
  ${OM_LAST_BUFFERED_AT_TIME},
4594
4778
  metadata,
4779
+ ${OM_SUPERSEDED_BY},
4595
4780
  ${OM_CREATED_AT},
4596
4781
  ${OM_UPDATED_AT}
4597
4782
  ) VALUES (
@@ -4627,6 +4812,7 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4627
4812
  :lastBufferedAtTokens,
4628
4813
  :lastBufferedAtTime,
4629
4814
  :metadata,
4815
+ :supersededBy,
4630
4816
  :createdAt,
4631
4817
  :updatedAt
4632
4818
  )`, {
@@ -4640,8 +4826,8 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4640
4826
  originType: record.originType ?? "initial",
4641
4827
  config: jsonBind(record.config ?? {}),
4642
4828
  generationCount: record.generationCount ?? 0,
4643
- lastObservedAt: record.lastObservedAt ?? null,
4644
- lastReflectionAt: record.originType === "reflection" ? timestamp : null,
4829
+ lastObservedAt: timestampBind(record.lastObservedAt),
4830
+ lastReflectionAt: record.originType === "reflection" ? timestampBind(timestamp) : null,
4645
4831
  pendingMessageTokens: Math.round(record.pendingMessageTokens ?? 0),
4646
4832
  totalTokensObserved: Math.round(record.totalTokensObserved ?? 0),
4647
4833
  observationTokenCount: Math.round(record.observationTokenCount ?? 0),
@@ -4660,10 +4846,11 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4660
4846
  isBufferingObservation: boolToNumber(record.isBufferingObservation),
4661
4847
  isBufferingReflection: boolToNumber(record.isBufferingReflection),
4662
4848
  lastBufferedAtTokens: Math.round(record.lastBufferedAtTokens ?? 0),
4663
- lastBufferedAtTime: record.lastBufferedAtTime ?? null,
4849
+ lastBufferedAtTime: timestampBind(record.lastBufferedAtTime),
4664
4850
  metadata: nullableJsonBind(record.metadata),
4665
- createdAt: record.createdAt,
4666
- updatedAt: record.updatedAt
4851
+ supersededBy: record.supersededBy ?? null,
4852
+ createdAt: timestampBind(record.createdAt),
4853
+ updatedAt: timestampBind(record.updatedAt)
4667
4854
  });
4668
4855
  }
4669
4856
  async function updateOMFlag(ctx, id, column, value, operation) {
@@ -4687,7 +4874,7 @@ async function updateOMColumns(ctx, id, operation, columns) {
4687
4874
  WHERE id = :id`, {
4688
4875
  ...binds,
4689
4876
  id,
4690
- updatedAt: /* @__PURE__ */ new Date()
4877
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4691
4878
  })).rowsAffected, operation, id);
4692
4879
  });
4693
4880
  }
@@ -4725,6 +4912,7 @@ function omSelect() {
4725
4912
  ${OM_LAST_BUFFERED_AT_TOKENS} AS "lastBufferedAtTokens",
4726
4913
  ${OM_LAST_BUFFERED_AT_TIME} AS "lastBufferedAtTime",
4727
4914
  metadata AS "metadata",
4915
+ ${OM_SUPERSEDED_BY} AS "supersededBy",
4728
4916
  ${OM_CREATED_AT} AS "createdAt",
4729
4917
  ${OM_UPDATED_AT} AS "updatedAt"`;
4730
4918
  }
@@ -4760,15 +4948,60 @@ function parseOMRow(row) {
4760
4948
  config: parseJson(row.config),
4761
4949
  metadata: parseOptionalJsonObject(row.metadata, { emptyObjectAsUndefined: true }),
4762
4950
  observedMessageIds: parseOptionalStringArray(row.observedMessageIds),
4763
- observedTimezone: row.observedTimezone ? String(row.observedTimezone) : void 0
4951
+ observedTimezone: row.observedTimezone ? String(row.observedTimezone) : void 0,
4952
+ supersededBy: row.supersededBy ? String(row.supersededBy) : null
4764
4953
  };
4765
4954
  }
4955
+ /** The canonical head row of a lookup key (non-locking read). */
4956
+ async function getHeadOMRow(ctx, connection, lookupKey) {
4957
+ return rows(await connection.execute(`${omSelect()} FROM ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4958
+ WHERE ${OM_LOOKUP_KEY} = :lookupKey
4959
+ ORDER BY ${OM_HEAD_ORDER}
4960
+ FETCH FIRST 1 ROWS ONLY`, asBindParameters({ lookupKey }), executeOptions()))[0];
4961
+ }
4962
+ /** Row-locks one record by primary key for the rest of the transaction; `undefined` when missing. */
4963
+ async function findOMRowForUpdate(ctx, connection, id) {
4964
+ return rows(await connection.execute(`${omSelect()} FROM ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)} WHERE id = :id FOR UPDATE`, { id }, executeOptions()))[0];
4965
+ }
4966
+ /**
4967
+ * Row-locks one record by primary key. Observational memory updates are incremental and
4968
+ * order-sensitive, so mutating paths derive their next state from a locked row.
4969
+ * Throws "record not found" when it is missing.
4970
+ */
4971
+ async function lockOMRow(ctx, connection, id, operation) {
4972
+ const row = await findOMRowForUpdate(ctx, connection, id);
4973
+ if (!row) {
4974
+ assertRowsAffected(0, operation, id);
4975
+ throw new Error(`Observational memory record not found: ${id}`);
4976
+ }
4977
+ return row;
4978
+ }
4979
+ /**
4980
+ * The live, row-locked record a lifecycle write aimed at `row` lands on: `row` itself while
4981
+ * live, otherwise the head of its lookup key (locked, then re-checked — a rollover may retire
4982
+ * it between the head read and the lock).
4983
+ */
4984
+ async function resolveLiveOMRow(ctx, connection, row) {
4985
+ let current = row;
4986
+ for (let hop = 0; current.supersededBy; hop++) {
4987
+ const head = hop < OM_MAX_HEAD_HOPS ? await getHeadOMRow(ctx, connection, current.lookupKey) : void 0;
4988
+ const locked = head && head.id !== current.id ? await findOMRowForUpdate(ctx, connection, head.id) : void 0;
4989
+ 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`));
4990
+ current = locked;
4991
+ }
4992
+ return current;
4993
+ }
4766
4994
  //#endregion
4767
4995
  //#region src/storage/domains/memory/observational-buffering.ts
4768
4996
  async function updateBufferedObservations(ctx, input) {
4769
4997
  try {
4770
- await ctx.db.tx(async (_client, connection) => {
4771
- const existingChunks = parseBufferedChunks((await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS")).bufferedObservationChunks);
4998
+ return await ctx.db.tx(async (_client, connection) => {
4999
+ const row = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS"));
5000
+ const existingChunks = parseBufferedChunks(row.bufferedObservationChunks);
5001
+ if (existingChunks.some((existing) => existing.cycleId === input.chunk.cycleId) || isBufferedChunkCoveredByCursor(input.chunk.lastObservedAt, row.lastObservedAt)) return {
5002
+ persisted: false,
5003
+ recordId: row.id
5004
+ };
4772
5005
  const newChunk = {
4773
5006
  id: `ombuf-${globalThis.crypto.randomUUID()}`,
4774
5007
  cycleId: input.chunk.cycleId,
@@ -4784,18 +5017,21 @@ async function updateBufferedObservations(ctx, input) {
4784
5017
  extractedValues: input.chunk.extractedValues,
4785
5018
  extractionFailures: input.chunk.extractionFailures
4786
5019
  };
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);
5020
+ const lastBufferedAtTime = maxObservationCursor(row.lastBufferedAtTime, input.lastBufferedAtTime);
4795
5021
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4796
5022
  SET ${OM_BUFFERED_OBSERVATION_CHUNKS} = :bufferedObservationChunks,
4797
- ${OM_UPDATED_AT} = :updatedAt${lastBufferedAtTimeSql}
4798
- WHERE id = :id`, asBindParameters(binds))).rowsAffected, "UPDATE_BUFFERED_OBSERVATIONS", input.id);
5023
+ ${OM_LAST_BUFFERED_AT_TIME} = :lastBufferedAtTime,
5024
+ ${OM_UPDATED_AT} = :updatedAt
5025
+ WHERE id = :id`, asBindParameters({
5026
+ id: row.id,
5027
+ bufferedObservationChunks: nullableJsonBind([...existingChunks, newChunk]),
5028
+ lastBufferedAtTime: timestampBind(lastBufferedAtTime),
5029
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
5030
+ }))).rowsAffected, "UPDATE_BUFFERED_OBSERVATIONS", row.id);
5031
+ return {
5032
+ persisted: true,
5033
+ recordId: row.id
5034
+ };
4799
5035
  });
4800
5036
  } catch (error) {
4801
5037
  if (error instanceof _mastra_core_error.MastraError) throw error;
@@ -4806,10 +5042,22 @@ async function swapBufferedToActive(ctx, input) {
4806
5042
  try {
4807
5043
  return await ctx.db.tx(async (_client, connection) => {
4808
5044
  const row = await lockOMRow(ctx, connection, input.id, "SWAP_BUFFERED_TO_ACTIVE");
4809
- const chunks = input.bufferedChunks?.length ? input.bufferedChunks : parseBufferedChunks(row.bufferedObservationChunks);
5045
+ if (row.supersededBy) return {
5046
+ ...emptySwapResult(),
5047
+ retired: true
5048
+ };
5049
+ const refreshedWeights = new Map((input.bufferedChunks ?? []).map((chunk) => [chunk.id, chunk.messageTokens]));
5050
+ const chunks = parseBufferedChunks(row.bufferedObservationChunks).map((chunk) => {
5051
+ const weight = refreshedWeights.get(chunk.id);
5052
+ return weight === void 0 ? chunk : {
5053
+ ...chunk,
5054
+ messageTokens: weight
5055
+ };
5056
+ });
4810
5057
  if (chunks.length === 0) return emptySwapResult();
4811
5058
  const activation = calculateBufferedActivation(chunks, input);
4812
5059
  const lastObservedAt = input.lastObservedAt ?? (activation.activatedChunks.at(-1)?.lastObservedAt ? toDate(activation.activatedChunks.at(-1).lastObservedAt) : /* @__PURE__ */ new Date());
5060
+ const storedCursor = maxObservationCursor(row.lastObservedAt, lastObservedAt);
4813
5061
  const boundary = `\n\n--- message boundary (${lastObservedAt.toISOString()}) ---\n\n`;
4814
5062
  const existingActive = stringOrEmpty(row.activeObservations);
4815
5063
  const newActive = existingActive ? `${existingActive}${boundary}${activation.activatedContent}` : activation.activatedContent;
@@ -4827,8 +5075,8 @@ async function swapBufferedToActive(ctx, input) {
4827
5075
  observationTokens: activation.activatedTokens,
4828
5076
  pendingMessageTokens: pendingTokens,
4829
5077
  bufferedObservationChunks: nullableJsonBind(activation.remainingChunks.length > 0 ? activation.remainingChunks : null),
4830
- lastObservedAt,
4831
- updatedAt: /* @__PURE__ */ new Date()
5078
+ lastObservedAt: timestampBind(storedCursor),
5079
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4832
5080
  })).rowsAffected, "SWAP_BUFFERED_TO_ACTIVE", input.id);
4833
5081
  return activation.result;
4834
5082
  });
@@ -4856,7 +5104,7 @@ async function updateBufferedReflection(ctx, input) {
4856
5104
  tokenCount: Math.round(input.tokenCount),
4857
5105
  inputTokenCount: Math.round(input.inputTokenCount),
4858
5106
  reflectedObservationLineCount: Math.round(input.reflectedObservationLineCount),
4859
- updatedAt: /* @__PURE__ */ new Date()
5107
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4860
5108
  })).rowsAffected, "UPDATE_BUFFERED_REFLECTION", input.id);
4861
5109
  });
4862
5110
  } catch (error) {
@@ -4865,66 +5113,32 @@ async function updateBufferedReflection(ctx, input) {
4865
5113
  }
4866
5114
  }
4867
5115
  async function swapBufferedReflectionToActive(ctx, input) {
5116
+ const { currentRecord } = input;
4868
5117
  try {
4869
5118
  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);
5119
+ const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
5120
+ if (!row) return currentRecord;
5121
+ if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
5122
+ const stored = parseOMRow(row);
5123
+ const bufferedReflection = stored.bufferedReflection ?? "";
5124
+ 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);
5125
+ if (!isAppendOnlySince(stored.activeObservations, currentRecord.activeObservations)) return stored;
5126
+ const reflectedLineCount = stored.reflectedObservationLineCount ?? 0;
5127
+ const unreflectedContent = stored.activeObservations.split("\n").slice(reflectedLineCount).join("\n").trim();
5128
+ 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
5129
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, _mastra_core_storage.TABLE_OBSERVATIONAL_MEMORY)}
4904
5130
  SET ${OM_BUFFERED_REFLECTION} = NULL,
4905
5131
  ${OM_BUFFERED_REFLECTION_TOKENS} = NULL,
4906
5132
  ${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);
5133
+ ${OM_REFLECTED_OBSERVATION_LINE_COUNT} = NULL
5134
+ WHERE id = :id`, { id: stored.id })).rowsAffected, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", stored.id);
4913
5135
  return newRecord;
4914
5136
  });
4915
5137
  } catch (error) {
4916
5138
  if (error instanceof _mastra_core_error.MastraError) throw error;
4917
- throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "FAILED", { id: input.currentRecord.id }, error);
5139
+ throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "FAILED", { id: currentRecord.id }, error);
4918
5140
  }
4919
5141
  }
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
5142
  function emptySwapResult() {
4929
5143
  return {
4930
5144
  chunksActivated: 0,
@@ -5459,6 +5673,15 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5459
5673
  async init() {
5460
5674
  await initMemorySchema(this.ctx);
5461
5675
  }
5676
+ /**
5677
+ * Mark observational memory rows that are not their key's canonical head as superseded.
5678
+ * Runs as part of `init()`; `OracleStore` also runs it on every start when the memory schema
5679
+ * migration is unchanged and skipped.
5680
+ * @internal
5681
+ */
5682
+ async backfillObservationalMemorySupersededBy() {
5683
+ await this.ctx.db.withConnection((connection) => backfillObservationalMemorySupersededBy(this.ctx, connection));
5684
+ }
5462
5685
  async dangerouslyClearAll() {
5463
5686
  await clearAllMemoryTables(this.ctx);
5464
5687
  }
@@ -5595,6 +5818,9 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5595
5818
  return insertObservationalMemoryRecord(this.ctx, record);
5596
5819
  }
5597
5820
  async updateActiveObservations(input) {
5821
+ assertActiveObservationsApplied(await this.commitActiveObservations(input), input.id);
5822
+ }
5823
+ async commitActiveObservations(input) {
5598
5824
  return updateActiveObservations(this.ctx, input);
5599
5825
  }
5600
5826
  async createReflectionGeneration(input) {
@@ -5622,6 +5848,9 @@ var MemoryOracle = class MemoryOracle extends _mastra_core_storage.MemoryStorage
5622
5848
  return updateObservationalMemoryConfig(this.ctx, input);
5623
5849
  }
5624
5850
  async updateBufferedObservations(input) {
5851
+ await this.appendBufferedObservations(input);
5852
+ }
5853
+ async appendBufferedObservations(input) {
5625
5854
  return updateBufferedObservations(this.ctx, input);
5626
5855
  }
5627
5856
  async swapBufferedToActive(input) {
@@ -8501,7 +8730,7 @@ function checksumMigration(input) {
8501
8730
  //#region src/storage/index.ts
8502
8731
  const STORE_NAME = "ORACLEDB";
8503
8732
  const DOMAIN_SCHEMA_VERSIONS = {
8504
- R001_MEMORY_SCHEMA: 1,
8733
+ R001_MEMORY_SCHEMA: 2,
8505
8734
  R002_WORKFLOWS_SCHEMA: 1,
8506
8735
  R003_OBSERVABILITY_SCHEMA: 1,
8507
8736
  R004_SCORES_SCHEMA: 1,
@@ -8589,6 +8818,10 @@ var OracleStore = class extends _mastra_core_storage.MastraCompositeStore {
8589
8818
  async runMigrations(forceRepeatable) {
8590
8819
  try {
8591
8820
  const results = await this.migrationRegistry.run(this.storageMigrations(), { forceRepeatable });
8821
+ if (results.find((result) => result.id === "R001_MEMORY_SCHEMA")?.status === "skipped") {
8822
+ const memory = this.stores.memory;
8823
+ if (memory instanceof MemoryOracle) await memory.backfillObservationalMemorySupersededBy();
8824
+ }
8592
8825
  this.isInitialized = true;
8593
8826
  return results;
8594
8827
  } catch (error) {