@spinabot/brigade 1.0.1 → 1.0.2

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.
Files changed (51) hide show
  1. package/convex/_generated/api.d.ts +85 -0
  2. package/convex/_generated/api.js +23 -0
  3. package/convex/_generated/dataModel.d.ts +60 -0
  4. package/convex/_generated/server.d.ts +143 -0
  5. package/convex/_generated/server.js +93 -0
  6. package/convex/admin.d.ts +57 -0
  7. package/convex/admin.ts +315 -0
  8. package/convex/auth.d.ts +159 -0
  9. package/convex/auth.ts +217 -0
  10. package/convex/blobs.d.ts +38 -0
  11. package/convex/blobs.ts +115 -0
  12. package/convex/channels.d.ts +150 -0
  13. package/convex/channels.ts +455 -0
  14. package/convex/config.d.ts +67 -0
  15. package/convex/config.ts +168 -0
  16. package/convex/cron.d.ts +237 -0
  17. package/convex/cron.ts +199 -0
  18. package/convex/execApprovals.d.ts +31 -0
  19. package/convex/execApprovals.ts +58 -0
  20. package/convex/extensions.d.ts +30 -0
  21. package/convex/extensions.ts +51 -0
  22. package/convex/health.d.ts +18 -0
  23. package/convex/health.ts +69 -0
  24. package/convex/instance.d.ts +34 -0
  25. package/convex/instance.ts +82 -0
  26. package/convex/logs.d.ts +178 -0
  27. package/convex/logs.ts +253 -0
  28. package/convex/memory.d.ts +354 -0
  29. package/convex/memory.ts +536 -0
  30. package/convex/messages.d.ts +124 -0
  31. package/convex/messages.ts +347 -0
  32. package/convex/org.d.ts +75 -0
  33. package/convex/org.ts +99 -0
  34. package/convex/schema.d.ts +1130 -0
  35. package/convex/schema.ts +847 -0
  36. package/convex/sessions.d.ts +100 -0
  37. package/convex/sessions.ts +105 -0
  38. package/convex/skills.d.ts +73 -0
  39. package/convex/skills.ts +102 -0
  40. package/convex/subagents.d.ts +214 -0
  41. package/convex/subagents.ts +99 -0
  42. package/convex/tsconfig.json +23 -0
  43. package/convex/whatsappAuth.d.ts +52 -0
  44. package/convex/whatsappAuth.ts +151 -0
  45. package/convex/workspace.d.ts +49 -0
  46. package/convex/workspace.ts +106 -0
  47. package/dist/buildstamp.json +1 -1
  48. package/package.json +7 -1
  49. package/scripts/convex-dev.mjs +321 -0
  50. package/scripts/convex-push.mjs +69 -0
  51. package/scripts/install-convex.mjs +123 -0
