@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.js CHANGED
@@ -3345,6 +3345,71 @@ function jsonComparableValue$1(value) {
3345
3345
  return value;
3346
3346
  }
3347
3347
  //#endregion
3348
+ //#region ../../packages/_internal-core/dist/memory/index.js
3349
+ /**
3350
+ * A buffered chunk stores `lastObservedAt = max message time + 1ms`. It is wholly covered
3351
+ * (every message it observed is already at or before the cursor) iff
3352
+ * `cursor >= chunk.lastObservedAt - 1ms`.
3353
+ */
3354
+ function isBufferedChunkCoveredByCursor(chunkLastObservedAt, cursor) {
3355
+ if (!cursor) return false;
3356
+ return new Date(cursor).getTime() >= new Date(chunkLastObservedAt).getTime() - 1;
3357
+ }
3358
+ /** The later of two cursors; `undefined` only when both are absent. */
3359
+ function maxObservationCursor(a, b) {
3360
+ if (!a) return b ? new Date(b) : void 0;
3361
+ if (!b) return new Date(a);
3362
+ return new Date(Math.max(new Date(a).getTime(), new Date(b).getTime()));
3363
+ }
3364
+ /**
3365
+ * Decide the new generation's text when a reflection built from `snapshot` commits against
3366
+ * the stored record.
3367
+ *
3368
+ * - Stored text equals the snapshot: the reflection replaces it.
3369
+ * - Stored text extends the snapshot (activation only appends): the appended tail is kept
3370
+ * after the reflection, and its tokens are added.
3371
+ * - Anything else (a non-append rewrite): `null` — the reflection must not be applied.
3372
+ */
3373
+ function planReflectionGenerationText(input) {
3374
+ const stored = input.storedObservations ?? "";
3375
+ const snapshot = input.snapshotObservations ?? "";
3376
+ if (stored === snapshot) return {
3377
+ mode: "equal",
3378
+ observations: input.reflection,
3379
+ tokenCount: input.tokenCount
3380
+ };
3381
+ if (!stored.startsWith(snapshot)) return null;
3382
+ const tail = stored.slice(snapshot.length).trimStart();
3383
+ if (!tail.trim()) return {
3384
+ mode: "equal",
3385
+ observations: input.reflection,
3386
+ tokenCount: input.tokenCount
3387
+ };
3388
+ return {
3389
+ mode: "append",
3390
+ observations: input.reflection ? `${input.reflection}\n\n${tail}` : tail,
3391
+ tokenCount: input.tokenCount + Math.max(0, (input.storedObservationTokenCount ?? 0) - (input.snapshotObservationTokenCount ?? 0))
3392
+ };
3393
+ }
3394
+ /**
3395
+ * Backs `updateActiveObservations` in adapters that implement `commitActiveObservations`.
3396
+ * Callers of `updateActiveObservations` (which resolves `void`) can't see a commit result, so a
3397
+ * write that was not applied throws instead of resolving as if it had committed.
3398
+ */
3399
+ function assertActiveObservationsApplied(result, id) {
3400
+ if (result.applied) return;
3401
+ 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`);
3402
+ }
3403
+ /**
3404
+ * Whether the stored text can still accept a reflection built from `snapshot`
3405
+ * (equal, or only appended to since).
3406
+ */
3407
+ function isAppendOnlySince(storedObservations, snapshotObservations) {
3408
+ const stored = storedObservations ?? "";
3409
+ const snapshot = snapshotObservations ?? "";
3410
+ return stored === snapshot || stored.startsWith(snapshot);
3411
+ }
3412
+ //#endregion
3348
3413
  //#region src/storage/domains/memory/utils.ts
3349
3414
  const STORE_NAME$5 = "ORACLEDB";
3350
3415
  const EMPTY_STRING_SENTINEL = "__MASTRA_ORACLE_EMPTY_STRING__";
@@ -3382,6 +3447,17 @@ function chunkValues(values, size = 900) {
3382
3447
  for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
3383
3448
  return chunks;
3384
3449
  }
3450
+ /**
3451
+ * Bind an instant as TIMESTAMP WITH TIME ZONE. A bare `Date` binds as TIMESTAMP WITH LOCAL
3452
+ * TIME ZONE, which the server converts with the session's fixed offset (captured at connect
3453
+ * time); an instant on the other side of a DST change is then stored shifted by an hour.
3454
+ */
3455
+ function timestampBind(value) {
3456
+ return value === void 0 || value === null ? null : {
3457
+ type: oracledb.DB_TYPE_TIMESTAMP_TZ,
3458
+ val: toDate(value)
3459
+ };
3460
+ }
3385
3461
  function boolToNumber(value) {
3386
3462
  return value ? 1 : 0;
3387
3463
  }
@@ -3530,12 +3606,48 @@ const OM_LAST_BUFFERED_AT_TOKENS = "\"lastBufferedAtTokens\"";
3530
3606
  const OM_LAST_BUFFERED_AT_TIME = "\"lastBufferedAtTime\"";
3531
3607
  const OM_CREATED_AT = "\"createdAt\"";
3532
3608
  const OM_UPDATED_AT = "\"updatedAt\"";
3609
+ const OM_SUPERSEDED_BY = "\"supersededBy\"";
3533
3610
  async function initMemorySchema(ctx) {
3534
3611
  await ctx.db.withConnection(async (connection) => {
3535
3612
  await createTables(ctx, connection);
3536
3613
  await createIndexes(ctx, connection);
3614
+ await backfillObservationalMemorySupersededBy(ctx, connection);
3537
3615
  });
3538
3616
  }
3617
+ /**
3618
+ * Retire every live observational memory row that sorts after its key's canonical head
3619
+ * (`generationCount DESC, createdAt ASC, id ASC`), marking it superseded by the head. Rows
3620
+ * retired by older adapter versions (which never set `supersededBy`) become frozen. Idempotent.
3621
+ * One statement: the head's ordering key is part of the condition, so a row created by a
3622
+ * concurrent rollover (higher generation) never matches, and a row locked by one is
3623
+ * re-evaluated after it commits.
3624
+ */
3625
+ async function backfillObservationalMemorySupersededBy(ctx, connection) {
3626
+ const om = table(ctx, TABLE_OBSERVATIONAL_MEMORY);
3627
+ const heads = `
3628
+ SELECT ${OM_LOOKUP_KEY} AS head_key, id AS head_id, ${OM_GENERATION_COUNT} AS head_gen, ${OM_CREATED_AT} AS head_created
3629
+ FROM (
3630
+ SELECT ${OM_LOOKUP_KEY}, id, ${OM_GENERATION_COUNT}, ${OM_CREATED_AT},
3631
+ ROW_NUMBER() OVER (PARTITION BY ${OM_LOOKUP_KEY} ORDER BY ${OM_GENERATION_COUNT} DESC, ${OM_CREATED_AT} ASC, id ASC) AS rn
3632
+ FROM ${om}
3633
+ WHERE ${OM_LOOKUP_KEY} IN (
3634
+ SELECT ${OM_LOOKUP_KEY} FROM ${om} WHERE ${OM_SUPERSEDED_BY} IS NULL GROUP BY ${OM_LOOKUP_KEY} HAVING COUNT(*) > 1
3635
+ )
3636
+ )
3637
+ WHERE rn = 1`;
3638
+ await connection.execute(`UPDATE ${om} r
3639
+ SET r.${OM_SUPERSEDED_BY} = (SELECT h.head_id FROM (${heads}) h WHERE h.head_key = r.${OM_LOOKUP_KEY})
3640
+ WHERE r.${OM_SUPERSEDED_BY} IS NULL
3641
+ AND EXISTS (
3642
+ SELECT 1 FROM (${heads}) h
3643
+ WHERE h.head_key = r.${OM_LOOKUP_KEY}
3644
+ AND (
3645
+ r.${OM_GENERATION_COUNT} < h.head_gen
3646
+ OR (r.${OM_GENERATION_COUNT} = h.head_gen AND r.${OM_CREATED_AT} > h.head_created)
3647
+ OR (r.${OM_GENERATION_COUNT} = h.head_gen AND r.${OM_CREATED_AT} = h.head_created AND r.id > h.head_id)
3648
+ )
3649
+ )`, {}, { autoCommit: true });
3650
+ }
3539
3651
  async function clearAllMemoryTables(ctx) {
3540
3652
  await ctx.db.tx(async (client) => {
3541
3653
  await client.none(`DELETE FROM ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}`);
@@ -3609,7 +3721,8 @@ async function createTables(ctx, connection) {
3609
3721
  ${OM_LAST_BUFFERED_AT_TIME} TIMESTAMP WITH TIME ZONE,
3610
3722
  metadata JSON,
3611
3723
  ${OM_CREATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL,
3612
- ${OM_UPDATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL
3724
+ ${OM_UPDATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL,
3725
+ ${OM_SUPERSEDED_BY} VARCHAR2(512)
3613
3726
  )`, [-955]);
