@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.
@@ -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