@mastra/spanner 1.9.3 → 1.9.4

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.
@@ -3,7 +3,7 @@ name: mastra-spanner
3
3
  description: Documentation for @mastra/spanner. Use when working with @mastra/spanner APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/spanner"
6
- version: "1.9.3"
6
+ version: "1.9.4"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.9.3",
2
+ "version": "1.9.4",
3
3
  "package": "@mastra/spanner",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -6657,6 +6657,59 @@ var MCPServersSpanner = class MCPServersSpanner extends _mastra_core_storage.MCP
6657
6657
  }
6658
6658
  };
6659
6659
  //#endregion
6660
+ //#region src/storage/db/run-fencing.ts
6661
+ /** Epoch milliseconds on the database clock, read when the statement runs. */
6662
+ const DB_NOW_MS = "UNIX_MILLIS(CURRENT_TIMESTAMP())";
6663
+ /**
6664
+ * The check half of a fenced write; call it first inside the write's
6665
+ * read-write transaction. Reads in a read-write transaction lock the rows they
6666
+ * return until commit, so a claim that updates the row either waits for the
6667
+ * write to commit or aborts it, and the retried write sees the new claim.
6668
+ */
6669
+ async function assertRunFence(tx, claimsTable, fence, operation) {
6670
+ const [rows] = await tx.run({
6671
+ sql: `SELECT generation, ${quoteIdent("ownerId", "column name")} AS ownerId
6672
+ FROM ${quoteIdent(claimsTable, "table name")}
6673
+ WHERE ${quoteIdent("runId", "column name")} = @runId`,
6674
+ params: { runId: fence.runId },
6675
+ json: true
6676
+ });
6677
+ const row = rows[0];
6678
+ if (!(0, _mastra_core_storage.matchesRunFence)(row ? {
6679
+ generation: Number(row.generation),
6680
+ ownerId: String(row.ownerId)
6681
+ } : null, fence)) throw new _mastra_core_storage.RunFenceConflictError(fence, operation);
6682
+ }
6683
+ /**
6684
+ * Runs `fn` in a read-write transaction and commits it, retrying the whole
6685
+ * transaction when Spanner aborts it for conflicting with another one.
6686
+ */
6687
+ function inTransaction(db, fn) {
6688
+ return db.runWithAbortRetry(() => db.database.runTransactionAsync(async (tx) => {
6689
+ try {
6690
+ const result = await fn(tx);
6691
+ await tx.commit();
6692
+ return result;
6693
+ } catch (err) {
6694
+ await tx.rollback().catch((rollbackErr) => {
6695
+ throw new AggregateError([err, rollbackErr], "Transaction and rollback both failed");
6696
+ });
6697
+ throw err;
6698
+ }
6699
+ }));
6700
+ }
6701
+ /**
6702
+ * Runs `write` directly when it carries no fence, or in a transaction behind
6703
+ * the fence check when it does.
6704
+ */
6705
+ function withRunFence(db, claimsTable, fence, operation, write) {
6706
+ if (!fence) return write(void 0);
6707
+ return inTransaction(db, async (tx) => {
6708
+ await assertRunFence(tx, claimsTable, fence, operation);
6709
+ return write(tx);
6710
+ });
6711
+ }
6712
+ //#endregion
6660
6713
  //#region src/storage/domains/memory/index.ts
6661
6714
  function buildSpannerMessageMetadataFilter(metadataFilter) {
6662
6715
  if (!metadataFilter) return {
@@ -6720,7 +6773,7 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
6720
6773
  this.skipDefaultIndexes = skipDefaultIndexes;
6721
6774
  this.indexes = indexes?.filter((idx) => MemorySpanner.MANAGED_TABLES.includes(idx.table));
6722
6775
  }
6723
- /** Creates the threads/messages/resources tables and any indexes. */
6776
+ /** Creates the threads/messages/resources and run fence tables and any indexes. */
6724
6777
  async init() {
6725
6778
  await this.db.createTable({
6726
6779
  tableName: _mastra_core_storage.TABLE_THREADS,
@@ -6734,6 +6787,10 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
6734
6787
  tableName: _mastra_core_storage.TABLE_RESOURCES,
6735
6788
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_RESOURCES]
6736
6789
  });
6790
+ await this.db.createTable({
6791
+ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
6792
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_MEMORY_RUN_FENCES]
6793
+ });
6737
6794
  await this.createDefaultIndexes();