3614
3727
  await ensureObservationalMemoryColumns(ctx, connection);
3615
3728
  await relaxObservationalMemoryNullability(ctx, connection);
@@ -3747,6 +3860,10 @@ async function ensureObservationalMemoryColumns(ctx, connection) {
3747
3860
  {
3748
3861
  name: OM_UPDATED_AT,
3749
3862
  type: "TIMESTAMP WITH TIME ZONE"
3863
+ },
3864
+ {
3865
+ name: OM_SUPERSEDED_BY,
3866
+ type: "VARCHAR2(512)"
3750
3867
  }
3751
3868
  ];
3752
3869
  for (const column of columns) await executeDdl(connection, `ALTER TABLE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)} ADD (${column.name} ${column.type})`, [-1430]);
@@ -4262,14 +4379,15 @@ function mergeMessageContent(existingContent, updateContent) {
4262
4379
  }
4263
4380
  //#endregion
4264
4381
  //#region src/storage/domains/memory/observational.ts
4382
+ /** Hops a write aimed at a retired record may follow to reach the head before giving up. */
4383
+ const OM_MAX_HEAD_HOPS = 3;
4384
+ /** Canonical head order for a lookup key. */
4385
+ const OM_HEAD_ORDER = `${OM_GENERATION_COUNT} DESC, ${OM_CREATED_AT} ASC, id ASC`;
4265
4386
  async function getObservationalMemory(ctx, threadId, resourceId) {
4266
4387
  try {
4267
4388
  const lookupKey = getOMKey(threadId, resourceId);
4268
4389
  return await ctx.db.withConnection(async (connection) => {
4269
- const row = rows(await connection.execute(`${omSelect()} FROM ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4270
- WHERE ${OM_LOOKUP_KEY} = :lookupKey
4271
- ORDER BY ${OM_GENERATION_COUNT} DESC
4272
- FETCH FIRST 1 ROWS ONLY`, asBindParameters({ lookupKey }), executeOptions()))[0];
4390
+ const row = await getHeadOMRow(ctx, connection, lookupKey);
4273
4391
  return row ? parseOMRow(row) : null;
4274
4392
  });
4275
4393
  } catch (error) {
@@ -4301,11 +4419,11 @@ async function getObservationalMemoryHistory(ctx, threadId, resourceId, limit =
4301
4419
  }
4302
4420
  if (options?.from) {
4303
4421
  conditions.push(`${OM_CREATED_AT} >= :fromDate`);
4304
- binds.fromDate = options.from;
4422
+ binds.fromDate = timestampBind(options.from);
4305
4423
  }
4306
4424
  if (options?.to) {
4307
4425
  conditions.push(`${OM_CREATED_AT} <= :toDate`);
4308
- binds.toDate = options.to;
4426
+ binds.toDate = timestampBind(options.to);
4309
4427
  }
4310
4428
  if (options?.groupId !== void 0) {
4311
4429
  conditions.push(`(DBMS_LOB.INSTR(${OM_ACTIVE_OBSERVATIONS}, :groupPrefix) > 0 OR EXISTS (
@@ -4344,6 +4462,7 @@ async function getObservationalMemoryHistory(ctx, threadId, resourceId, limit =
4344
4462
  }
4345
4463
  async function initializeObservationalMemory(ctx, input) {
4346
4464
  const now = /* @__PURE__ */ new Date();
4465
+ const lookupKey = getOMKey(input.threadId, input.resourceId);
4347
4466
  const record = {
4348
4467
  id: globalThis.crypto.randomUUID(),
4349
4468
  scope: input.scope,
@@ -4365,13 +4484,22 @@ async function initializeObservationalMemory(ctx, input) {
4365
4484
  lastBufferedAtTokens: 0,
4366
4485
  lastBufferedAtTime: null,
4367
4486
  config: input.config,
4368
- observedTimezone: input.observedTimezone
4487
+ observedTimezone: input.observedTimezone,
4488
+ supersededBy: null
4369
4489
  };
4370
4490
  try {
4371
- await ctx.db.tx(async (_client, connection) => {
4491
+ const existing = await getObservationalMemory(ctx, input.threadId, input.resourceId);
4492
+ if (existing) return existing;
4493
+ if (await ctx.db.tx(async (_client, connection) => {
4494
+ const omTable = table(ctx, TABLE_OBSERVATIONAL_MEMORY);
4495
+ await connection.execute(`LOCK TABLE ${omTable} IN SHARE ROW EXCLUSIVE MODE`);
4496
+ 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;
4372
4497
  await insertOMRecord(ctx, connection, record);
4373
- });
4374
- return record;
4498
+ return true;
4499
+ })) return record;
4500
+ const head = await getObservationalMemory(ctx, input.threadId, input.resourceId);
4501
+ if (!head) throw new Error(`Observational memory record not found after initialization: ${lookupKey}`);
4502
+ return head;
4375
4503
  } catch (error) {
4376
4504
  throw storageError$1("INITIALIZE_OBSERVATIONAL_MEMORY", "FAILED", {
4377
4505
  threadId: input.threadId ?? "",
@@ -4393,7 +4521,17 @@ async function insertObservationalMemoryRecord(ctx, record) {
4393
4521
  }
4394
4522
  async function updateActiveObservations(ctx, input) {
4395
4523
  try {
4396
- await ctx.db.tx(async (_client, connection) => {
4524
+ return await ctx.db.tx(async (_client, connection) => {
4525
+ const row = await lockOMRow(ctx, connection, input.id, "UPDATE_ACTIVE_OBSERVATIONS");
4526
+ if (row.supersededBy) return {
4527
+ applied: false,
4528
+ reason: "retired"
4529
+ };
4530
+ if (input.expectedActiveObservations !== void 0 && input.expectedActiveObservations !== stringOrEmpty(row.activeObservations)) return {
4531
+ applied: false,
4532
+ reason: "conflict"
4533
+ };
4534
+ const lastObservedAt = maxObservationCursor(row.lastObservedAt, input.lastObservedAt);
4397
4535
  assertRowsAffected((await connection.execute(`
4398
4536
  UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4399
4537
  SET ${OM_ACTIVE_OBSERVATIONS} = :activeObservations,
@@ -4407,12 +4545,13 @@ async function updateActiveObservations(ctx, input) {
4407
4545
  WHERE id = :id`, {
4408
4546
  id: input.id,
4409
4547
  activeObservations: nullableClobBind(input.observations),
4410
- lastObservedAt: input.lastObservedAt,
4548
+ lastObservedAt: timestampBind(lastObservedAt),
4411
4549
  tokenCount: Math.round(input.tokenCount),
4412
4550
  observedMessageIds: nullableJsonBind(input.observedMessageIds),
4413
4551
  observedTimezone: input.observedTimezone ?? null,
4414
- updatedAt: /* @__PURE__ */ new Date()
4552
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4415
4553
  })).rowsAffected, "UPDATE_ACTIVE_OBSERVATIONS", input.id);
4554
+ return { applied: true };
4416
4555
  });
4417
4556
  } catch (error) {
4418
4557
  if (error instanceof MastraError) throw error;
@@ -4420,39 +4559,73 @@ async function updateActiveObservations(ctx, input) {
4420
4559
  }
4421
4560
  }
4422
4561
  async function createReflectionGeneration(ctx, input) {
4562
+ const { currentRecord } = input;
4563
+ try {
4564
+ return await ctx.db.tx(async (_client, connection) => {
4565
+ const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
4566
+ if (!row) return currentRecord;
4567
+ if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
4568
+ const stored = parseOMRow(row);
4569
+ const plan = planReflectionGenerationText({
4570
+ storedObservations: stored.activeObservations,
4571
+ storedObservationTokenCount: stored.observationTokenCount,
4572
+ snapshotObservations: currentRecord.activeObservations,
4573
+ snapshotObservationTokenCount: currentRecord.observationTokenCount,
4574
+ reflection: input.reflection,
4575
+ tokenCount: input.tokenCount
4576
+ });
4577
+ if (!plan) return stored;
4578
+ return rollOverOMRow(ctx, connection, stored, plan.observations, plan.tokenCount, input.newRecordId);
4579
+ });
4580
+ } catch (error) {
4581
+ if (error instanceof MastraError) throw error;
4582
+ throw storageError$1("CREATE_REFLECTION_GENERATION", "FAILED", { id: currentRecord.id }, error);
4583
+ }
4584
+ }
4585
+ /**
4586
+ * Create the next generation from the stored (live, row-locked) record and retire it in the
4587
+ * same transaction. Buffered chunks move to the new generation; the cursor, buffering markers,
4588
+ * flags, and counters carry over; buffered reflection state does not.
4589
+ */
4590
+ async function rollOverOMRow(ctx, connection, stored, observations, tokenCount, newRecordId) {
4423
4591
  const now = /* @__PURE__ */ new Date();
4424
4592
  const record = {
4425
- id: globalThis.crypto.randomUUID(),
4426
- scope: input.currentRecord.scope,
4427
- threadId: input.currentRecord.threadId,
4428
- resourceId: input.currentRecord.resourceId,
4593
+ id: newRecordId ?? globalThis.crypto.randomUUID(),
4594
+ scope: stored.scope,
4595
+ threadId: stored.threadId,
4596
+ resourceId: stored.resourceId,
4429
4597
  createdAt: now,
4430
4598
  updatedAt: now,
4431
- lastObservedAt: input.currentRecord.lastObservedAt,
4599
+ lastObservedAt: stored.lastObservedAt,
4432
4600
  originType: "reflection",
4433
- generationCount: input.currentRecord.generationCount + 1,
4434
- activeObservations: input.reflection,
4435
- totalTokensObserved: input.currentRecord.totalTokensObserved,
4436
- observationTokenCount: Math.round(input.tokenCount),
4437
- pendingMessageTokens: 0,
4601
+ generationCount: stored.generationCount + 1,
4602
+ activeObservations: observations,
4603
+ totalTokensObserved: stored.totalTokensObserved,
4604
+ observationTokenCount: Math.round(tokenCount),
4605
+ pendingMessageTokens: stored.pendingMessageTokens,
4438
4606
  isReflecting: false,
4439
4607
  isObserving: false,
4440
- isBufferingObservation: false,
4608
+ isBufferingObservation: stored.isBufferingObservation,
4441
4609
  isBufferingReflection: false,
4442
- lastBufferedAtTokens: 0,
4443
- lastBufferedAtTime: null,
4444
- config: input.currentRecord.config,
4445
- metadata: input.currentRecord.metadata,
4446
- observedTimezone: input.currentRecord.observedTimezone
4610
+ lastBufferedAtTokens: stored.lastBufferedAtTokens,
4611
+ lastBufferedAtTime: stored.lastBufferedAtTime ?? null,
4612
+ bufferedObservationChunks: stored.bufferedObservationChunks?.length ? stored.bufferedObservationChunks : void 0,
4613
+ config: stored.config,
4614
+ metadata: stored.metadata,
4615
+ observedTimezone: stored.observedTimezone,
4616
+ supersededBy: null
4447
4617
  };
4448
- try {
4449
- await ctx.db.tx(async (_client, connection) => {
4450
- await insertOMRecord(ctx, connection, record, now);
4451
- });
4452
- return record;
4453
- } catch (error) {
4454
- throw storageError$1("CREATE_REFLECTION_GENERATION", "FAILED", { id: input.currentRecord.id }, error);
4455
- }
4618
+ await insertOMRecord(ctx, connection, record, now);
4619
+ if ((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4620
+ SET "supersededBy" = :newId,
4621
+ "bufferedObservationChunks" = NULL,
4622
+ "updatedAt" = :updatedAt
4623
+ WHERE id = :id AND "supersededBy" IS NULL`, {
4624
+ newId: record.id,
4625
+ updatedAt: timestampBind(now),
4626
+ id: stored.id
4627
+ })).rowsAffected !== 1) throw storageError$1("CREATE_REFLECTION_GENERATION", "RETIRE_FAILED", { id: stored.id }, /* @__PURE__ */ new Error(`Failed to retire observational memory record ${stored.id}`));
4628
+ return record;
4456
4629
  }
4457
4630
  async function setReflectingFlag(ctx, id, isReflecting) {
4458
4631
  await updateOMFlag(ctx, id, OM_IS_REFLECTING, isReflecting, "SET_REFLECTING_FLAG");
@@ -4467,9 +4640,10 @@ async function setBufferingObservationFlag(ctx, id, isBuffering, lastBufferedAtT
4467
4640
  const binds = {
4468
4641
  id,
4469
4642
  isBuffering: boolToNumber(isBuffering),
4470
- updatedAt: /* @__PURE__ */ new Date()
4643
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4471
4644
  };
4472
4645
  if (lastBufferedAtTokens !== void 0) binds.lastBufferedAtTokens = Math.round(lastBufferedAtTokens);
4646
+ binds.id = (await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, id, "SET_BUFFERING_OBSERVATION_FLAG"))).id;
4473
4647
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4474
4648
  SET ${OM_IS_BUFFERING_OBSERVATION} = :isBuffering,
4475
4649
  ${OM_UPDATED_AT} = :updatedAt
@@ -4500,7 +4674,17 @@ async function clearObservationalMemory(ctx, threadId, resourceId) {
4500
4674
  }
4501
4675
  async function setPendingMessageTokens(ctx, id, tokenCount) {
4502
4676
  try {
4503
- await updateOMColumns(ctx, id, "SET_PENDING_MESSAGE_TOKENS", { [OM_PENDING_MESSAGE_TOKENS]: Math.round(tokenCount) });
4677
+ await ctx.db.tx(async (_client, connection) => {
4678
+ const target = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, id, "SET_PENDING_MESSAGE_TOKENS"));
4679
+ assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4680
+ SET ${OM_PENDING_MESSAGE_TOKENS} = :tokenCount,
4681
+ ${OM_UPDATED_AT} = :updatedAt
4682
+ WHERE id = :id`, {
4683
+ tokenCount: Math.round(tokenCount),
4684
+ updatedAt: timestampBind(/* @__PURE__ */ new Date()),
4685
+ id: target.id
4686
+ })).rowsAffected, "SET_PENDING_MESSAGE_TOKENS", id);
4687
+ });
4504
4688
  } catch (error) {
4505
4689
  if (error instanceof MastraError) throw error;
4506
4690
  throw storageError$1("SET_PENDING_MESSAGE_TOKENS", "FAILED", {
@@ -4522,7 +4706,7 @@ async function updateObservationalMemoryConfig(ctx, input) {
4522
4706
  WHERE id = :id`, {
4523
4707
  id: input.id,
4524
4708
  config: jsonBind(merged),
4525
- updatedAt: /* @__PURE__ */ new Date()
4709
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4526
4710
  })).rowsAffected, "UPDATE_OM_CONFIG", input.id);
