@mastra/libsql 1.26.0-alpha.2 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,17 +1,360 @@
1
1
  import { createClient } from "@libsql/client";
2
2
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
- import { AGENTS_SCHEMA, AGENT_VERSIONS_SCHEMA, AgentsStorage, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, ExperimentsStorage, FAVORITES_SCHEMA, FactoryStorage, FavoritesStorage, HarnessStorage, KNOWLEDGE_SEMANTIC_OUTBOX_SCHEMA, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MCP_CLIENTS_SCHEMA, MCP_CLIENT_VERSIONS_SCHEMA, MCP_SERVERS_SCHEMA, MCP_SERVER_VERSIONS_SCHEMA, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PROMPT_BLOCKS_SCHEMA, PROMPT_BLOCK_VERSIONS_SCHEMA, PromptBlocksStorage, SCORERS_SCHEMA, SCORER_DEFINITIONS_SCHEMA, SCORER_DEFINITION_VERSIONS_SCHEMA, SKILLS_SCHEMA, SKILL_BLOBS_SCHEMA, SKILL_VERSIONS_SCHEMA, SPAN_SCHEMA, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_CHANNEL_CONFIG, TABLE_CHANNEL_INSTALLATIONS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_HARNESS_SESSIONS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MESSAGES, TABLE_NOTIFICATIONS, 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_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, THREAD_STATE_SCHEMA, TOOL_PROVIDER_CONNECTIONS_SCHEMA, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WORKSPACES_SCHEMA, WORKSPACE_VERSIONS_SCHEMA, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, canonicalizeKnowledgeScope, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getSqlType, hasErrorCode, isKnowledgeScopeVisible, knowledgeScopeKey, knowledgeSemanticDocumentId, knowledgeSemanticIdempotencyKey, listTracesArgsSchema, matchesExpectedWorkflowStatus, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, parseKnowledgeNodeCursor, parseKnowledgeWikilinks, resolveListOrderBy, resolveRetentionTargets, retentionCutoffMs, runRetentionBatches, safelyParseJSON, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
3
+ import { AGENTS_SCHEMA, AGENT_VERSIONS_SCHEMA, AgentsStorage, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, ExperimentsStorage, FAVORITES_SCHEMA, FactoryStorage, FavoritesStorage, HarnessStorage, KNOWLEDGE_SEMANTIC_OUTBOX_SCHEMA, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MCP_CLIENTS_SCHEMA, MCP_CLIENT_VERSIONS_SCHEMA, MCP_SERVERS_SCHEMA, MCP_SERVER_VERSIONS_SCHEMA, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PROMPT_BLOCKS_SCHEMA, PROMPT_BLOCK_VERSIONS_SCHEMA, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, SCORERS_SCHEMA, SCORER_DEFINITIONS_SCHEMA, SCORER_DEFINITION_VERSIONS_SCHEMA, SKILLS_SCHEMA, SKILL_BLOBS_SCHEMA, SKILL_VERSIONS_SCHEMA, SPAN_SCHEMA, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_CHANNEL_CONFIG, TABLE_CHANNEL_INSTALLATIONS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_HARNESS_SESSIONS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, 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_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, THREAD_STATE_SCHEMA, TOOL_PROVIDER_CONNECTIONS_SCHEMA, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WORKSPACES_SCHEMA, WORKSPACE_VERSIONS_SCHEMA, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, canonicalizeKnowledgeScope, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getSqlType, hasErrorCode, isKnowledgeScopeVisible, isRunFenceConflictError, knowledgeScopeKey, knowledgeSemanticDocumentId, knowledgeSemanticIdempotencyKey, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, parseKnowledgeNodeCursor, parseKnowledgeWikilinks, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, runRetentionBatches, safelyParseJSON, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
4
4
  import { parseFieldKey, parseSqlIdentifier } from "@mastra/core/utils";
5
5
  import { MastraVector, validateTopK, validateUpsertInput } from "@mastra/core/vector";
6
+ import { basename, dirname, isAbsolute, join, resolve } from "path";
7
+ import { AsyncLocalStorage } from "async_hooks";
6
8
  import { BaseFilterTranslator } from "@mastra/core/vector/filter";
7
9
  import { realpath } from "fs/promises";
8
- import { basename, dirname, isAbsolute, join, resolve } from "path";
9
10
  import { MastraBase } from "@mastra/core/base";
10
11
  import { randomUUID } from "crypto";
11
12
  import { coreFeatures } from "@mastra/core/features";
12
13
  import { MessageList } from "@mastra/core/agent";
13
14
  import { saveScorePayloadSchema } from "@mastra/core/evals";
14
15
  import { skillSnapshotFieldValuesEqual } from "@mastra/core/storage/domains/skills";
16
+ //#region src/shared/file-transaction-gate.ts
17
+ /** Interactive transactions open in this process, per database file. */
18
+ const openTransactions = /* @__PURE__ */ new Map();
19
+ /** The database file a local `file:` url points at, or `undefined` for anything else. */
20
+ function localDatabaseFileKey(url) {
21
+ if (!url.startsWith("file:") || url.includes(":memory:")) return void 0;
22
+ const path = url.slice(5).split("?")[0];
23
+ return path ? resolve(path.replace(/^\/\/(?=\/)/, "")) : void 0;
24
+ }
25
+ /**
26
+ * Wraps a local `file:` client so its calls wait for interactive transactions
27
+ * that other clients in this process hold open on the same database file,
28
+ * instead of blocking the thread.
29
+ *
30
+ * `@libsql/client` runs local statements synchronously. When one client holds
31
+ * a write transaction open across an `await` and another client writes the same
32
+ * file, SQLite's busy handler spins on the event loop's only thread for
33
+ * `busy_timeout`, so the transaction holding the lock can't resume and the write
34
+ * fails with `SQLITE_BUSY`. Waiting lets the transaction finish first.
35
+ *
36
+ * Only calls on other clients wait; a client already orders its own writes
37
+ * behind its transactions (see `withClientWriteLock`). A call waits at most
38
+ * `maxWaitMs` (the busy timeout), then goes ahead as it would without the gate,
39
+ * so code that awaits another client from inside its own transaction can't hang.
40
+ * Clients in other processes are unaffected and still rely on `busy_timeout`.
41
+ */
42
+ function gateLocalFileClient(client, key, { maxWaitMs = 5e3 } = {}) {
43
+ let gated;
44
+ const othersOpen = () => [...openTransactions.get(key) ?? []].filter((open) => open.owner !== gated);
45
+ const waitForOthers = () => {
46
+ if (othersOpen().length === 0) return;
47
+ return (async () => {
48
+ let timer;
49
+ const gaveUp = new Promise((r) => timer = setTimeout(r, maxWaitMs, "timeout"));
50
+ try {
51
+ for (let open = othersOpen(); open.length > 0; open = othersOpen()) if (await Promise.race([Promise.all(open.map((o) => o.done)), gaveUp]) === "timeout") return false;
52
+ return true;
53
+ } finally {
54
+ clearTimeout(timer);
55
+ }
56
+ })();
57
+ };
58
+ const call = (run) => {
59
+ const waiting = waitForOthers();
60
+ return waiting ? waiting.then(run) : run();
61
+ };
62
+ const transaction = async (open, mode) => {
63
+ const deadline = Date.now() + maxWaitMs;
64
+ for (let waiting = waitForOthers(); waiting; waiting = Date.now() < deadline ? waitForOthers() : void 0) if (!await waiting) break;
65
+ let finish;
66
+ const entry = {
67
+ owner: gated,
68
+ done: new Promise((r) => finish = r)
69
+ };
70
+ let set = openTransactions.get(key);
71
+ if (!set) openTransactions.set(key, set = /* @__PURE__ */ new Set());
72
+ set.add(entry);
73
+ const end = () => {
74
+ if (!set.delete(entry)) return;
75
+ if (set.size === 0 && openTransactions.get(key) === set) openTransactions.delete(key);
76
+ finish();
77
+ };
78
+ let tx;
79
+ try {
80
+ tx = await open(mode);
81
+ } catch (error) {
82
+ end();
83
+ throw error;
84
+ }
85
+ return new Proxy(tx, { get(target, prop) {
86
+ switch (prop) {
87
+ case "execute":
88
+ case "batch":
89
+ case "executeMultiple": {
90
+ const method = Reflect.get(target, prop);
91
+ return async (...args) => {
92
+ try {
93
+ return await method.apply(target, args);
94
+ } finally {
95
+ if (target.closed) end();
96
+ }
97
+ };
98
+ }
99
+ case "commit":
100
+ case "rollback": {
101
+ const method = Reflect.get(target, prop);
102
+ return async () => {
103
+ try {
104
+ await method.call(target);
105
+ } finally {
106
+ end();
107
+ }
108
+ };
109
+ }
110
+ case "close": {
111
+ const method = Reflect.get(target, prop);
112
+ return () => {
113
+ try {
114
+ method.call(target);
115
+ } finally {
116
+ end();
117
+ }
118
+ };
119
+ }
120
+ default: {
121
+ const value = Reflect.get(target, prop);
122
+ return typeof value === "function" ? value.bind(target) : value;
123
+ }
124
+ }
125
+ } });
126
+ };
127
+ gated = new Proxy(client, { get(target, prop) {
128
+ switch (prop) {
129
+ case "transaction": {
130
+ const open = Reflect.get(target, prop);
131
+ return (mode) => transaction(open.bind(target), mode);
132
+ }
133
+ case "execute":
134
+ case "batch":
135
+ case "executeMultiple":
136
+ case "migrate": {
137
+ const method = Reflect.get(target, prop);
138
+ return (...args) => call(() => method.apply(target, args));
139
+ }
140
+ default: {
141
+ const value = Reflect.get(target, prop);
142
+ return typeof value === "function" ? value.bind(target) : value;
143
+ }
144
+ }
145
+ } });
146
+ return gated;
147
+ }
148
+ /** Applies {@link gateLocalFileClient} when `url` is a local database file; returns `client` otherwise. */
149
+ function gateIfLocalFile(client, url, options) {
150
+ const key = localDatabaseFileKey(url);
151
+ return key ? gateLocalFileClient(client, key, options) : client;
152
+ }
153
+ //#endregion
154
+ //#region src/shared/reset-after-busy-client.ts
155
+ function isBusy(error) {
156
+ return error?.code === "SQLITE_BUSY";
157
+ }
158
+ function firstOpen(hold) {
159
+ while (hold && !hold.open) hold = hold.outer;
160
+ return hold;
161
+ }
162
+ /**
163
+ * Wraps a pooled local `file:` client so a statement refused with `SQLITE_BUSY`
164
+ * doesn't leave its connection unusable.
165
+ *
166
+ * libsql leaves a write statement whose run failed busy in progress until it is
167
+ * garbage collected (tursodatabase/libsql-js#237). Until then no transaction on
168
+ * that connection can commit ("cannot commit transaction - SQL statements in
169
+ * progress"), and an autocommit write on it stays uncommitted while holding the
170
+ * database's write lock. The pool hands the same connection out again, so
171
+ * retrying the write doesn't help.
172
+ *
173
+ * After a busy failure the wrapper reopens the client's connections, which
174
+ * finalizes the stuck statement. Reopening closes every connection, including
175
+ * borrowed ones, so it waits until no call or transaction made through the
176
+ * wrapper is in flight. New calls wait for the reset rather than join the calls
177
+ * it waits for, so a client that is never idle still gets reset. A call made
178
+ * while its caller holds an open transaction, or from inside another call,
179
+ * doesn't wait: the reset waits for what its caller holds, so waiting would
180
+ * deadlock. Reopened connections start from the pool's connection options,
181
+ * so `afterReset` runs right after to restore any per-connection settings
182
+ * applied after opening, and calls that waited for the reset go on once it
183
+ * settles, even if it fails. A call waits at most `maxWaitMs` (default 5s, the
184
+ * busy timeout) so a long-held transaction can't stall every later call; after
185
+ * that it goes ahead on the current pool. A transaction SQLite rolled back on its
186
+ * own is released as soon as the failing call returns. It must use the client it was given, not the
187
+ * wrapper: calls through the wrapper would wait for the reset it is part of.
188
+ * Every other member passes through untouched.
189
+ */
190
+ function resetConnectionsAfterBusy(client, { afterReset, maxWaitMs = 5e3 } = {}) {
191
+ let inFlight = 0;
192
+ let pendingReset;
193
+ const holds = new AsyncLocalStorage();
194
+ const finishReset = () => {
195
+ const reset = pendingReset;
196
+ pendingReset = void 0;
197
+ reset?.finish();
198
+ };
199
+ const settle = () => {
200
+ inFlight--;
201
+ if (client.closed) return finishReset();
202
+ if (!pendingReset || inFlight > 0) return;
203
+ client.reconnect();
204
+ if (!afterReset) return finishReset();
205
+ const reset = pendingReset;
206
+ Promise.resolve().then(afterReset).catch(() => {}).finally(() => {
207
+ if (pendingReset === reset) finishReset();
208
+ });
209
+ };
210
+ const noteBusy = (error) => {
211
+ if (!isBusy(error) || pendingReset) return;
212
+ let finish;
213
+ pendingReset = {
214
+ done: new Promise((resolve) => finish = resolve),
215
+ finish
216
+ };
217
+ };
218
+ const enter = (callerHolds) => {
219
+ if (!pendingReset || callerHolds || client.closed) {
220
+ inFlight++;
221
+ return;
222
+ }
223
+ return (async () => {
224
+ let timer;
225
+ const gaveUp = new Promise((resolve) => timer = setTimeout(resolve, maxWaitMs, "timeout"));
226
+ try {
227
+ while (pendingReset && !client.closed) if (await Promise.race([pendingReset.done, gaveUp]) === "timeout") break;
228
+ } finally {
229
+ clearTimeout(timer);
230
+ }
231
+ inFlight++;
232
+ })();
233
+ };
234
+ const run = async (call) => {
235
+ const hold = {
236
+ open: true,
237
+ outer: firstOpen(holds.getStore())
238
+ };
239
+ const waiting = enter(hold.outer !== void 0);
240
+ if (waiting) await waiting;
241
+ try {
242
+ return await holds.run(hold, call);
243
+ } catch (error) {
244
+ noteBusy(error);
245
+ throw error;
246
+ } finally {
247
+ hold.open = false;
248
+ settle();
249
+ }
250
+ };
251
+ const transaction = (open, mode) => {
252
+ const hold = {
253
+ open: true,
254
+ outer: firstOpen(holds.getStore())
255
+ };
256
+ const waiting = enter(hold.outer !== void 0);
257
+ holds.enterWith(hold);
258
+ return openTransaction(open, mode, hold, waiting);
259
+ };
260
+ const openTransaction = async (open, mode, hold, waiting) => {
261
+ let tx;
262
+ try {
263
+ if (waiting) await waiting;
264
+ tx = await open(mode);
265
+ } catch (error) {
266
+ hold.open = false;
267
+ noteBusy(error);
268
+ settle();
269
+ throw error;
270
+ }
271
+ const end = () => {
272
+ if (!hold.open) return;
273
+ hold.open = false;
274
+ settle();
275
+ };
276
+ return new Proxy(tx, { get(target, prop) {
277
+ switch (prop) {
278
+ case "execute":
279
+ case "batch":
280
+ case "executeMultiple": {
281
+ const method = Reflect.get(target, prop);
282
+ return async (...args) => {
283
+ try {
284
+ return await method.apply(target, args);
285
+ } catch (error) {
286
+ noteBusy(error);
287
+ if (target.closed) try {
288
+ target.close();
289
+ } finally {
290
+ end();
291
+ }
292
+ throw error;
293
+ }
294
+ };
295
+ }
296
+ case "commit":
297
+ case "rollback": {
298
+ const method = Reflect.get(target, prop);
299
+ return async () => {
300
+ try {
301
+ await method.call(target);
302
+ } catch (error) {
303
+ noteBusy(error);
304
+ throw error;
305
+ } finally {
306
+ end();
307
+ }
308
+ };
309
+ }
310
+ case "close": {
311
+ const method = Reflect.get(target, prop);
312
+ return () => {
313
+ try {
314
+ method.call(target);
315
+ } finally {
316
+ end();
317
+ }
318
+ };
319
+ }
320
+ default: {
321
+ const value = Reflect.get(target, prop);
322
+ return typeof value === "function" ? value.bind(target) : value;
323
+ }
324
+ }
325
+ } });
326
+ };
327
+ return new Proxy(client, { get(target, prop) {
328
+ switch (prop) {
329
+ case "transaction": {
330
+ const open = Reflect.get(target, prop);
331
+ return (mode) => transaction(open.bind(target), mode);
332
+ }
333
+ case "execute":
334
+ case "batch":
335
+ case "executeMultiple":
336
+ case "migrate": {
337
+ const method = Reflect.get(target, prop);
338
+ return (...args) => run(() => method.apply(target, args));
339
+ }
340
+ case "close": {
341
+ const method = Reflect.get(target, prop);
342
+ return () => {
343
+ try {
344
+ method.call(target);
345
+ } finally {
346
+ finishReset();
347
+ }
348
+ };
349
+ }
350
+ default: {
351
+ const value = Reflect.get(target, prop);
352
+ return typeof value === "function" ? value.bind(target) : value;
353
+ }
354
+ }
355
+ } });
356
+ }
357
+ //#endregion
15
358
  //#region src/shared/single-connection-client.ts
16
359
  /**
17
360
  * Whether `@libsql/client` backs this database with exactly one connection.
@@ -520,7 +863,7 @@ var LibSQLVector = class extends MastraVector {
520
863
  this.turso = isSingleConnectionDatabase({
521
864
  url,
522
865
  syncUrl
523
- }) ? gateSingleConnectionClient(client) : client;
866
+ }) ? gateSingleConnectionClient(client) : isLocalDb ? gateIfLocalFile(resetConnectionsAfterBusy(client, { maxWaitMs: 5e3 }), url, { maxWaitMs: 5e3 }) : client;
524
867
  this.maxRetries = maxRetries;
525
868
  this.initialBackoffMs = initialBackoffMs;
526
869
  if (!Number.isInteger(vectorTopKOverFetchMultiplier) || vectorTopKOverFetchMultiplier < 1) throw new Error("vectorTopKOverFetchMultiplier must be a positive integer");
@@ -1111,6 +1454,110 @@ var LibSQLVector = class extends MastraVector {
1111
1454
  }
1112
1455
  };
1113
1456
  //#endregion
1457
+ //#region src/storage/db/write-lock.ts
1458
+ /**
1459
+ * Per-client write serialization.
1460
+ *
1461
+ * `@libsql/client` >= 0.18.0 pools connections for local `file:` databases, but
1462
+ * SQLite still admits one writer at a time: an interactive
1463
+ * `client.transaction('write')` holds `BEGIN` open across every
1464
+ * `await tx.execute(...)`, and any other write on the same database in that
1465
+ * window contends on the file lock and can fail with `SQLITE_BUSY` once
1466
+ * `busy_timeout` expires.
1467
+ *
1468
+ * This is dormant under the default engine but the evented engine runs many
1469
+ * concurrent workflow snapshot writes per agent run, so a write issued by an
1470
+ * unrelated domain (e.g. creating a dataset experiment) can fail spuriously.
1471
+ *
1472
+ * Serializing every write on a given client closes that window: writes — both
1473
+ * autocommit statements and full interactive transactions — run one at a time,
1474
+ * so none can interleave with an open transaction. Reads are intentionally not
1475
+ * gated; WAL readers never observe a partial write and must not queue behind a
1476
+ * long-running writer.
1477
+ *
1478
+ * `:memory:` databases and embedded replicas get a single pooled connection
1479
+ * instead; see `shared/single-connection-client.ts`, which gates *all* calls
1480
+ * (reads included) behind open transactions for those clients.
1481
+ */
1482
+ const clientWriteChains = /* @__PURE__ */ new WeakMap();
1483
+ /**
1484
+ * Runs `fn` after every previously-enqueued write on `client` has settled, and
1485
+ * returns its result. The chain advances regardless of whether `fn` resolves or
1486
+ * rejects, so one failed write never wedges the queue.
1487
+ */
1488
+ function withClientWriteLock(client, fn) {
1489
+ const result = (clientWriteChains.get(client) ?? Promise.resolve()).then(fn, fn);
1490
+ clientWriteChains.set(client, result.then(() => void 0, () => void 0));
1491
+ return result;
1492
+ }
1493
+ //#endregion
1494
+ //#region src/storage/db/run-fencing.ts
1495
+ /**
1496
+ * Epoch milliseconds on the database clock. `julianday('now')` rather than
1497
+ * `unixepoch('subsec')`, which needs SQLite 3.42 on the server.
1498
+ */
1499
+ const DB_NOW_MS = `CAST(ROUND((julianday('now') - 2440587.5) * 86400000) AS INTEGER)`;
1500
+ /**
1501
+ * Runs `fn` in a write transaction, behind every other write on `client`.
1502
+ * `'write'` opens the transaction with `BEGIN IMMEDIATE`, which takes the
1503
+ * database's write lock up front, so nothing written between a read and a
1504
+ * write inside `fn` can come from another connection.
1505
+ */
1506
+ function inWriteTransaction(client, fn) {
1507
+ return withClientWriteLock(client, async () => {
1508
+ const tx = await client.transaction("write");
1509
+ try {
1510
+ const result = await fn(tx);
1511
+ await tx.commit();
1512
+ return result;
1513
+ } catch (error) {
1514
+ if (!tx.closed) await tx.rollback();
1515
+ throw error;
1516
+ } finally {
1517
+ if (!tx.closed) tx.close();
1518
+ }
1519
+ });
1520
+ }
1521
+ /**
1522
+ * The check half of a fenced write; call it first inside the write's
1523
+ * transaction. The transaction already holds the database's write lock, so
1524
+ * a claim can't commit between this check and the write.
1525
+ */
1526
+ async function assertRunFence(tx, { claimsTable, fence, operation }) {
1527
+ const row = (await tx.execute({
1528
+ sql: `SELECT generation, ownerId FROM ${claimsTable} WHERE runId = ?`,
1529
+ args: [fence.runId]
1530
+ })).rows[0];
1531
+ if (!matchesRunFence(row ? {
1532
+ generation: Number(row.generation),
1533
+ ownerId: String(row.ownerId)
1534
+ } : null, fence)) throw new RunFenceConflictError(fence, operation);
1535
+ }
1536
+ /**
1537
+ * Runs `write` behind the client's write lock: directly when there is no
1538
+ * fence to check, or in a write transaction behind the check when there is.
1539
+ * `write` must issue every statement through the writer it is given; on a
1540
+ * single-connection client, a call on the client itself would wait for the
1541
+ * open transaction forever.
1542
+ */
1543
+ function withRunFence(client, check, write) {
1544
+ if (!check) return withClientWriteLock(client, () => write({
1545
+ execute: (statement) => client.execute(statement),
1546
+ batch: (statements) => client.batch(statements, "write")
1547
+ }));
1548
+ return inWriteTransaction(client, async (tx) => {
1549
+ await assertRunFence(tx, check);
1550
+ return write({
1551
+ execute: (statement) => tx.execute(statement),
1552
+ batch: async (statements) => {
1553
+ const results = [];
1554
+ for (const statement of statements) results.push(await tx.execute(statement));
1555
+ return results;
1556
+ }
1557
+ });
1558
+ });
1559
+ }
1560
+ //#endregion
1114
1561
  //#region src/storage/db/utils.ts
1115
1562
  const MAX_DEPTH = 100;
1116
1563
  /**
@@ -1203,6 +1650,7 @@ function createExecuteWriteOperationWithRetry({ logger, maxRetries, initialBacko
1203
1650
  while (attempts < maxRetries) try {
1204
1651
  return await operationFn();
1205
1652
  } catch (error) {
1653
+ if (isRunFenceConflictError(error)) throw error;
1206
1654
  logger.debug(`LibSQLStore: Error caught in retry loop for ${operationDescription}`, {
1207
1655
  errorType: error.constructor.name,
1208
1656
  errorCode: error.code,
@@ -1363,43 +1811,6 @@ function transformFromSqlRow({ tableName, sqlRow }) {
1363
1811
  return result;
1364
1812
  }
1365
1813
  //#endregion
1366
- //#region src/storage/db/write-lock.ts
1367
- /**
1368
- * Per-client write serialization.
1369
- *
1370
- * `@libsql/client` >= 0.18.0 pools connections for local `file:` databases, but
1371
- * SQLite still admits one writer at a time: an interactive
1372
- * `client.transaction('write')` holds `BEGIN` open across every
1373
- * `await tx.execute(...)`, and any other write on the same database in that
1374
- * window contends on the file lock and can fail with `SQLITE_BUSY` once
1375
- * `busy_timeout` expires.
1376
- *
1377
- * This is dormant under the default engine but the evented engine runs many
1378
- * concurrent workflow snapshot writes per agent run, so a write issued by an
1379
- * unrelated domain (e.g. creating a dataset experiment) can fail spuriously.
1380
- *
1381
- * Serializing every write on a given client closes that window: writes — both
1382
- * autocommit statements and full interactive transactions — run one at a time,
1383
- * so none can interleave with an open transaction. Reads are intentionally not
1384
- * gated; WAL readers never observe a partial write and must not queue behind a
1385
- * long-running writer.
1386
- *
1387
- * `:memory:` databases and embedded replicas get a single pooled connection
1388
- * instead; see `shared/single-connection-client.ts`, which gates *all* calls
1389
- * (reads included) behind open transactions for those clients.
1390
- */
1391
- const clientWriteChains = /* @__PURE__ */ new WeakMap();
1392
- /**
1393
- * Runs `fn` after every previously-enqueued write on `client` has settled, and
1394
- * returns its result. The chain advances regardless of whether `fn` resolves or
1395
- * rejects, so one failed write never wedges the queue.
1396
- */
1397
- function withClientWriteLock(client, fn) {
1398
- const result = (clientWriteChains.get(client) ?? Promise.resolve()).then(fn, fn);
1399
- clientWriteChains.set(client, result.then(() => void 0, () => void 0));
1400
- return result;
1401
- }
1402
- //#endregion
1403
1814
  //#region src/storage/db/index.ts
1404
1815
  /**
1405
1816
  * Default SQLite `busy_timeout` (ms) for local LibSQL connections. Chosen to
@@ -1500,10 +1911,10 @@ var LibSQLDB = class extends MastraBase {
1500
1911
  /**
1501
1912
  * Internal insert implementation without retry logic.
1502
1913
  */
1503
- async doInsert({ tableName, record }) {
1914
+ async doInsert({ tableName, record, check }) {
1504
1915
  const filteredRecord = await this.filterRecordToKnownColumns(tableName, record);
1505
1916
  if (Object.keys(filteredRecord).length === 0) return;
1506
- await withClientWriteLock(this.client, () => this.client.execute(prepareStatement({
1917
+ await withRunFence(this.client, check, (writer) => writer.execute(prepareStatement({
1507
1918
  tableName,
1508
1919
  record: filteredRecord
1509
1920
  })));
@@ -1514,6 +1925,7 @@ var LibSQLDB = class extends MastraBase {
1514
1925
  * @param args - The insert arguments
1515
1926
  * @param args.tableName - The name of the table to insert into
1516
1927
  * @param args.record - The record to insert (key-value pairs)
1928
+ * @param args.check - The run fence the insert must carry, if any
1517
1929
  */
1518
1930
  insert(args) {
1519
1931
  return this.executeWriteOperationWithRetry(() => this.doInsert(args), `insert into table ${args.tableName}`);
@@ -8918,6 +9330,10 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
8918
9330
  * Retention-eligible tables. `threads`, `messages`, and `resources` all anchor
8919
9331
  * on `createdAt` and are indexed for fast batched deletes. Cascade order is
8920
9332
  * enforced in `prune()` (children before threads), not here.
9333
+ *
9334
+ * Run fences anchor on `retiredAt` (epoch ms), which stays NULL until the
9335
+ * execution holding the fence settles, so a running execution's fence is
9336
+ * never pruned.
8921
9337
  */
8922
9338
  static retentionTables = {
8923
9339
  messages: {
@@ -8934,6 +9350,12 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
8934
9350
  table: TABLE_THREADS,
8935
9351
  column: "createdAt",
8936
9352
  indexed: true
9353
+ },
9354
+ runFences: {
9355
+ table: TABLE_MEMORY_RUN_FENCES,
9356
+ column: "retiredAt",
9357
+ indexed: true,
9358
+ anchorType: "epoch-ms"
8937
9359
  }
8938
9360
  };
8939
9361
  #client;
@@ -8956,6 +9378,13 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
8956
9378
  #write(operation, fn) {
8957
9379
  return this.#db.executeWriteOperationWithRetry(() => withClientWriteLock(this.#client, fn), operation);
8958
9380
  }
9381
+ /**
9382
+ * `#write` for a write that may carry a run fence: with a check, it runs in a write
9383
+ * transaction behind the check, and a retry reruns the whole transaction.
9384
+ */
9385
+ #fencedWrite(operation, check, write) {
9386
+ return this.#db.executeWriteOperationWithRetry(() => withRunFence(this.#client, check, write), operation);
9387
+ }
8959
9388
  async init() {
8960
9389
  await this.#db.createTable({
8961
9390
  tableName: TABLE_THREADS,
@@ -8969,6 +9398,10 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
8969
9398
  tableName: TABLE_RESOURCES,
8970
9399
  schema: TABLE_SCHEMAS[TABLE_RESOURCES]
8971
9400
  });
9401
+ await this.#db.createTable({
9402
+ tableName: TABLE_MEMORY_RUN_FENCES,
9403
+ schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_MEMORY_RUN_FENCES]
9404
+ });
8972
9405
  const omSchema = OBSERVATIONAL_MEMORY_TABLE_SCHEMA?.[OM_TABLE];
8973
9406
  if (omSchema) {
8974
9407
  await this.#db.createTable({
@@ -9020,6 +9453,70 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9020
9453
  await this.#db.deleteData({ tableName: TABLE_THREADS });
9021
9454
  await this.#db.deleteData({ tableName: TABLE_RESOURCES });
9022
9455
  await this.#db.deleteData({ tableName: OM_TABLE });
9456
+ await this.#db.deleteData({ tableName: TABLE_MEMORY_RUN_FENCES });
9457
+ }
9458
+ supportsRunFencing() {
9459
+ return true;
9460
+ }
9461
+ async raiseRunFence(fence) {
9462
+ try {
9463
+ return await this.#db.executeWriteOperationWithRetry(() => inWriteTransaction(this.#client, async (tx) => {
9464
+ const row = (await tx.execute({
9465
+ sql: `SELECT generation, ownerId FROM ${TABLE_MEMORY_RUN_FENCES} WHERE runId = ?`,
9466
+ args: [fence.runId]
9467
+ })).rows[0];
9468
+ const generation = row ? Number(row.generation) : void 0;
9469
+ const raises = generation === void 0 || generation < fence.generation;
9470
+ const reaffirms = generation === fence.generation && String(row.ownerId) === fence.ownerId;
9471
+ if (!raises && !reaffirms) return false;
9472
+ await tx.execute({
9473
+ sql: `INSERT INTO ${TABLE_MEMORY_RUN_FENCES} (runId, generation, ownerId) VALUES (?, ?, ?)
9474
+ ON CONFLICT(runId) DO UPDATE SET generation = excluded.generation, ownerId = excluded.ownerId, retiredAt = NULL`,
9475
+ args: [
9476
+ fence.runId,
9477
+ fence.generation,
9478
+ fence.ownerId
9479
+ ]
9480
+ });
9481
+ return true;
9482
+ }), "raiseRunFence");
9483
+ } catch (error) {
9484
+ throw new MastraError({
9485
+ id: createStorageErrorId("LIBSQL", "RAISE_RUN_FENCE", "FAILED"),
9486
+ domain: ErrorDomain.STORAGE,
9487
+ category: ErrorCategory.THIRD_PARTY,
9488
+ details: { runId: fence.runId }
9489
+ }, error);
9490
+ }
9491
+ }
9492
+ async retireRunFence(fence) {
9493
+ try {
9494
+ return (await this.#db.executeWriteOperationWithRetry(() => withClientWriteLock(this.#client, () => this.#client.execute({
9495
+ sql: `UPDATE ${TABLE_MEMORY_RUN_FENCES} SET retiredAt = ${DB_NOW_MS}
9496
+ WHERE runId = ? AND generation = ? AND ownerId = ?`,
9497
+ args: [
9498
+ fence.runId,
9499
+ fence.generation,
9500
+ fence.ownerId
9501
+ ]
9502
+ })), "retireRunFence")).rowsAffected > 0;
9503
+ } catch (error) {
9504
+ throw new MastraError({
9505
+ id: createStorageErrorId("LIBSQL", "RETIRE_RUN_FENCE", "FAILED"),
9506
+ domain: ErrorDomain.STORAGE,
9507
+ category: ErrorCategory.THIRD_PARTY,
9508
+ details: { runId: fence.runId }
9509
+ }, error);
9510
+ }
9511
+ }
9512
+ /** The check a write must pass: its own fence, otherwise the one in scope. */
9513
+ #runFenceCheck(fence, operation) {
9514
+ const resolved = resolveRunFence(this, fence);
9515
+ return resolved && {
9516
+ claimsTable: TABLE_MEMORY_RUN_FENCES,
9517
+ fence: resolved,
9518
+ operation
9519
+ };
9023
9520
  }