6738
6795
  await this.createCustomIndexes();
6739
6796
  }
@@ -6759,11 +6816,92 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
6759
6816
  if (!this.indexes || this.indexes.length === 0) return;
6760
6817
  await this.db.createIndexes(this.indexes);
6761
6818
  }
6762
- /** Removes every row from threads/messages/resources tables. Intended for tests. */
6819
+ /** Removes every row from threads/messages/resources and run fence tables. Intended for tests. */
6763
6820
  async dangerouslyClearAll() {
6764
6821
  await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_MESSAGES });
6765
6822
  await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_THREADS });
6766
6823
  await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_RESOURCES });
6824
+ await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES });
6825
+ }
6826
+ supportsRunFencing() {
6827
+ return true;
6828
+ }
6829
+ async raiseRunFence(fence) {
6830
+ const table = quoteIdent(_mastra_core_storage.TABLE_MEMORY_RUN_FENCES, "table name");
6831
+ const runIdCol = quoteIdent("runId", "column name");
6832
+ const ownerIdCol = quoteIdent("ownerId", "column name");
6833
+ const retiredAtCol = quoteIdent("retiredAt", "column name");
6834
+ try {
6835
+ return await inTransaction(this.db, async (tx) => {
6836
+ const [rows] = await tx.run({
6837
+ sql: `SELECT generation, ${ownerIdCol} AS ownerId FROM ${table} WHERE ${runIdCol} = @runId`,
6838
+ params: { runId: fence.runId },
6839
+ json: true
6840
+ });
6841
+ const row = rows[0];
6842
+ if (!row) {
6843
+ await tx.runUpdate({
6844
+ sql: `INSERT INTO ${table} (${runIdCol}, generation, ${ownerIdCol}) VALUES (@runId, @generation, @ownerId)`,
6845
+ params: {
6846
+ runId: fence.runId,
6847
+ generation: fence.generation,
6848
+ ownerId: fence.ownerId
6849
+ }
6850
+ });
6851
+ return true;
6852
+ }
6853
+ const generation = Number(row.generation);
6854
+ const raises = generation < fence.generation;
6855
+ const reaffirms = generation === fence.generation && String(row.ownerId) === fence.ownerId;
6856
+ if (!raises && !reaffirms) return false;
6857
+ await tx.runUpdate({
6858
+ sql: `UPDATE ${table} SET generation = @generation, ${ownerIdCol} = @ownerId, ${retiredAtCol} = NULL
6859
+ WHERE ${runIdCol} = @runId`,
6860
+ params: {
6861
+ runId: fence.runId,
6862
+ generation: fence.generation,
6863
+ ownerId: fence.ownerId
6864
+ }
6865
+ });
6866
+ return true;
6867
+ });
6868
+ } catch (error) {
6869
+ throw new _mastra_core_error.MastraError({
6870
+ id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "RAISE_RUN_FENCE", "FAILED"),
6871
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
6872
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
6873
+ details: { runId: fence.runId }
6874
+ }, error);
6875
+ }
6876
+ }
6877
+ async retireRunFence(fence) {
6878
+ const table = quoteIdent(_mastra_core_storage.TABLE_MEMORY_RUN_FENCES, "table name");
6879
+ try {
6880
+ return await inTransaction(this.db, async (tx) => {
6881
+ const [count] = await tx.runUpdate({
6882
+ sql: `UPDATE ${table} SET ${quoteIdent("retiredAt", "column name")} = ${DB_NOW_MS}
6883
+ WHERE ${quoteIdent("runId", "column name")} = @runId AND generation = @generation
6884
+ AND ${quoteIdent("ownerId", "column name")} = @ownerId`,
6885
+ params: {
6886
+ runId: fence.runId,
6887
+ generation: fence.generation,
6888
+ ownerId: fence.ownerId
6889
+ }
6890
+ });
6891
+ return count === 1;
6892
+ });
6893
+ } catch (error) {
6894
+ throw new _mastra_core_error.MastraError({
6895
+ id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "RETIRE_RUN_FENCE", "FAILED"),
6896
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
6897
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
6898
+ details: { runId: fence.runId }
6899
+ }, error);
6900
+ }
6901
+ }
6902
+ /** The fence check for a write already running in a read-write transaction. */
6903
+ async #assertFence(tx, fence, operation) {
6904
+ if (fence) await assertRunFence(tx, _mastra_core_storage.TABLE_MEMORY_RUN_FENCES, fence, operation);
6767
6905
  }