4527
4711
  });
4528
4712
  } catch (error) {
@@ -4568,6 +4752,7 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4568
4752
  ${OM_LAST_BUFFERED_AT_TOKENS},
4569
4753
  ${OM_LAST_BUFFERED_AT_TIME},
4570
4754
  metadata,
4755
+ ${OM_SUPERSEDED_BY},
4571
4756
  ${OM_CREATED_AT},
4572
4757
  ${OM_UPDATED_AT}
4573
4758
  ) VALUES (
@@ -4603,6 +4788,7 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4603
4788
  :lastBufferedAtTokens,
4604
4789
  :lastBufferedAtTime,
4605
4790
  :metadata,
4791
+ :supersededBy,
4606
4792
  :createdAt,
4607
4793
  :updatedAt
4608
4794
  )`, {
@@ -4616,8 +4802,8 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4616
4802
  originType: record.originType ?? "initial",
4617
4803
  config: jsonBind(record.config ?? {}),
4618
4804
  generationCount: record.generationCount ?? 0,
4619
- lastObservedAt: record.lastObservedAt ?? null,
4620
- lastReflectionAt: record.originType === "reflection" ? timestamp : null,
4805
+ lastObservedAt: timestampBind(record.lastObservedAt),
4806
+ lastReflectionAt: record.originType === "reflection" ? timestampBind(timestamp) : null,
4621
4807
  pendingMessageTokens: Math.round(record.pendingMessageTokens ?? 0),
4622
4808
  totalTokensObserved: Math.round(record.totalTokensObserved ?? 0),
4623
4809
  observationTokenCount: Math.round(record.observationTokenCount ?? 0),
@@ -4636,10 +4822,11 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4636
4822
  isBufferingObservation: boolToNumber(record.isBufferingObservation),
4637
4823
  isBufferingReflection: boolToNumber(record.isBufferingReflection),
4638
4824
  lastBufferedAtTokens: Math.round(record.lastBufferedAtTokens ?? 0),
4639
- lastBufferedAtTime: record.lastBufferedAtTime ?? null,
4825
+ lastBufferedAtTime: timestampBind(record.lastBufferedAtTime),
4640
4826
  metadata: nullableJsonBind(record.metadata),
4641
- createdAt: record.createdAt,
4642
- updatedAt: record.updatedAt
4827
+ supersededBy: record.supersededBy ?? null,
4828
+ createdAt: timestampBind(record.createdAt),
4829
+ updatedAt: timestampBind(record.updatedAt)
4643
4830
  });
4644
4831
  }
4645
4832
  async function updateOMFlag(ctx, id, column, value, operation) {
@@ -4663,7 +4850,7 @@ async function updateOMColumns(ctx, id, operation, columns) {
4663
4850
  WHERE id = :id`, {
4664
4851
  ...binds,
4665
4852
  id,
4666
- updatedAt: /* @__PURE__ */ new Date()
4853
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4667
4854
  })).rowsAffected, operation, id);