9024
9521
  /**
9025
9522
  * Delete rows older than each policy's `maxAge`, batched and cancellable.
@@ -9035,7 +9532,8 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9035
9532
  order: [
9036
9533
  "messages",
9037
9534
  "resources",
9038
- "threads"
9535
+ "threads",
9536
+ "runFences"
9039
9537
  ]
9040
9538
  });
9041
9539
  return runPrune({
@@ -9417,7 +9915,7 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9417
9915
  throw mastraError;
9418
9916
  }
9419
9917
  }
9420
- async saveMessages({ messages }) {
9918
+ async saveMessages({ messages, fence }) {
9421
9919
  if (messages.length === 0) return { messages };
9422
9920
  try {
9423
9921
  const threadId = messages[0]?.threadId;
@@ -9455,13 +9953,16 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9455
9953
  const BATCH_SIZE = 50;
9456
9954
  const messageStatements = batchStatements.slice(0, -1);
9457
9955
  const threadUpdateStatement = batchStatements[batchStatements.length - 1];
9458
- for (let i = 0; i < messageStatements.length; i += BATCH_SIZE) {
9459
- const batch = messageStatements.slice(i, i + BATCH_SIZE);
9460
- if (batch.length > 0) await this.#write("saveMessages", () => this.#client.batch(batch, "write"));
9461
- }
9462
- if (threadUpdateStatement) await this.#write("saveMessages", () => this.#client.execute(threadUpdateStatement));
9956
+ await this.#fencedWrite("saveMessages", this.#runFenceCheck(fence, "saveMessages"), async (writer) => {
9957
+ for (let i = 0; i < messageStatements.length; i += BATCH_SIZE) {
9958
+ const batch = messageStatements.slice(i, i + BATCH_SIZE);
9959
+ if (batch.length > 0) await writer.batch(batch);
9960
+ }
9961
+ if (threadUpdateStatement) await writer.execute(threadUpdateStatement);
9962
+ });
9463
9963
  return { messages: new MessageList().add(messages, "memory").get.all.db() };
9464
9964
  } catch (error) {
9965
+ if (isRunFenceConflictError(error)) throw error;
9465
9966
  throw new MastraError({
9466
9967
  id: createStorageErrorId("LIBSQL", "SAVE_MESSAGES", "FAILED"),
9467
9968
  domain: ErrorDomain.STORAGE,
@@ -9469,11 +9970,14 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9469
9970
  }, error);
9470
9971
  }
9471
9972
  }
9472
- async updateMessages({ messages }) {
9973
+ async updateMessages({ messages, fence }) {
9473
9974
  if (messages.length === 0) return [];
9975
+ return this.#fencedWrite("updateMessages", this.#runFenceCheck(fence, "updateMessages"), (writer) => this.#updateMessages(writer, messages));
9976
+ }
9977
+ async #updateMessages(writer, messages) {
9474
9978
  const messageIds = messages.map((m) => m.id);
9475
9979
  const selectSql = `SELECT * FROM ${TABLE_MESSAGES} WHERE id IN (${messageIds.map(() => "?").join(",")})`;
9476
- const existingMessages = (await this.#client.execute({
9980
+ const existingMessages = (await writer.execute({
9477
9981
  sql: selectSql,
9478
9982
  args: messageIds
9479
9983
  })).rows.map((row) => this.parseRow(row));
@@ -9525,48 +10029,44 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9525
10029
  sql: `UPDATE ${TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
9526
10030
  args: [now, threadId]
9527
10031
  });
9528
- await this.#write("updateMessages", () => this.#client.batch(batchStatements, "write"));
9529
- return (await this.#client.execute({
10032
+ await writer.batch(batchStatements);
10033
+ return (await writer.execute({
9530
10034
  sql: selectSql,
9531
10035
  args: messageIds
9532
10036
  })).rows.map((row) => this.parseRow(row));
9533
10037
  }
9534
- async deleteMessages(messageIds) {
10038
+ async deleteMessages(messageIds, options) {
9535
10039
  if (!messageIds || messageIds.length === 0) return;
9536
10040
  try {
9537
10041
  const BATCH_SIZE = 100;
9538
- await this.#write("deleteMessages", async () => {
10042
+ const check = this.#runFenceCheck(options?.fence, "deleteMessages");
10043
+ await this.#db.executeWriteOperationWithRetry(() => inWriteTransaction(this.#client, async (tx) => {
10044
+ if (check) await assertRunFence(tx, check);
9539
10045
  const threadIds = /* @__PURE__ */ new Set();
