@mastra/convex 1.4.0 → 1.5.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/CHANGELOG.md +50 -0
- package/dist/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +2 -136
- package/dist/index.cjs +2653 -3043
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2607 -2898
- package/dist/index.js.map +1 -1
- package/dist/schema.cjs +358 -130
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +328 -2
- package/dist/schema.js.map +1 -1
- package/dist/server/index.cjs +37 -141
- package/dist/server/index.js +3 -4
- package/dist/server-13j_f22k.cjs +1882 -0
- package/dist/server-13j_f22k.cjs.map +1 -0
- package/dist/server-DJt7jjL-.js +1853 -0
- package/dist/server-DJt7jjL-.js.map +1 -0
- package/dist/storage/domains/memory/index.d.ts.map +1 -1
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +10 -9
- package/dist/chunk-2KILIA33.cjs +0 -1776
- package/dist/chunk-2KILIA33.cjs.map +0 -1
- package/dist/chunk-MC2EQATP.js +0 -1770
- package/dist/chunk-MC2EQATP.js.map +0 -1
- package/dist/chunk-QWWFHN52.js +0 -234
- package/dist/chunk-QWWFHN52.js.map +0 -1
- package/dist/chunk-ZCCKLZQH.cjs +0 -295
- package/dist/chunk-ZCCKLZQH.cjs.map +0 -1
- package/dist/server/index.cjs.map +0 -1
- package/dist/server/index.js.map +0 -1
|
@@ -0,0 +1,1882 @@
|
|
|
1
|
+
require("./schema.cjs");
|
|
2
|
+
let convex_server = require("convex/server");
|
|
3
|
+
let _mastra_core_storage_constants = require("@mastra/core/storage/constants");
|
|
4
|
+
let convex_values = require("convex/values");
|
|
5
|
+
//#region src/server/cache.ts
|
|
6
|
+
const CACHE_TABLE = "mastra_cache";
|
|
7
|
+
const CACHE_LIST_TABLE = "mastra_cache_list_items";
|
|
8
|
+
const CACHE_MUTATION_BATCH_SIZE = 25;
|
|
9
|
+
function encodeValue(value) {
|
|
10
|
+
return JSON.stringify(value === void 0 ? null : value);
|
|
11
|
+
}
|
|
12
|
+
function decodeValue(value) {
|
|
13
|
+
return JSON.parse(value);
|
|
14
|
+
}
|
|
15
|
+
function isExpired(doc, now) {
|
|
16
|
+
return doc.expiresAt !== null && doc.expiresAt <= now;
|
|
17
|
+
}
|
|
18
|
+
function normalizeListRange(from, to, length) {
|
|
19
|
+
const normalizedFrom = from < 0 ? Math.max(length + from, 0) : from;
|
|
20
|
+
const normalizedTo = to < 0 ? length + to : to;
|
|
21
|
+
if (normalizedTo < normalizedFrom || normalizedFrom >= length) return null;
|
|
22
|
+
return {
|
|
23
|
+
from: normalizedFrom,
|
|
24
|
+
to: normalizedTo
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function findCacheDoc(ctx, key) {
|
|
28
|
+
return await ctx.db.query(CACHE_TABLE).withIndex("by_key", (q) => q.eq("key", key)).first();
|
|
29
|
+
}
|
|
30
|
+
async function deleteCacheKey(ctx, key) {
|
|
31
|
+
const [doc, listItems] = await Promise.all([findCacheDoc(ctx, key), ctx.db.query(CACHE_LIST_TABLE).withIndex("by_key_index", (q) => q.eq("key", key)).take(26)]);
|
|
32
|
+
if (doc && doc.kind !== "deleted") await ctx.db.patch(doc._id, { kind: "deleted" });
|
|
33
|
+
for (const item of listItems.slice(0, CACHE_MUTATION_BATCH_SIZE)) await ctx.db.delete(item._id);
|
|
34
|
+
const hasMore = listItems.length > CACHE_MUTATION_BATCH_SIZE;
|
|
35
|
+
if (doc && !hasMore) await ctx.db.delete(doc._id);
|
|
36
|
+
return { hasMore };
|
|
37
|
+
}
|
|
38
|
+
async function getLiveCacheDoc(ctx, key, now) {
|
|
39
|
+
const doc = await findCacheDoc(ctx, key);
|
|
40
|
+
if (!doc) return {
|
|
41
|
+
doc: null,
|
|
42
|
+
hasMore: false
|
|
43
|
+
};
|
|
44
|
+
if (doc.kind === "deleted") return {
|
|
45
|
+
doc: null,
|
|
46
|
+
hasMore: (await deleteCacheKey(ctx, key)).hasMore
|
|
47
|
+
};
|
|
48
|
+
if (!isExpired(doc, now)) return {
|
|
49
|
+
doc,
|
|
50
|
+
hasMore: false
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
doc: null,
|
|
54
|
+
hasMore: (await deleteCacheKey(ctx, key)).hasMore
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function writeCacheDoc(ctx, key, existing, patch) {
|
|
58
|
+
if (existing) {
|
|
59
|
+
await ctx.db.patch(existing._id, patch);
|
|
60
|
+
return {
|
|
61
|
+
...existing,
|
|
62
|
+
...patch
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
_id: await ctx.db.insert(CACHE_TABLE, {
|
|
67
|
+
key,
|
|
68
|
+
...patch
|
|
69
|
+
}),
|
|
70
|
+
key,
|
|
71
|
+
...patch
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function clearPrefix(ctx, keyPrefix) {
|
|
75
|
+
const [docs, orphanListItems] = await Promise.all([ctx.db.query(CACHE_TABLE).withIndex("by_key_prefix", (q) => q.eq("keyPrefix", keyPrefix)).take(26), ctx.db.query(CACHE_LIST_TABLE).withIndex("by_key_prefix", (q) => q.eq("keyPrefix", keyPrefix)).take(1)]);
|
|
76
|
+
if (docs.length > 0) {
|
|
77
|
+
for (const doc of docs.slice(0, CACHE_MUTATION_BATCH_SIZE)) {
|
|
78
|
+
if (doc.kind === "list" || doc.kind === "deleted") return (await deleteCacheKey(ctx, doc.key)).hasMore || docs.length > 1 || orphanListItems.length > 0;
|
|
79
|
+
await ctx.db.delete(doc._id);
|
|
80
|
+
}
|
|
81
|
+
return docs.length > CACHE_MUTATION_BATCH_SIZE || orphanListItems.length > 0;
|
|
82
|
+
}
|
|
83
|
+
const listItems = await ctx.db.query(CACHE_LIST_TABLE).withIndex("by_key_prefix", (q) => q.eq("keyPrefix", keyPrefix)).take(26);
|
|
84
|
+
for (const item of listItems.slice(0, CACHE_MUTATION_BATCH_SIZE)) await ctx.db.delete(item._id);
|
|
85
|
+
return listItems.length > CACHE_MUTATION_BATCH_SIZE;
|
|
86
|
+
}
|
|
87
|
+
async function handleCacheOperation(ctx, request) {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
switch (request.op) {
|
|
90
|
+
case "get": {
|
|
91
|
+
const { doc, hasMore } = await getLiveCacheDoc(ctx, request.key, now);
|
|
92
|
+
if (hasMore) return {
|
|
93
|
+
ok: true,
|
|
94
|
+
result: null,
|
|
95
|
+
hasMore: true
|
|
96
|
+
};
|
|
97
|
+
if (!doc || doc.kind !== "value") return {
|
|
98
|
+
ok: true,
|
|
99
|
+
result: null
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
result: decodeValue(doc.value ?? "null")
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
case "set": {
|
|
107
|
+
let existing = await findCacheDoc(ctx, request.key);
|
|
108
|
+
if (existing && (isExpired(existing, now) || existing.kind !== "value")) {
|
|
109
|
+
if ((await deleteCacheKey(ctx, request.key)).hasMore) return {
|
|
110
|
+
ok: true,
|
|
111
|
+
hasMore: true
|
|
112
|
+
};
|
|
113
|
+
existing = null;
|
|
114
|
+
}
|
|
115
|
+
await writeCacheDoc(ctx, request.key, existing, {
|
|
116
|
+
keyPrefix: request.keyPrefix,
|
|
117
|
+
kind: "value",
|
|
118
|
+
value: encodeValue(request.value),
|
|
119
|
+
expiresAt: request.expiresAt
|
|
120
|
+
});
|
|
121
|
+
return { ok: true };
|
|
122
|
+
}
|
|
123
|
+
case "listLength": {
|
|
124
|
+
const { doc, hasMore } = await getLiveCacheDoc(ctx, request.key, now);
|
|
125
|
+
if (hasMore) return {
|
|
126
|
+
ok: true,
|
|
127
|
+
result: 0,
|
|
128
|
+
hasMore: true
|
|
129
|
+
};
|
|
130
|
+
if (!doc) return {
|
|
131
|
+
ok: true,
|
|
132
|
+
result: 0
|
|
133
|
+
};
|
|
134
|
+
if (doc.kind !== "list") return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: `${request.key} exists but is not an array`
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
result: doc.counter ?? 0
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
case "listPush": {
|
|
144
|
+
let existing = await findCacheDoc(ctx, request.key);
|
|
145
|
+
if (existing && (isExpired(existing, now) || existing.kind === "deleted")) {
|
|
146
|
+
if ((await deleteCacheKey(ctx, request.key)).hasMore) return {
|
|
147
|
+
ok: true,
|
|
148
|
+
hasMore: true
|
|
149
|
+
};
|
|
150
|
+
existing = null;
|
|
151
|
+
}
|
|
152
|
+
if (existing && existing.kind !== "list") return {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: `${request.key} exists but is not an array`
|
|
155
|
+
};
|
|
156
|
+
const doc = existing ? await writeCacheDoc(ctx, request.key, existing, {
|
|
157
|
+
kind: "list",
|
|
158
|
+
keyPrefix: request.keyPrefix,
|
|
159
|
+
counter: (existing.counter ?? 0) + 1,
|
|
160
|
+
expiresAt: request.expiresAt
|
|
161
|
+
}) : await writeCacheDoc(ctx, request.key, null, {
|
|
162
|
+
kind: "list",
|
|
163
|
+
keyPrefix: request.keyPrefix,
|
|
164
|
+
counter: 1,
|
|
165
|
+
expiresAt: request.expiresAt
|
|
166
|
+
});
|
|
167
|
+
await ctx.db.insert(CACHE_LIST_TABLE, {
|
|
168
|
+
key: request.key,
|
|
169
|
+
keyPrefix: request.keyPrefix,
|
|
170
|
+
index: (doc.counter ?? 1) - 1,
|
|
171
|
+
value: encodeValue(request.value)
|
|
172
|
+
});
|
|
173
|
+
return { ok: true };
|
|
174
|
+
}
|
|
175
|
+
case "listFromTo": {
|
|
176
|
+
const { doc, hasMore } = await getLiveCacheDoc(ctx, request.key, now);
|
|
177
|
+
if (hasMore) return {
|
|
178
|
+
ok: true,
|
|
179
|
+
result: [],
|
|
180
|
+
hasMore: true
|
|
181
|
+
};
|
|
182
|
+
if (!doc || doc.kind !== "list") return {
|
|
183
|
+
ok: true,
|
|
184
|
+
result: []
|
|
185
|
+
};
|
|
186
|
+
const range = normalizeListRange(request.from, request.to, doc.counter ?? 0);
|
|
187
|
+
if (!range) return {
|
|
188
|
+
ok: true,
|
|
189
|
+
result: []
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
result: (await ctx.db.query(CACHE_LIST_TABLE).withIndex("by_key_index", (q) => {
|
|
194
|
+
return q.eq("key", request.key).gte("index", range.from).lte("index", range.to);
|
|
195
|
+
}).collect()).map((item) => decodeValue(item.value))
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
case "delete": return {
|
|
199
|
+
ok: true,
|
|
200
|
+
hasMore: (await deleteCacheKey(ctx, request.key)).hasMore
|
|
201
|
+
};
|
|
202
|
+
case "clear": return {
|
|
203
|
+
ok: true,
|
|
204
|
+
hasMore: await clearPrefix(ctx, request.keyPrefix)
|
|
205
|
+
};
|
|
206
|
+
case "increment": {
|
|
207
|
+
let existing = await findCacheDoc(ctx, request.key);
|
|
208
|
+
if (existing && (isExpired(existing, now) || existing.kind === "deleted")) {
|
|
209
|
+
if ((await deleteCacheKey(ctx, request.key)).hasMore) return {
|
|
210
|
+
ok: true,
|
|
211
|
+
hasMore: true
|
|
212
|
+
};
|
|
213
|
+
existing = null;
|
|
214
|
+
}
|
|
215
|
+
if (existing && existing.kind !== "counter") return {
|
|
216
|
+
ok: false,
|
|
217
|
+
error: `${request.key} exists but is not a number`
|
|
218
|
+
};
|
|
219
|
+
const nextCounter = (existing?.counter ?? 0) + 1;
|
|
220
|
+
await writeCacheDoc(ctx, request.key, existing, {
|
|
221
|
+
kind: "counter",
|
|
222
|
+
keyPrefix: request.keyPrefix,
|
|
223
|
+
counter: nextCounter,
|
|
224
|
+
expiresAt: request.expiresAt
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
result: nextCounter
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
error: `Unsupported operation ${request.op}`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const mastraCache = (0, convex_server.mutationGeneric)(async (ctx, request) => handleCacheOperation(ctx, request));
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/server/index-map.ts
|
|
240
|
+
const TABLE_INDEX_MAP = {
|
|
241
|
+
mastra_messages: [
|
|
242
|
+
{
|
|
243
|
+
name: "by_thread_created",
|
|
244
|
+
fields: ["thread_id", "createdAt"]
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "by_thread",
|
|
248
|
+
fields: ["thread_id"]
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "by_resource",
|
|
252
|
+
fields: ["resourceId"]
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "by_record_id",
|
|
256
|
+
fields: ["id"]
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
mastra_threads: [
|
|
260
|
+
{
|
|
261
|
+
name: "by_resource",
|
|
262
|
+
fields: ["resourceId"]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "by_created",
|
|
266
|
+
fields: ["createdAt"]
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "by_updated",
|
|
270
|
+
fields: ["updatedAt"]
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "by_record_id",
|
|
274
|
+
fields: ["id"]
|
|
275
|
+
}
|
|
276
|
+
],
|
|
277
|
+
mastra_resources: [{
|
|
278
|
+
name: "by_updated",
|
|
279
|
+
fields: ["updatedAt"]
|
|
280
|
+
}, {
|
|
281
|
+
name: "by_record_id",
|
|
282
|
+
fields: ["id"]
|
|
283
|
+
}],
|
|
284
|
+
mastra_workflow_snapshots: [
|
|
285
|
+
{
|
|
286
|
+
name: "by_workflow_run",
|
|
287
|
+
fields: ["workflow_name", "run_id"]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "by_workflow",
|
|
291
|
+
fields: ["workflow_name"]
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "by_resource",
|
|
295
|
+
fields: ["resourceId"]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: "by_created",
|
|
299
|
+
fields: ["createdAt"]
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "by_record_id",
|
|
303
|
+
fields: ["id"]
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
mastra_scorers: [
|
|
307
|
+
{
|
|
308
|
+
name: "by_entity",
|
|
309
|
+
fields: ["entityId", "entityType"]
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: "by_scorer",
|
|
313
|
+
fields: ["scorerId"]
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "by_run",
|
|
317
|
+
fields: ["runId"]
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "by_created",
|
|
321
|
+
fields: ["createdAt"]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "by_record_id",
|
|
325
|
+
fields: ["id"]
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
mastra_schedules: [
|
|
329
|
+
{
|
|
330
|
+
name: "by_workflow_status",
|
|
331
|
+
fields: ["workflow_id", "status"]
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "by_workflow_id",
|
|
335
|
+
fields: ["workflow_id"]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "by_owner",
|
|
339
|
+
fields: ["owner_type", "owner_id"]
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "by_owner_id",
|
|
343
|
+
fields: ["owner_id"]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "by_status_next_fire_at",
|
|
347
|
+
fields: ["status", "next_fire_at"]
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "by_created",
|
|
351
|
+
fields: ["created_at"]
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: "by_record_id",
|
|
355
|
+
fields: ["id"]
|
|
356
|
+
}
|
|
357
|
+
],
|
|
358
|
+
mastra_schedule_triggers: [
|
|
359
|
+
{
|
|
360
|
+
name: "by_schedule_actual",
|
|
361
|
+
fields: ["schedule_id", "actual_fire_at"]
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: "by_parent_trigger",
|
|
365
|
+
fields: ["parent_trigger_id"]
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "by_record_id",
|
|
369
|
+
fields: ["id"]
|
|
370
|
+
}
|
|
371
|
+
],
|
|
372
|
+
mastra_channel_installations: [
|
|
373
|
+
{
|
|
374
|
+
name: "by_platform_agent",
|
|
375
|
+
fields: ["platform", "agentId"]
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: "by_webhook",
|
|
379
|
+
fields: ["webhookId"]
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "by_platform",
|
|
383
|
+
fields: ["platform"]
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "by_record_id",
|
|
387
|
+
fields: ["id"]
|
|
388
|
+
}
|
|
389
|
+
],
|
|
390
|
+
mastra_channel_config: [{
|
|
391
|
+
name: "by_platform",
|
|
392
|
+
fields: ["platform"]
|
|
393
|
+
}, {
|
|
394
|
+
name: "by_record_id",
|
|
395
|
+
fields: ["id"]
|
|
396
|
+
}],
|
|
397
|
+
mastra_background_tasks: [
|
|
398
|
+
{
|
|
399
|
+
name: "by_agent_status",
|
|
400
|
+
fields: ["agent_id", "status"]
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: "by_status_created",
|
|
404
|
+
fields: ["status", "createdAt"]
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: "by_run",
|
|
408
|
+
fields: ["run_id"]
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: "by_tool_call",
|
|
412
|
+
fields: ["tool_call_id"]
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: "by_thread",
|
|
416
|
+
fields: ["thread_id"]
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "by_resource",
|
|
420
|
+
fields: ["resource_id"]
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: "by_tool",
|
|
424
|
+
fields: ["tool_name"]
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: "by_created",
|
|
428
|
+
fields: ["createdAt"]
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: "by_record_id",
|
|
432
|
+
fields: ["id"]
|
|
433
|
+
}
|
|
434
|
+
],
|
|
435
|
+
mastra_vector_indexes: [{
|
|
436
|
+
name: "by_name",
|
|
437
|
+
fields: ["indexName"]
|
|
438
|
+
}, {
|
|
439
|
+
name: "by_record_id",
|
|
440
|
+
fields: ["id"]
|
|
441
|
+
}],
|
|
442
|
+
mastra_observational_memory: [{
|
|
443
|
+
name: "by_lookup_key",
|
|
444
|
+
fields: ["lookupKey", "generationCount"]
|
|
445
|
+
}, {
|
|
446
|
+
name: "by_record_id",
|
|
447
|
+
fields: ["id"]
|
|
448
|
+
}]
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* Finds the best matching index for the given equality filters on a Convex table.
|
|
452
|
+
*
|
|
453
|
+
* Returns the index name and the subset of filters that form the index prefix,
|
|
454
|
+
* or null when no index matches.
|
|
455
|
+
*
|
|
456
|
+
* The "best" index is the one whose prefix has the most consecutive fields
|
|
457
|
+
* covered by the provided filters. For example, given filters for `thread_id`
|
|
458
|
+
* and `createdAt` on mastra_messages, the composite `by_thread_created` index
|
|
459
|
+
* (fields: [thread_id, createdAt]) is preferred over `by_thread` (fields: [thread_id]).
|
|
460
|
+
*/
|
|
461
|
+
function findBestIndex(convexTable, filters) {
|
|
462
|
+
const indexes = TABLE_INDEX_MAP[convexTable];
|
|
463
|
+
if (!indexes || filters.length === 0) return null;
|
|
464
|
+
const filtersByField = /* @__PURE__ */ new Map();
|
|
465
|
+
for (const f of filters) filtersByField.set(f.field, f);
|
|
466
|
+
let best = null;
|
|
467
|
+
for (const index of indexes) {
|
|
468
|
+
let prefixLength = 0;
|
|
469
|
+
const indexedFilters = [];
|
|
470
|
+
for (const field of index.fields) {
|
|
471
|
+
const filter = filtersByField.get(field);
|
|
472
|
+
if (filter) {
|
|
473
|
+
prefixLength++;
|
|
474
|
+
indexedFilters.push(filter);
|
|
475
|
+
} else break;
|
|
476
|
+
}
|
|
477
|
+
if (prefixLength > 0 && (!best || prefixLength > best.prefixLength)) best = {
|
|
478
|
+
indexName: index.name,
|
|
479
|
+
indexedFilters,
|
|
480
|
+
prefixLength
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return best ? {
|
|
484
|
+
indexName: best.indexName,
|
|
485
|
+
indexedFilters: best.indexedFilters
|
|
486
|
+
} : null;
|
|
487
|
+
}
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/server/observational-memory.ts
|
|
490
|
+
const OM_QUERY_MAX_DOCS = 1e4;
|
|
491
|
+
/**
|
|
492
|
+
* Parse the stored bufferedObservationChunks JSON string. Tolerates null,
|
|
493
|
+
* missing, and malformed values by returning an empty array.
|
|
494
|
+
*/
|
|
495
|
+
function parseStoredChunks(value) {
|
|
496
|
+
if (typeof value !== "string" || !value) return [];
|
|
497
|
+
try {
|
|
498
|
+
const parsed = JSON.parse(value);
|
|
499
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
500
|
+
} catch {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Select how many buffered chunks to activate.
|
|
506
|
+
*
|
|
507
|
+
* Finds the chunk boundary closest to the activation target, biased over
|
|
508
|
+
* (prefer removing slightly more than the target so remaining context lands at
|
|
509
|
+
* or below the retention floor), with an overshoot safeguard that falls back
|
|
510
|
+
* to the best under boundary. Ported from the core in-memory reference.
|
|
511
|
+
*/
|
|
512
|
+
function selectActivationBoundary(chunks, opts) {
|
|
513
|
+
const retentionFloor = opts.messageTokensThreshold * (1 - opts.activationRatio);
|
|
514
|
+
const targetMessageTokens = Math.max(0, opts.currentPendingTokens - retentionFloor);
|
|
515
|
+
let cumulativeMessageTokens = 0;
|
|
516
|
+
let bestOverBoundary = 0;
|
|
517
|
+
let bestOverTokens = 0;
|
|
518
|
+
let bestUnderBoundary = 0;
|
|
519
|
+
let bestUnderTokens = 0;
|
|
520
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
521
|
+
cumulativeMessageTokens += chunks[i].messageTokens ?? 0;
|
|
522
|
+
const boundary = i + 1;
|
|
523
|
+
if (cumulativeMessageTokens >= targetMessageTokens) {
|
|
524
|
+
if (bestOverBoundary === 0 || cumulativeMessageTokens < bestOverTokens) {
|
|
525
|
+
bestOverBoundary = boundary;
|
|
526
|
+
bestOverTokens = cumulativeMessageTokens;
|
|
527
|
+
}
|
|
528
|
+
} else if (cumulativeMessageTokens > bestUnderTokens) {
|
|
529
|
+
bestUnderBoundary = boundary;
|
|
530
|
+
bestUnderTokens = cumulativeMessageTokens;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const maxOvershoot = retentionFloor * .95;
|
|
534
|
+
const overshoot = bestOverTokens - targetMessageTokens;
|
|
535
|
+
const remainingAfterOver = opts.currentPendingTokens - bestOverTokens;
|
|
536
|
+
const remainingAfterUnder = opts.currentPendingTokens - bestUnderTokens;
|
|
537
|
+
const minRemaining = Math.min(1e3, retentionFloor);
|
|
538
|
+
if (opts.forceMaxActivation && bestOverBoundary > 0 && remainingAfterOver >= minRemaining) return bestOverBoundary;
|
|
539
|
+
if (bestOverBoundary > 0 && overshoot <= maxOvershoot && remainingAfterOver >= minRemaining) return bestOverBoundary;
|
|
540
|
+
if (bestUnderBoundary > 0 && remainingAfterUnder >= minRemaining) return bestUnderBoundary;
|
|
541
|
+
if (bestOverBoundary > 0) return bestOverBoundary;
|
|
542
|
+
return 1;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Merge a buffered reflection with the observations added after the reflection
|
|
546
|
+
* started. Lines 0..reflectedLineCount of activeObservations were reflected on
|
|
547
|
+
* and are replaced by the reflection; later lines are appended as-is.
|
|
548
|
+
*/
|
|
549
|
+
function mergeReflectionWithUnreflected(activeObservations, bufferedReflection, reflectedLineCount) {
|
|
550
|
+
const unreflectedContent = (activeObservations || "").split("\n").slice(reflectedLineCount).join("\n").trim();
|
|
551
|
+
return unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
|
|
552
|
+
}
|
|
553
|
+
function isPlainObj(value) {
|
|
554
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Deep-merge two plain config objects (source wins; undefined source values
|
|
558
|
+
* are skipped). Mirrors MemoryStorage.deepMergeConfig in @mastra/core.
|
|
559
|
+
*/
|
|
560
|
+
function deepMergeOMConfig(target, source) {
|
|
561
|
+
const output = { ...target };
|
|
562
|
+
for (const key of Object.keys(source)) {
|
|
563
|
+
const tVal = target[key];
|
|
564
|
+
const sVal = source[key];
|
|
565
|
+
if (isPlainObj(tVal) && isPlainObj(sVal)) output[key] = deepMergeOMConfig(tVal, sVal);
|
|
566
|
+
else if (sVal !== void 0) output[key] = sVal;
|
|
567
|
+
}
|
|
568
|
+
return output;
|
|
569
|
+
}
|
|
570
|
+
function parseJsonObject(value) {
|
|
571
|
+
if (typeof value !== "string" || !value) return {};
|
|
572
|
+
try {
|
|
573
|
+
const parsed = JSON.parse(value);
|
|
574
|
+
return isPlainObj(parsed) ? parsed : {};
|
|
575
|
+
} catch {
|
|
576
|
+
return {};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async function findRecordById(ctx, convexTable, id) {
|
|
580
|
+
return await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", id)).unique();
|
|
581
|
+
}
|
|
582
|
+
function requireRecord(doc, id) {
|
|
583
|
+
if (!doc) throw new Error(`Observational memory record not found: ${id}`);
|
|
584
|
+
return doc;
|
|
585
|
+
}
|
|
586
|
+
const EMPTY_SWAP_RESULT = {
|
|
587
|
+
chunksActivated: 0,
|
|
588
|
+
messageTokensActivated: 0,
|
|
589
|
+
observationTokensActivated: 0,
|
|
590
|
+
messagesActivated: 0,
|
|
591
|
+
activatedCycleIds: [],
|
|
592
|
+
activatedMessageIds: []
|
|
593
|
+
};
|
|
594
|
+
async function handleObservationalMemoryOperation(ctx, convexTable, request) {
|
|
595
|
+
switch (request.op) {
|
|
596
|
+
case "omGetLatest": return {
|
|
597
|
+
ok: true,
|
|
598
|
+
result: await ctx.db.query(convexTable).withIndex("by_lookup_key", (q) => q.eq("lookupKey", request.lookupKey)).order("desc").first() ?? null
|
|
599
|
+
};
|
|
600
|
+
case "omGetHistory": {
|
|
601
|
+
let docs = await ctx.db.query(convexTable).withIndex("by_lookup_key", (q) => q.eq("lookupKey", request.lookupKey)).order("desc").take(OM_QUERY_MAX_DOCS);
|
|
602
|
+
if (request.from) docs = docs.filter((doc) => typeof doc.createdAt === "string" && doc.createdAt >= request.from);
|
|
603
|
+
if (request.to) docs = docs.filter((doc) => typeof doc.createdAt === "string" && doc.createdAt <= request.to);
|
|
604
|
+
if (request.offset != null) docs = docs.slice(request.offset);
|
|
605
|
+
return {
|
|
606
|
+
ok: true,
|
|
607
|
+
result: docs.slice(0, request.limit)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
case "omUpdateActive": {
|
|
611
|
+
const doc = requireRecord(await findRecordById(ctx, convexTable, request.id), request.id);
|
|
612
|
+
const safeTokenCount = Number.isFinite(request.tokenCount) && request.tokenCount >= 0 ? request.tokenCount : 0;
|
|
613
|
+
await ctx.db.patch(doc._id, {
|
|
614
|
+
activeObservations: request.observations,
|
|
615
|
+
lastObservedAt: request.lastObservedAt,
|
|
616
|
+
pendingMessageTokens: 0,
|
|
617
|
+
observationTokenCount: safeTokenCount,
|
|
618
|
+
totalTokensObserved: Number(doc.totalTokensObserved || 0) + safeTokenCount,
|
|
619
|
+
observedMessageIds: request.observedMessageIds,
|
|
620
|
+
updatedAt: request.updatedAt
|
|
621
|
+
});
|
|
622
|
+
return { ok: true };
|
|
623
|
+
}
|
|
624
|
+
case "omAppendBufferedChunk": {
|
|
625
|
+
const doc = requireRecord(await findRecordById(ctx, convexTable, request.id), request.id);
|
|
626
|
+
const chunks = parseStoredChunks(doc.bufferedObservationChunks);
|
|
627
|
+
chunks.push(request.chunk);
|
|
628
|
+
const patch = {
|
|
629
|
+
bufferedObservationChunks: JSON.stringify(chunks),
|
|
630
|
+
updatedAt: request.updatedAt
|
|
631
|
+
};
|
|
632
|
+
if (request.lastBufferedAtTime) patch.lastBufferedAtTime = request.lastBufferedAtTime;
|
|
633
|
+
await ctx.db.patch(doc._id, patch);
|
|
634
|
+
return { ok: true };
|
|
635
|
+
}
|
|
636
|
+
case "omSwapBuffered": {
|
|
637
|
+
const doc = requireRecord(await findRecordById(ctx, convexTable, request.id), request.id);
|
|
638
|
+
const persistedChunks = parseStoredChunks(doc.bufferedObservationChunks);
|
|
639
|
+
if (persistedChunks.length === 0) return {
|
|
640
|
+
ok: true,
|
|
641
|
+
result: EMPTY_SWAP_RESULT
|
|
642
|
+
};
|
|
643
|
+
const chunks = Array.isArray(request.bufferedChunks) ? request.bufferedChunks : persistedChunks;
|
|
644
|
+
if (chunks.length === 0) return {
|
|
645
|
+
ok: true,
|
|
646
|
+
result: EMPTY_SWAP_RESULT
|
|
647
|
+
};
|
|
648
|
+
const chunksToActivate = selectActivationBoundary(chunks, {
|
|
649
|
+
activationRatio: request.activationRatio,
|
|
650
|
+
messageTokensThreshold: request.messageTokensThreshold,
|
|
651
|
+
currentPendingTokens: request.currentPendingTokens,
|
|
652
|
+
forceMaxActivation: request.forceMaxActivation
|
|
653
|
+
});
|
|
654
|
+
const activatedChunks = chunks.slice(0, chunksToActivate);
|
|
655
|
+
const remainingChunks = chunks.slice(chunksToActivate);
|
|
656
|
+
const activatedContent = activatedChunks.map((c) => c.observations).join("\n\n");
|
|
657
|
+
const activatedTokens = activatedChunks.reduce((sum, c) => sum + c.tokenCount, 0);
|
|
658
|
+
const activatedMessageTokens = activatedChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
|
|
659
|
+
const activatedMessageCount = activatedChunks.reduce((sum, c) => sum + (c.messageIds?.length ?? 0), 0);
|
|
660
|
+
const activatedCycleIds = activatedChunks.map((c) => c.cycleId).filter((id) => !!id);
|
|
661
|
+
const activatedMessageIds = activatedChunks.flatMap((c) => c.messageIds ?? []);
|
|
662
|
+
const latestChunk = activatedChunks[activatedChunks.length - 1];
|
|
663
|
+
const lastObservedAt = request.lastObservedAt ?? latestChunk?.lastObservedAt ?? request.now;
|
|
664
|
+
const existingActive = doc.activeObservations || "";
|
|
665
|
+
const boundary = `\n\n--- message boundary (${lastObservedAt}) ---\n\n`;
|
|
666
|
+
const newActive = existingActive ? `${existingActive}${boundary}${activatedContent}` : activatedContent;
|
|
667
|
+
await ctx.db.patch(doc._id, {
|
|
668
|
+
activeObservations: newActive,
|
|
669
|
+
observationTokenCount: Number(doc.observationTokenCount || 0) + activatedTokens,
|
|
670
|
+
pendingMessageTokens: Math.max(0, Number(doc.pendingMessageTokens || 0) - activatedMessageTokens),
|
|
671
|
+
bufferedObservationChunks: remainingChunks.length > 0 ? JSON.stringify(remainingChunks) : null,
|
|
672
|
+
lastObservedAt,
|
|
673
|
+
updatedAt: request.now
|
|
674
|
+
});
|
|
675
|
+
const latestChunkHints = activatedChunks[activatedChunks.length - 1];
|
|
676
|
+
return {
|
|
677
|
+
ok: true,
|
|
678
|
+
result: {
|
|
679
|
+
chunksActivated: activatedChunks.length,
|
|
680
|
+
messageTokensActivated: activatedMessageTokens,
|
|
681
|
+
observationTokensActivated: activatedTokens,
|
|
682
|
+
messagesActivated: activatedMessageCount,
|
|
683
|
+
activatedCycleIds,
|
|
684
|
+
activatedMessageIds,
|
|
685
|
+
observations: activatedContent,
|
|
686
|
+
perChunk: activatedChunks.map((c) => ({
|
|
687
|
+
cycleId: c.cycleId ?? "",
|
|
688
|
+
messageTokens: c.messageTokens ?? 0,
|
|
689
|
+
observationTokens: c.tokenCount,
|
|
690
|
+
messageCount: c.messageIds?.length ?? 0,
|
|
691
|
+
observations: c.observations
|
|
692
|
+
})),
|
|
693
|
+
suggestedContinuation: latestChunkHints?.suggestedContinuation ?? void 0,
|
|
694
|
+
currentTask: latestChunkHints?.currentTask ?? void 0
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
case "omUpdateBufferedReflection": {
|
|
699
|
+
const doc = requireRecord(await findRecordById(ctx, convexTable, request.id), request.id);
|
|
700
|
+
const existingContent = doc.bufferedReflection || "";
|
|
701
|
+
await ctx.db.patch(doc._id, {
|
|
702
|
+
bufferedReflection: existingContent ? `${existingContent}\n\n${request.reflection}` : request.reflection,
|
|
703
|
+
bufferedReflectionTokens: Number(doc.bufferedReflectionTokens || 0) + request.tokenCount,
|
|
704
|
+
bufferedReflectionInputTokens: Number(doc.bufferedReflectionInputTokens || 0) + request.inputTokenCount,
|
|
705
|
+
reflectedObservationLineCount: request.reflectedObservationLineCount,
|
|
706
|
+
updatedAt: request.updatedAt
|
|
707
|
+
});
|
|
708
|
+
return { ok: true };
|
|
709
|
+
}
|
|
710
|
+
case "omSwapBufferedReflection": {
|
|
711
|
+
const { currentRecord, newId, tokenCount, now } = request;
|
|
712
|
+
const doc = requireRecord(await findRecordById(ctx, convexTable, currentRecord.id), currentRecord.id);
|
|
713
|
+
const bufferedReflection = doc.bufferedReflection || "";
|
|
714
|
+
if (!bufferedReflection) throw new Error("No buffered reflection to swap");
|
|
715
|
+
const newObservations = mergeReflectionWithUnreflected(doc.activeObservations || "", bufferedReflection, Number(doc.reflectedObservationLineCount || 0));
|
|
716
|
+
const newRecord = {
|
|
717
|
+
id: newId,
|
|
718
|
+
lookupKey: currentRecord.lookupKey,
|
|
719
|
+
scope: currentRecord.scope,
|
|
720
|
+
resourceId: currentRecord.resourceId,
|
|
721
|
+
threadId: currentRecord.threadId,
|
|
722
|
+
activeObservations: newObservations,
|
|
723
|
+
activeObservationsPendingUpdate: null,
|
|
724
|
+
originType: "reflection",
|
|
725
|
+
config: currentRecord.config,
|
|
726
|
+
generationCount: currentRecord.generationCount + 1,
|
|
727
|
+
lastObservedAt: currentRecord.lastObservedAt,
|
|
728
|
+
lastReflectionAt: now,
|
|
729
|
+
pendingMessageTokens: 0,
|
|
730
|
+
totalTokensObserved: currentRecord.totalTokensObserved,
|
|
731
|
+
observationTokenCount: tokenCount,
|
|
732
|
+
isObserving: false,
|
|
733
|
+
isReflecting: false,
|
|
734
|
+
isBufferingObservation: false,
|
|
735
|
+
isBufferingReflection: false,
|
|
736
|
+
lastBufferedAtTokens: 0,
|
|
737
|
+
lastBufferedAtTime: null,
|
|
738
|
+
observedTimezone: currentRecord.observedTimezone,
|
|
739
|
+
metadata: currentRecord.metadata,
|
|
740
|
+
createdAt: now,
|
|
741
|
+
updatedAt: now
|
|
742
|
+
};
|
|
743
|
+
await ctx.db.insert(convexTable, newRecord);
|
|
744
|
+
await ctx.db.patch(doc._id, {
|
|
745
|
+
bufferedReflection: null,
|
|
746
|
+
bufferedReflectionTokens: null,
|
|
747
|
+
bufferedReflectionInputTokens: null,
|
|
748
|
+
reflectedObservationLineCount: null,
|
|
749
|
+
updatedAt: now
|
|
750
|
+
});
|
|
751
|
+
return {
|
|
752
|
+
ok: true,
|
|
753
|
+
result: newRecord
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
case "omUpdateConfig": {
|
|
757
|
+
const doc = requireRecord(await findRecordById(ctx, convexTable, request.id), request.id);
|
|
758
|
+
const merged = deepMergeOMConfig(parseJsonObject(doc.config), parseJsonObject(request.config));
|
|
759
|
+
await ctx.db.patch(doc._id, {
|
|
760
|
+
config: JSON.stringify(merged),
|
|
761
|
+
updatedAt: request.updatedAt
|
|
762
|
+
});
|
|
763
|
+
return { ok: true };
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region src/server/workflow-snapshot.ts
|
|
769
|
+
const PENDING_MARKER_KEY = "__mastra_pending__";
|
|
770
|
+
function isPendingMarker(val) {
|
|
771
|
+
return val !== null && typeof val === "object" && Object.prototype.hasOwnProperty.call(val, PENDING_MARKER_KEY) && val[PENDING_MARKER_KEY] === true && Object.keys(val).length === 1;
|
|
772
|
+
}
|
|
773
|
+
function isSuspendedStepResult(val) {
|
|
774
|
+
const result = val;
|
|
775
|
+
return val !== null && typeof val === "object" && "status" in val && result?.status === "suspended" && ("suspendPayload" in val || "suspendedAt" in val);
|
|
776
|
+
}
|
|
777
|
+
function canResetWithPendingMarker(val) {
|
|
778
|
+
if (val == null || isPendingMarker(val)) return true;
|
|
779
|
+
return isSuspendedStepResult(val);
|
|
780
|
+
}
|
|
781
|
+
function createEmptyWorkflowSnapshot(runId) {
|
|
782
|
+
return {
|
|
783
|
+
context: {},
|
|
784
|
+
activePaths: [],
|
|
785
|
+
activeStepsPath: {},
|
|
786
|
+
timestamp: Date.now(),
|
|
787
|
+
suspendedPaths: {},
|
|
788
|
+
resumeLabels: {},
|
|
789
|
+
serializedStepGraph: [],
|
|
790
|
+
value: {},
|
|
791
|
+
waitingPaths: {},
|
|
792
|
+
status: "pending",
|
|
793
|
+
runId
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function mergeWorkflowStepResult({ snapshot, stepId, result, requestContext }) {
|
|
797
|
+
if (!snapshot?.context) throw new Error(`Snapshot context not found for runId ${snapshot?.runId}`);
|
|
798
|
+
const existingResult = snapshot.context[stepId];
|
|
799
|
+
if (existingResult && "output" in existingResult && Array.isArray(existingResult.output) && result && typeof result === "object" && "output" in result && Array.isArray(result.output)) {
|
|
800
|
+
const existingOutput = existingResult.output;
|
|
801
|
+
const newOutput = result.output;
|
|
802
|
+
const mergedOutput = [...existingOutput];
|
|
803
|
+
const hasPendingMarker = newOutput.some(isPendingMarker);
|
|
804
|
+
for (let i = 0; i < Math.max(existingOutput.length, newOutput.length); i++) if (i < newOutput.length) {
|
|
805
|
+
const newVal = newOutput[i];
|
|
806
|
+
if (isPendingMarker(newVal)) {
|
|
807
|
+
if (i >= existingOutput.length || canResetWithPendingMarker(existingOutput[i])) mergedOutput[i] = null;
|
|
808
|
+
} else if (newVal !== null && newVal !== void 0 && !hasPendingMarker) mergedOutput[i] = newVal;
|
|
809
|
+
else if (i >= existingOutput.length) mergedOutput[i] = null;
|
|
810
|
+
}
|
|
811
|
+
snapshot.context[stepId] = {
|
|
812
|
+
...existingResult,
|
|
813
|
+
...hasPendingMarker ? {} : result,
|
|
814
|
+
output: mergedOutput
|
|
815
|
+
};
|
|
816
|
+
} else snapshot.context[stepId] = result;
|
|
817
|
+
snapshot.requestContext = {
|
|
818
|
+
...snapshot.requestContext,
|
|
819
|
+
...requestContext
|
|
820
|
+
};
|
|
821
|
+
return JSON.parse(JSON.stringify(snapshot.context));
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/server/storage.ts
|
|
825
|
+
const TABLE_VECTOR_INDEXES = "mastra_vector_indexes";
|
|
826
|
+
const VECTOR_TABLE_PREFIX = "mastra_vector_";
|
|
827
|
+
const CONVEX_TABLE_WORKFLOW_SNAPSHOTS = "mastra_workflow_snapshots";
|
|
828
|
+
const CONVEX_TABLE_BACKGROUND_TASKS = "mastra_background_tasks";
|
|
829
|
+
const CONVEX_TABLE_DOCUMENTS = "mastra_documents";
|
|
830
|
+
const CONVEX_TABLE_OBSERVATIONAL_MEMORY = "mastra_observational_memory";
|
|
831
|
+
const STORAGE_MUTATION_BATCH_SIZE = 25;
|
|
832
|
+
const LOAD_MANY_MAX_IDS_PER_REQUEST = 10;
|
|
833
|
+
const DEFAULT_SCHEDULE_QUERY_LIMIT = 100;
|
|
834
|
+
const BACKGROUND_TASK_FIELD_ALIASES = {
|
|
835
|
+
tool_call_id: "toolCallId",
|
|
836
|
+
toolCallId: "tool_call_id",
|
|
837
|
+
tool_name: "toolName",
|
|
838
|
+
toolName: "tool_name",
|
|
839
|
+
agent_id: "agentId",
|
|
840
|
+
agentId: "agent_id",
|
|
841
|
+
run_id: "runId",
|
|
842
|
+
runId: "run_id",
|
|
843
|
+
thread_id: "threadId",
|
|
844
|
+
threadId: "thread_id",
|
|
845
|
+
resource_id: "resourceId",
|
|
846
|
+
resourceId: "resource_id",
|
|
847
|
+
suspend_payload: "suspendPayload",
|
|
848
|
+
suspendPayload: "suspend_payload",
|
|
849
|
+
retry_count: "retryCount",
|
|
850
|
+
retryCount: "retry_count",
|
|
851
|
+
max_retries: "maxRetries",
|
|
852
|
+
maxRetries: "max_retries",
|
|
853
|
+
timeout_ms: "timeoutMs",
|
|
854
|
+
timeoutMs: "timeout_ms"
|
|
855
|
+
};
|
|
856
|
+
function normalizeScheduleQueryLimit(limit) {
|
|
857
|
+
if (limit == null || !Number.isFinite(limit)) return DEFAULT_SCHEDULE_QUERY_LIMIT;
|
|
858
|
+
return Math.max(0, Math.floor(limit));
|
|
859
|
+
}
|
|
860
|
+
function normalizeLoadManyIds(ids) {
|
|
861
|
+
if (ids.length > LOAD_MANY_MAX_IDS_PER_REQUEST) throw new Error(`loadMany supports at most ${LOAD_MANY_MAX_IDS_PER_REQUEST} ids per request`);
|
|
862
|
+
return [...new Set(ids)];
|
|
863
|
+
}
|
|
864
|
+
function applyConvexEqualityFilters(query, filters, indexedFields = /* @__PURE__ */ new Set()) {
|
|
865
|
+
const remainingFilters = filters?.filter((filter) => !indexedFields.has(filter.field));
|
|
866
|
+
if (!remainingFilters?.length) return query;
|
|
867
|
+
return query.filter((q) => {
|
|
868
|
+
const predicates = remainingFilters.map((filter) => q.eq(q.field(filter.field), filter.value));
|
|
869
|
+
return predicates.length === 1 ? predicates[0] : q.and(...predicates);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
async function mapInBatches$1(inputs, batchSize, mapper) {
|
|
873
|
+
const results = [];
|
|
874
|
+
for (let index = 0; index < inputs.length; index += batchSize) results.push(...await Promise.all(inputs.slice(index, index + batchSize).map(mapper)));
|
|
875
|
+
return results;
|
|
876
|
+
}
|
|
877
|
+
async function deleteDocs(ctx, docs) {
|
|
878
|
+
await mapInBatches$1(docs, STORAGE_MUTATION_BATCH_SIZE, (doc) => ctx.db.delete(doc._id));
|
|
879
|
+
}
|
|
880
|
+
async function findExistingDocsByIds(ids, findDoc) {
|
|
881
|
+
return (await mapInBatches$1([...new Set(ids)], STORAGE_MUTATION_BATCH_SIZE, findDoc)).filter((doc) => Boolean(doc));
|
|
882
|
+
}
|
|
883
|
+
function isBackgroundTasksTable(convexTable, request) {
|
|
884
|
+
return convexTable === CONVEX_TABLE_BACKGROUND_TASKS && request.tableName === _mastra_core_storage_constants.TABLE_BACKGROUND_TASKS;
|
|
885
|
+
}
|
|
886
|
+
function matchesFilters(record, filters) {
|
|
887
|
+
return filters.every((filter) => {
|
|
888
|
+
if (record[filter.field] === filter.value) return true;
|
|
889
|
+
const alternateField = BACKGROUND_TASK_FIELD_ALIASES[filter.field];
|
|
890
|
+
return alternateField ? record[alternateField] === filter.value : false;
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
function mergeLegacyRecord(record, patch) {
|
|
894
|
+
const merged = { ...record };
|
|
895
|
+
for (const [field, value] of Object.entries(patch)) {
|
|
896
|
+
const alternateField = BACKGROUND_TASK_FIELD_ALIASES[field];
|
|
897
|
+
if (alternateField) delete merged[alternateField];
|
|
898
|
+
merged[field] = value;
|
|
899
|
+
}
|
|
900
|
+
return merged;
|
|
901
|
+
}
|
|
902
|
+
function stripPatchKeys(record, keys) {
|
|
903
|
+
const stripped = { ...record };
|
|
904
|
+
for (const key of keys) delete stripped[key];
|
|
905
|
+
return stripped;
|
|
906
|
+
}
|
|
907
|
+
function dedupeByRecordId(records) {
|
|
908
|
+
const seen = /* @__PURE__ */ new Set();
|
|
909
|
+
return records.filter((record) => {
|
|
910
|
+
if (record?.id == null) return true;
|
|
911
|
+
const id = String(record.id);
|
|
912
|
+
if (seen.has(id)) return false;
|
|
913
|
+
seen.add(id);
|
|
914
|
+
return true;
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
function isMissingBackgroundTaskSchemaError(error) {
|
|
918
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
919
|
+
return message.includes(CONVEX_TABLE_BACKGROUND_TASKS) && (message.includes("does not exist") || message.includes("not found") || message.includes("not defined") || message.includes("no such"));
|
|
920
|
+
}
|
|
921
|
+
async function findGenericDocumentById(ctx, tableName, id) {
|
|
922
|
+
return await ctx.db.query(CONVEX_TABLE_DOCUMENTS).withIndex("by_table_primary", (q) => q.eq("table", tableName).eq("primaryKey", String(id))).unique();
|
|
923
|
+
}
|
|
924
|
+
async function findGenericDocumentsByTable(ctx, tableName, limit) {
|
|
925
|
+
return await ctx.db.query(CONVEX_TABLE_DOCUMENTS).withIndex("by_table", (q) => q.eq("table", tableName)).take(limit);
|
|
926
|
+
}
|
|
927
|
+
async function filterLegacyRecordsWithoutTypedCopy(ctx, convexTable, legacyRecords) {
|
|
928
|
+
return (await mapInBatches$1(legacyRecords, STORAGE_MUTATION_BATCH_SIZE, async (record) => {
|
|
929
|
+
if (record.id == null) return record;
|
|
930
|
+
return await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", String(record.id))).unique() ? null : record;
|
|
931
|
+
})).filter((record) => Boolean(record));
|
|
932
|
+
}
|
|
933
|
+
function coalesceTypedRecordsForBatchInsert(records) {
|
|
934
|
+
const recordsById = /* @__PURE__ */ new Map();
|
|
935
|
+
for (const record of records) {
|
|
936
|
+
const id = record.id;
|
|
937
|
+
if (!id) continue;
|
|
938
|
+
const key = String(id);
|
|
939
|
+
recordsById.set(key, {
|
|
940
|
+
...recordsById.get(key) ?? {},
|
|
941
|
+
...record
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return [...recordsById.values()];
|
|
945
|
+
}
|
|
946
|
+
function coalesceLastRecordById(records) {
|
|
947
|
+
const recordsById = /* @__PURE__ */ new Map();
|
|
948
|
+
for (const record of records) {
|
|
949
|
+
const id = record.id;
|
|
950
|
+
if (!id) continue;
|
|
951
|
+
recordsById.set(String(id), record);
|
|
952
|
+
}
|
|
953
|
+
return [...recordsById.values()];
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Determines which Convex table to use based on the logical table name.
|
|
957
|
+
* Returns the Convex table name and whether it's a typed table or fallback.
|
|
958
|
+
*/
|
|
959
|
+
function resolveTable(tableName) {
|
|
960
|
+
switch (tableName) {
|
|
961
|
+
case _mastra_core_storage_constants.TABLE_THREADS: return {
|
|
962
|
+
convexTable: "mastra_threads",
|
|
963
|
+
isTyped: true
|
|
964
|
+
};
|
|
965
|
+
case _mastra_core_storage_constants.TABLE_MESSAGES: return {
|
|
966
|
+
convexTable: "mastra_messages",
|
|
967
|
+
isTyped: true
|
|
968
|
+
};
|
|
969
|
+
case _mastra_core_storage_constants.TABLE_RESOURCES: return {
|
|
970
|
+
convexTable: "mastra_resources",
|
|
971
|
+
isTyped: true
|
|
972
|
+
};
|
|
973
|
+
case _mastra_core_storage_constants.TABLE_WORKFLOW_SNAPSHOT: return {
|
|
974
|
+
convexTable: CONVEX_TABLE_WORKFLOW_SNAPSHOTS,
|
|
975
|
+
isTyped: true
|
|
976
|
+
};
|
|
977
|
+
case _mastra_core_storage_constants.TABLE_SCORERS: return {
|
|
978
|
+
convexTable: "mastra_scorers",
|
|
979
|
+
isTyped: true
|
|
980
|
+
};
|
|
981
|
+
case _mastra_core_storage_constants.TABLE_SCHEDULES: return {
|
|
982
|
+
convexTable: "mastra_schedules",
|
|
983
|
+
isTyped: true
|
|
984
|
+
};
|
|
985
|
+
case _mastra_core_storage_constants.TABLE_SCHEDULE_TRIGGERS: return {
|
|
986
|
+
convexTable: "mastra_schedule_triggers",
|
|
987
|
+
isTyped: true
|
|
988
|
+
};
|
|
989
|
+
case _mastra_core_storage_constants.TABLE_CHANNEL_INSTALLATIONS: return {
|
|
990
|
+
convexTable: "mastra_channel_installations",
|
|
991
|
+
isTyped: true
|
|
992
|
+
};
|
|
993
|
+
case _mastra_core_storage_constants.TABLE_CHANNEL_CONFIG: return {
|
|
994
|
+
convexTable: "mastra_channel_config",
|
|
995
|
+
isTyped: true
|
|
996
|
+
};
|
|
997
|
+
case _mastra_core_storage_constants.TABLE_BACKGROUND_TASKS: return {
|
|
998
|
+
convexTable: CONVEX_TABLE_BACKGROUND_TASKS,
|
|
999
|
+
isTyped: true
|
|
1000
|
+
};
|
|
1001
|
+
case TABLE_VECTOR_INDEXES: return {
|
|
1002
|
+
convexTable: "mastra_vector_indexes",
|
|
1003
|
+
isTyped: true
|
|
1004
|
+
};
|
|
1005
|
+
case CONVEX_TABLE_OBSERVATIONAL_MEMORY: return {
|
|
1006
|
+
convexTable: CONVEX_TABLE_OBSERVATIONAL_MEMORY,
|
|
1007
|
+
isTyped: true
|
|
1008
|
+
};
|
|
1009
|
+
default:
|
|
1010
|
+
if (tableName.startsWith(VECTOR_TABLE_PREFIX)) return {
|
|
1011
|
+
convexTable: "mastra_vectors",
|
|
1012
|
+
isTyped: true
|
|
1013
|
+
};
|
|
1014
|
+
return {
|
|
1015
|
+
convexTable: "mastra_documents",
|
|
1016
|
+
isTyped: false
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Main storage mutation handler.
|
|
1022
|
+
* Routes operations to the appropriate typed table.
|
|
1023
|
+
*/
|
|
1024
|
+
const mastraStorage = (0, convex_server.mutationGeneric)(async (ctx, request) => {
|
|
1025
|
+
try {
|
|
1026
|
+
const { convexTable, isTyped } = resolveTable(request.tableName);
|
|
1027
|
+
if (request.tableName.startsWith(VECTOR_TABLE_PREFIX) && request.tableName !== TABLE_VECTOR_INDEXES) return await handleVectorOperation(ctx, request);
|
|
1028
|
+
if (isTyped) {
|
|
1029
|
+
if (isBackgroundTasksTable(convexTable, request)) try {
|
|
1030
|
+
return await handleTypedOperation(ctx, convexTable, request);
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
if (!isMissingBackgroundTaskSchemaError(error)) throw error;
|
|
1033
|
+
return handleGenericOperation(ctx, request);
|
|
1034
|
+
}
|
|
1035
|
+
return handleTypedOperation(ctx, convexTable, request);
|
|
1036
|
+
}
|
|
1037
|
+
return handleGenericOperation(ctx, request);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
return {
|
|
1040
|
+
ok: false,
|
|
1041
|
+
error: error.message
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
function parseStoredSnapshot(stored, runId) {
|
|
1046
|
+
if (typeof stored === "string") return JSON.parse(stored);
|
|
1047
|
+
return JSON.parse(JSON.stringify(stored ?? createEmptyWorkflowSnapshot(runId)));
|
|
1048
|
+
}
|
|
1049
|
+
function parseMetadataForMerge(metadata) {
|
|
1050
|
+
if (metadata == null) return {};
|
|
1051
|
+
if (typeof metadata === "string") try {
|
|
1052
|
+
return parseMetadataForMerge(JSON.parse(metadata));
|
|
1053
|
+
} catch {
|
|
1054
|
+
return {};
|
|
1055
|
+
}
|
|
1056
|
+
if (typeof metadata === "object" && !Array.isArray(metadata)) return metadata;
|
|
1057
|
+
return {};
|
|
1058
|
+
}
|
|
1059
|
+
function mergeMetadata(existing, update) {
|
|
1060
|
+
return {
|
|
1061
|
+
...parseMetadataForMerge(existing),
|
|
1062
|
+
...update ?? {}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Handle operations on typed tables (threads, messages, etc.)
|
|
1067
|
+
* Records are stored with their `id` field as a regular field (not _id).
|
|
1068
|
+
* We query by the `id` field to find/update records.
|
|
1069
|
+
*/
|
|
1070
|
+
async function handleTypedOperation(ctx, convexTable, request) {
|
|
1071
|
+
switch (request.op) {
|
|
1072
|
+
case "omGetLatest":
|
|
1073
|
+
case "omGetHistory":
|
|
1074
|
+
case "omUpdateActive":
|
|
1075
|
+
case "omAppendBufferedChunk":
|
|
1076
|
+
case "omSwapBuffered":
|
|
1077
|
+
case "omUpdateBufferedReflection":
|
|
1078
|
+
case "omSwapBufferedReflection":
|
|
1079
|
+
case "omUpdateConfig":
|
|
1080
|
+
if (convexTable !== CONVEX_TABLE_OBSERVATIONAL_MEMORY) throw new Error(`${request.op} is only supported for ${CONVEX_TABLE_OBSERVATIONAL_MEMORY}`);
|
|
1081
|
+
return handleObservationalMemoryOperation(ctx, convexTable, request);
|
|
1082
|
+
case "createSchedule": {
|
|
1083
|
+
if (convexTable !== "mastra_schedules") throw new Error(`createSchedule is only supported for mastra_schedules`);
|
|
1084
|
+
const record = request.record;
|
|
1085
|
+
const id = record.id;
|
|
1086
|
+
if (!id) throw new Error(`Schedule is missing an id`);
|
|
1087
|
+
if (await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", id)).unique()) throw new Error(`Schedule with id "${id}" already exists`);
|
|
1088
|
+
await ctx.db.insert(convexTable, record);
|
|
1089
|
+
return { ok: true };
|
|
1090
|
+
}
|
|
1091
|
+
case "recordScheduleTrigger": {
|
|
1092
|
+
if (convexTable !== "mastra_schedule_triggers") throw new Error(`recordScheduleTrigger is only supported for mastra_schedule_triggers`);
|
|
1093
|
+
const record = request.record;
|
|
1094
|
+
const id = record.id;
|
|
1095
|
+
if (!id) throw new Error(`Schedule trigger is missing an id`);
|
|
1096
|
+
if (await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", id)).unique()) throw new Error(`Schedule trigger with id "${id}" already exists`);
|
|
1097
|
+
await ctx.db.insert(convexTable, record);
|
|
1098
|
+
return { ok: true };
|
|
1099
|
+
}
|
|
1100
|
+
case "listDueSchedules":
|
|
1101
|
+
if (convexTable !== "mastra_schedules") throw new Error(`listDueSchedules is only supported for mastra_schedules`);
|
|
1102
|
+
return {
|
|
1103
|
+
ok: true,
|
|
1104
|
+
result: await ctx.db.query(convexTable).withIndex("by_status_next_fire_at", (q) => q.eq("status", "active").lte("next_fire_at", request.now)).take(normalizeScheduleQueryLimit(request.limit))
|
|
1105
|
+
};
|
|
1106
|
+
case "updateScheduleNextFire": {
|
|
1107
|
+
if (convexTable !== "mastra_schedules") throw new Error(`updateScheduleNextFire is only supported for mastra_schedules`);
|
|
1108
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", request.id)).unique();
|
|
1109
|
+
if (!existing || existing.status !== "active" || existing.next_fire_at !== request.expectedNextFireAt) return {
|
|
1110
|
+
ok: true,
|
|
1111
|
+
result: false
|
|
1112
|
+
};
|
|
1113
|
+
await ctx.db.patch(existing._id, {
|
|
1114
|
+
next_fire_at: request.newNextFireAt,
|
|
1115
|
+
last_fire_at: request.lastFireAt,
|
|
1116
|
+
last_run_id: request.lastRunId,
|
|
1117
|
+
updated_at: Date.now()
|
|
1118
|
+
});
|
|
1119
|
+
return {
|
|
1120
|
+
ok: true,
|
|
1121
|
+
result: true
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
case "updateSchedule": {
|
|
1125
|
+
if (convexTable !== "mastra_schedules") throw new Error(`updateSchedule is only supported for mastra_schedules`);
|
|
1126
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", request.id)).unique();
|
|
1127
|
+
if (!existing) throw new Error(`Schedule ${request.id} not found`);
|
|
1128
|
+
await ctx.db.patch(existing._id, request.patch);
|
|
1129
|
+
return {
|
|
1130
|
+
ok: true,
|
|
1131
|
+
result: {
|
|
1132
|
+
...existing,
|
|
1133
|
+
...request.patch
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
case "listScheduleTriggers":
|
|
1138
|
+
if (convexTable !== "mastra_schedule_triggers") throw new Error(`listScheduleTriggers is only supported for mastra_schedule_triggers`);
|
|
1139
|
+
return {
|
|
1140
|
+
ok: true,
|
|
1141
|
+
result: await ctx.db.query(convexTable).withIndex("by_schedule_actual", (q) => {
|
|
1142
|
+
let builder = q.eq("schedule_id", request.scheduleId);
|
|
1143
|
+
if (request.fromActualFireAt != null) builder = builder.gte("actual_fire_at", request.fromActualFireAt);
|
|
1144
|
+
if (request.toActualFireAt != null) builder = builder.lt("actual_fire_at", request.toActualFireAt);
|
|
1145
|
+
return builder;
|
|
1146
|
+
}).order("desc").take(normalizeScheduleQueryLimit(request.limit))
|
|
1147
|
+
};
|
|
1148
|
+
case "deleteScheduleTriggers": {
|
|
1149
|
+
if (convexTable !== "mastra_schedule_triggers") throw new Error(`deleteScheduleTriggers is only supported for mastra_schedule_triggers`);
|
|
1150
|
+
const docs = await ctx.db.query(convexTable).withIndex("by_schedule_actual", (q) => q.eq("schedule_id", request.scheduleId)).take(26);
|
|
1151
|
+
const hasMore = docs.length > STORAGE_MUTATION_BATCH_SIZE;
|
|
1152
|
+
await deleteDocs(ctx, hasMore ? docs.slice(0, STORAGE_MUTATION_BATCH_SIZE) : docs);
|
|
1153
|
+
return {
|
|
1154
|
+
ok: true,
|
|
1155
|
+
hasMore
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
case "insert": {
|
|
1159
|
+
const record = request.record;
|
|
1160
|
+
const id = record.id;
|
|
1161
|
+
if (!id) throw new Error(`Record is missing an id`);
|
|
1162
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", id)).unique();
|
|
1163
|
+
if (existing) {
|
|
1164
|
+
const { id: _, ...updateData } = record;
|
|
1165
|
+
await ctx.db.patch(existing._id, updateData);
|
|
1166
|
+
} else await ctx.db.insert(convexTable, record);
|
|
1167
|
+
return { ok: true };
|
|
1168
|
+
}
|
|
1169
|
+
case "batchInsert":
|
|
1170
|
+
await mapInBatches$1(coalesceTypedRecordsForBatchInsert(request.records), STORAGE_MUTATION_BATCH_SIZE, async (record) => {
|
|
1171
|
+
const id = record.id;
|
|
1172
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", id)).unique();
|
|
1173
|
+
if (existing) {
|
|
1174
|
+
const { id: _, ...updateData } = record;
|
|
1175
|
+
await ctx.db.patch(existing._id, updateData);
|
|
1176
|
+
} else await ctx.db.insert(convexTable, record);
|
|
1177
|
+
});
|
|
1178
|
+
return { ok: true };
|
|
1179
|
+
case "updateThread": {
|
|
1180
|
+
if (convexTable !== "mastra_threads") return {
|
|
1181
|
+
ok: false,
|
|
1182
|
+
error: `Unsupported operation ${request.op} for table ${request.tableName}`
|
|
1183
|
+
};
|
|
1184
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", request.id)).unique();
|
|
1185
|
+
if (!existing) return {
|
|
1186
|
+
ok: true,
|
|
1187
|
+
result: null
|
|
1188
|
+
};
|
|
1189
|
+
const patchRecord = {
|
|
1190
|
+
title: request.title,
|
|
1191
|
+
metadata: mergeMetadata(existing.metadata, request.metadata),
|
|
1192
|
+
updatedAt: request.updatedAt
|
|
1193
|
+
};
|
|
1194
|
+
await ctx.db.patch(existing._id, patchRecord);
|
|
1195
|
+
return {
|
|
1196
|
+
ok: true,
|
|
1197
|
+
result: {
|
|
1198
|
+
...existing,
|
|
1199
|
+
...patchRecord
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
case "updateResource": {
|
|
1204
|
+
if (convexTable !== "mastra_resources") return {
|
|
1205
|
+
ok: false,
|
|
1206
|
+
error: `Unsupported operation ${request.op} for table ${request.tableName}`
|
|
1207
|
+
};
|
|
1208
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", request.resourceId)).unique();
|
|
1209
|
+
if (!existing) {
|
|
1210
|
+
const record = {
|
|
1211
|
+
id: request.resourceId,
|
|
1212
|
+
...request.workingMemory !== void 0 ? { workingMemory: request.workingMemory } : {},
|
|
1213
|
+
metadata: request.metadata ?? {},
|
|
1214
|
+
createdAt: request.createdAt,
|
|
1215
|
+
updatedAt: request.updatedAt
|
|
1216
|
+
};
|
|
1217
|
+
await ctx.db.insert(convexTable, record);
|
|
1218
|
+
return {
|
|
1219
|
+
ok: true,
|
|
1220
|
+
result: record
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
const patchRecord = { updatedAt: request.updatedAt };
|
|
1224
|
+
if (request.workingMemory !== void 0) patchRecord.workingMemory = request.workingMemory;
|
|
1225
|
+
if (request.metadata !== void 0) patchRecord.metadata = mergeMetadata(existing.metadata, request.metadata);
|
|
1226
|
+
await ctx.db.patch(existing._id, patchRecord);
|
|
1227
|
+
return {
|
|
1228
|
+
ok: true,
|
|
1229
|
+
result: {
|
|
1230
|
+
...existing,
|
|
1231
|
+
...patchRecord
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
case "patch": {
|
|
1236
|
+
const patchRecord = stripPatchKeys(request.record, ["id"]);
|
|
1237
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", request.id)).unique();
|
|
1238
|
+
if (!existing) {
|
|
1239
|
+
if (isBackgroundTasksTable(convexTable, request)) {
|
|
1240
|
+
const legacy = await findGenericDocumentById(ctx, request.tableName, request.id);
|
|
1241
|
+
if (legacy) {
|
|
1242
|
+
await ctx.db.patch(legacy._id, { record: mergeLegacyRecord(legacy.record, patchRecord) });
|
|
1243
|
+
return {
|
|
1244
|
+
ok: true,
|
|
1245
|
+
result: true
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
ok: true,
|
|
1251
|
+
result: false
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
await ctx.db.patch(existing._id, patchRecord);
|
|
1255
|
+
if (isBackgroundTasksTable(convexTable, request)) {
|
|
1256
|
+
const legacy = await findGenericDocumentById(ctx, request.tableName, request.id);
|
|
1257
|
+
if (legacy) await ctx.db.delete(legacy._id);
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
ok: true,
|
|
1261
|
+
result: true
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
case "load": {
|
|
1265
|
+
const keys = request.keys;
|
|
1266
|
+
if (keys.id) {
|
|
1267
|
+
const doc = await ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", keys.id)).unique();
|
|
1268
|
+
if (!doc && isBackgroundTasksTable(convexTable, request)) return {
|
|
1269
|
+
ok: true,
|
|
1270
|
+
result: (await findGenericDocumentById(ctx, request.tableName, String(keys.id)))?.record ?? null
|
|
1271
|
+
};
|
|
1272
|
+
return {
|
|
1273
|
+
ok: true,
|
|
1274
|
+
result: doc || null
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
if (convexTable === CONVEX_TABLE_WORKFLOW_SNAPSHOTS && typeof keys.workflow_name === "string" && typeof keys.run_id === "string") return {
|
|
1278
|
+
ok: true,
|
|
1279
|
+
result: await ctx.db.query(convexTable).withIndex("by_workflow_run", (q) => q.eq("workflow_name", keys.workflow_name).eq("run_id", keys.run_id)).unique() || null
|
|
1280
|
+
};
|
|
1281
|
+
return {
|
|
1282
|
+
ok: true,
|
|
1283
|
+
result: (await ctx.db.query(convexTable).take(1e4)).find((doc) => Object.entries(keys).every(([key, value]) => doc[key] === value)) || null
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
case "loadMany": {
|
|
1287
|
+
const ids = normalizeLoadManyIds(request.ids);
|
|
1288
|
+
const typedDocs = (await mapInBatches$1(ids, STORAGE_MUTATION_BATCH_SIZE, (id) => ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", id)).unique())).filter(Boolean);
|
|
1289
|
+
if (!isBackgroundTasksTable(convexTable, request)) return {
|
|
1290
|
+
ok: true,
|
|
1291
|
+
result: typedDocs
|
|
1292
|
+
};
|
|
1293
|
+
const typedDocsById = new Map(typedDocs.map((doc) => [String(doc.id), doc]));
|
|
1294
|
+
const legacyDocs = await mapInBatches$1(ids.filter((id) => !typedDocsById.has(id)), STORAGE_MUTATION_BATCH_SIZE, (id) => findGenericDocumentById(ctx, request.tableName, id));
|
|
1295
|
+
const legacyRecordsById = new Map(legacyDocs.filter((doc) => Boolean(doc)).map((doc) => [String(doc.record.id), doc.record]));
|
|
1296
|
+
return {
|
|
1297
|
+
ok: true,
|
|
1298
|
+
result: ids.map((id) => typedDocsById.get(id) ?? legacyRecordsById.get(id)).filter(Boolean)
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
case "queryTable": {
|
|
1302
|
+
const maxDocs = request.limit ? Math.min(request.limit * 2, 1e4) : 1e4;
|
|
1303
|
+
let query;
|
|
1304
|
+
let indexedFields = /* @__PURE__ */ new Set();
|
|
1305
|
+
if (request.indexHint) {
|
|
1306
|
+
const hint = request.indexHint;
|
|
1307
|
+
if (hint.index === "by_workflow") query = ctx.db.query(convexTable).withIndex("by_workflow", (q) => q.eq("workflow_name", hint.workflowName));
|
|
1308
|
+
else if (hint.index === "by_workflow_run") query = ctx.db.query(convexTable).withIndex("by_workflow_run", (q) => q.eq("workflow_name", hint.workflowName).eq("run_id", hint.runId));
|
|
1309
|
+
else query = ctx.db.query(convexTable);
|
|
1310
|
+
} else if (request.filters && request.filters.length > 0) {
|
|
1311
|
+
const match = findBestIndex(convexTable, request.filters);
|
|
1312
|
+
if (match) {
|
|
1313
|
+
query = ctx.db.query(convexTable).withIndex(match.indexName, (q) => {
|
|
1314
|
+
let builder = q;
|
|
1315
|
+
for (const filter of match.indexedFilters) builder = builder.eq(filter.field, filter.value);
|
|
1316
|
+
return builder;
|
|
1317
|
+
});
|
|
1318
|
+
indexedFields = new Set(match.indexedFilters.map((filter) => filter.field));
|
|
1319
|
+
} else query = ctx.db.query(convexTable);
|
|
1320
|
+
} else query = ctx.db.query(convexTable);
|
|
1321
|
+
let docs = await applyConvexEqualityFilters(query, request.filters, indexedFields).take(maxDocs);
|
|
1322
|
+
if (isBackgroundTasksTable(convexTable, request)) {
|
|
1323
|
+
let legacyRecords = (await findGenericDocumentsByTable(ctx, request.tableName, maxDocs)).map((doc) => doc.record);
|
|
1324
|
+
if (request.filters && request.filters.length > 0) legacyRecords = legacyRecords.filter((record) => matchesFilters(record, request.filters));
|
|
1325
|
+
legacyRecords = await filterLegacyRecordsWithoutTypedCopy(ctx, convexTable, legacyRecords);
|
|
1326
|
+
docs.push(...legacyRecords);
|
|
1327
|
+
docs = dedupeByRecordId(docs);
|
|
1328
|
+
}
|
|
1329
|
+
if (request.limit) docs = docs.slice(0, request.limit);
|
|
1330
|
+
return {
|
|
1331
|
+
ok: true,
|
|
1332
|
+
result: docs
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
case "clearTable":
|
|
1336
|
+
case "dropTable": {
|
|
1337
|
+
const docs = await ctx.db.query(convexTable).take(26);
|
|
1338
|
+
const hasMore = docs.length > STORAGE_MUTATION_BATCH_SIZE;
|
|
1339
|
+
let docsToDelete = hasMore ? docs.slice(0, STORAGE_MUTATION_BATCH_SIZE) : docs;
|
|
1340
|
+
let legacyHasMore = false;
|
|
1341
|
+
if (!hasMore && docsToDelete.length < STORAGE_MUTATION_BATCH_SIZE && isBackgroundTasksTable(convexTable, request)) {
|
|
1342
|
+
const remainingBatchSize = STORAGE_MUTATION_BATCH_SIZE - docsToDelete.length;
|
|
1343
|
+
const legacyDocs = await findGenericDocumentsByTable(ctx, request.tableName, remainingBatchSize + 1);
|
|
1344
|
+
legacyHasMore = legacyDocs.length > remainingBatchSize;
|
|
1345
|
+
docsToDelete = docsToDelete.concat(legacyHasMore ? legacyDocs.slice(0, remainingBatchSize) : legacyDocs);
|
|
1346
|
+
}
|
|
1347
|
+
await deleteDocs(ctx, docsToDelete);
|
|
1348
|
+
return {
|
|
1349
|
+
ok: true,
|
|
1350
|
+
hasMore: hasMore || legacyHasMore
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
case "deleteMany": {
|
|
1354
|
+
const docsToDelete = await findExistingDocsByIds(request.ids, (id) => ctx.db.query(convexTable).withIndex("by_record_id", (q) => q.eq("id", id)).unique());
|
|
1355
|
+
if (isBackgroundTasksTable(convexTable, request)) docsToDelete.push(...await findExistingDocsByIds(request.ids, (id) => findGenericDocumentById(ctx, request.tableName, id)));
|
|
1356
|
+
await deleteDocs(ctx, docsToDelete);
|
|
1357
|
+
return { ok: true };
|
|
1358
|
+
}
|
|
1359
|
+
case "mergeWorkflowStepResult": {
|
|
1360
|
+
if (convexTable !== CONVEX_TABLE_WORKFLOW_SNAPSHOTS) return {
|
|
1361
|
+
ok: false,
|
|
1362
|
+
error: `Unsupported operation ${request.op} for table ${request.tableName}`
|
|
1363
|
+
};
|
|
1364
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_workflow_run", (q) => q.eq("workflow_name", request.workflowName).eq("run_id", request.runId)).unique();
|
|
1365
|
+
if (!existing) return {
|
|
1366
|
+
ok: false,
|
|
1367
|
+
error: `Workflow snapshot not found for runId ${request.runId}`
|
|
1368
|
+
};
|
|
1369
|
+
const snapshot = parseStoredSnapshot(existing.snapshot, request.runId);
|
|
1370
|
+
if (!snapshot.context) return {
|
|
1371
|
+
ok: false,
|
|
1372
|
+
error: `Snapshot for runId ${request.runId} is missing or has invalid context`
|
|
1373
|
+
};
|
|
1374
|
+
const context = mergeWorkflowStepResult({
|
|
1375
|
+
snapshot,
|
|
1376
|
+
stepId: request.stepId,
|
|
1377
|
+
result: JSON.parse(request.result),
|
|
1378
|
+
requestContext: JSON.parse(request.requestContext)
|
|
1379
|
+
});
|
|
1380
|
+
await ctx.db.patch(existing._id, {
|
|
1381
|
+
snapshot: JSON.stringify(snapshot),
|
|
1382
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1383
|
+
});
|
|
1384
|
+
return {
|
|
1385
|
+
ok: true,
|
|
1386
|
+
result: JSON.stringify(context)
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
case "mergeWorkflowState": {
|
|
1390
|
+
if (convexTable !== CONVEX_TABLE_WORKFLOW_SNAPSHOTS) return {
|
|
1391
|
+
ok: false,
|
|
1392
|
+
error: `Unsupported operation ${request.op} for table ${request.tableName}`
|
|
1393
|
+
};
|
|
1394
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_workflow_run", (q) => q.eq("workflow_name", request.workflowName).eq("run_id", request.runId)).unique();
|
|
1395
|
+
if (!existing) return {
|
|
1396
|
+
ok: false,
|
|
1397
|
+
error: `Workflow snapshot not found for runId ${request.runId}`
|
|
1398
|
+
};
|
|
1399
|
+
const snapshot = parseStoredSnapshot(existing.snapshot, request.runId);
|
|
1400
|
+
if (!snapshot.context) return {
|
|
1401
|
+
ok: false,
|
|
1402
|
+
error: `Snapshot for runId ${request.runId} is missing or has invalid context`
|
|
1403
|
+
};
|
|
1404
|
+
const mergedSnapshot = {
|
|
1405
|
+
...snapshot,
|
|
1406
|
+
...JSON.parse(request.opts)
|
|
1407
|
+
};
|
|
1408
|
+
await ctx.db.patch(existing._id, {
|
|
1409
|
+
snapshot: JSON.stringify(mergedSnapshot),
|
|
1410
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1411
|
+
});
|
|
1412
|
+
return {
|
|
1413
|
+
ok: true,
|
|
1414
|
+
result: JSON.stringify(mergedSnapshot)
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
default: return {
|
|
1418
|
+
ok: false,
|
|
1419
|
+
error: `Unsupported operation ${request.op}`
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Handle operations on the vectors table.
|
|
1425
|
+
* Vectors are stored with indexName to support multiple indexes.
|
|
1426
|
+
*/
|
|
1427
|
+
async function handleVectorOperation(ctx, request) {
|
|
1428
|
+
const indexName = request.tableName.replace(VECTOR_TABLE_PREFIX, "");
|
|
1429
|
+
const convexTable = "mastra_vectors";
|
|
1430
|
+
switch (request.op) {
|
|
1431
|
+
case "insert": {
|
|
1432
|
+
const record = request.record;
|
|
1433
|
+
const id = record.id;
|
|
1434
|
+
if (!id) throw new Error(`Vector record is missing an id`);
|
|
1435
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_index_id", (q) => q.eq("indexName", indexName).eq("id", id)).unique();
|
|
1436
|
+
if (existing) await ctx.db.patch(existing._id, {
|
|
1437
|
+
embedding: record.embedding,
|
|
1438
|
+
metadata: record.metadata
|
|
1439
|
+
});
|
|
1440
|
+
else await ctx.db.insert(convexTable, {
|
|
1441
|
+
id,
|
|
1442
|
+
indexName,
|
|
1443
|
+
embedding: record.embedding,
|
|
1444
|
+
metadata: record.metadata
|
|
1445
|
+
});
|
|
1446
|
+
return { ok: true };
|
|
1447
|
+
}
|
|
1448
|
+
case "batchInsert":
|
|
1449
|
+
await mapInBatches$1(coalesceLastRecordById(request.records), STORAGE_MUTATION_BATCH_SIZE, async (record) => {
|
|
1450
|
+
const id = record.id;
|
|
1451
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_index_id", (q) => q.eq("indexName", indexName).eq("id", id)).unique();
|
|
1452
|
+
if (existing) await ctx.db.patch(existing._id, {
|
|
1453
|
+
embedding: record.embedding,
|
|
1454
|
+
metadata: record.metadata
|
|
1455
|
+
});
|
|
1456
|
+
else await ctx.db.insert(convexTable, {
|
|
1457
|
+
id,
|
|
1458
|
+
indexName,
|
|
1459
|
+
embedding: record.embedding,
|
|
1460
|
+
metadata: record.metadata
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
return { ok: true };
|
|
1464
|
+
case "patch": {
|
|
1465
|
+
const patchRecord = stripPatchKeys(request.record, ["id", "indexName"]);
|
|
1466
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_index_id", (q) => q.eq("indexName", indexName).eq("id", request.id)).unique();
|
|
1467
|
+
if (!existing) return {
|
|
1468
|
+
ok: true,
|
|
1469
|
+
result: false
|
|
1470
|
+
};
|
|
1471
|
+
await ctx.db.patch(existing._id, patchRecord);
|
|
1472
|
+
return {
|
|
1473
|
+
ok: true,
|
|
1474
|
+
result: true
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
case "load": {
|
|
1478
|
+
const keys = request.keys;
|
|
1479
|
+
if (keys.id) return {
|
|
1480
|
+
ok: true,
|
|
1481
|
+
result: await ctx.db.query(convexTable).withIndex("by_index_id", (q) => q.eq("indexName", indexName).eq("id", keys.id)).unique() || null
|
|
1482
|
+
};
|
|
1483
|
+
return {
|
|
1484
|
+
ok: true,
|
|
1485
|
+
result: null
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
case "loadMany": return {
|
|
1489
|
+
ok: true,
|
|
1490
|
+
result: await findExistingDocsByIds(normalizeLoadManyIds(request.ids), (id) => ctx.db.query(convexTable).withIndex("by_index_id", (q) => q.eq("indexName", indexName).eq("id", id)).unique())
|
|
1491
|
+
};
|
|
1492
|
+
case "queryTable": {
|
|
1493
|
+
if (request.cursor !== void 0 && request.pageSize === void 0) throw new Error("queryTable cursor requires pageSize");
|
|
1494
|
+
if (request.pageSize !== void 0) {
|
|
1495
|
+
if (!Number.isInteger(request.pageSize) || request.pageSize <= 0) throw new Error("queryTable pageSize must be a positive integer");
|
|
1496
|
+
if (request.limit !== void 0) throw new Error("queryTable limit cannot be combined with pageSize");
|
|
1497
|
+
const page = await ctx.db.query(convexTable).withIndex("by_index", (q) => q.eq("indexName", indexName)).paginate({
|
|
1498
|
+
cursor: request.cursor ?? null,
|
|
1499
|
+
numItems: request.pageSize
|
|
1500
|
+
});
|
|
1501
|
+
let docs = page.page;
|
|
1502
|
+
if (request.filters && request.filters.length > 0) docs = docs.filter((doc) => request.filters.every((filter) => doc[filter.field] === filter.value));
|
|
1503
|
+
return {
|
|
1504
|
+
ok: true,
|
|
1505
|
+
result: docs,
|
|
1506
|
+
hasMore: !page.isDone,
|
|
1507
|
+
continuationCursor: page.continueCursor
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
const maxDocs = request.limit ? Math.min(request.limit * 2, 1e4) : 1e4;
|
|
1511
|
+
let docs = await ctx.db.query(convexTable).withIndex("by_index", (q) => q.eq("indexName", indexName)).take(maxDocs);
|
|
1512
|
+
if (request.filters && request.filters.length > 0) docs = docs.filter((doc) => request.filters.every((filter) => doc[filter.field] === filter.value));
|
|
1513
|
+
if (request.limit) docs = docs.slice(0, request.limit);
|
|
1514
|
+
return {
|
|
1515
|
+
ok: true,
|
|
1516
|
+
result: docs
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
case "clearTable":
|
|
1520
|
+
case "dropTable": {
|
|
1521
|
+
const docs = await ctx.db.query(convexTable).withIndex("by_index", (q) => q.eq("indexName", indexName)).take(26);
|
|
1522
|
+
const hasMore = docs.length > STORAGE_MUTATION_BATCH_SIZE;
|
|
1523
|
+
await deleteDocs(ctx, hasMore ? docs.slice(0, STORAGE_MUTATION_BATCH_SIZE) : docs);
|
|
1524
|
+
return {
|
|
1525
|
+
ok: true,
|
|
1526
|
+
hasMore
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
case "deleteMany":
|
|
1530
|
+
await deleteDocs(ctx, await findExistingDocsByIds(request.ids, (id) => ctx.db.query(convexTable).withIndex("by_index_id", (q) => q.eq("indexName", indexName).eq("id", id)).unique()));
|
|
1531
|
+
return { ok: true };
|
|
1532
|
+
default: return {
|
|
1533
|
+
ok: false,
|
|
1534
|
+
error: `Unsupported operation ${request.op}`
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Handle operations on the generic documents table.
|
|
1540
|
+
* Used as fallback for unknown table names.
|
|
1541
|
+
*/
|
|
1542
|
+
async function handleGenericOperation(ctx, request) {
|
|
1543
|
+
const tableName = request.tableName;
|
|
1544
|
+
const convexTable = "mastra_documents";
|
|
1545
|
+
switch (request.op) {
|
|
1546
|
+
case "insert": {
|
|
1547
|
+
const record = request.record;
|
|
1548
|
+
if (!record.id) throw new Error(`Record for table ${tableName} is missing an id`);
|
|
1549
|
+
const primaryKey = String(record.id);
|
|
1550
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_table_primary", (q) => q.eq("table", tableName).eq("primaryKey", primaryKey)).unique();
|
|
1551
|
+
if (existing) await ctx.db.patch(existing._id, { record });
|
|
1552
|
+
else await ctx.db.insert(convexTable, {
|
|
1553
|
+
table: tableName,
|
|
1554
|
+
primaryKey,
|
|
1555
|
+
record
|
|
1556
|
+
});
|
|
1557
|
+
return { ok: true };
|
|
1558
|
+
}
|
|
1559
|
+
case "batchInsert":
|
|
1560
|
+
await mapInBatches$1(coalesceLastRecordById(request.records), STORAGE_MUTATION_BATCH_SIZE, async (record) => {
|
|
1561
|
+
const primaryKey = String(record.id);
|
|
1562
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_table_primary", (q) => q.eq("table", tableName).eq("primaryKey", primaryKey)).unique();
|
|
1563
|
+
if (existing) await ctx.db.patch(existing._id, { record });
|
|
1564
|
+
else await ctx.db.insert(convexTable, {
|
|
1565
|
+
table: tableName,
|
|
1566
|
+
primaryKey,
|
|
1567
|
+
record
|
|
1568
|
+
});
|
|
1569
|
+
});
|
|
1570
|
+
return { ok: true };
|
|
1571
|
+
case "patch": {
|
|
1572
|
+
const patchRecord = stripPatchKeys(request.record, ["id"]);
|
|
1573
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_table_primary", (q) => q.eq("table", tableName).eq("primaryKey", String(request.id))).unique();
|
|
1574
|
+
if (!existing) return {
|
|
1575
|
+
ok: true,
|
|
1576
|
+
result: false
|
|
1577
|
+
};
|
|
1578
|
+
await ctx.db.patch(existing._id, { record: tableName === _mastra_core_storage_constants.TABLE_BACKGROUND_TASKS ? mergeLegacyRecord(existing.record, patchRecord) : {
|
|
1579
|
+
...existing.record,
|
|
1580
|
+
...patchRecord
|
|
1581
|
+
} });
|
|
1582
|
+
return {
|
|
1583
|
+
ok: true,
|
|
1584
|
+
result: true
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
case "load": {
|
|
1588
|
+
const keys = request.keys;
|
|
1589
|
+
if (keys.id) {
|
|
1590
|
+
const existing = await ctx.db.query(convexTable).withIndex("by_table_primary", (q) => q.eq("table", tableName).eq("primaryKey", String(keys.id))).unique();
|
|
1591
|
+
return {
|
|
1592
|
+
ok: true,
|
|
1593
|
+
result: existing ? existing.record : null
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
const match = (await ctx.db.query(convexTable).withIndex("by_table", (q) => q.eq("table", tableName)).take(1e4)).find((doc) => Object.entries(keys).every(([key, value]) => doc.record?.[key] === value));
|
|
1597
|
+
return {
|
|
1598
|
+
ok: true,
|
|
1599
|
+
result: match ? match.record : null
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
case "loadMany": return {
|
|
1603
|
+
ok: true,
|
|
1604
|
+
result: (await mapInBatches$1(normalizeLoadManyIds(request.ids), STORAGE_MUTATION_BATCH_SIZE, (id) => findGenericDocumentById(ctx, tableName, id))).filter((doc) => Boolean(doc)).map((doc) => doc.record)
|
|
1605
|
+
};
|
|
1606
|
+
case "queryTable": {
|
|
1607
|
+
const maxDocs = request.limit ? Math.min(request.limit * 2, 1e4) : 1e4;
|
|
1608
|
+
let records = (await ctx.db.query(convexTable).withIndex("by_table", (q) => q.eq("table", tableName)).take(maxDocs)).map((doc) => doc.record);
|
|
1609
|
+
if (request.filters && request.filters.length > 0) records = records.filter((record) => tableName === _mastra_core_storage_constants.TABLE_BACKGROUND_TASKS ? matchesFilters(record, request.filters) : request.filters.every((filter) => record?.[filter.field] === filter.value));
|
|
1610
|
+
if (request.limit) records = records.slice(0, request.limit);
|
|
1611
|
+
return {
|
|
1612
|
+
ok: true,
|
|
1613
|
+
result: records
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
case "clearTable":
|
|
1617
|
+
case "dropTable": {
|
|
1618
|
+
const docs = await ctx.db.query(convexTable).withIndex("by_table", (q) => q.eq("table", tableName)).take(26);
|
|
1619
|
+
const hasMore = docs.length > STORAGE_MUTATION_BATCH_SIZE;
|
|
1620
|
+
await deleteDocs(ctx, hasMore ? docs.slice(0, STORAGE_MUTATION_BATCH_SIZE) : docs);
|
|
1621
|
+
return {
|
|
1622
|
+
ok: true,
|
|
1623
|
+
hasMore
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
case "deleteMany":
|
|
1627
|
+
await deleteDocs(ctx, await findExistingDocsByIds(request.ids, (id) => ctx.db.query(convexTable).withIndex("by_table_primary", (q) => q.eq("table", tableName).eq("primaryKey", String(id))).unique()));
|
|
1628
|
+
return { ok: true };
|
|
1629
|
+
default: return {
|
|
1630
|
+
ok: false,
|
|
1631
|
+
error: `Unsupported operation ${request.op}`
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
//#endregion
|
|
1636
|
+
//#region src/server/native-vector.ts
|
|
1637
|
+
const DEFAULT_ID_FIELD = "id";
|
|
1638
|
+
const DEFAULT_ID_INDEX = "by_record_id";
|
|
1639
|
+
const DEFAULT_VECTOR_FIELD = "embedding";
|
|
1640
|
+
const DEFAULT_METADATA_FIELD = "metadata";
|
|
1641
|
+
const NATIVE_VECTOR_BATCH_SIZE = 25;
|
|
1642
|
+
const MAX_CONVEX_VECTOR_RESULTS = 256;
|
|
1643
|
+
const nativeVectorFilterValueValidator = convex_values.v.union(convex_values.v.string(), convex_values.v.number(), convex_values.v.boolean(), convex_values.v.null());
|
|
1644
|
+
const nativeVectorFilterClauseValidator = convex_values.v.object({
|
|
1645
|
+
field: convex_values.v.string(),
|
|
1646
|
+
value: nativeVectorFilterValueValidator
|
|
1647
|
+
});
|
|
1648
|
+
const nativeVectorFilterValidator = convex_values.v.union(nativeVectorFilterClauseValidator, convex_values.v.object({ $or: convex_values.v.array(nativeVectorFilterClauseValidator) }));
|
|
1649
|
+
const nativeVectorIndexConfigValidator = convex_values.v.object({
|
|
1650
|
+
tableName: convex_values.v.string(),
|
|
1651
|
+
vectorIndexName: convex_values.v.string(),
|
|
1652
|
+
dimension: convex_values.v.optional(convex_values.v.number()),
|
|
1653
|
+
idField: convex_values.v.optional(convex_values.v.string()),
|
|
1654
|
+
idIndexName: convex_values.v.optional(convex_values.v.string()),
|
|
1655
|
+
vectorField: convex_values.v.optional(convex_values.v.string()),
|
|
1656
|
+
metadataField: convex_values.v.optional(convex_values.v.string()),
|
|
1657
|
+
filterFields: convex_values.v.optional(convex_values.v.array(convex_values.v.string()))
|
|
1658
|
+
});
|
|
1659
|
+
function idField(config) {
|
|
1660
|
+
return config.idField ?? DEFAULT_ID_FIELD;
|
|
1661
|
+
}
|
|
1662
|
+
function idIndexName(config) {
|
|
1663
|
+
return config.idIndexName ?? DEFAULT_ID_INDEX;
|
|
1664
|
+
}
|
|
1665
|
+
function vectorField(config) {
|
|
1666
|
+
return config.vectorField ?? DEFAULT_VECTOR_FIELD;
|
|
1667
|
+
}
|
|
1668
|
+
function metadataField(config) {
|
|
1669
|
+
return config.metadataField ?? DEFAULT_METADATA_FIELD;
|
|
1670
|
+
}
|
|
1671
|
+
function asTableName(tableName) {
|
|
1672
|
+
return tableName;
|
|
1673
|
+
}
|
|
1674
|
+
function asConvexId(id) {
|
|
1675
|
+
return id;
|
|
1676
|
+
}
|
|
1677
|
+
function pickFilterFields(metadata, filterFields) {
|
|
1678
|
+
const fields = {};
|
|
1679
|
+
if (!metadata || !filterFields) return fields;
|
|
1680
|
+
for (const field of filterFields) {
|
|
1681
|
+
const value = metadata[field];
|
|
1682
|
+
if (value !== void 0) fields[field] = value;
|
|
1683
|
+
}
|
|
1684
|
+
return fields;
|
|
1685
|
+
}
|
|
1686
|
+
function isMetadataRecord(value) {
|
|
1687
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1688
|
+
}
|
|
1689
|
+
function validateMetadataArray(metadata, idsLength) {
|
|
1690
|
+
if (metadata === void 0) return void 0;
|
|
1691
|
+
if (!Array.isArray(metadata)) throw new Error("Native vector upsert: metadata must be an array matching ids when provided");
|
|
1692
|
+
if (metadata.length !== idsLength) throw new Error(`Native vector upsert: metadata length (${metadata.length}) must match ids length (${idsLength})`);
|
|
1693
|
+
if (!metadata.every(isMetadataRecord)) throw new Error("Native vector upsert: metadata entries must be objects when provided");
|
|
1694
|
+
return metadata;
|
|
1695
|
+
}
|
|
1696
|
+
function validateMetadataRecord(metadata) {
|
|
1697
|
+
if (metadata === void 0) return void 0;
|
|
1698
|
+
if (!isMetadataRecord(metadata)) throw new Error("Native vector update: metadata must be an object when provided");
|
|
1699
|
+
return metadata;
|
|
1700
|
+
}
|
|
1701
|
+
function clearMissingFilterFields(patch, metadata, filterFields) {
|
|
1702
|
+
if (!filterFields) return;
|
|
1703
|
+
for (const field of filterFields) if (metadata[field] === void 0) patch[field] = void 0;
|
|
1704
|
+
}
|
|
1705
|
+
function omitVectorField(doc, config) {
|
|
1706
|
+
const { [vectorField(config)]: _, ...docWithoutVector } = doc;
|
|
1707
|
+
return docWithoutVector;
|
|
1708
|
+
}
|
|
1709
|
+
function buildRecord({ config, id, vector, metadata }) {
|
|
1710
|
+
return {
|
|
1711
|
+
[idField(config)]: id,
|
|
1712
|
+
[vectorField(config)]: vector,
|
|
1713
|
+
...metadata !== void 0 ? { [metadataField(config)]: metadata } : {},
|
|
1714
|
+
...pickFilterFields(metadata, config.filterFields)
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
async function findByRecordId(ctx, config, id) {
|
|
1718
|
+
return ctx.db.query(asTableName(config.tableName)).withIndex(idIndexName(config), (q) => q.eq(idField(config), id)).unique();
|
|
1719
|
+
}
|
|
1720
|
+
async function mapInBatches(inputs, mapper) {
|
|
1721
|
+
const results = [];
|
|
1722
|
+
for (let index = 0; index < inputs.length; index += NATIVE_VECTOR_BATCH_SIZE) results.push(...await Promise.all(inputs.slice(index, index + NATIVE_VECTOR_BATCH_SIZE).map((input, batchIndex) => mapper(input, index + batchIndex))));
|
|
1723
|
+
return results;
|
|
1724
|
+
}
|
|
1725
|
+
function buildVectorFilter(q, filter) {
|
|
1726
|
+
if (!filter) return void 0;
|
|
1727
|
+
if ("$or" in filter) return q.or(...filter.$or.map((clause) => q.eq(clause.field, clause.value)));
|
|
1728
|
+
return q.eq(filter.field, filter.value);
|
|
1729
|
+
}
|
|
1730
|
+
const mastraNativeVectorAction = (0, convex_server.actionGeneric)({
|
|
1731
|
+
args: {
|
|
1732
|
+
config: nativeVectorIndexConfigValidator,
|
|
1733
|
+
vector: convex_values.v.array(convex_values.v.number()),
|
|
1734
|
+
limit: convex_values.v.optional(convex_values.v.number()),
|
|
1735
|
+
filter: convex_values.v.optional(nativeVectorFilterValidator)
|
|
1736
|
+
},
|
|
1737
|
+
handler: async (ctx, args) => {
|
|
1738
|
+
const config = args.config;
|
|
1739
|
+
const limit = args.limit;
|
|
1740
|
+
const filter = args.filter;
|
|
1741
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit < 1 || limit > MAX_CONVEX_VECTOR_RESULTS)) throw new Error(`Native vector query: limit must be an integer between 1 and ${MAX_CONVEX_VECTOR_RESULTS}`);
|
|
1742
|
+
return (await ctx.vectorSearch(asTableName(config.tableName), config.vectorIndexName, {
|
|
1743
|
+
vector: args.vector,
|
|
1744
|
+
...limit !== void 0 ? { limit } : {},
|
|
1745
|
+
...filter ? { filter: (q) => buildVectorFilter(q, filter) } : {}
|
|
1746
|
+
})).map((result) => ({
|
|
1747
|
+
id: String(result._id),
|
|
1748
|
+
score: result._score
|
|
1749
|
+
}));
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
const mastraNativeVectorQuery = (0, convex_server.queryGeneric)({
|
|
1753
|
+
args: {
|
|
1754
|
+
op: convex_values.v.union(convex_values.v.literal("getByConvexIds"), convex_values.v.literal("describe"), convex_values.v.literal("listByIds")),
|
|
1755
|
+
config: nativeVectorIndexConfigValidator,
|
|
1756
|
+
ids: convex_values.v.optional(convex_values.v.array(convex_values.v.string())),
|
|
1757
|
+
includeVector: convex_values.v.optional(convex_values.v.boolean()),
|
|
1758
|
+
countLimit: convex_values.v.optional(convex_values.v.number())
|
|
1759
|
+
},
|
|
1760
|
+
handler: async (ctx, args) => {
|
|
1761
|
+
const config = args.config;
|
|
1762
|
+
switch (args.op) {
|
|
1763
|
+
case "getByConvexIds": {
|
|
1764
|
+
const ids = args.ids;
|
|
1765
|
+
if (!ids) throw new Error("Native vector query: ids are required");
|
|
1766
|
+
const includeVector = args.includeVector === true;
|
|
1767
|
+
return (await mapInBatches(ids, (id) => ctx.db.get(asConvexId(id)))).filter((doc) => Boolean(doc)).map((doc) => includeVector ? doc : omitVectorField(doc, config));
|
|
1768
|
+
}
|
|
1769
|
+
case "describe": {
|
|
1770
|
+
const limit = Math.max(1, Math.min(args.countLimit ?? 1e4, 1e4));
|
|
1771
|
+
const docs = await ctx.db.query(asTableName(config.tableName)).take(limit + 1);
|
|
1772
|
+
return {
|
|
1773
|
+
count: Math.min(docs.length, limit),
|
|
1774
|
+
countIsLimited: docs.length > limit
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
case "listByIds": {
|
|
1778
|
+
const ids = args.ids;
|
|
1779
|
+
if (!ids) throw new Error("Native vector query: ids are required");
|
|
1780
|
+
return mapInBatches(ids, (id) => findByRecordId(ctx, config, id));
|
|
1781
|
+
}
|
|
1782
|
+
default: throw new Error(`Unsupported native vector query operation: ${args.op}`);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
const mastraNativeVectorMutation = (0, convex_server.mutationGeneric)({
|
|
1787
|
+
args: {
|
|
1788
|
+
op: convex_values.v.union(convex_values.v.literal("upsert"), convex_values.v.literal("updateById"), convex_values.v.literal("deleteByIds")),
|
|
1789
|
+
config: nativeVectorIndexConfigValidator,
|
|
1790
|
+
ids: convex_values.v.optional(convex_values.v.array(convex_values.v.string())),
|
|
1791
|
+
vectors: convex_values.v.optional(convex_values.v.array(convex_values.v.array(convex_values.v.number()))),
|
|
1792
|
+
metadata: convex_values.v.optional(convex_values.v.any()),
|
|
1793
|
+
id: convex_values.v.optional(convex_values.v.string()),
|
|
1794
|
+
vector: convex_values.v.optional(convex_values.v.array(convex_values.v.number()))
|
|
1795
|
+
},
|
|
1796
|
+
handler: async (ctx, args) => {
|
|
1797
|
+
const config = args.config;
|
|
1798
|
+
switch (args.op) {
|
|
1799
|
+
case "upsert": {
|
|
1800
|
+
const ids = args.ids;
|
|
1801
|
+
const vectors = args.vectors;
|
|
1802
|
+
if (!ids || !vectors) throw new Error("Native vector upsert: ids and vectors are required");
|
|
1803
|
+
if (vectors.length !== ids.length) throw new Error(`Native vector upsert: vectors length (${vectors.length}) must match ids length (${ids.length})`);
|
|
1804
|
+
const metadata = validateMetadataArray(args.metadata, ids.length);
|
|
1805
|
+
if (new Set(ids).size !== ids.length) throw new Error("Native vector upsert: ids must be unique");
|
|
1806
|
+
await mapInBatches(ids, async (id, index) => {
|
|
1807
|
+
const record = buildRecord({
|
|
1808
|
+
config,
|
|
1809
|
+
id,
|
|
1810
|
+
vector: vectors[index],
|
|
1811
|
+
metadata: metadata?.[index]
|
|
1812
|
+
});
|
|
1813
|
+
const existing = await findByRecordId(ctx, config, id);
|
|
1814
|
+
if (existing?._id) {
|
|
1815
|
+
const { _id: _, _creationTime: __, ...patch } = record;
|
|
1816
|
+
if (metadata?.[index] !== void 0) clearMissingFilterFields(patch, metadata[index], config.filterFields);
|
|
1817
|
+
await ctx.db.patch(existing._id, patch);
|
|
1818
|
+
} else await ctx.db.insert(asTableName(config.tableName), record);
|
|
1819
|
+
});
|
|
1820
|
+
return { ok: true };
|
|
1821
|
+
}
|
|
1822
|
+
case "updateById": {
|
|
1823
|
+
if (!args.id) throw new Error("Native vector update: id is required");
|
|
1824
|
+
const existing = await findByRecordId(ctx, config, args.id);
|
|
1825
|
+
if (!existing?._id) return { ok: true };
|
|
1826
|
+
const patch = {};
|
|
1827
|
+
if (args.vector) patch[vectorField(config)] = args.vector;
|
|
1828
|
+
const metadata = validateMetadataRecord(args.metadata);
|
|
1829
|
+
if (metadata !== void 0) {
|
|
1830
|
+
const existingMetadata = isMetadataRecord(existing[metadataField(config)]) ? existing[metadataField(config)] : {};
|
|
1831
|
+
patch[metadataField(config)] = {
|
|
1832
|
+
...existingMetadata,
|
|
1833
|
+
...metadata
|
|
1834
|
+
};
|
|
1835
|
+
Object.assign(patch, pickFilterFields(patch[metadataField(config)], config.filterFields));
|
|
1836
|
+
}
|
|
1837
|
+
if (Object.keys(patch).length > 0) await ctx.db.patch(existing._id, patch);
|
|
1838
|
+
return { ok: true };
|
|
1839
|
+
}
|
|
1840
|
+
case "deleteByIds": {
|
|
1841
|
+
const ids = args.ids;
|
|
1842
|
+
if (!ids) throw new Error("Native vector deleteByIds: ids are required");
|
|
1843
|
+
await mapInBatches((await mapInBatches(ids, (id) => findByRecordId(ctx, config, id))).filter((doc) => Boolean(doc?._id)), (doc) => ctx.db.delete(doc._id));
|
|
1844
|
+
return { ok: true };
|
|
1845
|
+
}
|
|
1846
|
+
default: throw new Error(`Unsupported native vector mutation operation: ${args.op}`);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
//#endregion
|
|
1851
|
+
Object.defineProperty(exports, "mastraCache", {
|
|
1852
|
+
enumerable: true,
|
|
1853
|
+
get: function() {
|
|
1854
|
+
return mastraCache;
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
Object.defineProperty(exports, "mastraNativeVectorAction", {
|
|
1858
|
+
enumerable: true,
|
|
1859
|
+
get: function() {
|
|
1860
|
+
return mastraNativeVectorAction;
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
Object.defineProperty(exports, "mastraNativeVectorMutation", {
|
|
1864
|
+
enumerable: true,
|
|
1865
|
+
get: function() {
|
|
1866
|
+
return mastraNativeVectorMutation;
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
Object.defineProperty(exports, "mastraNativeVectorQuery", {
|
|
1870
|
+
enumerable: true,
|
|
1871
|
+
get: function() {
|
|
1872
|
+
return mastraNativeVectorQuery;
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
Object.defineProperty(exports, "mastraStorage", {
|
|
1876
|
+
enumerable: true,
|
|
1877
|
+
get: function() {
|
|
1878
|
+
return mastraStorage;
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
//# sourceMappingURL=server-13j_f22k.cjs.map
|