4668
4855
  });
4669
4856
  }
@@ -4701,6 +4888,7 @@ function omSelect() {
4701
4888
  ${OM_LAST_BUFFERED_AT_TOKENS} AS "lastBufferedAtTokens",
4702
4889
  ${OM_LAST_BUFFERED_AT_TIME} AS "lastBufferedAtTime",
4703
4890
  metadata AS "metadata",
4891
+ ${OM_SUPERSEDED_BY} AS "supersededBy",
4704
4892
  ${OM_CREATED_AT} AS "createdAt",
4705
4893
  ${OM_UPDATED_AT} AS "updatedAt"`;
4706
4894
  }
@@ -4736,15 +4924,60 @@ function parseOMRow(row) {
4736
4924
  config: parseJson(row.config),
4737
4925
  metadata: parseOptionalJsonObject(row.metadata, { emptyObjectAsUndefined: true }),
4738
4926
  observedMessageIds: parseOptionalStringArray(row.observedMessageIds),
4739
- observedTimezone: row.observedTimezone ? String(row.observedTimezone) : void 0
4927
+ observedTimezone: row.observedTimezone ? String(row.observedTimezone) : void 0,
4928
+ supersededBy: row.supersededBy ? String(row.supersededBy) : null
4740
4929
  };
4741
4930
  }
4931
+ /** The canonical head row of a lookup key (non-locking read). */
4932
+ async function getHeadOMRow(ctx, connection, lookupKey) {
4933
+ return rows(await connection.execute(`${omSelect()} FROM ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4934
+ WHERE ${OM_LOOKUP_KEY} = :lookupKey
4935
+ ORDER BY ${OM_HEAD_ORDER}
4936
+ FETCH FIRST 1 ROWS ONLY`, asBindParameters({ lookupKey }), executeOptions()))[0];
4937
+ }
4938
+ /** Row-locks one record by primary key for the rest of the transaction; `undefined` when missing. */
4939
+ async function findOMRowForUpdate(ctx, connection, id) {
4940
+ return rows(await connection.execute(`${omSelect()} FROM ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)} WHERE id = :id FOR UPDATE`, { id }, executeOptions()))[0];
4941
+ }
4942
+ /**
4943
+ * Row-locks one record by primary key. Observational memory updates are incremental and
4944
+ * order-sensitive, so mutating paths derive their next state from a locked row.
4945
+ * Throws "record not found" when it is missing.
4946
+ */
4947
+ async function lockOMRow(ctx, connection, id, operation) {
4948
+ const row = await findOMRowForUpdate(ctx, connection, id);
4949
+ if (!row) {
4950
+ assertRowsAffected(0, operation, id);
4951
+ throw new Error(`Observational memory record not found: ${id}`);
4952
+ }
4953
+ return row;
4954
+ }
4955
+ /**
4956
+ * The live, row-locked record a lifecycle write aimed at `row` lands on: `row` itself while
4957
+ * live, otherwise the head of its lookup key (locked, then re-checked — a rollover may retire
4958
+ * it between the head read and the lock).
4959
+ */
4960
+ async function resolveLiveOMRow(ctx, connection, row) {
4961
+ let current = row;
4962
+ for (let hop = 0; current.supersededBy; hop++) {
4963
+ const head = hop < OM_MAX_HEAD_HOPS ? await getHeadOMRow(ctx, connection, current.lookupKey) : void 0;
4964
+ const locked = head && head.id !== current.id ? await findOMRowForUpdate(ctx, connection, head.id) : void 0;
4965
+ 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`));
4966
+ current = locked;
4967
+ }
4968
+ return current;
4969
+ }
4742
4970
  //#endregion
