@spinabot/brigade 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/_generated/api.d.ts +85 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/admin.d.ts +57 -0
- package/convex/admin.ts +315 -0
- package/convex/auth.d.ts +159 -0
- package/convex/auth.ts +217 -0
- package/convex/blobs.d.ts +38 -0
- package/convex/blobs.ts +115 -0
- package/convex/channels.d.ts +150 -0
- package/convex/channels.ts +455 -0
- package/convex/config.d.ts +67 -0
- package/convex/config.ts +168 -0
- package/convex/cron.d.ts +237 -0
- package/convex/cron.ts +199 -0
- package/convex/execApprovals.d.ts +31 -0
- package/convex/execApprovals.ts +58 -0
- package/convex/extensions.d.ts +30 -0
- package/convex/extensions.ts +51 -0
- package/convex/health.d.ts +18 -0
- package/convex/health.ts +69 -0
- package/convex/instance.d.ts +34 -0
- package/convex/instance.ts +82 -0
- package/convex/logs.d.ts +178 -0
- package/convex/logs.ts +253 -0
- package/convex/memory.d.ts +354 -0
- package/convex/memory.ts +536 -0
- package/convex/messages.d.ts +124 -0
- package/convex/messages.ts +347 -0
- package/convex/org.d.ts +75 -0
- package/convex/org.ts +99 -0
- package/convex/schema.d.ts +1130 -0
- package/convex/schema.ts +847 -0
- package/convex/sessions.d.ts +100 -0
- package/convex/sessions.ts +105 -0
- package/convex/skills.d.ts +73 -0
- package/convex/skills.ts +102 -0
- package/convex/subagents.d.ts +214 -0
- package/convex/subagents.ts +99 -0
- package/convex/tsconfig.json +23 -0
- package/convex/whatsappAuth.d.ts +52 -0
- package/convex/whatsappAuth.ts +151 -0
- package/convex/workspace.d.ts +49 -0
- package/convex/workspace.ts +106 -0
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/convex-cmd.d.ts +27 -0
- package/dist/cli/commands/convex-cmd.d.ts.map +1 -0
- package/dist/cli/commands/convex-cmd.js +162 -0
- package/dist/cli/commands/convex-cmd.js.map +1 -0
- package/dist/cli/program/build-program.d.ts.map +1 -1
- package/dist/cli/program/build-program.js +64 -0
- package/dist/cli/program/build-program.js.map +1 -1
- package/dist/config/paths.d.ts +3 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +39 -0
- package/dist/config/paths.js.map +1 -1
- package/package.json +7 -1
- package/scripts/convex-dev.mjs +321 -0
- package/scripts/convex-push.mjs +69 -0
- package/scripts/install-convex.mjs +123 -0
package/convex/memory.ts
ADDED
|
@@ -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
|