9540
- const tx = await this.#client.transaction("write");
9541
- try {
9542
- for (let i = 0; i < messageIds.length; i += BATCH_SIZE) {
9543
- const batch = messageIds.slice(i, i + BATCH_SIZE);
9544
- const placeholders = batch.map(() => "?").join(",");
9545
- (await tx.execute({
9546
- sql: `SELECT DISTINCT thread_id FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
9547
- args: batch
9548
- })).rows?.forEach((row) => {
9549
- if (row.thread_id) threadIds.add(row.thread_id);
9550
- });
9551
- await tx.execute({
9552
- sql: `DELETE FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
9553
- args: batch
9554
- });
9555
- }
9556
- if (threadIds.size > 0) {
9557
- const now = (/* @__PURE__ */ new Date()).toISOString();
9558
- for (const threadId of threadIds) await tx.execute({
9559
- sql: `UPDATE "${TABLE_THREADS}" SET "updatedAt" = ? WHERE id = ?`,
9560
- args: [now, threadId]
9561
- });
9562
- }
9563
- await tx.commit();
9564
- } catch (error) {
9565
- await tx.rollback();
9566
- throw error;
10046
+ for (let i = 0; i < messageIds.length; i += BATCH_SIZE) {
10047
+ const batch = messageIds.slice(i, i + BATCH_SIZE);
10048
+ const placeholders = batch.map(() => "?").join(",");
10049
+ (await tx.execute({
10050
+ sql: `SELECT DISTINCT thread_id FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
10051
+ args: batch
10052
+ })).rows?.forEach((row) => {
10053
+ if (row.thread_id) threadIds.add(row.thread_id);
10054
+ });
10055
+ await tx.execute({
10056
+ sql: `DELETE FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
10057
+ args: batch
10058
+ });
9567
10059
  }
9568
- });
10060
+ if (threadIds.size > 0) {
10061
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10062
+ for (const threadId of threadIds) await tx.execute({
10063
+ sql: `UPDATE "${TABLE_THREADS}" SET "updatedAt" = ? WHERE id = ?`,
10064
+ args: [now, threadId]
10065
+ });
10066
+ }
10067
+ }), "deleteMessages");
9569
10068
  } catch (error) {
10069
+ if (isRunFenceConflictError(error)) throw error;
9570
10070
  throw new MastraError({
9571
10071
  id: createStorageErrorId("LIBSQL", "DELETE_MESSAGES", "FAILED"),
9572
10072
  domain: ErrorDomain.STORAGE,
@@ -9590,13 +10090,18 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9590
10090
  };
9591
10091
  }
9592
10092
  async saveResource({ resource }) {
10093
+ return this.#insertResource(resource, void 0);
10094
+ }
10095
+ async #insertResource(resource, check) {
9593
10096
  await this.#db.insert({
9594
10097
  tableName: TABLE_RESOURCES,
9595
- record: { ...resource }
10098
+ record: { ...resource },
10099
+ check
9596
10100
  });
