@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.js CHANGED
@@ -2,7 +2,7 @@ import { MastraVector, validateTopK, validateUpsertInput } from "@mastra/core/ve
2
2
  import oracledb from "oracledb";
3
3
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
4
4
  import { createHash } from "crypto";
5
- import { AgentsStorage, MCPClientsStorage, MastraCompositeStore, MemoryStorage, ObservabilityStorage, ScorerDefinitionsStorage, ScoresStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MESSAGES, TABLE_OBSERVATIONAL_MEMORY, TABLE_RESOURCES, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_SNAPSHOT, TraceStatus, WorkflowsStorage, calculatePagination, createStorageErrorId, createVectorErrorId, executeRetentionPrune, listLogsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, normalizePerPage, resolveRetentionTargets, retentionCutoffMs, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
5
+ import { AgentsStorage, MCPClientsStorage, MastraCompositeStore, MemoryStorage, ObservabilityStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, ScorerDefinitionsStorage, ScoresStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_OBSERVATIONAL_MEMORY, TABLE_RESOURCES, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TraceStatus, WorkflowsStorage, calculatePagination, createStorageErrorId, createVectorErrorId, executeRetentionPrune, isRunFenceConflictError, listLogsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, normalizePerPage, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
6
6
  import { MessageList } from "@mastra/core/agent";
7
7
  import { saveScorePayloadSchema } from "@mastra/core/evals";
8
8
  //#region src/shared/connection.ts
@@ -3345,6 +3345,124 @@ 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
3413
+ //#region src/storage/db/run-fencing.ts
3414
+ const SINCE_EPOCH = `(SYS_EXTRACT_UTC(SYSTIMESTAMP) - TIMESTAMP '1970-01-01 00:00:00')`;
3415
+ /**
3416
+ * Epoch milliseconds on the database clock. Oracle evaluates `SYSTIMESTAMP`
3417
+ * once per statement, so every reference here reads the same instant, and a
3418
+ * statement that waits on a lock compares against the time it started. That
3419
+ * error only makes an existing lease look longer and a new lease shorter.
3420
+ */
3421
+ const DB_NOW_MS = `(EXTRACT(DAY FROM ${SINCE_EPOCH}) * 86400000 + EXTRACT(HOUR FROM ${SINCE_EPOCH}) * 3600000 + EXTRACT(MINUTE FROM ${SINCE_EPOCH}) * 60000 + FLOOR(EXTRACT(SECOND FROM ${SINCE_EPOCH}) * 1000))`;
3422
+ const ORA_UNIQUE_CONSTRAINT = -1;
3423
+ const ORA_DEADLOCK = -60;
3424
+ const MAX_CLAIM_ATTEMPTS = 5;
3425
+ /**
3426
+ * The check half of a fenced write; call it first inside the write's
3427
+ * transaction. Oracle has no shared row lock, so this takes the claim row
3428
+ * `FOR UPDATE`: a claim that updates the row waits for the write to commit,
3429
+ * and a write queued behind a claim re-reads the row once the claim commits
3430
+ * and sees the new generation. Writes from one run serialize on its row,
3431
+ * which they mostly do anyway.
3432
+ */
3433
+ async function assertRunFence(client, claimsTable, fence, operation) {
3434
+ const row = await client.oneOrNone(`SELECT generation AS "generation", "ownerId" FROM ${claimsTable} WHERE "runId" = :runId FOR UPDATE`, { runId: fence.runId });
3435
+ if (!matchesRunFence(row ? {
3436
+ generation: Number(row.generation),
3437
+ ownerId: String(row.ownerId)
3438
+ } : null, fence)) throw new RunFenceConflictError(fence, operation);
3439
+ }
3440
+ /**
3441
+ * Runs `write` in a transaction, behind the fence check when it carries a
3442
+ * fence.
3443
+ */
3444
+ function withRunFence(db, claimsTable, fence, operation, write) {
3445
+ return db.tx(async (client) => {
3446
+ if (fence) await assertRunFence(client, claimsTable, fence, operation);
3447
+ return write(client);
3448
+ });
3449
+ }
3450
+ /**
3451
+ * Runs a transaction that creates or updates a run's claim row, retrying when
3452
+ * two first claims race to insert it. `FOR UPDATE` locks nothing while the row
3453
+ * is missing, so racing first claims both insert; the second waits on the
3454
+ * primary key and fails once the first commits. On retry the row exists and
3455
+ * the claim serializes behind its row lock. A deadlock is retried too.
3456
+ */
3457
+ async function claimTransaction(db, fn) {
3458
+ for (let attempt = 1;; attempt++) try {
3459
+ return await db.tx((client) => fn(client));
3460
+ } catch (error) {
3461
+ const retryable = isOracleErrorCode(error, [ORA_UNIQUE_CONSTRAINT, ORA_DEADLOCK]);
3462
+ if (attempt >= MAX_CLAIM_ATTEMPTS || !retryable) throw error;
3463
+ }
3464
+ }
3465
+ //#endregion
3348
3466
  //#region src/storage/domains/memory/utils.ts
3349
3467
  const STORE_NAME$5 = "ORACLEDB";
3350
3468
  const EMPTY_STRING_SENTINEL = "__MASTRA_ORACLE_EMPTY_STRING__";
@@ -3352,6 +3470,14 @@ const STRING_SENTINEL_ESCAPE_PREFIX = "__MASTRA_ORACLE_ESCAPED__";
3352
3470
  function table(ctx, tableName) {
3353
3471
  return qualifyName(tableName, ctx.schemaName);
3354
3472
  }
3473
+ /**
3474
+ * The fence check for a memory write; call it first inside the write's
3475
+ * transaction. `fence` is the one `MemoryOracle` already resolved against the
3476
+ * run fence scope, so an unfenced write skips the check.
3477
+ */
3478
+ async function assertMemoryFence(ctx, client, fence, operation) {
3479
+ if (fence) await assertRunFence(client, table(ctx, TABLE_MEMORY_RUN_FENCES), fence, operation);
3480
+ }
3355
3481
  function storageError$1(operation, reason, details, cause, category = ErrorCategory.THIRD_PARTY) {
3356
3482
  return createOracleStorageError({
3357
3483
  storeName: STORE_NAME$5,
@@ -3382,6 +3508,17 @@ function chunkValues(values, size = 900) {
3382
3508
  for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
3383
3509
  return chunks;
3384
3510
  }
3511
+ /**
3512
+ * Bind an instant as TIMESTAMP WITH TIME ZONE. A bare `Date` binds as TIMESTAMP WITH LOCAL
3513
+ * TIME ZONE, which the server converts with the session's fixed offset (captured at connect
3514
+ * time); an instant on the other side of a DST change is then stored shifted by an hour.
3515
+ */
3516
+ function timestampBind(value) {
3517
+ return value === void 0 || value === null ? null : {
3518
+ type: oracledb.DB_TYPE_TIMESTAMP_TZ,
3519
+ val: toDate(value)
3520
+ };
3521
+ }
3385
3522
  function boolToNumber(value) {
3386
3523
  return value ? 1 : 0;
3387
3524
  }
@@ -3530,12 +3667,48 @@ const OM_LAST_BUFFERED_AT_TOKENS = "\"lastBufferedAtTokens\"";
3530
3667
  const OM_LAST_BUFFERED_AT_TIME = "\"lastBufferedAtTime\"";
3531
3668
  const OM_CREATED_AT = "\"createdAt\"";
3532
3669
  const OM_UPDATED_AT = "\"updatedAt\"";
3670
+ const OM_SUPERSEDED_BY = "\"supersededBy\"";
3533
3671
  async function initMemorySchema(ctx) {
3534
3672
  await ctx.db.withConnection(async (connection) => {
3535
3673
  await createTables(ctx, connection);
3536
3674
  await createIndexes(ctx, connection);
3675
+ await backfillObservationalMemorySupersededBy(ctx, connection);
3537
3676
  });
3538
3677
  }
3678
+ /**
3679
+ * Retire every live observational memory row that sorts after its key's canonical head
3680
+ * (`generationCount DESC, createdAt ASC, id ASC`), marking it superseded by the head. Rows
3681
+ * retired by older adapter versions (which never set `supersededBy`) become frozen. Idempotent.
3682
+ * One statement: the head's ordering key is part of the condition, so a row created by a
3683
+ * concurrent rollover (higher generation) never matches, and a row locked by one is
3684
+ * re-evaluated after it commits.
3685
+ */
3686
+ async function backfillObservationalMemorySupersededBy(ctx, connection) {
3687
+ const om = table(ctx, TABLE_OBSERVATIONAL_MEMORY);
3688
+ const heads = `
3689
+ SELECT ${OM_LOOKUP_KEY} AS head_key, id AS head_id, ${OM_GENERATION_COUNT} AS head_gen, ${OM_CREATED_AT} AS head_created
3690
+ FROM (
3691
+ SELECT ${OM_LOOKUP_KEY}, id, ${OM_GENERATION_COUNT}, ${OM_CREATED_AT},
3692
+ ROW_NUMBER() OVER (PARTITION BY ${OM_LOOKUP_KEY} ORDER BY ${OM_GENERATION_COUNT} DESC, ${OM_CREATED_AT} ASC, id ASC) AS rn
3693
+ FROM ${om}
3694
+ WHERE ${OM_LOOKUP_KEY} IN (
3695
+ SELECT ${OM_LOOKUP_KEY} FROM ${om} WHERE ${OM_SUPERSEDED_BY} IS NULL GROUP BY ${OM_LOOKUP_KEY} HAVING COUNT(*) > 1
3696
+ )
3697
+ )
3698
+ WHERE rn = 1`;
3699
+ await connection.execute(`UPDATE ${om} r
3700
+ SET r.${OM_SUPERSEDED_BY} = (SELECT h.head_id FROM (${heads}) h WHERE h.head_key = r.${OM_LOOKUP_KEY})
3701
+ WHERE r.${OM_SUPERSEDED_BY} IS NULL
3702
+ AND EXISTS (
3703
+ SELECT 1 FROM (${heads}) h
3704
+ WHERE h.head_key = r.${OM_LOOKUP_KEY}
3705
+ AND (
3706
+ r.${OM_GENERATION_COUNT} < h.head_gen
3707
+ OR (r.${OM_GENERATION_COUNT} = h.head_gen AND r.${OM_CREATED_AT} > h.head_created)
3708
+ OR (r.${OM_GENERATION_COUNT} = h.head_gen AND r.${OM_CREATED_AT} = h.head_created AND r.id > h.head_id)
3709
+ )
3710
+ )`, {}, { autoCommit: true });
3711
+ }
3539
3712
  async function clearAllMemoryTables(ctx) {
3540
3713
  await ctx.db.tx(async (client) => {
3541
3714
  await client.none(`DELETE FROM ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}`);
@@ -3609,7 +3782,8 @@ async function createTables(ctx, connection) {
3609
3782
  ${OM_LAST_BUFFERED_AT_TIME} TIMESTAMP WITH TIME ZONE,
3610
3783
  metadata JSON,
3611
3784
  ${OM_CREATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL,
3612
- ${OM_UPDATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL
3785
+ ${OM_UPDATED_AT} TIMESTAMP WITH TIME ZONE NOT NULL,
3786
+ ${OM_SUPERSEDED_BY} VARCHAR2(512)
3613
3787
  )`, [-955]);
3614
3788
  await ensureObservationalMemoryColumns(ctx, connection);
3615
3789
  await relaxObservationalMemoryNullability(ctx, connection);
@@ -3747,6 +3921,10 @@ async function ensureObservationalMemoryColumns(ctx, connection) {
3747
3921
  {
3748
3922
  name: OM_UPDATED_AT,
3749
3923
  type: "TIMESTAMP WITH TIME ZONE"
3924
+ },
3925
+ {
3926
+ name: OM_SUPERSEDED_BY,
3927
+ type: "VARCHAR2(512)"
3750
3928
  }
3751
3929
  ];
3752
3930
  for (const column of columns) await executeDdl(connection, `ALTER TABLE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)} ADD (${column.name} ${column.type})`, [-1430]);
@@ -3857,7 +4035,7 @@ async function listMessagesByResourceId(ctx, args) {
3857
4035
  orderBy
3858
4036
  });
3859
4037
  }
3860
- async function saveMessages(ctx, { messages }) {
4038
+ async function saveMessages(ctx, { messages, fence }) {
3861
4039
  if (messages.length === 0) return { messages: [] };
3862
4040
  const threadIds = /* @__PURE__ */ new Set();
3863
4041
  try {
@@ -3867,6 +4045,7 @@ async function saveMessages(ctx, { messages }) {
3867
4045
  }
3868
4046
  for (const threadId of threadIds) if (!await ctx.getThreadById({ threadId })) throw storageError$1("SAVE_MESSAGES", "FAILED", { threadId }, /* @__PURE__ */ new Error(`Thread ${threadId} not found`), ErrorCategory.USER);
3869
4047
  await ctx.db.tx(async (client) => {
4048
+ await assertMemoryFence(ctx, client, fence, "saveMessages");
3870
4049
  await insertMessageBatch(ctx, client, messages);
3871
4050
  });
3872
4051
  return { messages: new MessageList().add(messages, "memory").get.all.db() };
@@ -3944,7 +4123,7 @@ async function insertMessageBatch(ctx, client, messages) {
3944
4123
  threadId: messageThreadId
3945
4124
  })));
3946
4125
  }
3947
- async function updateMessages(ctx, { messages }) {
4126
+ async function updateMessages(ctx, { messages, fence }) {
3948
4127
  if (messages.length === 0) return [];
3949
4128
  const messageIds = messages.map((message) => message.id);
3950
4129
  const existingMessages = (await ctx.listMessagesById({ messageIds })).messages;
@@ -3953,6 +4132,7 @@ async function updateMessages(ctx, { messages }) {
3953
4132
  const threadIdsToUpdate = /* @__PURE__ */ new Set();
3954
4133
  try {
3955
4134
  await ctx.db.tx(async (client) => {
4135
+ await assertMemoryFence(ctx, client, fence, "updateMessages");
3956
4136
  const semanticRecallMessageIds = [];
3957
4137
  for (const updatePayload of messages) {
3958
4138
  const existing = existingById.get(updatePayload.id);
@@ -3999,13 +4179,15 @@ async function updateMessages(ctx, { messages }) {
3999
4179
  });
4000
4180
  return (await ctx.listMessagesById({ messageIds })).messages;
4001
4181
  } catch (error) {
4182
+ if (error instanceof MastraError) throw error;
4002
4183
  throw storageError$1("UPDATE_MESSAGES", "FAILED", { messageIds: messageIds.join(",") }, error);
4003
4184
  }
4004
4185
  }
4005
- async function deleteMessages(ctx, messageIds) {
4186
+ async function deleteMessages(ctx, messageIds, fence) {
4006
4187
  if (!messageIds.length) return;
4007
4188
  try {
4008
4189
  await ctx.db.tx(async (client) => {
4190
+ await assertMemoryFence(ctx, client, fence, "deleteMessages");
4009
4191
  const threadIds = /* @__PURE__ */ new Set();
4010
4192
  for (const [chunkIndex, chunk] of chunkValues(messageIds).entries()) {
4011
4193
  const { sql, binds } = inClause(`messageId${chunkIndex}`, chunk);
@@ -4021,6 +4203,7 @@ async function deleteMessages(ctx, messageIds) {
4021
4203
  })));
4022
4204
  });
4023
4205
  } catch (error) {
4206
+ if (error instanceof MastraError) throw error;
4024
4207
  throw storageError$1("DELETE_MESSAGES", "FAILED", { messageIds: messageIds.join(",") }, error);
4025
4208
  }
4026
4209
  }
@@ -4262,14 +4445,15 @@ function mergeMessageContent(existingContent, updateContent) {
4262
4445
  }
4263
4446
  //#endregion
4264
4447
  //#region src/storage/domains/memory/observational.ts
4448
+ /** Hops a write aimed at a retired record may follow to reach the head before giving up. */
4449
+ const OM_MAX_HEAD_HOPS = 3;
4450
+ /** Canonical head order for a lookup key. */
4451
+ const OM_HEAD_ORDER = `${OM_GENERATION_COUNT} DESC, ${OM_CREATED_AT} ASC, id ASC`;
4265
4452
  async function getObservationalMemory(ctx, threadId, resourceId) {
4266
4453
  try {
4267
4454
  const lookupKey = getOMKey(threadId, resourceId);
4268
4455
  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];
4456
+ const row = await getHeadOMRow(ctx, connection, lookupKey);
4273
4457
  return row ? parseOMRow(row) : null;
4274
4458
  });
4275
4459
  } catch (error) {
@@ -4301,11 +4485,11 @@ async function getObservationalMemoryHistory(ctx, threadId, resourceId, limit =
4301
4485
  }
4302
4486
  if (options?.from) {
4303
4487
  conditions.push(`${OM_CREATED_AT} >= :fromDate`);
4304
- binds.fromDate = options.from;
4488
+ binds.fromDate = timestampBind(options.from);
4305
4489
  }
4306
4490
  if (options?.to) {
4307
4491
  conditions.push(`${OM_CREATED_AT} <= :toDate`);
4308
- binds.toDate = options.to;
4492
+ binds.toDate = timestampBind(options.to);
4309
4493
  }
4310
4494
  if (options?.groupId !== void 0) {
4311
4495
  conditions.push(`(DBMS_LOB.INSTR(${OM_ACTIVE_OBSERVATIONS}, :groupPrefix) > 0 OR EXISTS (
@@ -4344,6 +4528,7 @@ async function getObservationalMemoryHistory(ctx, threadId, resourceId, limit =
4344
4528
  }
4345
4529
  async function initializeObservationalMemory(ctx, input) {
4346
4530
  const now = /* @__PURE__ */ new Date();
4531
+ const lookupKey = getOMKey(input.threadId, input.resourceId);
4347
4532
  const record = {
4348
4533
  id: globalThis.crypto.randomUUID(),
4349
4534
  scope: input.scope,
@@ -4365,13 +4550,22 @@ async function initializeObservationalMemory(ctx, input) {
4365
4550
  lastBufferedAtTokens: 0,
4366
4551
  lastBufferedAtTime: null,
4367
4552
  config: input.config,
4368
- observedTimezone: input.observedTimezone
4553
+ observedTimezone: input.observedTimezone,
4554
+ supersededBy: null
4369
4555
  };
4370
4556
  try {
4371
- await ctx.db.tx(async (_client, connection) => {
4557
+ const existing = await getObservationalMemory(ctx, input.threadId, input.resourceId);
4558
+ if (existing) return existing;
4559
+ if (await ctx.db.tx(async (_client, connection) => {
4560
+ const omTable = table(ctx, TABLE_OBSERVATIONAL_MEMORY);
4561
+ await connection.execute(`LOCK TABLE ${omTable} IN SHARE ROW EXCLUSIVE MODE`);
4562
+ 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
4563
  await insertOMRecord(ctx, connection, record);
4373
- });
4374
- return record;
4564
+ return true;
4565
+ })) return record;
4566
+ const head = await getObservationalMemory(ctx, input.threadId, input.resourceId);
4567
+ if (!head) throw new Error(`Observational memory record not found after initialization: ${lookupKey}`);
4568
+ return head;
4375
4569
  } catch (error) {
4376
4570
  throw storageError$1("INITIALIZE_OBSERVATIONAL_MEMORY", "FAILED", {
4377
4571
  threadId: input.threadId ?? "",
@@ -4391,9 +4585,20 @@ async function insertObservationalMemoryRecord(ctx, record) {
4391
4585
  }, error);
4392
4586
  }
4393
4587
  }
4394
- async function updateActiveObservations(ctx, input) {
4588
+ async function updateActiveObservations(ctx, input, fence) {
4395
4589
  try {
4396
- await ctx.db.tx(async (_client, connection) => {
4590
+ return await ctx.db.tx(async (client, connection) => {
4591
+ await assertMemoryFence(ctx, client, fence, "commitActiveObservations");
4592
+ const row = await lockOMRow(ctx, connection, input.id, "UPDATE_ACTIVE_OBSERVATIONS");
4593
+ if (row.supersededBy) return {
4594
+ applied: false,
4595
+ reason: "retired"
4596
+ };
4597
+ if (input.expectedActiveObservations !== void 0 && input.expectedActiveObservations !== stringOrEmpty(row.activeObservations)) return {
4598
+ applied: false,
4599
+ reason: "conflict"
4600
+ };
4601
+ const lastObservedAt = maxObservationCursor(row.lastObservedAt, input.lastObservedAt);
4397
4602
  assertRowsAffected((await connection.execute(`
4398
4603
  UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4399
4604
  SET ${OM_ACTIVE_OBSERVATIONS} = :activeObservations,
@@ -4407,52 +4612,88 @@ async function updateActiveObservations(ctx, input) {
4407
4612
  WHERE id = :id`, {
4408
4613
  id: input.id,
4409
4614
  activeObservations: nullableClobBind(input.observations),
4410
- lastObservedAt: input.lastObservedAt,
4615
+ lastObservedAt: timestampBind(lastObservedAt),
4411
4616
  tokenCount: Math.round(input.tokenCount),
4412
4617
  observedMessageIds: nullableJsonBind(input.observedMessageIds),
4413
4618
  observedTimezone: input.observedTimezone ?? null,
4414
- updatedAt: /* @__PURE__ */ new Date()
4619
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4415
4620
  })).rowsAffected, "UPDATE_ACTIVE_OBSERVATIONS", input.id);
4621
+ return { applied: true };
4416
4622
  });
4417
4623
  } catch (error) {
4418
4624
  if (error instanceof MastraError) throw error;
4419
4625
  throw storageError$1("UPDATE_ACTIVE_OBSERVATIONS", "FAILED", { id: input.id }, error);
4420
4626
  }
4421
4627
  }
4422
- async function createReflectionGeneration(ctx, input) {
4628
+ async function createReflectionGeneration(ctx, input, fence) {
4629
+ const { currentRecord } = input;
4630
+ try {
4631
+ return await ctx.db.tx(async (client, connection) => {
4632
+ await assertMemoryFence(ctx, client, fence, "createReflectionGeneration");
4633
+ const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
4634
+ if (!row) return currentRecord;
4635
+ if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
4636
+ const stored = parseOMRow(row);
4637
+ const plan = planReflectionGenerationText({
4638
+ storedObservations: stored.activeObservations,
4639
+ storedObservationTokenCount: stored.observationTokenCount,
4640
+ snapshotObservations: currentRecord.activeObservations,
4641
+ snapshotObservationTokenCount: currentRecord.observationTokenCount,
4642
+ reflection: input.reflection,
4643
+ tokenCount: input.tokenCount
4644
+ });
4645
+ if (!plan) return stored;
4646
+ return rollOverOMRow(ctx, connection, stored, plan.observations, plan.tokenCount, input.newRecordId);
4647
+ });
4648
+ } catch (error) {
4649
+ if (error instanceof MastraError) throw error;
4650
+ throw storageError$1("CREATE_REFLECTION_GENERATION", "FAILED", { id: currentRecord.id }, error);
4651
+ }
4652
+ }
4653
+ /**
4654
+ * Create the next generation from the stored (live, row-locked) record and retire it in the
4655
+ * same transaction. Buffered chunks move to the new generation; the cursor, buffering markers,
4656
+ * flags, and counters carry over; buffered reflection state does not.
4657
+ */
4658
+ async function rollOverOMRow(ctx, connection, stored, observations, tokenCount, newRecordId) {
4423
4659
  const now = /* @__PURE__ */ new Date();
4424
4660
  const record = {
4425
- id: globalThis.crypto.randomUUID(),
4426
- scope: input.currentRecord.scope,
4427
- threadId: input.currentRecord.threadId,
4428
- resourceId: input.currentRecord.resourceId,
4661
+ id: newRecordId ?? globalThis.crypto.randomUUID(),
4662
+ scope: stored.scope,
4663
+ threadId: stored.threadId,
4664
+ resourceId: stored.resourceId,
4429
4665
  createdAt: now,
4430
4666
  updatedAt: now,
4431
- lastObservedAt: input.currentRecord.lastObservedAt,
4667
+ lastObservedAt: stored.lastObservedAt,
4432
4668
  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,
4669
+ generationCount: stored.generationCount + 1,
4670
+ activeObservations: observations,
4671
+ totalTokensObserved: stored.totalTokensObserved,
4672
+ observationTokenCount: Math.round(tokenCount),
4673
+ pendingMessageTokens: stored.pendingMessageTokens,
4438
4674
  isReflecting: false,
4439
4675
  isObserving: false,
4440
- isBufferingObservation: false,
4676
+ isBufferingObservation: stored.isBufferingObservation,
4441
4677
  isBufferingReflection: false,
4442
- lastBufferedAtTokens: 0,
4443
- lastBufferedAtTime: null,
4444
- config: input.currentRecord.config,
4445
- metadata: input.currentRecord.metadata,
4446
- observedTimezone: input.currentRecord.observedTimezone
4678
+ lastBufferedAtTokens: stored.lastBufferedAtTokens,
4679
+ lastBufferedAtTime: stored.lastBufferedAtTime ?? null,
4680
+ bufferedObservationChunks: stored.bufferedObservationChunks?.length ? stored.bufferedObservationChunks : void 0,
4681
+ config: stored.config,
4682
+ metadata: stored.metadata,
4683
+ observedTimezone: stored.observedTimezone,
4684
+ supersededBy: null
4447
4685
  };
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
- }
4686
+ await insertOMRecord(ctx, connection, record, now);
4687
+ if ((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4688
+ SET "supersededBy" = :newId,
4689
+ "bufferedObservationChunks" = NULL,
4690
+ "updatedAt" = :updatedAt
4691
+ WHERE id = :id AND "supersededBy" IS NULL`, {
4692
+ newId: record.id,
4693
+ updatedAt: timestampBind(now),
4694
+ id: stored.id
4695
+ })).rowsAffected !== 1) throw storageError$1("CREATE_REFLECTION_GENERATION", "RETIRE_FAILED", { id: stored.id }, /* @__PURE__ */ new Error(`Failed to retire observational memory record ${stored.id}`));
4696
+ return record;
4456
4697
  }
4457
4698
  async function setReflectingFlag(ctx, id, isReflecting) {
4458
4699
  await updateOMFlag(ctx, id, OM_IS_REFLECTING, isReflecting, "SET_REFLECTING_FLAG");
@@ -4467,9 +4708,10 @@ async function setBufferingObservationFlag(ctx, id, isBuffering, lastBufferedAtT
4467
4708
  const binds = {
4468
4709
  id,
4469
4710
  isBuffering: boolToNumber(isBuffering),
4470
- updatedAt: /* @__PURE__ */ new Date()
4711
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4471
4712
  };
4472
4713
  if (lastBufferedAtTokens !== void 0) binds.lastBufferedAtTokens = Math.round(lastBufferedAtTokens);
4714
+ binds.id = (await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, id, "SET_BUFFERING_OBSERVATION_FLAG"))).id;
4473
4715
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4474
4716
  SET ${OM_IS_BUFFERING_OBSERVATION} = :isBuffering,
4475
4717
  ${OM_UPDATED_AT} = :updatedAt
@@ -4500,7 +4742,17 @@ async function clearObservationalMemory(ctx, threadId, resourceId) {
4500
4742
  }
4501
4743
  async function setPendingMessageTokens(ctx, id, tokenCount) {
4502
4744
  try {
4503
- await updateOMColumns(ctx, id, "SET_PENDING_MESSAGE_TOKENS", { [OM_PENDING_MESSAGE_TOKENS]: Math.round(tokenCount) });
4745
+ await ctx.db.tx(async (_client, connection) => {
4746
+ const target = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, id, "SET_PENDING_MESSAGE_TOKENS"));
4747
+ assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4748
+ SET ${OM_PENDING_MESSAGE_TOKENS} = :tokenCount,
4749
+ ${OM_UPDATED_AT} = :updatedAt
4750
+ WHERE id = :id`, {
4751
+ tokenCount: Math.round(tokenCount),
4752
+ updatedAt: timestampBind(/* @__PURE__ */ new Date()),
4753
+ id: target.id
4754
+ })).rowsAffected, "SET_PENDING_MESSAGE_TOKENS", id);
4755
+ });
4504
4756
  } catch (error) {
4505
4757
  if (error instanceof MastraError) throw error;
4506
4758
  throw storageError$1("SET_PENDING_MESSAGE_TOKENS", "FAILED", {
@@ -4522,7 +4774,7 @@ async function updateObservationalMemoryConfig(ctx, input) {
4522
4774
  WHERE id = :id`, {
4523
4775
  id: input.id,
4524
4776
  config: jsonBind(merged),
4525
- updatedAt: /* @__PURE__ */ new Date()
4777
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4526
4778
  })).rowsAffected, "UPDATE_OM_CONFIG", input.id);
4527
4779
  });
4528
4780
  } catch (error) {
@@ -4568,6 +4820,7 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4568
4820
  ${OM_LAST_BUFFERED_AT_TOKENS},
4569
4821
  ${OM_LAST_BUFFERED_AT_TIME},
4570
4822
  metadata,
4823
+ ${OM_SUPERSEDED_BY},
4571
4824
  ${OM_CREATED_AT},
4572
4825
  ${OM_UPDATED_AT}
4573
4826
  ) VALUES (
@@ -4603,6 +4856,7 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4603
4856
  :lastBufferedAtTokens,
4604
4857
  :lastBufferedAtTime,
4605
4858
  :metadata,
4859
+ :supersededBy,
4606
4860
  :createdAt,
4607
4861
  :updatedAt
4608
4862
  )`, {
@@ -4616,8 +4870,8 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4616
4870
  originType: record.originType ?? "initial",
4617
4871
  config: jsonBind(record.config ?? {}),
4618
4872
  generationCount: record.generationCount ?? 0,
4619
- lastObservedAt: record.lastObservedAt ?? null,
4620
- lastReflectionAt: record.originType === "reflection" ? timestamp : null,
4873
+ lastObservedAt: timestampBind(record.lastObservedAt),
4874
+ lastReflectionAt: record.originType === "reflection" ? timestampBind(timestamp) : null,
4621
4875
  pendingMessageTokens: Math.round(record.pendingMessageTokens ?? 0),
4622
4876
  totalTokensObserved: Math.round(record.totalTokensObserved ?? 0),
4623
4877
  observationTokenCount: Math.round(record.observationTokenCount ?? 0),
@@ -4636,10 +4890,11 @@ async function insertOMRecord(ctx, connection, record, timestamp = record.create
4636
4890
  isBufferingObservation: boolToNumber(record.isBufferingObservation),
4637
4891
  isBufferingReflection: boolToNumber(record.isBufferingReflection),
4638
4892
  lastBufferedAtTokens: Math.round(record.lastBufferedAtTokens ?? 0),
4639
- lastBufferedAtTime: record.lastBufferedAtTime ?? null,
4893
+ lastBufferedAtTime: timestampBind(record.lastBufferedAtTime),
4640
4894
  metadata: nullableJsonBind(record.metadata),
4641
- createdAt: record.createdAt,
4642
- updatedAt: record.updatedAt
4895
+ supersededBy: record.supersededBy ?? null,
4896
+ createdAt: timestampBind(record.createdAt),
4897
+ updatedAt: timestampBind(record.updatedAt)
4643
4898
  });
4644
4899
  }
4645
4900
  async function updateOMFlag(ctx, id, column, value, operation) {
@@ -4663,7 +4918,7 @@ async function updateOMColumns(ctx, id, operation, columns) {
4663
4918
  WHERE id = :id`, {
4664
4919
  ...binds,
4665
4920
  id,
4666
- updatedAt: /* @__PURE__ */ new Date()
4921
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4667
4922
  })).rowsAffected, operation, id);
4668
4923
  });
4669
4924
  }
@@ -4701,6 +4956,7 @@ function omSelect() {
4701
4956
  ${OM_LAST_BUFFERED_AT_TOKENS} AS "lastBufferedAtTokens",
4702
4957
  ${OM_LAST_BUFFERED_AT_TIME} AS "lastBufferedAtTime",
4703
4958
  metadata AS "metadata",
4959
+ ${OM_SUPERSEDED_BY} AS "supersededBy",
4704
4960
  ${OM_CREATED_AT} AS "createdAt",
4705
4961
  ${OM_UPDATED_AT} AS "updatedAt"`;
4706
4962
  }
@@ -4736,15 +4992,61 @@ function parseOMRow(row) {
4736
4992
  config: parseJson(row.config),
4737
4993
  metadata: parseOptionalJsonObject(row.metadata, { emptyObjectAsUndefined: true }),
4738
4994
  observedMessageIds: parseOptionalStringArray(row.observedMessageIds),
4739
- observedTimezone: row.observedTimezone ? String(row.observedTimezone) : void 0
4995
+ observedTimezone: row.observedTimezone ? String(row.observedTimezone) : void 0,
4996
+ supersededBy: row.supersededBy ? String(row.supersededBy) : null
4740
4997
  };
4741
4998
  }
4999
+ /** The canonical head row of a lookup key (non-locking read). */
5000
+ async function getHeadOMRow(ctx, connection, lookupKey) {
5001
+ return rows(await connection.execute(`${omSelect()} FROM ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
5002
+ WHERE ${OM_LOOKUP_KEY} = :lookupKey
5003
+ ORDER BY ${OM_HEAD_ORDER}
5004
+ FETCH FIRST 1 ROWS ONLY`, asBindParameters({ lookupKey }), executeOptions()))[0];
5005
+ }
5006
+ /** Row-locks one record by primary key for the rest of the transaction; `undefined` when missing. */
5007
+ async function findOMRowForUpdate(ctx, connection, id) {
5008
+ return rows(await connection.execute(`${omSelect()} FROM ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)} WHERE id = :id FOR UPDATE`, { id }, executeOptions()))[0];
5009
+ }
5010
+ /**
5011
+ * Row-locks one record by primary key. Observational memory updates are incremental and
5012
+ * order-sensitive, so mutating paths derive their next state from a locked row.
5013
+ * Throws "record not found" when it is missing.
5014
+ */
5015
+ async function lockOMRow(ctx, connection, id, operation) {
5016
+ const row = await findOMRowForUpdate(ctx, connection, id);
5017
+ if (!row) {
5018
+ assertRowsAffected(0, operation, id);
5019
+ throw new Error(`Observational memory record not found: ${id}`);
5020
+ }
5021
+ return row;
5022
+ }
5023
+ /**
5024
+ * The live, row-locked record a lifecycle write aimed at `row` lands on: `row` itself while
5025
+ * live, otherwise the head of its lookup key (locked, then re-checked — a rollover may retire
5026
+ * it between the head read and the lock).
5027
+ */
5028
+ async function resolveLiveOMRow(ctx, connection, row) {
5029
+ let current = row;
5030
+ for (let hop = 0; current.supersededBy; hop++) {
5031
+ const head = hop < OM_MAX_HEAD_HOPS ? await getHeadOMRow(ctx, connection, current.lookupKey) : void 0;
5032
+ const locked = head && head.id !== current.id ? await findOMRowForUpdate(ctx, connection, head.id) : void 0;
5033
+ 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`));
5034
+ current = locked;
5035
+ }
5036
+ return current;
5037
+ }
4742
5038
  //#endregion
4743
5039
  //#region src/storage/domains/memory/observational-buffering.ts
4744
- async function updateBufferedObservations(ctx, input) {
5040
+ async function updateBufferedObservations(ctx, input, fence) {
4745
5041
  try {
4746
- await ctx.db.tx(async (_client, connection) => {
4747
- const existingChunks = parseBufferedChunks((await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS")).bufferedObservationChunks);
5042
+ return await ctx.db.tx(async (client, connection) => {
5043
+ await assertMemoryFence(ctx, client, fence, "appendBufferedObservations");
5044
+ const row = await resolveLiveOMRow(ctx, connection, await lockOMRow(ctx, connection, input.id, "UPDATE_BUFFERED_OBSERVATIONS"));
5045
+ const existingChunks = parseBufferedChunks(row.bufferedObservationChunks);
5046
+ if (existingChunks.some((existing) => existing.cycleId === input.chunk.cycleId) || isBufferedChunkCoveredByCursor(input.chunk.lastObservedAt, row.lastObservedAt)) return {
5047
+ persisted: false,
5048
+ recordId: row.id
5049
+ };
4748
5050
  const newChunk = {
4749
5051
  id: `ombuf-${globalThis.crypto.randomUUID()}`,
4750
5052
  cycleId: input.chunk.cycleId,
@@ -4760,32 +5062,48 @@ async function updateBufferedObservations(ctx, input) {
4760
5062
  extractedValues: input.chunk.extractedValues,
4761
5063
  extractionFailures: input.chunk.extractionFailures
4762
5064
  };
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);
5065
+ const lastBufferedAtTime = maxObservationCursor(row.lastBufferedAtTime, input.lastBufferedAtTime);
4771
5066
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4772
5067
  SET ${OM_BUFFERED_OBSERVATION_CHUNKS} = :bufferedObservationChunks,
4773
- ${OM_UPDATED_AT} = :updatedAt${lastBufferedAtTimeSql}
4774
- WHERE id = :id`, asBindParameters(binds))).rowsAffected, "UPDATE_BUFFERED_OBSERVATIONS", input.id);
5068
+ ${OM_LAST_BUFFERED_AT_TIME} = :lastBufferedAtTime,
5069
+ ${OM_UPDATED_AT} = :updatedAt
5070
+ WHERE id = :id`, asBindParameters({
5071
+ id: row.id,
5072
+ bufferedObservationChunks: nullableJsonBind([...existingChunks, newChunk]),
5073
+ lastBufferedAtTime: timestampBind(lastBufferedAtTime),
5074
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
5075
+ }))).rowsAffected, "UPDATE_BUFFERED_OBSERVATIONS", row.id);
5076
+ return {
5077
+ persisted: true,
5078
+ recordId: row.id
5079
+ };
4775
5080
  });
4776
5081
  } catch (error) {
4777
5082
  if (error instanceof MastraError) throw error;
4778
5083
  throw storageError$1("UPDATE_BUFFERED_OBSERVATIONS", "FAILED", { id: input.id }, error);
4779
5084
  }
4780
5085
  }
4781
- async function swapBufferedToActive(ctx, input) {
5086
+ async function swapBufferedToActive(ctx, input, fence) {
4782
5087
  try {
4783
- return await ctx.db.tx(async (_client, connection) => {
5088
+ return await ctx.db.tx(async (client, connection) => {
5089
+ await assertMemoryFence(ctx, client, fence, "swapBufferedToActive");
4784
5090
  const row = await lockOMRow(ctx, connection, input.id, "SWAP_BUFFERED_TO_ACTIVE");
4785
- const chunks = input.bufferedChunks?.length ? input.bufferedChunks : parseBufferedChunks(row.bufferedObservationChunks);
5091
+ if (row.supersededBy) return {
5092
+ ...emptySwapResult(),
5093
+ retired: true
5094
+ };
5095
+ const refreshedWeights = new Map((input.bufferedChunks ?? []).map((chunk) => [chunk.id, chunk.messageTokens]));
5096
+ const chunks = parseBufferedChunks(row.bufferedObservationChunks).map((chunk) => {
5097
+ const weight = refreshedWeights.get(chunk.id);
5098
+ return weight === void 0 ? chunk : {
5099
+ ...chunk,
5100
+ messageTokens: weight
5101
+ };
5102
+ });
4786
5103
  if (chunks.length === 0) return emptySwapResult();
4787
5104
  const activation = calculateBufferedActivation(chunks, input);
4788
5105
  const lastObservedAt = input.lastObservedAt ?? (activation.activatedChunks.at(-1)?.lastObservedAt ? toDate(activation.activatedChunks.at(-1).lastObservedAt) : /* @__PURE__ */ new Date());
5106
+ const storedCursor = maxObservationCursor(row.lastObservedAt, lastObservedAt);
4789
5107
  const boundary = `\n\n--- message boundary (${lastObservedAt.toISOString()}) ---\n\n`;
4790
5108
  const existingActive = stringOrEmpty(row.activeObservations);
4791
5109
  const newActive = existingActive ? `${existingActive}${boundary}${activation.activatedContent}` : activation.activatedContent;
@@ -4803,8 +5121,8 @@ async function swapBufferedToActive(ctx, input) {
4803
5121
  observationTokens: activation.activatedTokens,
4804
5122
  pendingMessageTokens: pendingTokens,
4805
5123
  bufferedObservationChunks: nullableJsonBind(activation.remainingChunks.length > 0 ? activation.remainingChunks : null),
4806
- lastObservedAt,
4807
- updatedAt: /* @__PURE__ */ new Date()
5124
+ lastObservedAt: timestampBind(storedCursor),
5125
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4808
5126
  })).rowsAffected, "SWAP_BUFFERED_TO_ACTIVE", input.id);
4809
5127
  return activation.result;
4810
5128
  });
@@ -4813,9 +5131,10 @@ async function swapBufferedToActive(ctx, input) {
4813
5131
  throw storageError$1("SWAP_BUFFERED_TO_ACTIVE", "FAILED", { id: input.id }, error);
4814
5132
  }
4815
5133
  }
4816
- async function updateBufferedReflection(ctx, input) {
5134
+ async function updateBufferedReflection(ctx, input, fence) {
4817
5135
  try {
4818
- await ctx.db.tx(async (_client, connection) => {
5136
+ await ctx.db.tx(async (client, connection) => {
5137
+ await assertMemoryFence(ctx, client, fence, "updateBufferedReflection");
4819
5138
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4820
5139
  SET ${OM_BUFFERED_REFLECTION} = CASE
4821
5140
  WHEN ${OM_BUFFERED_REFLECTION} IS NOT NULL AND DBMS_LOB.GETLENGTH(${OM_BUFFERED_REFLECTION}) > 0
@@ -4832,7 +5151,7 @@ async function updateBufferedReflection(ctx, input) {
4832
5151
  tokenCount: Math.round(input.tokenCount),
4833
5152
  inputTokenCount: Math.round(input.inputTokenCount),
4834
5153
  reflectedObservationLineCount: Math.round(input.reflectedObservationLineCount),
4835
- updatedAt: /* @__PURE__ */ new Date()
5154
+ updatedAt: timestampBind(/* @__PURE__ */ new Date())
4836
5155
  })).rowsAffected, "UPDATE_BUFFERED_REFLECTION", input.id);
4837
5156
  });
4838
5157
  } catch (error) {
@@ -4840,67 +5159,34 @@ async function updateBufferedReflection(ctx, input) {
4840
5159
  throw storageError$1("UPDATE_BUFFERED_REFLECTION", "FAILED", { id: input.id }, error);
4841
5160
  }
4842
5161
  }
4843
- async function swapBufferedReflectionToActive(ctx, input) {
5162
+ async function swapBufferedReflectionToActive(ctx, input, fence) {
5163
+ const { currentRecord } = input;
4844
5164
  try {
4845
- 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);
5165
+ return await ctx.db.tx(async (client, connection) => {
5166
+ await assertMemoryFence(ctx, client, fence, "swapBufferedReflectionToActive");
5167
+ const row = await findOMRowForUpdate(ctx, connection, currentRecord.id);
5168
+ if (!row) return currentRecord;
5169
+ if (row.supersededBy) return parseOMRow(await resolveLiveOMRow(ctx, connection, row));
5170
+ const stored = parseOMRow(row);
5171
+ const bufferedReflection = stored.bufferedReflection ?? "";
5172
+ 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);
5173
+ if (!isAppendOnlySince(stored.activeObservations, currentRecord.activeObservations)) return stored;
5174
+ const reflectedLineCount = stored.reflectedObservationLineCount ?? 0;
5175
+ const unreflectedContent = stored.activeObservations.split("\n").slice(reflectedLineCount).join("\n").trim();
5176
+ 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
5177
  assertRowsAffected((await connection.execute(`UPDATE ${table(ctx, TABLE_OBSERVATIONAL_MEMORY)}
4880
5178
  SET ${OM_BUFFERED_REFLECTION} = NULL,
4881
5179
  ${OM_BUFFERED_REFLECTION_TOKENS} = NULL,
4882
5180
  ${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);
5181
+ ${OM_REFLECTED_OBSERVATION_LINE_COUNT} = NULL
5182
+ WHERE id = :id`, { id: stored.id })).rowsAffected, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE", stored.id);
4889
5183
  return newRecord;
4890
5184
  });
4891
5185
  } catch (error) {
4892
5186
  if (error instanceof MastraError) throw error;
4893
- throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "FAILED", { id: input.currentRecord.id }, error);
5187
+ throw storageError$1("SWAP_BUFFERED_REFLECTION_TO_ACTIVE", "FAILED", { id: currentRecord.id }, error);
4894
5188
  }
4895
5189
  }
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
5190
  function emptySwapResult() {
4905
5191
  return {
4906
5192
  chunksActivated: 0,
@@ -5030,17 +5316,24 @@ async function saveResource(ctx, { resource }) {
5030
5316
  throw storageError$1("SAVE_RESOURCE", "FAILED", { resourceId: resource.id }, error);
5031
5317
  }
5032
5318
  }
5033
- async function updateResource(ctx, { resourceId, workingMemory, metadata }) {
5319
+ async function updateResource(ctx, { resourceId, workingMemory, metadata, fence }) {
5034
5320
  const existing = await ctx.getResourceById({ resourceId });
5035
- if (!existing) return ctx.saveResource({ resource: {
5321
+ const newResource = () => ({
5036
5322
  id: resourceId,
5037
5323
  workingMemory,
5038
5324
  metadata: metadata ?? {},
5039
5325
  createdAt: /* @__PURE__ */ new Date(),
5040
5326
  updatedAt: /* @__PURE__ */ new Date()
5041
- } });
5327
+ });
5328
+ if (!existing && !fence) return ctx.saveResource({ resource: newResource() });
5042
5329
  try {
5043
- return await ctx.db.tx(async (_client, connection) => {
5330
+ return await ctx.db.tx(async (client, connection) => {
5331
+ await assertMemoryFence(ctx, client, fence, "updateResource");
5332
+ if (!existing) {
5333
+ const resource = newResource();
5334
+ await mergeResourceRow(ctx, connection, resource);
5335
+ return resource;
5336
+ }
5044
5337
  const row = rows(await connection.execute(`${resourceSelect()} FROM ${table(ctx, TABLE_RESOURCES)} WHERE id = :resourceId FOR UPDATE`, { resourceId }, executeOptions()))[0];
5045
5338
  const locked = row ? parseResource(row) : existing;
5046
5339
  const updated = {
@@ -5134,11 +5427,15 @@ async function getThreadById(ctx, { threadId, resourceId }) {
5134
5427
  throw storageError$1("GET_THREAD_BY_ID", "FAILED", { threadId }, error);
5135
5428
  }
5136
5429
  }
5137
- async function saveThread(ctx, { thread }) {
5430
+ async function saveThread(ctx, { thread, fence }) {
5138
5431
  try {
5139
- await mergeThreadRow(ctx, ctx.db, thread);
5432
+ await ctx.db.tx(async (client) => {
5433
+ await assertMemoryFence(ctx, client, fence, "saveThread");
5434
+ await mergeThreadRow(ctx, client, thread);
5435
+ });
5140
5436
  return thread;
5141
5437
  } catch (error) {
5438
+ if (error instanceof MastraError) throw error;
5142
5439
  throw storageError$1("SAVE_THREAD", "FAILED", { threadId: thread.id }, error);
5143
5440
  }
5144
5441
  }
@@ -5223,7 +5520,7 @@ async function insertThreadRow(ctx, client, thread) {
5223
5520
  updatedAt: thread.updatedAt ?? /* @__PURE__ */ new Date()
5224
5521
  });
5225
5522
  }
5226
- async function updateThread(ctx, { id, title, metadata }) {
5523
+ async function updateThread(ctx, { id, title, metadata, fence }) {
5227
5524
  const existingThread = await ctx.getThreadById({ threadId: id });
5228
5525
  if (!existingThread) throw storageError$1("UPDATE_THREAD", "FAILED", {
5229
5526
  threadId: id,
@@ -5235,16 +5532,19 @@ async function updateThread(ctx, { id, title, metadata }) {
5235
5532
  };
5236
5533
  const updatedAt = /* @__PURE__ */ new Date();
5237
5534
  try {
5238
- await ctx.db.none(`
5239
- UPDATE ${table(ctx, TABLE_THREADS)}
5240
- SET title = COALESCE(:title, title),
5241
- metadata = :metadata,
5242
- ${THREAD_UPDATED_AT} = :updatedAt
5243
- WHERE id = :id`, {
5244
- id,
5245
- title: optionalStringBind(title),
5246
- metadata: jsonBind(mergedMetadata),
5247
- updatedAt
5535
+ await ctx.db.tx(async (client) => {
5536
+ await assertMemoryFence(ctx, client, fence, "updateThread");
5537
+ await client.none(`
5538
+ UPDATE ${table(ctx, TABLE_THREADS)}
5539
+ SET title = COALESCE(:title, title),
5540
+ metadata = :metadata,
5541
+ ${THREAD_UPDATED_AT} = :updatedAt
5542
+ WHERE id = :id`, {
5543
+ id,
5544
+ title: optionalStringBind(title),
5545
+ metadata: jsonBind(mergedMetadata),
5546
+ updatedAt
5547
+ });
5248
5548
  });
5249
5549
  const updatedThread = await ctx.getThreadById({ threadId: id });
5250
5550
  if (!updatedThread) throw storageError$1("UPDATE_THREAD", "FAILED", {
@@ -5396,7 +5696,8 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5396
5696
  TABLE_THREADS,
5397
5697
  TABLE_MESSAGES,
5398
5698
  TABLE_RESOURCES,
5399
- TABLE_OBSERVATIONAL_MEMORY
5699
+ TABLE_OBSERVATIONAL_MEMORY,
5700
+ TABLE_MEMORY_RUN_FENCES
5400
5701
  ];
5401
5702
  db;
5402
5703
  schemaName;
@@ -5434,18 +5735,77 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5434
5735
  }
5435
5736
  async init() {
5436
5737
  await initMemorySchema(this.ctx);
5738
+ await this.db.createTable({
5739
+ tableName: TABLE_MEMORY_RUN_FENCES,
5740
+ schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_MEMORY_RUN_FENCES]
5741
+ });
5742
+ }
5743
+ /**
5744
+ * Mark observational memory rows that are not their key's canonical head as superseded.
5745
+ * Runs as part of `init()`; `OracleStore` also runs it on every start when the memory schema
5746
+ * migration is unchanged and skipped.
5747
+ * @internal
5748
+ */
5749
+ async backfillObservationalMemorySupersededBy() {
5750
+ await this.ctx.db.withConnection((connection) => backfillObservationalMemorySupersededBy(this.ctx, connection));
5437
5751
  }
5438
5752
  async dangerouslyClearAll() {
5439
5753
  await clearAllMemoryTables(this.ctx);
5754
+ await this.db.clearTable(TABLE_MEMORY_RUN_FENCES);
5755
+ }
5756
+ supportsRunFencing() {
5757
+ return true;
5758
+ }
5759
+ async raiseRunFence(fence) {
5760
+ const fences = table(this.ctx, TABLE_MEMORY_RUN_FENCES);
5761
+ try {
5762
+ return await claimTransaction(this.db, async (client) => {
5763
+ const row = await client.oneOrNone(`SELECT generation AS "generation", "ownerId" FROM ${fences} WHERE "runId" = :runId FOR UPDATE`, { runId: fence.runId });
5764
+ const binds = {
5765
+ runId: fence.runId,
5766
+ generation: fence.generation,
5767
+ ownerId: fence.ownerId
5768
+ };
5769
+ if (!row) {
5770
+ await client.none(`INSERT INTO ${fences} ("runId", generation, "ownerId") VALUES (:runId, :generation, :ownerId)`, binds);
5771
+ return true;
5772
+ }
5773
+ const generation = Number(row.generation);
5774
+ if (!(generation < fence.generation) && !(generation === fence.generation && String(row.ownerId) === fence.ownerId)) return false;
5775
+ await client.none(`UPDATE ${fences} SET generation = :generation, "ownerId" = :ownerId, "retiredAt" = NULL WHERE "runId" = :runId`, binds);
5776
+ return true;
5777
+ });
5778
+ } catch (error) {
5779
+ throw storageError$1("RAISE_RUN_FENCE", "FAILED", { runId: fence.runId }, error);
5780
+ }
5781
+ }
5782
+ async retireRunFence(fence) {
5783
+ const fences = table(this.ctx, TABLE_MEMORY_RUN_FENCES);
5784
+ try {
5785
+ return await this.db.tx(async (client) => {
5786
+ const row = await client.oneOrNone(`SELECT generation AS "generation", "ownerId" FROM ${fences} WHERE "runId" = :runId FOR UPDATE`, { runId: fence.runId });
5787
+ if (!row || Number(row.generation) !== fence.generation || String(row.ownerId) !== fence.ownerId) return false;
5788
+ await client.none(`UPDATE ${fences} SET "retiredAt" = ${DB_NOW_MS} WHERE "runId" = :runId`, { runId: fence.runId });
5789
+ return true;
5790
+ });
5791
+ } catch (error) {
5792
+ throw storageError$1("RETIRE_RUN_FENCE", "FAILED", { runId: fence.runId }, error);
5793
+ }
5440
5794
  }
5441
5795
  async getThreadById(args) {
5442
5796
  return getThreadById(this.ctx, args);
5443
5797
  }
5444
- async saveThread(args) {
5445
- return saveThread(this.ctx, args);
5798
+ async saveThread({ fence, ...args }) {
5799
+ return saveThread(this.ctx, {
5800
+ ...args,
5801
+ fence: resolveRunFence(this, fence)
5802
+ });
5446
5803
  }
5447
- async updateThread(args) {
5448
- return updateThread(this.ctx, args);
5804
+ async updateThread({ fence, ...args }) {
5805
+ return updateThread(this.ctx, {
5806
+ ...args,
5807
+ fence: resolveRunFence(this, fence)
5808
+ });
5449
5809
  }
5450
5810
  async deleteThread(args) {
5451
5811
  return deleteThread(this.ctx, args);
@@ -5465,14 +5825,20 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5465
5825
  async listMessagesByResourceId(args) {
5466
5826
  return listMessagesByResourceId(this.ctx, args);
5467
5827
  }
5468
- async saveMessages(args) {
5469
- return saveMessages(this.ctx, args);
5828
+ async saveMessages({ fence, ...args }) {
5829
+ return saveMessages(this.ctx, {
5830
+ ...args,
5831
+ fence: resolveRunFence(this, fence)
5832
+ });
5470
5833
  }
5471
- async updateMessages(args) {
5472
- return updateMessages(this.ctx, args);
5834
+ async updateMessages({ fence, ...args }) {
5835
+ return updateMessages(this.ctx, {
5836
+ ...args,
5837
+ fence: resolveRunFence(this, fence)
5838
+ });
5473
5839
  }
5474
- async deleteMessages(messageIds) {
5475
- return deleteMessages(this.ctx, messageIds);
5840
+ async deleteMessages(messageIds, options) {
5841
+ return deleteMessages(this.ctx, messageIds, resolveRunFence(this, options?.fence));
5476
5842
  }
5477
5843
  async getResourceById(args) {
5478
5844
  return getResourceById(this.ctx, args);
@@ -5480,8 +5846,11 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5480
5846
  async saveResource(args) {
5481
5847
  return saveResource(this.ctx, args);
5482
5848
  }
5483
- async updateResource(args) {
5484
- return updateResource(this.ctx, args);
5849
+ async updateResource({ fence, ...args }) {
5850
+ return updateResource(this.ctx, {
5851
+ ...args,
5852
+ fence: resolveRunFence(this, fence)
5853
+ });
5485
5854
  }
5486
5855
  async cloneThread(args) {
5487
5856
  const sourceThread = await this.getThreadById({ threadId: args.sourceThreadId });
@@ -5571,10 +5940,13 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5571
5940
  return insertObservationalMemoryRecord(this.ctx, record);
5572
5941
  }
5573
5942
  async updateActiveObservations(input) {
5574
- return updateActiveObservations(this.ctx, input);
5943
+ assertActiveObservationsApplied(await this.commitActiveObservations(input), input.id);
5944
+ }
5945
+ async commitActiveObservations(input) {
5946
+ return updateActiveObservations(this.ctx, input, resolveRunFence(this, void 0));
5575
5947
  }
5576
5948
  async createReflectionGeneration(input) {
5577
- return createReflectionGeneration(this.ctx, input);
5949
+ return createReflectionGeneration(this.ctx, input, resolveRunFence(this, void 0));
5578
5950
  }
5579
5951
  async setReflectingFlag(id, isReflecting) {
5580
5952
  return setReflectingFlag(this.ctx, id, isReflecting);
@@ -5598,16 +5970,19 @@ var MemoryOracle = class MemoryOracle extends MemoryStorage {
5598
5970
  return updateObservationalMemoryConfig(this.ctx, input);
5599
5971
  }
5600
5972
  async updateBufferedObservations(input) {
5601
- return updateBufferedObservations(this.ctx, input);
5973
+ await this.appendBufferedObservations(input);
5974
+ }
5975
+ async appendBufferedObservations(input) {
5976
+ return updateBufferedObservations(this.ctx, input, resolveRunFence(this, void 0));
5602
5977
  }
5603
5978
  async swapBufferedToActive(input) {
5604
- return swapBufferedToActive(this.ctx, input);
5979
+ return swapBufferedToActive(this.ctx, input, resolveRunFence(this, void 0));
5605
5980
  }
5606
5981
  async updateBufferedReflection(input) {
5607
- return updateBufferedReflection(this.ctx, input);
5982
+ return updateBufferedReflection(this.ctx, input, resolveRunFence(this, void 0));
5608
5983
  }
5609
5984
  async swapBufferedReflectionToActive(input) {
5610
- return swapBufferedReflectionToActive(this.ctx, input);
5985
+ return swapBufferedReflectionToActive(this.ctx, input, resolveRunFence(this, void 0));
5611
5986
  }
5612
5987
  };
5613
5988
  //#endregion
@@ -7952,8 +8327,19 @@ const STORE_NAME$1 = "ORACLEDB";
7952
8327
  const WORKFLOW_CREATED_AT = "\"createdAt\"";
7953
8328
  const WORKFLOW_UPDATED_AT = "\"updatedAt\"";
7954
8329
  const WORKFLOW_RESOURCE_ID = "\"resourceId\"";
8330
+ const RUN_OWNER_COLUMNS = `generation AS "generation", "ownerId", "leaseExpiresAt", ${DB_NOW_MS} AS "nowMs"`;
8331
+ function toRunOwnershipRecord(runId, row) {
8332
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
8333
+ return {
8334
+ runId,
8335
+ generation: Number(row.generation),
8336
+ ownerId: String(row.ownerId),
8337
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
8338
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
8339
+ };
8340
+ }
7955
8341
  var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
7956
- static MANAGED_TABLES = [TABLE_WORKFLOW_SNAPSHOT];
8342
+ static MANAGED_TABLES = [TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKFLOW_RUN_OWNERS];
7957
8343
  db;
7958
8344
  schemaName;
7959
8345
  skipDefaultIndexes;
@@ -7974,10 +8360,114 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
7974
8360
  }
7975
8361
  async dangerouslyClearAll() {
7976
8362
  await this.db.clearTable(TABLE_WORKFLOW_SNAPSHOT);
8363
+ await this.db.clearTable(TABLE_WORKFLOW_RUN_OWNERS);
8364
+ }
8365
+ supportsRunFencing() {
8366
+ return true;
8367
+ }
8368
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
8369
+ const table = this.runOwnersTable();
8370
+ try {
8371
+ return await claimTransaction(this.db, async (client) => {
8372
+ const existing = await client.oneOrNone(`SELECT generation AS "generation" FROM ${table} WHERE "runId" = :runId FOR UPDATE`, { runId });
8373
+ if (!existing) {
8374
+ if (expectedGeneration !== void 0 && expectedGeneration !== 0) return {
8375
+ acquired: false,
8376
+ record: null
8377
+ };
8378
+ await client.none(`INSERT INTO ${table} ("runId", generation, "ownerId", "leaseExpiresAt", "updatedAt")
8379
+ VALUES (:runId, ${DB_NOW_MS}, :ownerId, ${DB_NOW_MS} + :leaseMs, ${DB_NOW_MS})`, {
8380
+ runId,
8381
+ ownerId,
8382
+ leaseMs
8383
+ });
8384
+ return {
8385
+ acquired: true,
8386
+ record: await this.readRunOwner(client, runId)
8387
+ };
8388
+ }
8389
+ await client.none(`UPDATE ${table}
8390
+ SET generation = generation + 1, "ownerId" = :ownerId, "leaseExpiresAt" = ${DB_NOW_MS} + :leaseMs,
8391
+ "updatedAt" = ${DB_NOW_MS}
8392
+ WHERE "runId" = :runId
8393
+ AND (:expectedGeneration IS NULL OR generation = :expectedGeneration)
8394
+ AND (:force = 1 OR "leaseExpiresAt" IS NULL OR "leaseExpiresAt" <= ${DB_NOW_MS})`, {
8395
+ runId,
8396
+ ownerId,
8397
+ leaseMs,
8398
+ expectedGeneration: expectedGeneration ?? null,
8399
+ force: force === true ? 1 : 0
8400
+ });
8401
+ const record = await this.readRunOwner(client, runId);
8402
+ return record && record.generation === Number(existing.generation) + 1 ? {
8403
+ acquired: true,
8404
+ record
8405
+ } : {
8406
+ acquired: false,
8407
+ record
8408
+ };
8409
+ });
8410
+ } catch (error) {
8411
+ throw this.storageError("CLAIM_RUN_OWNERSHIP", "FAILED", { runId }, error);
8412
+ }
8413
+ }
8414
+ async renewRunOwnership({ leaseMs, ...fence }) {
8415
+ try {
8416
+ return await this.db.tx(async (client) => {
8417
+ await client.none(`UPDATE ${this.runOwnersTable()}
8418
+ SET "leaseExpiresAt" = ${DB_NOW_MS} + :leaseMs, "updatedAt" = ${DB_NOW_MS}
8419
+ WHERE "runId" = :runId AND generation = :generation AND "ownerId" = :ownerId
8420
+ AND "leaseExpiresAt" IS NOT NULL`, {
8421
+ runId: fence.runId,
8422
+ generation: fence.generation,
8423
+ ownerId: fence.ownerId,
8424
+ leaseMs
8425
+ });
8426
+ const record = await this.readRunOwner(client, fence.runId);
8427
+ return record !== null && record.generation === fence.generation && record.ownerId === fence.ownerId && record.leaseExpiresAt !== null ? {
8428
+ renewed: true,
8429
+ record
8430
+ } : {
8431
+ renewed: false,
8432
+ record
8433
+ };
8434
+ });
8435
+ } catch (error) {
8436
+ throw this.storageError("RENEW_RUN_OWNERSHIP", "FAILED", { runId: fence.runId }, error);
8437
+ }
8438
+ }
8439
+ async releaseRunOwnership(fence) {
8440
+ try {
8441
+ return await this.db.tx(async (client) => {
8442
+ await client.none(`UPDATE ${this.runOwnersTable()} SET "leaseExpiresAt" = NULL, "updatedAt" = ${DB_NOW_MS}
8443
+ WHERE "runId" = :runId AND generation = :generation AND "ownerId" = :ownerId`, {
8444
+ runId: fence.runId,
8445
+ generation: fence.generation,
8446
+ ownerId: fence.ownerId
8447
+ });
8448
+ const record = await this.readRunOwner(client, fence.runId);
8449
+ return record !== null && record.generation === fence.generation && record.ownerId === fence.ownerId;
8450
+ });
8451
+ } catch (error) {
8452
+ throw this.storageError("RELEASE_RUN_OWNERSHIP", "FAILED", { runId: fence.runId }, error);
8453
+ }
7977
8454
  }
7978
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
8455
+ async getRunOwnership({ runId }) {
8456
+ try {
8457
+ return await this.readRunOwner(this.db, runId);
8458
+ } catch (error) {
8459
+ throw this.storageError("GET_RUN_OWNERSHIP", "FAILED", { runId }, error);
8460
+ }
8461
+ }
8462
+ async readRunOwner(client, runId) {
8463
+ const row = await client.oneOrNone(`SELECT ${RUN_OWNER_COLUMNS} FROM ${this.runOwnersTable()} WHERE "runId" = :runId`, { runId });
8464
+ return row ? toRunOwnershipRecord(runId, row) : null;
8465
+ }
8466
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
8467
+ const fence = resolveRunFence(this, explicitFence, runId);
7979
8468
  try {
7980
8469
  return await this.db.tx(async (client) => {
8470
+ if (fence) await assertRunFence(client, this.runOwnersTable(), fence, "updateWorkflowResults");
7981
8471
  await this.ensureWorkflowRunRow(client, workflowName, runId);
7982
8472
  const existing = await client.one(`SELECT snapshot AS "snapshot", ${WORKFLOW_RESOURCE_ID} AS "resourceId" FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId FOR UPDATE`, {
7983
8473
  workflowName,
@@ -8001,6 +8491,7 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8001
8491
  return snapshot.context;
8002
8492
  });
8003
8493
  } catch (error) {
8494
+ if (isRunFenceConflictError(error)) throw error;
8004
8495
  throw this.storageError("UPDATE_WORKFLOW_RESULTS", "FAILED", {
8005
8496
  workflowName,
8006
8497
  runId,
@@ -8008,9 +8499,11 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8008
8499
  }, error);
8009
8500
  }
8010
8501
  }
8011
- async updateWorkflowState({ workflowName, runId, opts }) {
8502
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
8503
+ const fence = resolveRunFence(this, explicitFence, runId);
8012
8504
  try {
8013
8505
  return await this.db.tx(async (client) => {
8506
+ if (fence) await assertRunFence(client, this.runOwnersTable(), fence, "updateWorkflowState");
8014
8507
  const existing = await client.oneOrNone(`SELECT snapshot AS "snapshot" FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId FOR UPDATE`, {
8015
8508
  workflowName,
8016
8509
  runId
@@ -8033,24 +8526,26 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8033
8526
  return updatedSnapshot;
8034
8527
  });
8035
8528
  } catch (error) {
8529
+ if (isRunFenceConflictError(error)) throw error;
8036
8530
  throw this.storageError("UPDATE_WORKFLOW_STATE", "FAILED", {
8037
8531
  workflowName,
8038
8532
  runId
8039
8533
  }, error);
8040
8534
  }
8041
8535
  }
8042
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
8536
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
8043
8537
  try {
8044
8538
  const now = /* @__PURE__ */ new Date();
8045
- await this.db.none(this.workflowMergeSql(), {
8539
+ await withRunFence(this.db, this.runOwnersTable(), resolveRunFence(this, fence, runId), "persistWorkflowSnapshot", (client) => client.none(this.workflowMergeSql(), {
8046
8540
  workflowName,
8047
8541
  runId,
8048
8542
  resourceId: resourceId ?? null,
8049
8543
  snapshot: jsonBind(snapshot),
8050
8544
  createdAt: createdAt ?? now,
8051
8545
  updatedAt: updatedAt ?? now
8052
- });
8546
+ }));
8053
8547
  } catch (error) {
8548
+ if (isRunFenceConflictError(error)) throw error;
8054
8549
  throw this.storageError("PERSIST_WORKFLOW_SNAPSHOT", "FAILED", {
8055
8550
  workflowName,
8056
8551
  runId
@@ -8114,13 +8609,14 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8114
8609
  }, error);
8115
8610
  }
8116
8611
  }
8117
- async deleteWorkflowRunById({ runId, workflowName }) {
8612
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
8118
8613
  try {
8119
- await this.db.none(`DELETE FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId`, {
8614
+ await withRunFence(this.db, this.runOwnersTable(), resolveRunFence(this, fence, runId), "deleteWorkflowRunById", (client) => client.none(`DELETE FROM ${this.table()} WHERE workflow_name = :workflowName AND run_id = :runId`, {
8120
8615
  workflowName,
8121
8616
  runId
8122
- });
8617
+ }));
8123
8618
  } catch (error) {
8619
+ if (isRunFenceConflictError(error)) throw error;
8124
8620
  throw this.storageError("DELETE_WORKFLOW_RUN_BY_ID", "FAILED", {
8125
8621
  runId,
8126
8622
  workflowName
@@ -8139,6 +8635,10 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8139
8635
  CONSTRAINT ${this.constraint("MASTRA_WORKFLOW_SNAPSHOT_PK")} PRIMARY KEY (workflow_name, run_id)
8140
8636
  )`, [-955]);
8141
8637
  await this.db.executeDdl(`ALTER TABLE ${this.table()} ADD (${WORKFLOW_RESOURCE_ID} VARCHAR2(512))`, [-1430]);
8638
+ await this.db.createTable({
8639
+ tableName: TABLE_WORKFLOW_RUN_OWNERS,
8640
+ schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS]
8641
+ });
8142
8642
  }
8143
8643
  async createIndexes() {
8144
8644
  await this.db.withConnection(async (connection) => {
@@ -8276,6 +8776,9 @@ var WorkflowsOracle = class WorkflowsOracle extends WorkflowsStorage {
8276
8776
  table() {
8277
8777
  return qualifyName(TABLE_WORKFLOW_SNAPSHOT, this.schemaName);
8278
8778
  }
8779
+ runOwnersTable() {
8780
+ return qualifyName(TABLE_WORKFLOW_RUN_OWNERS, this.schemaName);
8781
+ }
8279
8782
  indexName(indexName) {
8280
8783
  return indexNameForTable(indexName, "IDX");
8281
8784
  }
@@ -8477,7 +8980,7 @@ function checksumMigration(input) {
8477
8980
  //#region src/storage/index.ts
8478
8981
  const STORE_NAME = "ORACLEDB";
8479
8982
  const DOMAIN_SCHEMA_VERSIONS = {
8480
- R001_MEMORY_SCHEMA: 1,
8983
+ R001_MEMORY_SCHEMA: 2,
8481
8984
  R002_WORKFLOWS_SCHEMA: 1,
8482
8985
  R003_OBSERVABILITY_SCHEMA: 1,
8483
8986
  R004_SCORES_SCHEMA: 1,
@@ -8565,6 +9068,10 @@ var OracleStore = class extends MastraCompositeStore {
8565
9068
  async runMigrations(forceRepeatable) {
8566
9069
  try {
8567
9070
  const results = await this.migrationRegistry.run(this.storageMigrations(), { forceRepeatable });
9071
+ if (results.find((result) => result.id === "R001_MEMORY_SCHEMA")?.status === "skipped") {
9072
+ const memory = this.stores.memory;
9073
+ if (memory instanceof MemoryOracle) await memory.backfillObservationalMemorySupersededBy();
9074
+ }
8568
9075
  this.isInitialized = true;
8569
9076
  return results;
8570
9077
  } catch (error) {