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