@@ -0,0 +1,536 @@
1
+ // convex/memory.ts — memoryFacts + memoryExtractCursors + memoryConsolidateState
2
+ import { v } from "convex/values";
3
+ import { action, mutation, query } from "./_generated/server.js";
4
+
5
+ const Segment = v.union(
6
+ v.literal("identity"),
7
+ v.literal("preference"),
8
+ v.literal("correction"),
9
+ v.literal("relationship"),
10
+ v.literal("project"),
11
+ v.literal("knowledge"),
12
+ v.literal("context"),
13
+ );
14
+ const Tier = v.union(v.literal("short"), v.literal("long"), v.literal("permanent"));
15
+ const Lifecycle = v.union(v.literal("active"), v.literal("archived"), v.literal("pruned"));
16
+ const Origin = v.union(v.literal("owner"), v.literal("channel"));
17
+ const SourceType = v.union(
18
+ v.literal("user_instruction"),
19
+ v.literal("owner_message"),
20
+ v.literal("channel_message"),
21
+ v.literal("tool_output"),
22
+ v.literal("retrieved_document"),
23
+ v.literal("compaction"),
24
+ v.literal("extraction"),
25
+ v.literal("dream"),
26
+ );
27
+ const LinkKind = v.union(
28
+ // MUST mirror MemoryLinkKind (links.ts / MEMORY_LINK_KINDS) EXACTLY — a kind not
29
+ // listed here makes the fact write THROW in convex mode (strict object validator).
30
+ v.literal("supersedes"),
31
+ v.literal("transition"), // Step 19
32
+ v.literal("corrects"),
33
+ v.literal("derived_from"),
34
+ v.literal("supports"),
35
+ // typed factual taxonomy (the relationship extractor's closed set)
36
+ v.literal("causes"),
37
+ v.literal("caused_by"),
38
+ v.literal("part_of"),
39
+ v.literal("precedes"),
40
+ v.literal("follows"),
41
+ v.literal("enables"),
42
+ v.literal("blocks"),
43
+ v.literal("co_constrains"),
44
+ v.literal("located_at"),
45
+ v.literal("uses"),
46
+ v.literal("works_on"),
47
+ v.literal("contrasts_with"),
48
+ v.literal("contradicts"),
49
+ v.literal("relates_to"),
50
+ v.literal("same_topic"), // thematic / quarantined lane
51
+ v.literal("relates"), // legacy generic association (synonymy/bridge)
52
+ );
53
+ // `reason`/`strength` are OPTIONAL + additive — a store-minted edge (supersede/
54
+ // transition) carries neither; an extractor edge carries both. Optional ⇒ existing
55
+ // rows still validate (back-compat) and the round-trip through ctx.db.replace holds.
56
+ const Link = v.object({
57
+ kind: LinkKind,
58
+ target: v.string(),
59
+ reason: v.optional(v.string()),
60
+ strength: v.optional(v.number()),
61
+ });
62
+ const Status = v.union(
63
+ v.literal("asserted"),
64
+ v.literal("provisional"),
65
+ v.literal("confirmed"),
66
+ v.literal("disputed"),
67
+ v.literal("retracted"),
68
+ );
69
+ const Modality = v.union(
70
+ v.literal("text"),
71
+ v.literal("audio"),
72
+ v.literal("image"),
73
+ v.literal("video"),
74
+ v.literal("document"),
75
+ );
76
+
77
+ export const listFacts = query({
78
+ args: {
79
+ workspaceId: v.string(),
80
+ lifecycle: v.optional(Lifecycle),
81
+ limit: v.optional(v.number()),
82
+ },
83
+ handler: async (ctx, args) => {
84
+ const lifecycle = args.lifecycle ?? "active";
85
+ const q = ctx.db
86
+ .query("memoryFacts")
87
+ .withIndex("by_workspace_lifecycle_createdAt", (q2) =>
88
+ q2.eq("workspaceId", args.workspaceId).eq("lifecycle", lifecycle),
89
+ )
90
+ .order("desc");
91
+ const limit = args.limit && args.limit > 0 ? args.limit : 200;
92
+ return q.take(limit);
93
+ },
94
+ });
95
+
96
+ export const writeFact = mutation({
97
+ args: {
98
+ workspaceId: v.string(),
99
+ memoryId: v.string(),
100
+ content: v.bytes(),
101
+ segment: Segment,
102
+ tier: Tier,
103
+ importance: v.number(),
104
+ decayRate: v.number(),
105
+ sourceTurn: v.optional(v.string()),
106
+ supersedes: v.optional(v.array(v.string())),
107
+ createdByKind: v.optional(Origin),
108
+ createdByChannelId: v.optional(v.string()),
109
+ createdByConversationId: v.optional(v.string()),
110
+ createdBySessionKey: v.optional(v.string()),
111
+ createdByAccountId: v.optional(v.string()),
112
+ sourceType: v.optional(SourceType),
113
+ links: v.optional(v.array(Link)),
114
+ validFrom: v.optional(v.number()),
115
+ validTo: v.optional(v.number()),
116
+ confidence: v.optional(v.number()),
117
+ status: v.optional(Status),
118
+ sourcePointers: v.optional(v.array(v.string())),
119
+ modality: v.optional(Modality),
120
+ mediaPointer: v.optional(v.string()),
121
+ subjectKey: v.optional(v.string()),
122
+ metadata: v.optional(v.any()),
123
+ embedding: v.optional(v.array(v.number())),
124
+ },
125
+ handler: async (ctx, args) => {
126
+ const now = Date.now();
127
+ const id = await ctx.db.insert("memoryFacts", {
128
+ ...args,
129
+ accessCount: 0,
130
+ lastAccessedAt: now,
131
+ createdAt: now,
132
+ lifecycle: "active" as const,
133
+ });
134
+ if (args.supersedes && args.supersedes.length > 0) {
135
+ for (const supersededId of args.supersedes) {
136
+ const dead = await ctx.db
137
+ .query("memoryFacts")
138
+ .withIndex("by_workspace_memoryId", (q) =>
139
+ q.eq("workspaceId", args.workspaceId).eq("memoryId", supersededId),
140
+ )
141
+ .first();
142
+ if (dead && dead.lifecycle === "active") {
143
+ await ctx.db.patch(dead._id, { lifecycle: "archived" as const });
144
+ }
145
+ }
146
+ }
147
+ return await ctx.db.get(id);
148
+ },
149
+ });
150
+
151
+ /** Every fact row for a workspace across all lifecycles — boot hydration of
152
+ * the in-process facts cache. */
153
+ export const listAllFacts = query({
154
+ args: {
155
+ workspaceId: v.string(),
156
+ cursor: v.optional(v.union(v.string(), v.null())),
157
+ numItems: v.optional(v.number()),
158
+ },
159
+ handler: async (ctx, args) => {
160
+ // PAGINATED. Boot hydration reads EVERY fact for a workspace; a single
161
+ // `.collect()` blows Convex's 16 MiB per-execution read cap once memory
162
+ // grows (this runs on every boot). The client loops with `continueCursor`
163
+ // until `isDone` and concatenates the pages — lossless at any fact count.
164
+ const numItems = args.numItems && args.numItems > 0 ? Math.min(args.numItems, 512) : 256;
165
+ return await ctx.db
166
+ .query("memoryFacts")
167
+ .withIndex("by_workspace_memoryId", (q) => q.eq("workspaceId", args.workspaceId))
168
+ .paginate({ numItems, cursor: args.cursor ?? null });
169
+ },
170
+ });
171
+
172
+ /** Authoritative single-record upsert — every field caller-supplied
173
+ * (accessCount, lifecycle, timestamps included). The FactStore dispatch
174
+ * realises its whole-file diffs through this. */
175
+ export const upsertFactRecord = mutation({
176
+ args: {
177
+ workspaceId: v.string(),
178
+ memoryId: v.string(),
179
+ content: v.bytes(),
180
+ segment: Segment,
181
+ tier: Tier,
182
+ importance: v.number(),
183
+ decayRate: v.number(),
184
+ accessCount: v.number(),
185
+ lastAccessedAt: v.number(),
186
+ createdAt: v.number(),
187
+ lifecycle: Lifecycle,
188
+ sourceTurn: v.optional(v.string()),
189
+ supersedes: v.optional(v.array(v.string())),
190
+ createdByKind: v.optional(Origin),
191
+ createdByChannelId: v.optional(v.string()),
192
+ createdByConversationId: v.optional(v.string()),
193
+ createdBySessionKey: v.optional(v.string()),
194
+ createdByAccountId: v.optional(v.string()),
195
+ sourceType: v.optional(SourceType),
196
+ links: v.optional(v.array(Link)),
197
+ validFrom: v.optional(v.number()),
198
+ validTo: v.optional(v.number()),
199
+ confidence: v.optional(v.number()),
200
+ status: v.optional(Status),
201
+ sourcePointers: v.optional(v.array(v.string())),
202
+ modality: v.optional(Modality),
203
+ mediaPointer: v.optional(v.string()),
204
+ subjectKey: v.optional(v.string()),
205
+ metadata: v.optional(v.any()),
206
+ embedding: v.optional(v.array(v.number())),
207
+ },
208
+ handler: async (ctx, args) => {
209
+ const existing = await ctx.db
210
+ .query("memoryFacts")
211
+ .withIndex("by_workspace_memoryId", (q) =>
212
+ q.eq("workspaceId", args.workspaceId).eq("memoryId", args.memoryId),
213
+ )
214
+ .first();
215
+ if (existing) {
216
+ await ctx.db.replace(existing._id, args);
217
+ return;
218
+ }
219
+ await ctx.db.insert("memoryFacts", args);
220
+ },
221
+ });
222
+
223
+ export const deleteFactRecord = mutation({
224
+ args: { workspaceId: v.string(), memoryId: v.string() },
225
+ handler: async (ctx, args) => {
226
+ const existing = await ctx.db
227
+ .query("memoryFacts")
228
+ .withIndex("by_workspace_memoryId", (q) =>
229
+ q.eq("workspaceId", args.workspaceId).eq("memoryId", args.memoryId),
230
+ )
231
+ .first();
232
+ if (existing) await ctx.db.delete(existing._id);
233
+ },
234
+ });
235
+
236
+ // ── memory AUDIT EVENTS (the convex provenance trail; fs mode uses events.jsonl) ──
237
+ export const appendMemoryEvent = mutation({
238
+ args: {
239
+ workspaceId: v.string(),
240
+ at: v.number(),
241
+ kind: v.string(),
242
+ data: v.string(),
243
+ },
244
+ handler: async (ctx, args) => {
245
+ await ctx.db.insert("memoryEvents", args);
246
+ },
247
+ });
248
+
249
+ /** The audit trail, oldest-first. Bounded to the most-recent `limit` (default 1000,
250
+ * max 5000) to stay under Convex's 16 MiB per-execution read cap. */
251
+ export const listMemoryEvents = query({
252
+ args: { workspaceId: v.string(), limit: v.optional(v.number()) },
253
+ handler: async (ctx, args) => {
254
+ const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 5000) : 1000;
255
+ const rows = await ctx.db
256
+ .query("memoryEvents")
257
+ .withIndex("by_workspace_at", (q) => q.eq("workspaceId", args.workspaceId))
258
+ .order("desc")
259
+ .take(limit);
260
+ return rows.reverse().map((r) => r.data);
261
+ },
262
+ });
263
+
264
+ export const markAccessed = mutation({
265
+ args: { workspaceId: v.string(), memoryIds: v.array(v.string()) },
266
+ handler: async (ctx, args) => {
267
+ const now = Date.now();
268
+ for (const memoryId of args.memoryIds) {
269
+ const row = await ctx.db
270
+ .query("memoryFacts")
271
+ .withIndex("by_workspace_memoryId", (q) =>
272
+ q.eq("workspaceId", args.workspaceId).eq("memoryId", memoryId),
273
+ )
274
+ .first();
275
+ if (row) {
276
+ await ctx.db.patch(row._id, {
277
+ accessCount: (row.accessCount ?? 0) + 1,
278
+ lastAccessedAt: now,
279
+ });
280
+ }
281
+ }
282
+ },
283
+ });
284
+
285
+ // ⚠️ DEAD / superseded by Tideline 0.6. This DISCRETE per-tier decay
286
+ // (short→archived@7d / archived→pruned@30d / long→archived@90d) is NOT the
287
+ // live decay path: the gateway sweep runs `runDecayGc` (continuous
288
+ // `effectiveScore`, src/agents/memory/decay.ts) in BOTH modes, and nothing
289
+ // calls `ConvexMemoryStore.decay`. Kept only to avoid redeploy churn; DELETE
290
+ // on the next convex deploy. Calling it would re-diverge cognition from fs.
291
+ export const decay = mutation({
292
+ args: {
293
+ workspaceId: v.string(),
294
+ now: v.number(),
295
+ // Per-tier idle thresholds (ms). Defaults match the filesystem-mode
296
+ // agent-loop sweep: short→archived at 7d, archived→pruned at 30d,
297
+ // long→archived at 90d. `permanent` never decays.
298
+ shortIdleMs: v.optional(v.number()),
299
+ archivedIdleMs: v.optional(v.number()),
300
+ longIdleMs: v.optional(v.number()),
301
+ },
302
+ handler: async (ctx, args) => {
303
+ const shortIdle = args.shortIdleMs ?? 7 * 24 * 60 * 60 * 1000;
304
+ const archivedIdle = args.archivedIdleMs ?? 30 * 24 * 60 * 60 * 1000;
305
+ const longIdle = args.longIdleMs ?? 90 * 24 * 60 * 60 * 1000;
306
+
307
+ let archived = 0;
308
+ let pruned = 0;
309
+
310
+ // Active short → archived after `shortIdle`
311
+ const activeRows = await ctx.db
312
+ .query("memoryFacts")
313
+ .withIndex("by_workspace_lifecycle_createdAt", (q) =>
314
+ q.eq("workspaceId", args.workspaceId).eq("lifecycle", "active" as const),
315
+ )
316
+ // SAFETY BOUND (this is the DEAD path; live decay is runDecayGc): cap reads+patches
317
+ // under Convex's per-mutation 16 MiB-read / 8192-write budget so an accidental call
318
+ // can't bomb. Delete this whole mutation on the next deploy (see header).
319
+ .take(2000);
320
+ for (const row of activeRows) {
321
+ if (row.tier === "permanent") continue;
322
+ const idle = args.now - row.lastAccessedAt;
323
+ const threshold = row.tier === "short" ? shortIdle : longIdle;
324
+ if (idle > threshold) {
325
+ await ctx.db.patch(row._id, { lifecycle: "archived" as const });
326
+ archived += 1;
327
+ }
328
+ }
329
+
330
+ // Archived → pruned after `archivedIdle`
331
+ const archivedRows = await ctx.db
332
+ .query("memoryFacts")
333
+ .withIndex("by_workspace_lifecycle_createdAt", (q) =>
334
+ q.eq("workspaceId", args.workspaceId).eq("lifecycle", "archived" as const),
335
+ )
336
+ .take(2000); // SAFETY BOUND (dead path) — see the active-rows note above
337
+ for (const row of archivedRows) {
338
+ if (row.tier === "permanent") continue;
339
+ if (args.now - row.lastAccessedAt > archivedIdle) {
340
+ await ctx.db.patch(row._id, { lifecycle: "pruned" as const });
341
+ pruned += 1;
342
+ }
343
+ }
344
+
345
+ return { archived, pruned };
346
+ },
347
+ });
348
+
349
+ export const setLifecycle = mutation({
350
+ args: { workspaceId: v.string(), memoryIds: v.array(v.string()), lifecycle: Lifecycle },
351
+ handler: async (ctx, args) => {
352
+ for (const memoryId of args.memoryIds) {
353
+ const row = await ctx.db
354
+ .query("memoryFacts")
355
+ .withIndex("by_workspace_memoryId", (q) =>
356
+ q.eq("workspaceId", args.workspaceId).eq("memoryId", memoryId),
357
+ )
358
+ .first();
359
+ if (row && row.lifecycle !== args.lifecycle) {
360
+ await ctx.db.patch(row._id, { lifecycle: args.lifecycle });
361
+ }
362
+ }
363
+ },
364
+ });
365
+
366
+ export const countActiveFacts = query({
367
+ args: {
368
+ workspaceId: v.string(),
369
+ cursor: v.optional(v.union(v.string(), v.null())),
370
+ },
371
+ handler: async (ctx, args) => {
372
+ // PAGINATED count. A single `.collect()` of all active facts blows the
373
+ // 16 MiB read cap at scale. The client loops summing `count` until
374
+ // `isDone`. Returns only the page size (+cursor) — never the rows.
375
+ const res = await ctx.db
376
+ .query("memoryFacts")
377
+ .withIndex("by_workspace_lifecycle_createdAt", (q) =>
378
+ q.eq("workspaceId", args.workspaceId).eq("lifecycle", "active"),
379
+ )
380
+ .paginate({ numItems: 512, cursor: args.cursor ?? null });
381
+ return { count: res.page.length, isDone: res.isDone, continueCursor: res.continueCursor };
382
+ },
383
+ });
384
+
385
+ export const getExtractCursor = query({
386
+ args: { workspaceId: v.string(), sessionId: v.string() },
387
+ handler: async (ctx, args) => {
388
+ const row = await ctx.db
389
+ .query("memoryExtractCursors")
390
+ .withIndex("by_workspace_session", (q) =>
391
+ q.eq("workspaceId", args.workspaceId).eq("sessionId", args.sessionId),
392
+ )
393
+ .first();
394
+ return row?.processedCount ?? 0;
395
+ },
396
+ });
397
+
398
+ export const setExtractCursor = mutation({
399
+ args: { workspaceId: v.string(), sessionId: v.string(), processedCount: v.number() },
400
+ handler: async (ctx, args) => {
401
+ const existing = await ctx.db
402
+ .query("memoryExtractCursors")
403
+ .withIndex("by_workspace_session", (q) =>
404
+ q.eq("workspaceId", args.workspaceId).eq("sessionId", args.sessionId),
405
+ )
406
+ .first();
407
+ // `updatedAt` is stored for audit / introspection but is not read back by
408
+ // the client — only `processedCount` is queried (getExtractCursor). It is
409
+ // stamped here because the schema field is non-optional (see schema.ts).
410
+ const payload = { ...args, updatedAt: Date.now() };
411
+ if (existing) await ctx.db.replace(existing._id, payload);
412
+ else await ctx.db.insert("memoryExtractCursors", payload);
413
+ },
414
+ });
415
+
416
+ export const getConsolidateLastRunAt = query({
417
+ args: { workspaceId: v.string() },
418
+ handler: async (ctx, args) => {
419
+ const row = await ctx.db
420
+ .query("memoryConsolidateState")
421
+ .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
422
+ .first();
423
+ return row?.lastRunAt;
424
+ },
425
+ });
426
+
427
+ export const markConsolidateRunAt = mutation({
428
+ args: { workspaceId: v.string(), lastRunAt: v.number() },
429
+ handler: async (ctx, args) => {
430
+ const existing = await ctx.db
431
+ .query("memoryConsolidateState")
432
+ .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
433
+ .first();
434
+ if (existing) await ctx.db.replace(existing._id, args);
435
+ else await ctx.db.insert("memoryConsolidateState", args);
436
+ },
437
+ });
438
+
439
+ // Probe: does this backend expose the native `ctx.vectorSearch` over the
440
+ // `by_embedding` vectorIndex? (Older in-memory backends didn't — `findSimilar`
441
+ // falls back to a manual cosine scan.) Returns the ANN hits' ids + scores.
442
+ // ⚠️ LATENT / v2-only / origin-UNSAFE: this filters by workspaceId ONLY —
443
+ // it does NOT apply the per-origin (createdBy*) recall filter the isolation
444
+ // model requires, so it must NOT be wired into recall until it origin-filters
445
+ // server-side (extend the `ctx.vectorSearch` filter to constrain the
446
+ // createdBy* fields). Not on the recall path today.
447
+ export const vectorProbe = action({
448
+ args: { workspaceId: v.string(), embedding: v.array(v.number()), k: v.optional(v.number()) },
449
+ handler: async (ctx, args) => {
450
+ const results = await ctx.vectorSearch("memoryFacts", "by_embedding", {
451
+ vector: args.embedding,
452
+ limit: args.k && args.k > 0 ? args.k : 5,
453
+ filter: (q) => q.eq("workspaceId", args.workspaceId),
454
+ });
455
+ return results.map((r) => ({ id: r._id, score: r._score }));
456
+ },
457
+ });
458
+
459
+ // ⚠️ LATENT / v2-only. This runs BM25 full-text matching over the sealed
460
+ // `content` column, which stores CIPHERTEXT (v.bytes()), so it yields dead
461
+ // results while content is sealed at rest. It must NOT be wired into recall —
462
+ // live recall ranks BM25 in-app over decrypted content. Repurpose only if/when
463
+ // content stops being sealed, or this is reworked to scan decrypted rows.
464
+ export const searchContent = query({
465
+ args: { workspaceId: v.string(), query: v.string(), limit: v.optional(v.number()) },
466
+ handler: async (ctx, args) => {
467
+ const limit = args.limit && args.limit > 0 ? args.limit : 8;
468
+ const hits = await ctx.db
469
+ .query("memoryFacts")
470
+ .withSearchIndex("search_content", (q) =>
471
+ q
472
+ .search("content", args.query)
473
+ .eq("workspaceId", args.workspaceId)
474
+ .eq("lifecycle", "active" as const),
475
+ )
476
+ .take(limit);
477
+ return hits;
478
+ },
479
+ });
480
+
481
+ // Bound on the manual candidate scan below — a single `.collect()` over every
482
+ // active fact would hit the 16 MiB per-query read cap once memory grows (each row
483
+ // carries a 256-float embedding + encrypted content), so we scan at most the
484
+ // newest N. The cap-safe, index-served path is `vectorProbe` (an ACTION using the
485
+ // native `ctx.vectorSearch`); this query is the in-memory-backend fallback.
486
+ const VECTOR_SCAN_CAP = 2000;
487
+
488
+ // PR19 — Vector recall against the `memoryFacts.embedding` vectorIndex.
489
+ // ⚠️ LATENT / v2-only / origin-UNSAFE: this filters by workspaceId + lifecycle
490
+ // ONLY — it does NOT apply the per-origin (createdBy*) recall filter the isolation
491
+ // model requires, so it must NOT be wired into recall until it (or its native
492
+ // `vectorProbe`/`ctx.vectorSearch` replacement) origin-filters server-side.
493
+ // No live caller today (v1 recall runs BM25 in-app over the decrypted cache).
494
+ // Convex query handlers can't issue HTTP calls (queries are deterministic),
495
+ // so embedding generation happens at the CALLER (the adapter's `findSimilar`
496
+ // passes pre-computed embeddings). This query just does the ANN search
497
+ // against the index.
498
+ export const findSimilar = query({
499
+ args: {
500
+ workspaceId: v.string(),
501
+ embedding: v.array(v.number()),
502
+ k: v.optional(v.number()),
503
+ },
504
+ handler: async (ctx, args) => {
505
+ const k = args.k && args.k > 0 ? args.k : 5;
506
+ const hits = await ctx.db
507
+ .query("memoryFacts")
508
+ .withIndex("by_workspace_lifecycle_createdAt", (q) =>
509
+ q.eq("workspaceId", args.workspaceId).eq("lifecycle", "active" as const),
510
+ )
511
+ .order("desc") // NEWEST-first: the index is createdAt-ordered and Convex defaults to ascending, so without this .take() would scan the OLDEST N (matches listFacts + the cap comment above)
512
+ .take(VECTOR_SCAN_CAP);
513
+ // Compute cosine similarity client-side against the (capped) candidate set.
514
+ // (The schema declares a vectorIndex but Convex query helpers don't
515
+ // expose .vectorSearch on the in-memory backend yet; this fallback
516
+ // keeps the contract intact while emitting accurate scores. The native
517
+ // `vectorProbe` action is the index-served, cap-safe path.)
518
+ const queryVec = args.embedding;
519
+ const norm = (v: number[]) => Math.sqrt(v.reduce((s, x) => s + x * x, 0));
520
+ const queryNorm = norm(queryVec);
521
+ const scored: Array<{ row: typeof hits[number]; score: number }> = [];
522
+ for (const row of hits) {
523
+ const emb = row.embedding;
524
+ if (!emb || emb.length !== queryVec.length) continue;
525
+ let dot = 0;
526
+ for (let i = 0; i < emb.length; i++) {
527
+ dot += (emb[i] ?? 0) * (queryVec[i] ?? 0);
528
+ }
529
+ const rowNorm = norm(emb);
530
+ const score = queryNorm > 0 && rowNorm > 0 ? dot / (queryNorm * rowNorm) : 0;
531
+ scored.push({ row, score });
532
+ }
533
+ scored.sort((a, b) => b.score - a.score);
534
+ return scored.slice(0, k).map(({ row, score }) => ({ ...row, score }));
535
+ },
536
+ });
@@ -0,0 +1,124 @@
1
+ export declare const appendRecord: import("convex/server").RegisteredMutation<"public", {
2
+ customType?: string | undefined;
3
+ type: string;
4
+ agentId: string;
5
+ payload: ArrayBuffer;
6
+ sessionId: string;
7
+ }, Promise<{
8
+ seq: number;
9
+ }>>;
10
+ /** Ordered batch append — the convex-mode SessionManager write-behind queue
11
+ * flushes whole batches in one transaction so a mid-batch crash can't leave
12
+ * a torn parent-id chain. */
13
+ export declare const appendRecordsBatch: import("convex/server").RegisteredMutation<"public", {
14
+ agentId: string;
15
+ sessionId: string;
16
+ records: {
17
+ customType?: string | undefined;
18
+ type: string;
19
+ payload: ArrayBuffer;
20
+ }[];
21
+ }, Promise<{
22
+ lastSeq: number;
23
+ }>>;
24
+ /** Wholesale transcript replace — realises Pi's `_rewriteFile` (v1→v3
25
+ * migration, branch extraction) as one transaction. */
26
+ export declare const replaceTranscript: import("convex/server").RegisteredMutation<"public", {
27
+ agentId: string;
28
+ sessionId: string;
29
+ records: {
30
+ customType?: string | undefined;
31
+ type: string;
32
+ payload: ArrayBuffer;
33
+ }[];
34
+ }, Promise<{
35
+ count: number;
36
+ }>>;
37
+ export declare const readTranscript: import("convex/server").RegisteredQuery<"public", {
38
+ limit?: number | undefined;
39
+ afterSeq?: number | undefined;
40
+ agentId: string;
41
+ sessionId: string;
42
+ }, Promise<{
43
+ _id: import("convex/values").GenericId<"sessionTranscriptRecords">;
44
+ _creationTime: number;
45
+ customType?: string | undefined;
46
+ chunkIndex?: number | undefined;
47
+ chunkCount?: number | undefined;
48
+ type: string;
49
+ agentId: string;
50
+ payload: ArrayBuffer;
51
+ sessionId: string;
52
+ createdAt: number;
53
+ seq: number;
54
+ }[]>>;
55
+ /** Newest-first tail of (type, customType) only — for the bootstrap-delivery
56
+ * check, which must honour compaction-invalidation (a compaction newer than
57
+ * the marker means the bootstrap context was compacted out → re-deliver).
58
+ * Returns just the two fields the walk needs, not the sealed payloads. */
59
+ export declare const readMarkerTail: import("convex/server").RegisteredQuery<"public", {
60
+ limit?: number | undefined;
61
+ agentId: string;
62
+ sessionId: string;
63
+ }, Promise<{
64
+ type: string;
65
+ customType?: string;
66
+ }[]>>;
67
+ export declare const deleteTranscript: import("convex/server").RegisteredMutation<"public", {
68
+ agentId: string;
69
+ sessionId: string;
70
+ }, Promise<number>>;
71
+ export declare const inboxEnqueue: import("convex/server").RegisteredMutation<"public", {
72
+ ts?: number | undefined;
73
+ deliveryContext?: any;
74
+ contextKey?: string | undefined;
75
+ text: ArrayBuffer;
76
+ sessionKey: string;
77
+ trusted: boolean;
78
+ }, Promise<{
79
+ seq: number;
80
+ }>>;
81
+ export declare const inboxPeek: import("convex/server").RegisteredQuery<"public", {
82
+ sessionKey: string;
83
+ }, Promise<{
84
+ _id: import("convex/values").GenericId<"sessionInboxEvents">;
85
+ _creationTime: number;
86
+ deliveryContext?: any;
87
+ contextKey?: string | undefined;
88
+ text: ArrayBuffer;
89
+ sessionKey: string;
90
+ ts: number;
91
+ trusted: boolean;
92
+ seq: number;
93
+ }[]>>;
94
+ export declare const inboxDrain: import("convex/server").RegisteredMutation<"public", {
95
+ sessionKey: string;
96
+ }, Promise<{
97
+ _id: import("convex/values").GenericId<"sessionInboxEvents">;
98
+ _creationTime: number;
99
+ deliveryContext?: any;
100
+ contextKey?: string | undefined;
101
+ text: ArrayBuffer;
102
+ sessionKey: string;
103
+ ts: number;
104
+ trusted: boolean;
105
+ seq: number;
106
+ }[]>>;
107
+ export declare const inboxConsumePrefix: import("convex/server").RegisteredMutation<"public", {
108
+ sessionKey: string;
109
+ prefixLength: number;
110
+ }, Promise<{
111
+ _id: import("convex/values").GenericId<"sessionInboxEvents">;
112
+ _creationTime: number;
113
+ deliveryContext?: any;
114
+ contextKey?: string | undefined;
115
+ text: ArrayBuffer;
116
+ sessionKey: string;
117
+ ts: number;
118
+ trusted: boolean;
119
+ seq: number;
120
+ }[]>>;
121
+ export declare const inboxHasEvents: import("convex/server").RegisteredQuery<"public", {
122
+ sessionKey: string;
123
+ }, Promise<boolean>>;
124
+ //# sourceMappingURL=messages.d.ts.map