9597
10101
  return resource;
9598
10102
  }
9599
- async updateResource({ resourceId, workingMemory, metadata }) {
10103
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
10104
+ const check = this.#runFenceCheck(fence, "updateResource");
9600
10105
  const existingResource = await this.getResourceById({ resourceId });
9601
10106
  if (!existingResource) {
9602
10107
  const newResource = {
@@ -9606,7 +10111,7 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9606
10111
  createdAt: /* @__PURE__ */ new Date(),
9607
10112
  updatedAt: /* @__PURE__ */ new Date()
9608
10113
  };
9609
- return this.saveResource({ resource: newResource });
10114
+ return this.#insertResource(newResource, check);
9610
10115
  }
9611
10116
  const updatedResource = {
9612
10117
  ...existingResource,
@@ -9630,7 +10135,7 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9630
10135
  updates.push("updatedAt = ?");
9631
10136
  values.push(updatedResource.updatedAt.toISOString());
9632
10137
  values.push(resourceId);
9633
- await this.#write("updateResource", () => this.#client.execute({
10138
+ await this.#fencedWrite("updateResource", check, (writer) => writer.execute({
9634
10139
  sql: `UPDATE ${TABLE_RESOURCES} SET ${updates.join(", ")} WHERE id = ?`,
9635
10140
  args: values
9636
10141
  }));