4743
4971
  //#region src/storage/domains/memory/observational-buffering.ts
4744
4972
  async function updateBufferedObservations(ctx, input) {
4745
4973
  try {
4746
- await ctx.db.tx(async (_client, connection) => {
4747
- const existingChunks = parseBufferedChunks((await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS")).bufferedObservationChunks);
4974
+ return await ctx.db.tx(async (_client, connection) => {
4975
+ const row = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS"));
4976
+ const existingChunks = parseBufferedChunks(row.bufferedObservationChunks);
4977
+ if (existingChunks.some((existing) => existing.cycleId === input.chunk.cycleId) || isBufferedChunkCoveredByCursor(input.chunk.lastObservedAt, row.lastObservedAt)) return {
4978
+ persisted: false,
4979
+ recordId: row.id
4980
+ };
4748
4981
  const newChunk = {
4749
4982
  id: `ombuf-${globalThis.crypto.randomUUID()}`,
4750
4983
  cycleId: input.chunk.cycleId,
@@ -4760,18 +4993,21 @@ async function updateBufferedObservations(ctx, input) {
4760
4993
  extractedValues: input.chunk.extractedValues,
4761
4994
  extractionFailures: input.chunk.extractionFailures
4762
4995
  };
4763
- const updatedChunks = [...existingChunks, newChunk];
4764
- const lastBufferedAtTimeSql = input.lastBufferedAtTime === void 0 || input.lastBufferedAtTime === null ? "" : `,\n ${OM_LAST_BUFFERED_AT_TIME} = :lastBufferedAtTime`;
4765
- const binds = {
4766
- id: input.id,
4767
- bufferedObservationChunks: nullableJsonBind(updatedChunks),
4768
- updatedAt: /* @__PURE__ */ new Date()
4769
- };
4770
- if (input.lastBufferedAtTime !== void 0 && input.lastBufferedAtTime !== null) binds.lastBufferedAtTime = toDate(input.lastBufferedAtTime);
4996
+ const lastBufferedAtTime = maxObservationCursor(row.lastBufferedAtTime, input.lastBufferedAtTime);
4771
4997
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4772
4998
  SET ${OM_BUFFERED_OBSERVATION_CHUNKS} = :bufferedObservationChunks,
4773
- ${OM_UPDATED_AT} = :updatedAt${lastBufferedAtTimeSql}
4774
- WHERE id = :id`, asBindParameters(binds))).rowsAffected, "UPDATE_BUFFERED_OBSERVATIONS", input.id);
4999
+ ${OM_LAST_BUFFERED_AT_TIME} = :lastBufferedAtTime,
5000
+ ${OM_UPDATED_AT} = :updatedAt
5001
+ WHERE id = :id`, asBindParameters({
5002
+ id: row.id,
5003
+ bufferedObservationChunks: nullableJsonBind([...existingChunks, newChunk]),
5004
+ lastBufferedAtTime: timestampBind(lastBufferedAtTime),
5005
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
5006
+ }))).rowsAffected, "UPDATE_BUFFERED_OBSERVATIONS", row.id);
5007
+ return {
5008
+ persisted: true,
5009
+ recordId: row.id
5010
+ };
4775
5011
  });
4776
5012
  } catch (error) {
4777
5013
  if (error instanceof MastraError) throw error;
@@ -4782,10 +5018,22 @@ async function swapBufferedToActive(ctx, input) {
4782
5018
  try {
4783
5019
  return await ctx.db.tx(async (_client, connection) => {
4784
5020
  const row = await lockOMRow(ctx, connection, input.id, "SWAP_BUFFERED_TO_ACTIVE");
4785
- const chunks = input.bufferedChunks?.length ? input.bufferedChunks : parseBufferedChunks(row.bufferedObservationChunks);
5021
+ if (row.supersededBy) return {
5022
+ ...emptySwapResult(),
5023
+ retired: true
5024
+ };
5025
+ const refreshedWeights = new Map((input.bufferedChunks ?? []).map((chunk) => [chunk.id, chunk.messageTokens]));
5026
+ const chunks = parseBufferedChunks(row.bufferedObservationChunks).map((chunk) => {
5027
+ const weight = refreshedWeights.get(chunk.id);
5028
+ return weight === void 0 ? chunk : {
5029
+ ...chunk,
5030
+ messageTokens: weight
5031
+ };
5032
+ });
4786
5033
  if (chunks.length === 0) return emptySwapResult();
4787
5034
  const activation = calculateBufferedActivation(chunks, input);
4788
5035
  const lastObservedAt = input.lastObservedAt ?? (activation.activatedChunks.at(-1)?.lastObservedAt ? toDate(activation.activatedChunks.at(-1).lastObservedAt) : /* @__PURE__ */ new Date());
5036
+ const storedCursor = maxObservationCursor(row.lastObservedAt, lastObservedAt);
4789
5037
  const boundary = `\n\n--- message boundary (${lastObservedAt.toISOString()}) ---\n\n`;
4790
5038
  const existingActive = stringOrEmpty(row.activeObservations);
4791
5039
  const newActive = existingActive ? `${existingActive}${boundary}${activation.activatedContent}` : activation.activatedContent;
@@ -4803,8 +5051,8 @@ async function swapBufferedToActive(ctx, input) {
4803
5051
  observationTokens: activation.activatedTokens,
4804
5052
  pendingMessageTokens: pendingTokens,
4805
5053
  bufferedObservationChunks: nullableJsonBind(activation.remainingChunks.length > 0 ? activation.remainingChunks : null),
4806
- lastObservedAt,
4807
- updatedAt: /* @__PURE__ */ new Date()
5054
+ lastObservedAt: timestampBind(storedCursor),
5055
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4808
5056
  })).rowsAffected, "SWAP_BUFFERED_TO_ACTIVE", input.id);
4809
5057
  return activation.result;
4810
5058
  });
@@ -4832,7 +5080,7 @@ async function updateBufferedReflection(ctx, input) {
4832
5080
  tokenCount: Math.round(input.tokenCount),
4833
5081
  inputTokenCount: Math.round(input.inputTokenCount),
4834
5082
  reflectedObservationLineCount: Math.round(input.reflectedObservationLineCount),
4835
- updatedAt: /* @__PURE__ */ new Date()
5083
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4836
5084
  })).rowsAffected, "UPDATE_BUFFERED_REFLECTION", input.id);
