@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/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/reference-core-mastra-class.md +12 -4
- package/dist/docs/references/reference-storage-retention.md +21 -18
- package/dist/index.cjs +776 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +777 -111
- package/dist/index.js.map +1 -1
- package/dist/shared/file-transaction-gate.d.ts +28 -0
- package/dist/shared/file-transaction-gate.d.ts.map +1 -0
- package/dist/shared/reset-after-busy-client.d.ts +34 -0
- package/dist/shared/reset-after-busy-client.d.ts.map +1 -0
- package/dist/storage/db/index.d.ts +8 -3
- package/dist/storage/db/index.d.ts.map +1 -1
- package/dist/storage/db/run-fencing.d.ts +41 -0
- package/dist/storage/db/run-fencing.d.ts.map +1 -0
- package/dist/storage/db/utils.d.ts.map +1 -1
- package/dist/storage/domains/memory/index.d.ts +21 -7
- package/dist/storage/domains/memory/index.d.ts.map +1 -1
- package/dist/storage/domains/workflows/index.d.ts +21 -6
- package/dist/storage/domains/workflows/index.d.ts.map +1 -1
- package/dist/storage/factory-storage.d.ts.map +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/retention.d.ts +3 -3
- package/dist/storage/retention.d.ts.map +1 -1
- package/dist/vector/index.d.ts.map +1 -1
- package/package.json +6 -6
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
|
|
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
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
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
|
|
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
|
|
9529
|
-
return (await
|
|
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
|
-
|
|
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
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
})
|
|
9551
|
-
|
|
9552
|
-
|
|
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
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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 = {
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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);
|