@@ -9834,14 +10339,16 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9834
10339
  throw mastraError;
9835
10340
  }
9836
10341
  }
9837
- async saveThread({ thread }) {
10342
+ async saveThread({ thread, fence }) {
9838
10343
  try {
9839
10344
  await this.#db.insert({
9840
10345
  tableName: TABLE_THREADS,
9841
- record: { ...thread }
10346
+ record: { ...thread },
10347
+ check: this.#runFenceCheck(fence, "saveThread")
9842
10348
  });
9843
10349
  return thread;
9844
10350
  } catch (error) {
10351
+ if (isRunFenceConflictError(error)) throw error;
9845
10352
  const mastraError = new MastraError({
9846
10353
  id: createStorageErrorId("LIBSQL", "SAVE_THREAD", "FAILED"),
9847
10354
  domain: ErrorDomain.STORAGE,
@@ -9853,7 +10360,8 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9853
10360
  throw mastraError;
9854
10361
  }
9855
10362
  }
9856
- async updateThread({ id, title, metadata }) {
10363
+ async updateThread({ id, title, metadata, fence }) {
10364
+ const check = this.#runFenceCheck(fence, "updateThread");
9857
10365
  const thread = await this.getThreadById({ threadId: id });
9858
10366
  if (!thread) throw new MastraError({
9859
10367
  id: createStorageErrorId("LIBSQL", "UPDATE_THREAD", "NOT_FOUND"),
@@ -9876,7 +10384,7 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9876
10384
  updatedAt: now
9877
10385
  };
9878
10386
  try {
9879
- await this.#write("updateThread", () => this.#client.execute({
10387
+ await this.#fencedWrite("updateThread", check, (writer) => writer.execute({
9880
10388
  sql: `UPDATE ${TABLE_THREADS} SET title = COALESCE(?, title), metadata = jsonb(?), updatedAt = ? WHERE id = ?`,
9881
10389
  args: [
9882
10390
  title ?? null,
@@ -9887,6 +10395,7 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
9887
10395
  }));
9888
10396
  return updatedThread;
9889
10397
  } catch (error) {
10398
+ if (isRunFenceConflictError(error)) throw error;
9890
10399
  throw new MastraError({
9891
10400
  id: createStorageErrorId("LIBSQL", "UPDATE_THREAD", "FAILED"),
9892
10401
  domain: ErrorDomain.STORAGE,
@@ -10297,7 +10806,7 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
10297
10806
  try {
10298
10807
  const now = /* @__PURE__ */ new Date();
10299
10808
  const observedMessageIdsJson = input.observedMessageIds ? JSON.stringify(input.observedMessageIds) : null;
10300
- if ((await this.#write("updateActiveObservations", () => this.#client.execute({
10809
+ if ((await this.#fencedWrite("updateActiveObservations", this.#runFenceCheck(void 0, "updateActiveObservations"), (writer) => writer.execute({
10301
10810
  sql: `UPDATE "${OM_TABLE}" SET
10302
10811
  "activeObservations" = ?,
10303
10812
  "lastObservedAt" = ?,
@@ -10335,9 +10844,11 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
10335
10844
  }
10336
10845
  async createReflectionGeneration(input) {
10337
10846
  try {
10847
+ const check = this.#runFenceCheck(void 0, "createReflectionGeneration");
10338
10848
  return await this.#write("createReflectionGeneration", async () => {
10339
10849
  const tx = await this.#client.transaction("write");
10340
10850
  try {
10851
+ if (check) await assertRunFence(tx, check);
10341
10852
  const record = await this.#insertReflectionGeneration(tx, input);
10342
10853
  await tx.commit();
10343
10854
  return record;
@@ -10674,9 +11185,11 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
10674
11185
  async updateBufferedObservations(input) {
10675
11186
  try {
10676
11187
  const nowStr = (/* @__PURE__ */ new Date()).toISOString();
11188
+ const check = this.#runFenceCheck(void 0, "updateBufferedObservations");
10677
11189
  await this.#write("updateBufferedObservations", async () => {
10678
11190
  const tx = await this.#client.transaction("write");
10679
11191
  try {
11192
+ if (check) await assertRunFence(tx, check);
10680
11193
  const current = await tx.execute({
10681
11194
  sql: `SELECT "bufferedObservationChunks" FROM "${OM_TABLE}" WHERE id = ?`,
10682
11195
  args: [input.id]
@@ -10755,9 +11268,11 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
10755
11268
  async swapBufferedToActive(input) {
10756
11269
  try {
10757
11270
  const nowStr = (/* @__PURE__ */ new Date()).toISOString();
11271
+ const check = this.#runFenceCheck(void 0, "swapBufferedToActive");
10758
11272
  return await this.#write("swapBufferedToActive", async () => {
10759
11273
  const tx = await this.#client.transaction("write");
10760
11274
  try {
11275
+ if (check) await assertRunFence(tx, check);
10761
11276
  const current = await tx.execute({
10762
11277
  sql: `SELECT * FROM "${OM_TABLE}" WHERE id = ?`,
10763
11278
  args: [input.id]
@@ -10904,7 +11419,7 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
10904
11419
  async updateBufferedReflection(input) {
10905
11420
  try {
10906
11421
  const nowStr = (/* @__PURE__ */ new Date()).toISOString();
10907
- if ((await this.#write("updateBufferedReflection", () => this.#client.execute({
11422
+ if ((await this.#fencedWrite("updateBufferedReflection", this.#runFenceCheck(void 0, "updateBufferedReflection"), (writer) => writer.execute({
10908
11423
  sql: `UPDATE "${OM_TABLE}" SET
10909
11424
  "bufferedReflection" = CASE
10910
11425
  WHEN "bufferedReflection" IS NOT NULL AND "bufferedReflection" != ''
@@ -10944,9 +11459,11 @@ var MemoryLibSQL = class MemoryLibSQL extends MemoryStorage {
10944
11459
  }
10945
11460
  async swapBufferedReflectionToActive(input) {
10946
11461
  try {
11462
+ const check = this.#runFenceCheck(void 0, "swapBufferedReflectionToActive");
10947
11463
  return await this.#write("swapBufferedReflectionToActive", async () => {
10948
11464
  const tx = await this.#client.transaction("write");
10949
11465
  try {
11466
+ if (check) await assertRunFence(tx, check);
10950
11467
  const current = await tx.execute({
10951
11468
  sql: `SELECT * FROM "${OM_TABLE}" WHERE id = ?`,
10952
11469
  args: [input.currentRecord.id]
@@ -14446,16 +14963,39 @@ var WorkflowDefinitionsLibSQL = class extends WorkflowDefinitionsStorage {
14446
14963
  };
14447
14964
  //#endregion
14448
14965
  //#region src/storage/domains/workflows/index.ts
14966
+ const RUN_OWNER_COLUMNS = `generation, ownerId, leaseExpiresAt, ${DB_NOW_MS} AS nowMs`;
14967
+ function toRunOwnershipRecord(runId, row) {
14968
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
14969
+ return {
14970
+ runId,
14971
+ generation: Number(row.generation),
14972
+ ownerId: String(row.ownerId),
14973
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
14974
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
14975
+ };
14976
+ }
14449
14977
  var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
14450
14978
  /**
14451
14979
  * Workflow run snapshots accumulate as runs execute. Anchored on `updatedAt`
14452
14980
  * (last activity) so suspended/long-running runs are not pruned by start age.
14981
+ *
14982
+ * Run ownership rows are anchored on `updatedAt` (epoch ms), which every
14983
+ * claim, renewal and release refreshes, so a live execution's row is never
14984
+ * older than its renew interval.
14453
14985
  */
14454
- static retentionTables = { workflowSnapshot: {
14455
- table: TABLE_WORKFLOW_SNAPSHOT,
14456
- column: "updatedAt",
14457
- indexed: true
14458
- } };
14986
+ static retentionTables = {
14987
+ workflowSnapshot: {
14988
+ table: TABLE_WORKFLOW_SNAPSHOT,
14989
+ column: "updatedAt",
14990
+ indexed: true
14991
+ },
14992
+ runOwnership: {
14993
+ table: TABLE_WORKFLOW_RUN_OWNERS,
14994
+ column: "updatedAt",
14995
+ indexed: true,
14996
+ anchorType: "epoch-ms"
14997
+ }
14998
+ };
14459
14999
  #db;
14460
15000
  #client;
14461
15001
  executeWithRetry;
@@ -14507,16 +15047,132 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
14507
15047
  schema,
14508
15048
  ifNotExists: ["resourceId"]
14509
15049
  });
15050
+ await this.#db.createTable({
15051
+ tableName: TABLE_WORKFLOW_RUN_OWNERS,
15052
+ schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS]
15053
+ });
14510
15054
  }
14511
15055
  async dangerouslyClearAll() {
14512
15056
  await this.#db.deleteData({ tableName: TABLE_WORKFLOW_SNAPSHOT });
15057
+ await this.#db.deleteData({ tableName: TABLE_WORKFLOW_RUN_OWNERS });
15058
+ }
15059
+ supportsRunFencing() {
15060
+ return true;
15061
+ }
15062
+ #runFenceCheck(fence, runId, operation) {
15063
+ const resolved = resolveRunFence(this, fence, runId);
15064
+ return resolved && {
15065
+ claimsTable: TABLE_WORKFLOW_RUN_OWNERS,
15066
+ fence: resolved,
15067
+ operation
15068
+ };
14513
15069
  }
14514
- /** Delete workflow snapshots older than the `workflowSnapshot` policy's `maxAge`, batched. */
15070
+ async #readRunOwner(q, runId) {
15071
+ const row = (await q.execute({
15072
+ sql: `SELECT ${RUN_OWNER_COLUMNS} FROM ${TABLE_WORKFLOW_RUN_OWNERS} WHERE runId = ?`,
15073
+ args: [runId]
15074
+ })).rows[0];
15075
+ return row ? toRunOwnershipRecord(runId, row) : null;
15076
+ }
15077
+ #ownershipError(operation, runId, error) {
15078
+ return new MastraError({
15079
+ id: createStorageErrorId("LIBSQL", operation, "FAILED"),
15080
+ domain: ErrorDomain.STORAGE,
15081
+ category: ErrorCategory.THIRD_PARTY,
15082
+ details: { runId }
15083
+ }, error);
15084
+ }
15085
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
15086
+ try {
15087
+ return await this.executeWithRetry(() => inWriteTransaction(this.#client, async (tx) => {
15088
+ const current = await this.#readRunOwner(tx, runId);
15089
+ const generation = current?.generation ?? 0;
15090
+ if (expectedGeneration !== void 0 && expectedGeneration !== generation) return {
15091
+ acquired: false,
15092
+ record: current
15093
+ };
15094
+ if (current?.live && !force) return {
15095
+ acquired: false,
15096
+ record: current
15097
+ };
15098
+ await tx.execute({
15099
+ sql: `INSERT INTO ${TABLE_WORKFLOW_RUN_OWNERS} (runId, generation, ownerId, leaseExpiresAt, updatedAt)
15100
+ VALUES (?, COALESCE(?, MAX(1, ${DB_NOW_MS})), ?, ${DB_NOW_MS} + ?, ${DB_NOW_MS})
15101
+ ON CONFLICT(runId) DO UPDATE SET
15102
+ generation = excluded.generation,
15103
+ ownerId = excluded.ownerId,
15104
+ leaseExpiresAt = excluded.leaseExpiresAt,
15105
+ updatedAt = excluded.updatedAt`,
15106
+ args: [
15107
+ runId,
15108
+ current ? generation + 1 : null,
15109
+ ownerId,
15110
+ leaseMs
15111
+ ]
15112
+ });
15113
+ return {
15114
+ acquired: true,
15115
+ record: await this.#readRunOwner(tx, runId)
15116
+ };
15117
+ }), "claimRunOwnership");
15118
+ } catch (error) {
15119
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", runId, error);
15120
+ }
15121
+ }
15122
+ async renewRunOwnership({ leaseMs, ...fence }) {
15123
+ try {
15124
+ return await this.executeWithRetry(() => inWriteTransaction(this.#client, async (tx) => {
15125
+ const renewed = await tx.execute({
15126
+ sql: `UPDATE ${TABLE_WORKFLOW_RUN_OWNERS} SET leaseExpiresAt = ${DB_NOW_MS} + ?, updatedAt = ${DB_NOW_MS}
15127
+ WHERE runId = ? AND generation = ? AND ownerId = ? AND leaseExpiresAt IS NOT NULL`,
15128
+ args: [
15129
+ leaseMs,
15130
+ fence.runId,
15131
+ fence.generation,
15132
+ fence.ownerId
15133
+ ]
15134
+ });
15135
+ const record = await this.#readRunOwner(tx, fence.runId);
15136
+ return renewed.rowsAffected > 0 ? {
15137
+ renewed: true,
15138
+ record
15139
+ } : {
15140
+ renewed: false,
15141
+ record
15142
+ };
15143
+ }), "renewRunOwnership");
15144
+ } catch (error) {
15145
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, error);
15146
+ }
15147
+ }
15148
+ async releaseRunOwnership(fence) {
15149
+ try {
15150
+ return (await this.executeWithRetry(() => withClientWriteLock(this.#client, () => this.#client.execute({
15151
+ sql: `UPDATE ${TABLE_WORKFLOW_RUN_OWNERS} SET leaseExpiresAt = NULL, updatedAt = ${DB_NOW_MS}
15152
+ WHERE runId = ? AND generation = ? AND ownerId = ?`,
15153
+ args: [
15154
+ fence.runId,
15155
+ fence.generation,
15156
+ fence.ownerId
15157
+ ]
15158
+ })), "releaseRunOwnership")).rowsAffected > 0;
15159
+ } catch (error) {
15160
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
15161
+ }
15162
+ }
15163
+ async getRunOwnership({ runId }) {
15164
+ try {
15165
+ return await this.#readRunOwner(this.#client, runId);
15166
+ } catch (error) {
15167
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
15168
+ }
15169
+ }
15170
+ /** Delete workflow run snapshots and run ownership rows older than their policy's `maxAge`, batched. */
14515
15171
  async prune(policies, options) {
14516
15172
  const targets = resolveTargets({
14517
15173
  policies,
14518
15174
  descriptor: WorkflowsLibSQL.retentionTables,
14519
- order: ["workflowSnapshot"]
15175
+ order: ["workflowSnapshot", "runOwnership"]
14520
15176
  });
14521
15177
  return runPrune({
14522
15178
  db: this.#db,
@@ -14546,10 +15202,12 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
14546
15202
  this.logger.warn("LibSQL Workflows: Failed to set PRAGMA settings.", err);
14547
15203
  }
14548
15204
  }
14549
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
15205
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
15206
+ const check = this.#runFenceCheck(explicitFence, runId, "updateWorkflowResults");
14550
15207
  return this.executeWithRetry(() => withClientWriteLock(this.#client, async () => {
14551
15208
  const tx = await this.#client.transaction("write");
14552
15209
  try {
15210
+ if (check) await assertRunFence(tx, check);
14553
15211
  const existingSnapshotResult = await tx.execute({
14554
15212
  sql: `SELECT json(snapshot) as snapshot FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
14555
15213
  args: [workflowName, runId]
@@ -14602,10 +15260,12 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
14602
15260
  }
14603
15261
  }), "updateWorkflowResults");
14604
15262
  }
14605
- async updateWorkflowState({ workflowName, runId, opts }) {
15263
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
15264
+ const check = this.#runFenceCheck(explicitFence, runId, "updateWorkflowState");
14606
15265
  return this.executeWithRetry(() => withClientWriteLock(this.#client, async () => {
14607
15266
  const tx = await this.#client.transaction("write");
14608
15267
  try {
15268
+ if (check) await assertRunFence(tx, check);
14609
15269
  const existingSnapshotResult = await tx.execute({
14610
15270
  sql: `SELECT json(snapshot) as snapshot FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
14611
15271
  args: [workflowName, runId]
@@ -14646,7 +15306,7 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
14646
15306
  }
14647
15307
  }), "updateWorkflowState");
14648
15308
  }
14649
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
15309
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
14650
15310
  const now = /* @__PURE__ */ new Date();
14651
15311
  const createdAtValue = (createdAt ?? now).toISOString();
14652
15312
  const updatedAtValue = (updatedAt ?? now).toISOString();
@@ -14654,7 +15314,8 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
14654
15314
  workflowName,
14655
15315
  runId
14656
15316
  });