4837
5085
  });
4838
5086
  } catch (error) {
@@ -4841,66 +5089,32 @@ async function updateBufferedReflection(ctx, input) {
4841
5089
  }
4842
5090
  }
4843
5091
  async function swapBufferedReflectionToActive(ctx, input) {
5092
+ const { currentRecord } = input;
4844
5093
  try {
4845
5094
  return await ctx.db.tx(async (_client, connection) => {
4846
- const row = await lockOMRow(ctx, connection, input.currentRecord.id, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE");
4847
- const bufferedReflection = stringOrEmpty(row.bufferedReflection);
4848
- if (!bufferedReflection) throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NO_CONTENT", { id: input.currentRecord.id }, /* @__PURE__ */ new Error("No buffered reflection to swap"), ErrorCategory.USER);
4849
- const reflectedLineCount = numberOrZero(row.reflectedObservationLineCount);
4850
- const unreflectedContent = stringOrEmpty(row.activeObservations).split("\n").slice(reflectedLineCount).join("\n").trim();
4851
- const newObservations = unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
4852
- const now = /* @__PURE__ */ new Date();
4853
- const lockedRecord = parseOMRow(row);
4854
- const newRecord = {
4855
- id: globalThis.crypto.randomUUID(),
4856
- scope: lockedRecord.scope,
4857
- threadId: lockedRecord.threadId,
4858
- resourceId: lockedRecord.resourceId,
4859
- createdAt: now,
4860
- updatedAt: now,
4861
- lastObservedAt: lockedRecord.lastObservedAt,
4862
- originType: "reflection",
4863
- generationCount: lockedRecord.generationCount + 1,
4864
- activeObservations: newObservations,
4865
- totalTokensObserved: lockedRecord.totalTokensObserved,
4866
- observationTokenCount: Math.round(input.tokenCount),
4867
- pendingMessageTokens: 0,
4868
- isReflecting: false,
4869
- isObserving: false,
4870
- isBufferingObservation: false,
4871
- isBufferingReflection: false,
4872
- lastBufferedAtTokens: 0,
4873
- lastBufferedAtTime: null,
4874
- config: lockedRecord.config,
4875
- metadata: lockedRecord.metadata,
4876
- observedTimezone: lockedRecord.observedTimezone
4877
- };
4878
- await insertOMRecord(ctx, connection, newRecord, now);
5095
+ const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
5096
+ if (!row) return currentRecord;
5097
+ if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
5098
+ const stored = parseOMRow(row);
5099
+ const bufferedReflection = stored.bufferedReflection ?? "";
5100
+ if (!bufferedReflection) throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "NO_CONTENT", { id: currentRecord.id }, /* @__PURE__ */ new Error("No buffered reflection to swap"), ErrorCategory.USER);
5101
+ if (!isAppendOnlySince(stored.activeObservations, currentRecord.activeObservations)) return stored;
5102
+ const reflectedLineCount = stored.reflectedObservationLineCount ?? 0;
5103
+ const unreflectedContent = stored.activeObservations.split("\n").slice(reflectedLineCount).join("\n").trim();
5104
+ 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);
4879
5105
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4880
5106
  SET ${OM_BUFFERED_REFLECTION} = NULL,
4881
5107
  ${OM_BUFFERED_REFLECTION_TOKENS} = NULL,
4882
5108
  ${OM_BUFFERED_REFLECTION_INPUT_TOKENS} = NULL,
4883
- ${OM_REFLECTED_OBSERVATION_LINE_COUNT} = NULL,
4884
- ${OM_UPDATED_AT} = :updatedAt
4885
- WHERE id = :id`, {
4886
- id: input.currentRecord.id,
4887
- updatedAt: now
4888
- })).rowsAffected, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", input.currentRecord.id);
5109
+ ${OM_REFLECTED_OBSERVATION_LINE_COUNT} = NULL
5110
+ WHERE id = :id`, { id: stored.id })).rowsAffected, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", stored.id);
4889
5111
  return newRecord;
4890
5112
  });