6768
6906
  /**
6769
6907
  * Fetches a thread row by id, or `null` when absent.
@@ -6932,9 +7070,9 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
6932
7070
  }
6933
7071
  }
6934
7072
  /** Upserts a thread row by id (`INSERT OR UPDATE` semantics). */
6935
- async saveThread({ thread }) {
7073
+ async saveThread({ thread, fence }) {
6936
7074
  try {
6937
- await this.db.upsert({
7075
+ await withRunFence(this.db, _mastra_core_storage.TABLE_MEMORY_RUN_FENCES, (0, _mastra_core_storage.resolveRunFence)(this, fence), "saveThread", (tx) => this.db.upsert({
6938
7076
  tableName: _mastra_core_storage.TABLE_THREADS,
6939
7077
  record: {
6940
7078
  id: thread.id,
@@ -6943,10 +7081,12 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
6943
7081
  metadata: thread.metadata ?? {},
6944
7082
  createdAt: thread.createdAt,
6945
7083
  updatedAt: thread.updatedAt
6946
- }
6947
- });
7084
+ },
7085
+ transaction: tx
7086
+ }));
6948
7087
  return thread;
6949
7088
  } catch (error) {
7089
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
6950
7090
  throw new _mastra_core_error.MastraError({
6951
7091
  id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "SAVE_THREAD", "FAILED"),
6952
7092
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -6956,7 +7096,8 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
6956
7096
  }
6957
7097
  }
6958
7098
  /** Updates a thread's title and merges the metadata payload. */
6959
- async updateThread({ id, title, metadata }) {
7099
+ async updateThread({ id, title, metadata, fence: explicitFence }) {
7100
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence);
6960
7101
  const tableThreads = quoteIdent(_mastra_core_storage.TABLE_THREADS, "table name");
6961
7102
  const now = /* @__PURE__ */ new Date();
6962
7103
  let merged = {};
@@ -6964,6 +7105,7 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
6964
7105
  try {
6965
7106
  await this.db.runWithAbortRetry(() => this.database.runTransactionAsync(async (tx) => {
6966
7107
  try {
7108
+ await this.#assertFence(tx, fence, "updateThread");
6967
7109
  const [rows] = await tx.run({
6968
7110
  sql: `SELECT * FROM ${tableThreads} WHERE id = @id LIMIT 1`,
6969
7111
  params: { id },
@@ -7430,8 +7572,9 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7430
7572
  }
7431
7573
  }
7432
7574
  /** Upserts a batch of messages and bumps every touched thread's `updatedAt` in a single transaction. */
7433
- async saveMessages({ messages }) {
7575
+ async saveMessages({ messages, fence: explicitFence }) {
7434
7576
  if (messages.length === 0) return { messages: [] };
7577
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence);
7435
7578
  const threadIds = /* @__PURE__ */ new Set();
7436
7579
  for (const message of messages) {
7437
7580
  if (!message.threadId) throw new _mastra_core_error.MastraError({
@@ -7457,6 +7600,7 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7457
7600
  try {
7458
7601
  await this.db.runWithAbortRetry(() => this.database.runTransactionAsync(async (tx) => {
7459
7602
  try {
7603
+ await this.#assertFence(tx, fence, "saveMessages");
7460
7604
  for (const message of messages) {
7461
7605
  if (!message.resourceId) throw new Error("Expected to find a resourceId for message");
7462
7606
  const sql = `INSERT OR UPDATE INTO ${tableMessages} (id, ${quoteIdent("thread_id", "column name")}, content, ${quoteIdent("createdAt", "column name")}, role, type, ${quoteIdent("resourceId", "column name")})
@@ -7494,6 +7638,7 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7494
7638
  }));
7495
7639
  return { messages: new _mastra_core_agent.MessageList().add(messages, "memory").get.all.db() };
7496
7640
  } catch (error) {
7641
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
7497
7642
  throw new _mastra_core_error.MastraError({
7498
7643
  id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "SAVE_MESSAGES", "FAILED"),
7499
7644
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -7503,8 +7648,9 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7503
7648
  }
7504
7649
  }
7505
7650
  /** Merges partial updates onto existing messages and bumps affected thread `updatedAt`. */
7506
- async updateMessages({ messages }) {
7651
+ async updateMessages({ messages, fence: explicitFence }) {
7507
7652
  if (!messages || messages.length === 0) return [];
7653
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence);
7508
7654
  const messageIds = messages.map((m) => m.id);
7509
7655
  const params = {};
7510
7656
  const placeholders = messageIds.map((id, i) => {
@@ -7522,6 +7668,7 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7522
7668
  try {
7523
7669
  await this.db.runWithAbortRetry(() => this.database.runTransactionAsync(async (tx) => {
7524
7670
  try {
7671
+ await this.#assertFence(tx, fence, "updateMessages");
7525
7672
  const [existingRows] = await tx.run({
7526
7673
  sql: selectSql,
7527
7674
  params,
@@ -7597,6 +7744,7 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7597
7744
  }
7598
7745
  }));
7599
7746
  } catch (error) {
7747
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
7600
7748
  throw new _mastra_core_error.MastraError({
7601
7749
  id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "UPDATE_MESSAGES", "FAILED"),
7602
7750
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -7620,8 +7768,9 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7620
7768
  });
7621
7769
  }
7622
7770
  /** Deletes a batch of messages and bumps the affected threads' `updatedAt`. */
7623
- async deleteMessages(messageIds) {
7771
+ async deleteMessages(messageIds, options) {
7624
7772
  if (!messageIds || messageIds.length === 0) return;
7773
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, options?.fence);
7625
7774
  try {
7626
7775
  const messageTableName = quoteIdent(_mastra_core_storage.TABLE_MESSAGES, "table name");
7627
7776
  const threadTableName = quoteIdent(_mastra_core_storage.TABLE_THREADS, "table name");
@@ -7639,6 +7788,7 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7639
7788
  const threadIds = threadRows.map((r) => r.thread_id).filter(Boolean);
7640
7789
  await this.db.runWithAbortRetry(() => this.database.runTransactionAsync(async (tx) => {
7641
7790
  try {
7791
+ await this.#assertFence(tx, fence, "deleteMessages");
7642
7792
  await tx.runUpdate({
7643
7793
  sql: `DELETE FROM ${messageTableName} WHERE id IN (${placeholders})`,
7644
7794
  params
@@ -7656,6 +7806,7 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7656
7806
  }
7657
7807
  }));
7658
7808
  } catch (error) {
7809
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
7659
7810
  throw new _mastra_core_error.MastraError({
7660
7811
  id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "DELETE_MESSAGES", "FAILED"),
7661
7812
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -7714,13 +7865,15 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7714
7865
  return resource;
7715
7866
  }
7716
7867
  /** Updates a resource's `workingMemory` and merges its `metadata`. */
7717
- async updateResource({ resourceId, workingMemory, metadata }) {
7868
+ async updateResource({ resourceId, workingMemory, metadata, fence: explicitFence }) {
7869
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence);
7718
7870
  const tableResources = quoteIdent(_mastra_core_storage.TABLE_RESOURCES, "table name");
7719
7871
  const now = /* @__PURE__ */ new Date();
7720
7872
  let updated = null;
7721
7873
  try {
7722
7874
  await this.db.runWithAbortRetry(() => this.database.runTransactionAsync(async (tx) => {
7723
7875
  try {
7876
+ await this.#assertFence(tx, fence, "updateResource");
7724
7877
  const [rows] = await tx.run({
7725
7878
  sql: `SELECT * FROM ${tableResources} WHERE id = @id LIMIT 1`,
7726
7879
  params: { id: resourceId },
@@ -7784,6 +7937,7 @@ var MemorySpanner = class MemorySpanner extends _mastra_core_storage.MemoryStora
7784
7937
  }));
7785
7938
  return updated;
7786
7939
  } catch (error) {
7940
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
7787
7941
  const mastraError = new _mastra_core_error.MastraError({
7788
7942
  id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "UPDATE_RESOURCE", "FAILED"),
7789
7943
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -12291,6 +12445,30 @@ var WorkflowDefinitionsSpanner = class WorkflowDefinitionsSpanner extends _mastr
12291
12445
  };
12292
12446
  //#endregion
12293
12447
  //#region src/storage/domains/workflows/index.ts
12448
+ const RUN_OWNERS_TABLE = quoteIdent(_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, "table name");
12449
+ const RUN_ID = quoteIdent("runId", "column name");
12450
+ const OWNER_ID = quoteIdent("ownerId", "column name");
12451
+ const LEASE_EXPIRES_AT = quoteIdent("leaseExpiresAt", "column name");
12452
+ const UPDATED_AT = quoteIdent("updatedAt", "column name");
12453
+ /** Reads a run's ownership record, judging the lease against the database clock. */
12454
+ async function readRunOwner(q, runId) {
12455
+ const [rows] = await q.run({
12456
+ sql: `SELECT generation, ${OWNER_ID} AS ownerId, ${LEASE_EXPIRES_AT} AS leaseExpiresAt, ${DB_NOW_MS} AS nowMs
12457
+ FROM ${RUN_OWNERS_TABLE} WHERE ${RUN_ID} = @runId`,
12458
+ params: { runId },
12459
+ json: true
12460
+ });
12461
+ const row = rows[0];
12462
+ if (!row) return null;
12463
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
12464
+ return {
12465
+ runId,
12466
+ generation: Number(row.generation),
12467
+ ownerId: String(row.ownerId),
12468
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
12469
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
12470
+ };
12471
+ }
12294
12472
  /**
12295
12473
  * Spanner-backed storage for workflow run snapshots, including persistence,
12296
12474
  * incremental updates, and listing of historical runs.
@@ -12371,12 +12549,19 @@ var WorkflowsSpanner = class WorkflowsSpanner extends _mastra_core_storage.Workf
12371
12549
  const indexes = this.getDefaultIndexDefinitions().filter((idx) => hasStatus || !idx.columns.some((c) => c.startsWith("snapshotStatus")));
12372
12550
  await this.db.createIndexes(indexes);
12373
12551
  }
12374
- /** Creates the workflow snapshot table, the snapshotStatus generated column, and indexes. */
12552
+ /**
12553
+ * Creates the workflow snapshot and run ownership tables, the snapshotStatus
12554
+ * generated column, and indexes.
12555
+ */
12375
12556
  async init() {
12376
12557
  await this.db.createTable({
12377
12558
  tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
12378
12559
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT]
12379
12560
  });
12561
+ await this.db.createTable({
12562
+ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
12563
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]
12564
+ });
12380
12565
  await this.ensureStatusColumn();
12381
12566
  await this.createDefaultIndexes();
12382
12567
  await this.createCustomIndexes();
@@ -12425,9 +12610,121 @@ var WorkflowsSpanner = class WorkflowsSpanner extends _mastra_core_storage.Workf
12425
12610
  if (!this.indexes || this.indexes.length === 0) return;
12426
12611
  await this.db.createIndexes(this.indexes);
12427
12612
  }
12428
- /** Removes every workflow snapshot row. Intended for tests. */
12613
+ /** Removes every workflow snapshot and run ownership row. Intended for tests. */
12429
12614
  async dangerouslyClearAll() {
12430
12615
  await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT });
12616
+ await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS });
12617
+ }
12618
+ supportsRunFencing() {
12619
+ return true;
12620
+ }
12621
+ #ownershipError(operation, runId, error) {
12622
+ return new _mastra_core_error.MastraError({
12623
+ id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", operation, "FAILED"),
12624
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
12625
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
12626
+ details: { runId }
12627
+ }, error);
12628
+ }
12629
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
12630
+ try {
12631
+ return await inTransaction(this.db, async (tx) => {
12632
+ const existing = await readRunOwner(tx, runId);
12633
+ if (!existing) {
12634
+ if (expectedGeneration !== void 0 && expectedGeneration !== 0) return {
12635
+ acquired: false,
12636
+ record: null
12637
+ };
12638
+ await tx.runUpdate({
12639
+ sql: `INSERT INTO ${RUN_OWNERS_TABLE} (${RUN_ID}, generation, ${OWNER_ID}, ${LEASE_EXPIRES_AT}, ${UPDATED_AT})
12640
+ VALUES (@runId, ${DB_NOW_MS}, @ownerId, ${DB_NOW_MS} + @leaseMs, ${DB_NOW_MS})`,
12641
+ params: {
12642
+ runId,
12643
+ ownerId,
12644
+ leaseMs
12645
+ }
12646
+ });
12647
+ return {
12648
+ acquired: true,
12649
+ record: await readRunOwner(tx, runId)
12650
+ };
12651
+ }
12652
+ if (!(expectedGeneration === void 0 || existing.generation === expectedGeneration) || !force && existing.live) return {
12653
+ acquired: false,
12654
+ record: existing
12655
+ };
12656
+ await tx.runUpdate({
12657
+ sql: `UPDATE ${RUN_OWNERS_TABLE}
12658
+ SET generation = generation + 1, ${OWNER_ID} = @ownerId, ${LEASE_EXPIRES_AT} = ${DB_NOW_MS} + @leaseMs,
12659
+ ${UPDATED_AT} = ${DB_NOW_MS}
12660
+ WHERE ${RUN_ID} = @runId`,
12661
+ params: {
12662
+ runId,
12663
+ ownerId,
12664
+ leaseMs
12665
+ }
12666
+ });
12667
+ return {
12668
+ acquired: true,
12669
+ record: await readRunOwner(tx, runId)
12670
+ };
12671
+ });
12672
+ } catch (error) {
12673
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", runId, error);
12674
+ }
12675
+ }
12676
+ async renewRunOwnership({ leaseMs, ...fence }) {
12677
+ try {
12678
+ return await inTransaction(this.db, async (tx) => {
12679
+ const [count] = await tx.runUpdate({
12680
+ sql: `UPDATE ${RUN_OWNERS_TABLE}
12681
+ SET ${LEASE_EXPIRES_AT} = ${DB_NOW_MS} + @leaseMs, ${UPDATED_AT} = ${DB_NOW_MS}
12682
+ WHERE ${RUN_ID} = @runId AND generation = @generation AND ${OWNER_ID} = @ownerId
12683
+ AND ${LEASE_EXPIRES_AT} IS NOT NULL`,
12684
+ params: {
12685
+ leaseMs,
12686
+ runId: fence.runId,
12687
+ generation: fence.generation,
12688
+ ownerId: fence.ownerId
12689
+ }
12690
+ });
12691
+ const record = await readRunOwner(tx, fence.runId);
12692
+ return count === 1 && record ? {
12693
+ renewed: true,
12694
+ record
12695
+ } : {
12696
+ renewed: false,
12697
+ record
12698
+ };
12699
+ });
12700
+ } catch (error) {
12701
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, error);
12702
+ }
12703
+ }
12704
+ async releaseRunOwnership(fence) {
12705
+ try {
12706
+ return await inTransaction(this.db, async (tx) => {
12707
+ const [count] = await tx.runUpdate({
12708
+ sql: `UPDATE ${RUN_OWNERS_TABLE} SET ${LEASE_EXPIRES_AT} = NULL, ${UPDATED_AT} = ${DB_NOW_MS}
12709
+ WHERE ${RUN_ID} = @runId AND generation = @generation AND ${OWNER_ID} = @ownerId`,
12710
+ params: {
12711
+ runId: fence.runId,
12712
+ generation: fence.generation,
12713
+ ownerId: fence.ownerId
12714
+ }
12715
+ });
12716
+ return count === 1;
12717
+ });
12718
+ } catch (error) {
12719
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
12720
+ }
12721
+ }
12722
+ async getRunOwnership({ runId }) {
12723
+ try {
12724
+ return await readRunOwner(this.database, runId);
12725
+ } catch (error) {
12726
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
12727
+ }
12431
12728
  }
12432
12729
  /** Decodes a raw Spanner snapshot row into the public `WorkflowRun` shape. */
12433
12730
  parseWorkflowRun(row) {
@@ -12451,12 +12748,14 @@ var WorkflowsSpanner = class WorkflowsSpanner extends _mastra_core_storage.Workf
12451
12748
  };
12452
12749
  }
12453
12750
  /** Upserts a workflow run snapshot, preserving the original `createdAt` on update. */
12454
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
12751
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence: explicitFence }) {
12752
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
12455
12753
  const now = /* @__PURE__ */ new Date();
12456
12754
  const table = quoteIdent(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "table name");
12457
12755
  try {
12458
12756
  await this.db.runWithAbortRetry(() => this.database.runTransactionAsync(async (tx) => {
12459
12757
  try {
12758
+ if (fence) await assertRunFence(tx, _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, fence, "persistWorkflowSnapshot");
12460
12759
  const [rows] = await tx.run({
12461
12760
  sql: `SELECT ${quoteIdent("createdAt", "column name")}, ${quoteIdent("resourceId", "column name")} FROM ${table}
12462
12761
  WHERE workflow_name = @workflow_name AND run_id = @run_id`,
@@ -12490,6 +12789,7 @@ var WorkflowsSpanner = class WorkflowsSpanner extends _mastra_core_storage.Workf
12490
12789
  }
12491
12790
  }));
12492
12791
  } catch (error) {
12792
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
12493
12793
  throw new _mastra_core_error.MastraError({
12494
12794
  id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
12495
12795
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -12526,12 +12826,14 @@ var WorkflowsSpanner = class WorkflowsSpanner extends _mastra_core_storage.Workf
12526
12826
  }
12527
12827
  }
12528
12828
  /** Merges a step result and request-context delta into a snapshot inside an RW transaction. */
12529
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
12829
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
12830
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
12530
12831
  const table = quoteIdent(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "table name");
12531
12832
  try {
12532
12833
  let mergedContext = {};
12533
12834
  await this.db.runWithAbortRetry(() => this.database.runTransactionAsync(async (tx) => {
12534
12835
  try {
12836
+ if (fence) await assertRunFence(tx, _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, fence, "updateWorkflowResults");
12535
12837
  const [rows] = await tx.run({
12536
12838
  sql: `SELECT snapshot, ${quoteIdent("createdAt", "column name")} FROM ${table}
12537
12839
  WHERE workflow_name = @workflow_name AND run_id = @run_id`,
@@ -12591,6 +12893,7 @@ var WorkflowsSpanner = class WorkflowsSpanner extends _mastra_core_storage.Workf
12591
12893
  }));
12592
12894
  return mergedContext;
12593
12895
  } catch (error) {
12896
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
12594
12897
  throw new _mastra_core_error.MastraError({
12595
12898
  id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
12596
12899
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -12604,13 +12907,15 @@ var WorkflowsSpanner = class WorkflowsSpanner extends _mastra_core_storage.Workf
12604
12907
  }
12605
12908
  }
12606
12909
  /** Applies a top-level snapshot patch (e.g. status, value) inside an RW transaction. */
12607
- async updateWorkflowState({ workflowName, runId, opts }) {
12910
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
12911
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
12608
12912
  const table = quoteIdent(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "table name");
12609
12913
  try {
12610
12914
  let updated;
12611
12915
  await this.db.runWithAbortRetry(() => this.database.runTransactionAsync(async (tx) => {
12612
12916
  updated = void 0;
12613
12917
  try {
12918
+ if (fence) await assertRunFence(tx, _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, fence, "updateWorkflowState");
12614
12919
  const [rows] = await tx.run({
12615
12920
  sql: `SELECT snapshot FROM ${table} WHERE workflow_name = @workflow_name AND run_id = @run_id`,
12616
12921
  params: {
@@ -12721,17 +13026,18 @@ var WorkflowsSpanner = class WorkflowsSpanner extends _mastra_core_storage.Workf
12721
13026
  }
12722
13027
  }
12723
13028
  /** Deletes the snapshot row for a `(workflowName, runId)` pair. */
12724
- async deleteWorkflowRunById({ runId, workflowName }) {
13029
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
12725
13030
  const table = quoteIdent(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "table name");
12726
13031
  try {
12727
- await this.db.runDml({
13032
+ await withRunFence(this.db, _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "deleteWorkflowRunById", (tx) => this.db.runDml({
12728
13033
  sql: `DELETE FROM ${table} WHERE workflow_name = @workflow_name AND run_id = @run_id`,
12729
13034
  params: {
12730
13035
  workflow_name: workflowName,
12731
13036
  run_id: runId
12732
13037
  }
12733
- });
13038
+ }, tx));
12734
13039
  } catch (error) {
13040
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
12735
13041
  throw new _mastra_core_error.MastraError({
12736
13042
  id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
12737
13043
  domain: _mastra_core_error.ErrorDomain.STORAGE,