14657
- await this.executeWithRetry(() => withClientWriteLock(this.#client, () => this.#client.execute({
15317
+ const check = this.#runFenceCheck(fence, runId, "persistWorkflowSnapshot");
15318
+ await this.executeWithRetry(() => withRunFence(this.#client, check, (writer) => writer.execute({
14658
15319
  sql: `INSERT INTO ${TABLE_WORKFLOW_SNAPSHOT} (workflow_name, run_id, resourceId, snapshot, createdAt, updatedAt)
14659
15320
  VALUES (?, ?, ?, jsonb(?), ?, ?)
14660
15321
  ON CONFLICT(workflow_name, run_id)
@@ -14710,14 +15371,16 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
14710
15371
  }, error);
14711
15372
  }
14712
15373
  }
14713
- async deleteWorkflowRunById({ runId, workflowName }) {
15374
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
15375
+ const check = this.#runFenceCheck(fence, runId, "deleteWorkflowRunById");
14714
15376
  return this.executeWithRetry(async () => {
14715
15377
  try {
14716
- await this.#client.execute({
15378
+ await withRunFence(this.#client, check, (writer) => writer.execute({
14717
15379
  sql: `DELETE FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
14718
15380
  args: [workflowName, runId]
14719
- });
15381
+ }));
14720
15382
  } catch (error) {
15383
+ if (isRunFenceConflictError(error)) throw error;
14721
15384
  throw new MastraError({
14722
15385
  id: createStorageErrorId("LIBSQL", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
14723
15386
  domain: ErrorDomain.STORAGE,
@@ -15587,7 +16250,7 @@ var LibSQLFactoryStorage = class extends FactoryStorage {
15587
16250
  ...config.authToken ? { authToken: config.authToken } : {},
15588
16251
  ...isLocalDb ? { timeout: DEFAULT_CONNECTION_TIMEOUT_MS } : {}
15589
16252
  });
15590
- this.#client = isSingleConnectionDatabase(config) ? gateSingleConnectionClient(client) : client;
16253
+ this.#client = isSingleConnectionDatabase(config) ? gateSingleConnectionClient(client) : isLocalDb ? gateIfLocalFile(resetConnectionsAfterBusy(client, { maxWaitMs: DEFAULT_CONNECTION_TIMEOUT_MS }), config.url, { maxWaitMs: DEFAULT_CONNECTION_TIMEOUT_MS }) : client;
15591
16254
  this.ops = new LibSQLFactoryStorageOps(this.#client, this.#schemas, (fn) => withClientWriteLock(this.#client, fn));
15592
16255
  }
15593
16256
  getMastraStorage() {
@@ -15753,7 +16416,10 @@ var LibSQLStore = class extends MastraCompositeStore {
15753
16416
  ...config.syncInterval !== void 0 ? { syncInterval: config.syncInterval } : {},
15754
16417
  ...this.isLocalDb ? { timeout: this.connectionTimeoutMs } : {}
15755
16418
  });
15756
- this.client = isSingleConnectionDatabase(config) ? gateSingleConnectionClient(client) : client;
16419
+ this.client = isSingleConnectionDatabase(config) ? gateSingleConnectionClient(client) : this.isLocalDb ? gateIfLocalFile(resetConnectionsAfterBusy(client, {
16420
+ afterReset: () => this.applyLocalPragmas(client),
16421
+ maxWaitMs: this.connectionTimeoutMs
16422
+ }), config.url, { maxWaitMs: this.connectionTimeoutMs }) : client;
15757
16423
  this.pragmasReady = this.isLocalDb ? this.applyLocalPragmas() : Promise.resolve();
15758
16424
  } else {
15759
16425
  this.client = config.client;
@@ -15819,7 +16485,7 @@ var LibSQLStore = class extends MastraCompositeStore {
15819
16485
  threadState
15820
16486
  };
15821
16487
  }
15822
- async applyLocalPragmas() {
16488
+ async applyLocalPragmas(client = this.client) {
15823
16489
  const pragmas = [
15824
16490
  ["journal_mode=WAL", "PRAGMA journal_mode=WAL;"],
15825
16491
  [`busy_timeout=${this.connectionTimeoutMs}`, `PRAGMA busy_timeout=${this.connectionTimeoutMs};`],
@@ -15829,7 +16495,7 @@ var LibSQLStore = class extends MastraCompositeStore {
15829
16495
  [`mmap_size=${this.localPragmas.mmapSize}`, `PRAGMA mmap_size=${this.localPragmas.mmapSize};`]
15830
16496
  ];
15831
16497
  for (const [label, sql] of pragmas) try {
15832
- await this.client.execute(sql);
16498
+ await client.execute(sql);
15833
16499
  this.logger.debug(`LibSQLStore: PRAGMA ${label} set.`);
15834
16500
  } catch (err) {
15835
16501
  this.logger.warn(`LibSQLStore: Failed to set PRAGMA ${label}.`, err);