4891
5113
  } catch (error) {
4892
5114
  if (error instanceof MastraError) throw error;
4893
- throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "FAILED", { id: input.currentRecord.id }, error);
5115
+ throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "FAILED", { id: currentRecord.id }, error);
4894
5116
  }
4895
5117
  }
4896
- async function lockOMRow(ctx, connection, id, operation) {
4897
- const row = rows(await connection.execute(`${omSelect()} FROM ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)} WHERE id = :id FOR UPDATE`, { id }, executeOptions()))[0];
4898
- if (!row) {
4899
- assertRowsAffected(0, operation, id);
4900
- throw new Error(`Observational memory record not found: ${id}`);
4901
- }
4902
- return row;
4903
- }
4904
5118
  function emptySwapResult() {
4905
5119
  return {
4906
5120
  chunksActivated: 0,
@@ -5435,6 +5649,15 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5435
5649
  async init() {
5436
5650
  await initMemorySchema(this.ctx);
5437
5651
  }
5652
+ /**
5653
+ * Mark observational memory rows that are not their key's canonical head as superseded.
5654
+ * Runs as part of `init()`; `OracleStore` also runs it on every start when the memory schema
5655
+ * migration is unchanged and skipped.
5656
+ * @internal
5657
+ */
5658
+ async backfillObservationalMemorySupersededBy() {
5659
+ await this.ctx.db.withConnection((connection) => backfillObservationalMemorySupersededBy(this.ctx, connection));
5660
+ }
5438
5661
  async dangerouslyClearAll() {
5439
5662
  await clearAllMemoryTables(this.ctx);
5440
5663
  }
@@ -5571,6 +5794,9 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5571
5794
  return insertObservationalMemoryRecord(this.ctx, record);
5572
5795
  }
5573
5796
  async updateActiveObservations(input) {
5797
+ assertActiveObservationsApplied(await this.commitActiveObservations(input), input.id);
5798
+ }
5799
+ async commitActiveObservations(input) {
5574
5800
  return updateActiveObservations(this.ctx, input);
5575
5801
  }
5576
5802
  async createReflectionGeneration(input) {
@@ -5598,6 +5824,9 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5598
5824
  return updateObservationalMemoryConfig(this.ctx, input);
5599
5825
  }
5600
5826
  async updateBufferedObservations(input) {
5827
+ await this.appendBufferedObservations(input);
5828
+ }
5829
+ async appendBufferedObservations(input) {
5601
5830
  return updateBufferedObservations(this.ctx, input);
5602
5831
  }
5603
5832
  async swapBufferedToActive(input) {
@@ -8477,7 +8706,7 @@ function checksumMigration(input) {
8477
8706
  //#region src/storage/index.ts
8478
8707
  const STORE_NAME = "ORACLEDB";
8479
8708
  const DOMAIN_SCHEMA_VERSIONS = {
8480
- R001_MEMORY_SCHEMA: 1,
8709
+ R001_MEMORY_SCHEMA: 2,
8481
8710
  R002_WORKFLOWS_SCHEMA: 1,
8482
8711
  R003_OBSERVABILITY_SCHEMA: 1,
8483
8712
  R004_SCORES_SCHEMA: 1,
@@ -8565,6 +8794,10 @@ var OracleStore = class extends MastraCompositeStore {
8565
8794
  async runMigrations(forceRepeatable) {
8566
8795
  try {
8567
8796
  const results = await this.migrationRegistry.run(this.storageMigrations(), { forceRepeatable });
8797
+ if (results.find((result) => result.id === "R001_MEMORY_SCHEMA")?.status === "skipped") {
8798
+ const memory = this.stores.memory;
8799
+ if (memory instanceof MemoryOracle) await memory.backfillObservationalMemorySupersededBy();
8800
+ }
8568
8801
  this.isInitialized = true;
8569
8802
  return results;
8570
8803
  } catch (error) {