@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,847 @@
1
+ // convex/schema.ts
2
+ //
3
+ // Brigade Phase 2 — single-operator Convex schema.
4
+ //
5
+ // Every Brigade subsystem gets its proper-shape table (one row per record, the
6
+ // Convex agent-component pattern), NOT a `files` table holding
7
+ // JSONL blobs. Indexes + search + vector per domain. Encrypted-payload
8
+ // columns use v.bytes() at the schema layer; libsodium seal/open happens at
9
+ // the BrigadeStore adapter boundary so primitive code never sees ciphertext.
10
+ //
11
+ // Source-of-truth design doc:
12
+ // C:\Users\SmartSystems\.brigade-design-docs\toggle-migration-plan.md
13
+ // (Part B — 24 tables, locked 2026-06-09 after 18-agent audit)
14
+ //
15
+ // ownerId is present on every per-operator row. In Phase 2 (single operator)
16
+ // it's always the same value; Phase 3 multi-tenant turns it into the RLS key.
17
+
18
+ import { defineSchema, defineTable } from "convex/server";
19
+ import { v } from "convex/values";
20
+
21
+ const Enc = v.bytes;
22
+
23
+ export default defineSchema({
24
+ // ===========================================================================
25
+ // 1. CONFIG (REPORT 9)
26
+ // ===========================================================================
27
+ brigadeConfig: defineTable({
28
+ instanceId: v.string(),
29
+ schemaVersion: v.literal(2),
30
+ agents: v.optional(v.any()),
31
+ gateway: v.optional(v.any()),
32
+ session: v.optional(v.any()),
33
+ tools: v.optional(v.any()),
34
+ auth: v.optional(v.any()),
35
+ plugins: v.optional(v.any()),
36
+ skills: v.optional(v.any()),
37
+ channels: v.optional(v.any()),
38
+ bindings: v.optional(v.any()),
39
+ org: v.optional(v.any()),
40
+ wizard: v.optional(v.any()),
41
+ meta: v.optional(v.any()),
42
+ defaults: v.optional(v.any()),
43
+ // Catch-all for any top-level key not named above — preserves the disk
44
+ // path's unknown-key round-trip guarantee (io.ts orderTopLevelKeys).
45
+ extra: v.optional(v.any()),
46
+ // RESERVED (not yet written): gateway secrets are expected to be ${ENV}
47
+ // references, so no resolved secret is persisted today. These sealed columns
48
+ // are kept for the future "seal a literal gateway secret at rest" path; if
49
+ // that path never lands, drop them. Optional, so absence is valid for every
50
+ // current row.
51
+ encryptedGatewayAuthToken: v.optional(Enc()),
52
+ encryptedGatewayAuthPassword: v.optional(Enc()),
53
+ contentSha256: v.string(),
54
+ bytes: v.number(),
55
+ updatedAtMs: v.number(),
56
+ updatedByPid: v.optional(v.number()),
57
+ }).index("by_instance", ["instanceId"]),
58
+
59
+ brigadeConfigAudit: defineTable({
60
+ instanceId: v.string(),
61
+ ts: v.string(),
62
+ sha256: v.string(),
63
+ prevHash: v.optional(v.string()),
64
+ lineHash: v.string(),
65
+ seq: v.number(),
66
+ bytes: v.number(),
67
+ pid: v.optional(v.number()),
68
+ }).index("by_instance_seq", ["instanceId", "seq"]),
69
+
70
+ brigadeConfigBackups: defineTable({
71
+ instanceId: v.string(),
72
+ slot: v.number(),
73
+ contentSha256: v.string(),
74
+ payload: v.string(),
75
+ bytes: v.number(),
76
+ capturedAtMs: v.number(),
77
+ }).index("by_instance_slot", ["instanceId", "slot"]),
78
+
79
+ configHealth: defineTable({
80
+ ownerId: v.string(),
81
+ ts: v.string(),
82
+ configPath: v.string(),
83
+ bytes: v.number(),
84
+ sha256: v.string(),
85
+ mtimeMs: v.number(),
86
+ pid: v.number(),
87
+ }).index("by_owner", ["ownerId"]),
88
+
89
+ // ===========================================================================
90
+ // 2. WORKSPACE (REPORT 7)
91
+ // ===========================================================================
92
+ personaFiles: defineTable({
93
+ agentId: v.string(),
94
+ name: v.union(
95
+ v.literal("AGENTS.md"),
96
+ v.literal("SOUL.md"),
97
+ v.literal("IDENTITY.md"),
98
+ v.literal("USER.md"),
99
+ v.literal("TOOLS.md"),
100
+ v.literal("BOOTSTRAP.md"),
101
+ v.literal("MEMORY.md"),
102
+ v.literal("HEARTBEAT.md"),
103
+ ),
104
+ content: Enc(),
105
+ updatedAt: v.number(),
106
+ })
107
+ .index("by_agent_name", ["agentId", "name"])
108
+ .index("by_agent", ["agentId"]),
109
+
110
+ workspaceState: defineTable({
111
+ agentId: v.string(),
112
+ version: v.number(),
113
+ bootstrapSeededAt: v.optional(v.string()),
114
+ setupCompletedAt: v.optional(v.string()),
115
+ }).index("by_agent", ["agentId"]),
116
+
117
+ // ===========================================================================
118
+ // 3. MEMORY (REPORT 1)
119
+ // ===========================================================================
120
+ memoryFacts: defineTable({
121
+ workspaceId: v.string(),
122
+ memoryId: v.string(),
123
+ content: Enc(),
124
+ segment: v.union(
125
+ v.literal("identity"),
126
+ v.literal("preference"),
127
+ v.literal("correction"),
128
+ v.literal("relationship"),
129
+ v.literal("project"),
130
+ v.literal("knowledge"),
131
+ v.literal("context"),
132
+ ),
133
+ tier: v.union(v.literal("short"), v.literal("long"), v.literal("permanent")),
134
+ importance: v.number(),
135
+ decayRate: v.number(),
136
+ accessCount: v.number(),
137
+ lastAccessedAt: v.number(),
138
+ createdAt: v.number(),
139
+ sourceTurn: v.optional(v.string()),
140
+ supersedes: v.optional(v.array(v.string())),
141
+ lifecycle: v.union(v.literal("active"), v.literal("archived"), v.literal("pruned")),
142
+ createdByKind: v.optional(v.union(v.literal("owner"), v.literal("channel"))),
143
+ createdByChannelId: v.optional(v.string()),
144
+ createdByConversationId: v.optional(v.string()),
145
+ createdBySessionKey: v.optional(v.string()),
146
+ createdByAccountId: v.optional(v.string()),
147
+ sourceType: v.optional(
148
+ v.union(
149
+ v.literal("user_instruction"),
150
+ v.literal("owner_message"),
151
+ v.literal("channel_message"),
152
+ v.literal("tool_output"),
153
+ v.literal("retrieved_document"),
154
+ v.literal("compaction"),
155
+ v.literal("extraction"),
156
+ v.literal("dream"),
157
+ ),
158
+ ),
159
+ links: v.optional(
160
+ v.array(
161
+ v.object({
162
+ // MUST mirror MemoryLinkKind (links.ts / MEMORY_LINK_KINDS) EXACTLY.
163
+ kind: v.union(
164
+ v.literal("supersedes"),
165
+ v.literal("transition"), // Step 19
166
+ v.literal("corrects"),
167
+ v.literal("derived_from"),
168
+ v.literal("supports"),
169
+ v.literal("causes"),
170
+ v.literal("caused_by"),
171
+ v.literal("part_of"),
172
+ v.literal("precedes"),
173
+ v.literal("follows"),
174
+ v.literal("enables"),
175
+ v.literal("blocks"),
176
+ v.literal("co_constrains"),
177
+ v.literal("located_at"),
178
+ v.literal("uses"),
179
+ v.literal("works_on"),
180
+ v.literal("contrasts_with"),
181
+ v.literal("contradicts"),
182
+ v.literal("relates_to"),
183
+ v.literal("same_topic"),
184
+ v.literal("relates"),
185
+ ),
186
+ target: v.string(),
187
+ // optional + additive (back-compat with existing rows / store-minted edges)
188
+ reason: v.optional(v.string()),
189
+ strength: v.optional(v.number()),
190
+ }),
191
+ ),
192
+ ),
193
+ validFrom: v.optional(v.number()),
194
+ validTo: v.optional(v.number()),
195
+ confidence: v.optional(v.number()),
196
+ status: v.optional(
197
+ v.union(
198
+ v.literal("asserted"),
199
+ v.literal("provisional"),
200
+ v.literal("confirmed"),
201
+ v.literal("disputed"),
202
+ v.literal("retracted"),
203
+ ),
204
+ ),
205
+ sourcePointers: v.optional(v.array(v.string())),
206
+ modality: v.optional(
207
+ v.union(
208
+ v.literal("text"),
209
+ v.literal("audio"),
210
+ v.literal("image"),
211
+ v.literal("video"),
212
+ v.literal("document"),
213
+ ),
214
+ ),
215
+ mediaPointer: v.optional(v.string()),
216
+ subjectKey: v.optional(v.string()),
217
+ metadata: v.optional(v.any()),
218
+ embedding: v.optional(v.array(v.number())),
219
+ })
220
+ .index("by_workspace_lifecycle_createdAt", ["workspaceId", "lifecycle", "createdAt"])
221
+ .index("by_workspace_memoryId", ["workspaceId", "memoryId"])
222
+ .index("by_workspace_segment_lifecycle", ["workspaceId", "segment", "lifecycle"])
223
+ .index("by_workspace_origin", [
224
+ "workspaceId",
225
+ "createdByKind",
226
+ "createdByChannelId",
227
+ "createdByConversationId",
228
+ "createdBySessionKey",
229
+ ])
230
+ // ⚠️ DO NOT RELY ON THIS FOR RECALL while `content` is sealed. The
231
+ // `content` column stores CIPHERTEXT, so full-text (BM25) matching here
232
+ // runs over encrypted bytes and yields dead results. Live recall ranks
233
+ // BM25 in-app over decrypted content; this index is kept only because a
234
+ // latent server-side search query still references it (and is never on the
235
+ // live recall path). Drop or repurpose it if/when content stops being
236
+ // sealed at rest, or when the search query is reworked to scan decrypted
237
+ // rows.
238
+ .searchIndex("search_content", {
239
+ searchField: "content",
240
+ filterFields: [
241
+ "workspaceId",
242
+ "lifecycle",
243
+ "createdByKind",
244
+ "createdByChannelId",
245
+ "createdByConversationId",
246
+ "createdBySessionKey",
247
+ ],
248
+ })
249
+ .vectorIndex("by_embedding", {
250
+ vectorField: "embedding",
251
+ dimensions: 256,
252
+ filterFields: ["workspaceId", "lifecycle"],
253
+ }),
254
+
255
+ memoryExtractCursors: defineTable({
256
+ workspaceId: v.string(),
257
+ sessionId: v.string(),
258
+ processedCount: v.number(),
259
+ updatedAt: v.number(),
260
+ }).index("by_workspace_session", ["workspaceId", "sessionId"]),
261
+
262
+ memoryConsolidateState: defineTable({
263
+ workspaceId: v.string(),
264
+ lastRunAt: v.number(),
265
+ }).index("by_workspace", ["workspaceId"]),
266
+
267
+ // Append-only memory AUDIT trail (the convex-mode provenance log; fs mode uses
268
+ // events.jsonl). `data` is the full MemoryEvent JSON-serialized (loose by-kind
269
+ // shape); `at`/`kind` are surfaced for ordering + filtering.
270
+ memoryEvents: defineTable({
271
+ workspaceId: v.string(),
272
+ at: v.number(),
273
+ kind: v.string(),
274
+ data: v.string(),
275
+ }).index("by_workspace_at", ["workspaceId", "at"]),
276
+
277
+ // ===========================================================================
278
+ // 4 & 5. SESSIONS + MESSAGES (REPORT 2)
279
+ // ===========================================================================
280
+ sessions: defineTable({
281
+ agentId: v.string(),
282
+ sessionKey: v.string(),
283
+ sessionId: v.string(),
284
+ createdAt: v.number(),
285
+ lastUsedAt: v.number(),
286
+ provider: v.optional(v.string()),
287
+ modelId: v.optional(v.string()),
288
+ authProfile: v.optional(v.string()),
289
+ thinkingLevel: v.optional(v.string()),
290
+ subagent: v.optional(
291
+ v.object({
292
+ spawnDepth: v.number(),
293
+ spawnedBy: v.string(),
294
+ parentRunId: v.optional(v.string()),
295
+ label: v.optional(v.string()),
296
+ cleanup: v.optional(v.union(v.literal("delete"), v.literal("keep"))),
297
+ spawnedAt: v.string(),
298
+ spawnedWorkspaceDir: v.optional(v.string()),
299
+ }),
300
+ ),
301
+ extra: v.optional(Enc()),
302
+ })
303
+ .index("by_agent_key", ["agentId", "sessionKey"])
304
+ .index("by_agent_sessionId", ["agentId", "sessionId"])
305
+ .index("by_agent_lastUsed", ["agentId", "lastUsedAt"])
306
+ // RESERVED: not yet queried; kept for a planned reverse-lookup of all
307
+ // sessions spawned by a given parent (subagent provenance views).
308
+ .index("by_spawnedBy", ["subagent.spawnedBy"]),
309
+
310
+ sessionTranscriptRecords: defineTable({
311
+ agentId: v.string(),
312
+ sessionId: v.string(),
313
+ seq: v.number(),
314
+ type: v.string(),
315
+ customType: v.optional(v.string()),
316
+ // Sealed record payload. Convex caps a single DOCUMENT at 1 MiB, so a
317
+ // record whose sealed bytes exceed that (a turn carrying a huge tool
318
+ // result — scraped HTML, big research output) is SPLIT across several
319
+ // consecutive rows that share a `chunkCount` and differ by `chunkIndex`;
320
+ // `payload` then holds one slice of the sealed bytes. The reader
321
+ // concatenates the slices back into the whole sealed blob before
322
+ // decrypting. Normal records leave the chunk fields unset (one row, one
323
+ // payload). The text stays in the transcript table — no File-Storage
324
+ // spill — and no single row ever approaches the per-document limit.
325
+ payload: Enc(),
326
+ /** 0-based position of this slice within a chunked record; unset (→ 0)
327
+ * for a normal single-row record. */
328
+ chunkIndex: v.optional(v.number()),
329
+ /** Total slices for a chunked record (>1); unset (→ 1) when not chunked.
330
+ * All `chunkCount` rows are written in ONE mutation (atomic), so a
331
+ * group can never be torn across a crash. */
332
+ chunkCount: v.optional(v.number()),
333
+ createdAt: v.number(),
334
+ })
335
+ .index("by_session_seq", ["agentId", "sessionId", "seq"])
336
+ .index("by_session_type", ["agentId", "sessionId", "type"]),
337
+
338
+ sessionInboxEvents: defineTable({
339
+ sessionKey: v.string(),
340
+ seq: v.number(),
341
+ text: Enc(),
342
+ ts: v.number(),
343
+ contextKey: v.optional(v.string()),
344
+ deliveryContext: v.optional(v.any()),
345
+ trusted: v.boolean(),
346
+ })
347
+ .index("by_session_seq", ["sessionKey", "seq"])
348
+ .index("by_session_ts", ["sessionKey", "ts"]),
349
+
350
+ // ===========================================================================
351
+ // 6. LOGS (REPORT 10)
352
+ // ===========================================================================
353
+ sessionEvents: defineTable({
354
+ ts: v.string(),
355
+ day: v.string(),
356
+ ownerId: v.string(),
357
+ agentId: v.string(),
358
+ sessionKey: v.string(),
359
+ type: v.string(),
360
+ inner: v.optional(v.string()),
361
+ delta: v.optional(v.string()),
362
+ toolCallId: v.optional(v.string()),
363
+ toolName: v.optional(v.string()),
364
+ args: v.optional(Enc()),
365
+ result: v.optional(Enc()),
366
+ isError: v.optional(v.boolean()),
367
+ role: v.optional(v.string()),
368
+ content: v.optional(Enc()),
369
+ stopReason: v.optional(v.string()),
370
+ errorMessage: v.optional(v.string()),
371
+ attempt: v.optional(v.number()),
372
+ maxAttempts: v.optional(v.number()),
373
+ delayMs: v.optional(v.number()),
374
+ aborted: v.optional(v.boolean()),
375
+ willRetry: v.optional(v.boolean()),
376
+ messageCount: v.optional(v.number()),
377
+ // auto_retry_end carries these — kept so a failed-then-recovered turn is
378
+ // fully reconstructable from the convex log (disk parity).
379
+ success: v.optional(v.boolean()),
380
+ finalError: v.optional(v.string()),
381
+ })
382
+ .index("by_owner_day", ["ownerId", "day"])
383
+ .index("by_owner_session", ["ownerId", "sessionKey", "ts"])
384
+ .index("by_owner_error", ["ownerId", "isError", "ts"]),
385
+
386
+ subsystemLog: defineTable({
387
+ time: v.string(),
388
+ day: v.string(),
389
+ ownerId: v.string(),
390
+ level: v.string(),
391
+ subsystem: v.string(),
392
+ message: v.string(),
393
+ fields: v.optional(v.any()),
394
+ })
395
+ .index("by_owner_day", ["ownerId", "day"])
396
+ .index("by_owner_subsystem_time", ["ownerId", "subsystem", "time"])
397
+ .index("by_owner_level_time", ["ownerId", "level", "time"]),
398
+
399
+ // ===========================================================================
400
+ // 7. CRON (REPORT 3)
401
+ // ===========================================================================
402
+ cronJobs: defineTable({
403
+ jobId: v.string(),
404
+ ownerUserId: v.string(),
405
+ name: v.string(),
406
+ description: v.optional(v.string()),
407
+ enabled: v.boolean(),
408
+ agentId: v.optional(v.string()),
409
+ sessionKey: v.optional(v.string()),
410
+ scheduleKind: v.union(v.literal("cron"), v.literal("every"), v.literal("at")),
411
+ scheduleExpr: v.optional(v.string()),
412
+ scheduleTz: v.optional(v.string()),
413
+ scheduleStaggerMs: v.optional(v.number()),
414
+ scheduleEveryMs: v.optional(v.number()),
415
+ scheduleAnchorMs: v.optional(v.number()),
416
+ scheduleAt: v.optional(v.number()),
417
+ sessionTarget: v.string(),
418
+ wakeMode: v.optional(v.string()),
419
+ payload: Enc(),
420
+ delivery: v.optional(Enc()),
421
+ failureAlert: v.optional(v.any()),
422
+ deleteAfterRun: v.optional(v.boolean()),
423
+ createdByKind: v.union(v.literal("owner"), v.literal("channel"), v.literal("legacy")),
424
+ createdByChannelId: v.optional(v.string()),
425
+ createdByConversationId: v.optional(v.string()),
426
+ createdByAccountId: v.optional(v.string()),
427
+ createdAtMs: v.number(),
428
+ updatedAtMs: v.number(),
429
+ stateNextRunAtMs: v.optional(v.number()),
430
+ stateLastRunAtMs: v.optional(v.number()),
431
+ stateRunningAtMs: v.optional(v.number()),
432
+ stateLastStatus: v.optional(v.string()),
433
+ stateLastError: v.optional(v.string()),
434
+ stateScheduleErrorCount: v.optional(v.number()),
435
+ stateConsecutiveErrorCount: v.optional(v.number()),
436
+ stateLastFailureAlertAtMs: v.optional(v.number()),
437
+ stateLastDelivered: v.optional(v.boolean()),
438
+ stateLastDeliveryStatus: v.optional(v.string()),
439
+ stateLastDeliveryError: v.optional(v.string()),
440
+ })
441
+ .index("by_owner_enabled_next", ["ownerUserId", "enabled", "stateNextRunAtMs"])
442
+ .index("by_owner_job", ["ownerUserId", "jobId"])
443
+ .index("by_owner_channel_conv", ["ownerUserId", "createdByChannelId", "createdByConversationId"])
444
+ .searchIndex("search_name_desc", {
445
+ searchField: "name",
446
+ filterFields: ["ownerUserId"],
447
+ }),
448
+
449
+ cronRuns: defineTable({
450
+ ownerUserId: v.string(),
451
+ jobId: v.string(),
452
+ ts: v.number(),
453
+ status: v.union(v.literal("ok"), v.literal("error"), v.literal("skipped")),
454
+ error: v.optional(v.string()),
455
+ summary: v.optional(Enc()),
456
+ delivered: v.optional(v.boolean()),
457
+ deliveryStatus: v.optional(v.string()),
458
+ deliveryError: v.optional(v.string()),
459
+ sessionId: v.optional(v.string()),
460
+ sessionKey: v.optional(v.string()),
461
+ runAtMs: v.optional(v.number()),
462
+ durationMs: v.optional(v.number()),
463
+ nextRunAtMs: v.optional(v.number()),
464
+ model: v.optional(v.string()),
465
+ provider: v.optional(v.string()),
466
+ usageInput: v.optional(v.number()),
467
+ usageOutput: v.optional(v.number()),
468
+ usageCacheRead: v.optional(v.number()),
469
+ usageCacheWrite: v.optional(v.number()),
470
+ usageTotalTokens: v.optional(v.number()),
471
+ usageCostUsd: v.optional(v.number()),
472
+ })
473
+ .index("by_owner_job_ts", ["ownerUserId", "jobId", "ts"])
474
+ .index("by_owner_job_status_ts", ["ownerUserId", "jobId", "status", "ts"]),
475
+
476
+ cronServiceState: defineTable({
477
+ ownerUserId: v.string(),
478
+ lastReapAtMs: v.optional(v.number()),
479
+ lastTickArmedAt: v.optional(v.number()),
480
+ lastTickExpectedDelayMs: v.optional(v.number()),
481
+ }).index("by_owner", ["ownerUserId"]),
482
+
483
+ // ===========================================================================
484
+ // 8. CHANNELS (REPORT 4)
485
+ // ===========================================================================
486
+ channelAccess: defineTable({
487
+ ownerId: v.string(),
488
+ channelId: v.string(),
489
+ accountId: v.string(),
490
+ kind: v.union(
491
+ v.literal("allow-from"),
492
+ v.literal("group-allow-from"),
493
+ v.literal("pairing"),
494
+ ),
495
+ senderId: Enc(),
496
+ senderName: v.optional(v.string()),
497
+ code: v.optional(Enc()),
498
+ createdAt: v.optional(v.number()),
499
+ lastSeenAt: v.optional(v.number()),
500
+ })
501
+ .index("by_owner_channel_account_kind", ["ownerId", "channelId", "accountId", "kind"]),
502
+
503
+ whatsappAuthFile: defineTable({
504
+ ownerId: v.string(),
505
+ accountId: v.string(),
506
+ fileKey: v.string(),
507
+ contentB64: Enc(),
508
+ contentVersion: v.number(),
509
+ updatedAt: v.number(),
510
+ })
511
+ .index("by_owner_account_file", ["ownerId", "accountId", "fileKey"])
512
+ .index("by_owner_account", ["ownerId", "accountId"]),
513
+
514
+ channelMediaBlob: defineTable({
515
+ ownerId: v.string(),
516
+ channelId: v.string(),
517
+ accountId: v.string(),
518
+ messageId: v.string(),
519
+ index: v.number(),
520
+ mimeType: v.string(),
521
+ fileName: v.optional(v.string()),
522
+ storageId: v.id("_storage"),
523
+ bytes: v.number(),
524
+ createdAt: v.number(),
525
+ }).index("by_owner_channel_account_msg", ["ownerId", "channelId", "accountId", "messageId"]),
526
+
527
+ // ===========================================================================
528
+ // 9. AUTH (REPORT 8)
529
+ // ===========================================================================
530
+ authProfiles: defineTable({
531
+ ownerId: v.string(),
532
+ agentId: v.string(),
533
+ profileId: v.string(),
534
+ provider: v.string(),
535
+ alias: v.optional(v.string()),
536
+ type: v.union(
537
+ v.literal("api_key"),
538
+ v.literal("oauth"),
539
+ v.literal("token"),
540
+ ),
541
+ keyEnc: v.optional(Enc()),
542
+ keyRef: v.optional(
543
+ v.object({ source: v.string(), provider: v.string(), id: v.string() }),
544
+ ),
545
+ tokenEnc: v.optional(Enc()),
546
+ tokenRef: v.optional(
547
+ v.object({ source: v.string(), provider: v.string(), id: v.string() }),
548
+ ),
549
+ accessEnc: v.optional(Enc()),
550
+ refreshEnc: v.optional(Enc()),
551
+ expires: v.optional(v.number()),
552
+ metadata: v.optional(v.any()),
553
+ updatedAt: v.number(),
554
+ })
555
+ .index("by_owner_agent", ["ownerId", "agentId"])
556
+ .index("by_owner_agent_provider", ["ownerId", "agentId", "provider"])
557
+ .index("by_owner_agent_profileId", ["ownerId", "agentId", "profileId"]),
558
+
559
+ profileState: defineTable({
560
+ ownerId: v.string(),
561
+ agentId: v.string(),
562
+ profileId: v.string(),
563
+ provider: v.string(),
564
+ lastUsed: v.optional(v.number()),
565
+ cooldownUntil: v.optional(v.number()),
566
+ cooldownReason: v.optional(v.string()),
567
+ cooldownModel: v.optional(v.string()),
568
+ disabledUntil: v.optional(v.number()),
569
+ disabledReason: v.optional(v.string()),
570
+ errorCount: v.optional(v.number()),
571
+ failureCounts: v.optional(v.any()),
572
+ lastFailureAt: v.optional(v.number()),
573
+ isLastGood: v.boolean(),
574
+ explicitOrder: v.optional(v.number()),
575
+ })
576
+ .index("by_owner_agent_provider", ["ownerId", "agentId", "provider"])
577
+ .index("by_owner_agent_profileId", ["ownerId", "agentId", "profileId"])
578
+ .index("by_cooldown_until", ["ownerId", "agentId", "cooldownUntil"]),
579
+
580
+ // Whole-file auth state blobs — auth-state.json / profile-state.json
581
+ // round-trip VERBATIM as sealed payloads. Blob-per-file (not per-row
582
+ // flattening) is deliberate: the failover `order` field is a per-provider
583
+ // ARRAY (`{provider: [profileId…]}`) that a per-row `explicitOrder` rank
584
+ // cannot represent without semantic drift, and `lastGood` reconstruction
585
+ // from per-row flags proved fragile (two winners on a race). The
586
+ // per-row `profileState` table above stays for queryable cooldown views;
587
+ // the blob is the source of truth the runtime round-trips.
588
+ authFiles: defineTable({
589
+ ownerId: v.string(),
590
+ agentId: v.string(),
591
+ // "models" is the per-USER models.json (custom provider catalog —
592
+ // Ollama etc.); stored under agentId "main" since the file is global.
593
+ kind: v.union(
594
+ v.literal("auth-state"),
595
+ v.literal("profile-state"),
596
+ v.literal("models"),
597
+ ),
598
+ payload: v.bytes(),
599
+ updatedAt: v.number(),
600
+ }).index("by_owner_agent_kind", ["ownerId", "agentId", "kind"]),
601
+
602
+ // Small system-level key/value facts (encryption-key fingerprint, schema
603
+ // markers). Generic so future singletons don't need their own tables.
604
+ systemMeta: defineTable({
605
+ key: v.string(),
606
+ value: v.string(),
607
+ updatedAt: v.number(),
608
+ }).index("by_key", ["key"]),
609
+
610
+ // Tracks an in-flight server-side factory reset (one row per table per run).
611
+ // Deliberately NOT in admin.ts RESETTABLE_TABLES so a reset never erases its
612
+ // own progress. `resetStart` seeds these, self-scheduling `resetWorker`s patch
613
+ // them, and `resetStatus` aggregates them for the polling client.
614
+ resetProgress: defineTable({
615
+ runId: v.string(),
616
+ table: v.string(),
617
+ deleted: v.number(),
618
+ done: v.boolean(),
619
+ updatedAt: v.number(),
620
+ })
621
+ .index("by_run", ["runId"])
622
+ .index("by_run_table", ["runId", "table"]),
623
+
624
+ // WhatsApp Baileys auth — replaces the ~900-file multi-file auth dir in
625
+ // convex mode. creds.json rides as ONE sealed BufferJSON blob (small,
626
+ // atomic updates); every signal key (pre-key / session / sender-key /
627
+ // app-state-sync-key / …) is a row keyed (keyType, keyId). Oversized
628
+ // values (LTHashState app-state-sync-version grows with contacts and can
629
+ // exceed the mutation arg cap) spill to Convex File Storage via
630
+ // `storageId`. keyType is a plain string — Baileys adds types across
631
+ // versions and a locked union would reject them.
632
+ whatsappAuthCreds: defineTable({
633
+ ownerId: v.string(),
634
+ accountId: v.string(),
635
+ payload: v.bytes(),
636
+ updatedAt: v.number(),
637
+ }).index("by_owner_account", ["ownerId", "accountId"]),
638
+
639
+ whatsappAuthKeys: defineTable({
640
+ ownerId: v.string(),
641
+ accountId: v.string(),
642
+ keyType: v.string(),
643
+ keyId: v.string(),
644
+ payload: v.optional(v.bytes()),
645
+ storageId: v.optional(v.id("_storage")),
646
+ updatedAt: v.number(),
647
+ })
648
+ .index("by_owner_account_type_id", ["ownerId", "accountId", "keyType", "keyId"])
649
+ .index("by_owner_account", ["ownerId", "accountId"]),
650
+
651
+ // ===========================================================================
652
+ // 10. EXEC APPROVALS (REPORT 8)
653
+ // ===========================================================================
654
+ execApprovals: defineTable({
655
+ ownerId: v.string(),
656
+ agentId: v.string(),
657
+ kind: v.union(v.literal("exact"), v.literal("pattern")),
658
+ value: v.string(),
659
+ valueNormalised: v.string(),
660
+ createdAt: v.number(),
661
+ })
662
+ .index("by_owner_agent_kind", ["ownerId", "agentId", "kind"])
663
+ .index("by_owner_agent_value", ["ownerId", "agentId", "valueNormalised"]),
664
+
665
+ // ===========================================================================
666
+ // 11. SKILLS (REPORT 6)
667
+ // ===========================================================================
668
+ skills: defineTable({
669
+ ownerId: v.string(),
670
+ source: v.union(
671
+ v.literal("bundled"),
672
+ v.literal("config"),
673
+ v.literal("managed"),
674
+ v.literal("personal"),
675
+ v.literal("project"),
676
+ v.literal("workspace"),
677
+ ),
678
+ agentId: v.union(v.string(), v.null()),
679
+ name: v.string(),
680
+ description: v.string(),
681
+ frontmatter: v.string(),
682
+ body: v.string(),
683
+ eligibility: v.object({
684
+ os: v.array(v.string()),
685
+ requiresBins: v.array(v.string()),
686
+ requiresAnyBins: v.array(v.string()),
687
+ requiresEnv: v.array(v.string()),
688
+ requiresConfig: v.array(v.string()),
689
+ }),
690
+ disableModelInvocation: v.boolean(),
691
+ createdAt: v.number(),
692
+ updatedAt: v.number(),
693
+ })
694
+ .index("by_owner_name", ["ownerId", "name"])
695
+ .index("by_owner_scope_name", ["ownerId", "source", "agentId", "name"])
696
+ .index("by_owner_source", ["ownerId", "source"]),
697
+
698
+ // ===========================================================================
699
+ // 12. EXTENSIONS (REPORT 11)
700
+ // ===========================================================================
701
+ extensions: defineTable({
702
+ moduleId: v.string(),
703
+ origin: v.union(v.literal("bundled"), v.literal("user")),
704
+ bundleBytes: v.optional(Enc()),
705
+ sourceLabel: v.string(),
706
+ manifest: v.optional(v.any()),
707
+ enabled: v.boolean(),
708
+ config: v.optional(Enc()),
709
+ bundleSha: v.optional(v.string()),
710
+ createdAt: v.number(),
711
+ updatedAt: v.number(),
712
+ createdBy: v.string(),
713
+ })
714
+ .index("by_moduleId", ["moduleId"])
715
+ .index("by_origin_enabled", ["origin", "enabled"]),
716
+
717
+ // ===========================================================================
718
+ // 13. ORG (REPORT 5)
719
+ // ===========================================================================
720
+ orgDeriveAudit: defineTable({
721
+ ts: v.string(),
722
+ topOrder: v.string(),
723
+ mode: v.union(
724
+ v.literal("derived"),
725
+ v.literal("explicit"),
726
+ v.literal("open"),
727
+ ),
728
+ edgeCount: v.number(),
729
+ memberCount: v.number(),
730
+ extraAllowCount: v.number(),
731
+ extraDenyCount: v.number(),
732
+ warnings: v.number(),
733
+ ownerId: v.string(),
734
+ })
735
+ .index("by_owner_ts", ["ownerId", "ts"])
736
+ .index("by_owner_topOrder", ["ownerId", "topOrder"]),
737
+
738
+ orgChartCache: defineTable({
739
+ hash: v.string(),
740
+ pngBytes: v.bytes(),
741
+ width: v.number(),
742
+ height: v.number(),
743
+ themeId: v.string(),
744
+ themeName: v.string(),
745
+ mimeType: v.literal("image/png"),
746
+ mtimeMs: v.number(),
747
+ transient: v.boolean(),
748
+ ownerId: v.string(),
749
+ })
750
+ .index("by_owner_hash", ["ownerId", "hash"])
751
+ .index("by_owner_mtime", ["ownerId", "mtimeMs"]),
752
+
753
+ // ===========================================================================
754
+ // 14. SUBAGENTS (REPORT 12)
755
+ // ===========================================================================
756
+ subagentRuns: defineTable({
757
+ runId: v.string(),
758
+ childSessionKey: v.string(),
759
+ requesterSessionKey: v.string(),
760
+ controllerSessionKey: v.optional(v.string()),
761
+ requesterDisplayKey: v.string(),
762
+ requesterOrigin: v.optional(Enc()),
763
+ task: Enc(),
764
+ cleanup: v.union(v.literal("delete"), v.literal("keep")),
765
+ label: v.optional(v.string()),
766
+ model: v.optional(v.string()),
767
+ workspaceDir: v.optional(v.string()),
768
+ runTimeoutSeconds: v.optional(v.number()),
769
+ spawnMode: v.optional(v.union(v.literal("run"), v.literal("session"))),
770
+ createdAt: v.number(),
771
+ startedAt: v.optional(v.number()),
772
+ sessionStartedAt: v.optional(v.number()),
773
+ accumulatedRuntimeMs: v.optional(v.number()),
774
+ endedAt: v.optional(v.number()),
775
+ outcome: v.optional(
776
+ v.object({
777
+ status: v.union(
778
+ v.literal("ok"),
779
+ v.literal("error"),
780
+ v.literal("timeout"),
781
+ v.literal("abort"),
782
+ ),
783
+ text: v.optional(Enc()),
784
+ error: v.optional(v.string()),
785
+ reason: v.optional(v.string()),
786
+ }),
787
+ ),
788
+ archiveAtMs: v.optional(v.number()),
789
+ cleanupCompletedAt: v.optional(v.number()),
790
+ cleanupHandled: v.optional(v.boolean()),
791
+ suppressAnnounceReason: v.optional(
792
+ v.union(v.literal("steer-restart"), v.literal("killed")),
793
+ ),
794
+ expectsCompletionMessage: v.optional(v.boolean()),
795
+ announceRetryCount: v.optional(v.number()),
796
+ lastAnnounceRetryAt: v.optional(v.number()),
797
+ endedReason: v.optional(v.string()),
798
+ wakeOnDescendantSettle: v.optional(v.boolean()),
799
+ frozenResultText: v.optional(Enc()),
800
+ frozenResultCapturedAt: v.optional(v.number()),
801
+ fallbackFrozenResultText: v.optional(Enc()),
802
+ fallbackFrozenResultCapturedAt: v.optional(v.number()),
803
+ endedHookEmittedAt: v.optional(v.number()),
804
+ completionAnnouncedAt: v.optional(v.number()),
805
+ attachmentsDir: v.optional(v.string()),
806
+ attachmentsRootDir: v.optional(v.string()),
807
+ retainAttachmentsOnKeep: v.optional(v.boolean()),
808
+ ownerId: v.string(),
809
+ })
810
+ .index("by_runId", ["ownerId", "runId"])
811
+ .index("by_childSessionKey_active", ["ownerId", "childSessionKey", "endedAt"])
812
+ .index("by_requester_createdAt", ["ownerId", "requesterSessionKey", "createdAt"])
813
+ .index("by_controller_active", ["ownerId", "controllerSessionKey", "endedAt"])
814
+ .index("by_requester_active", ["ownerId", "requesterSessionKey", "endedAt"]),
815
+
816
+ // ===========================================================================
817
+ // 15. INSTANCE / GATEWAY (REPORT 13)
818
+ // ===========================================================================
819
+ gatewayCoord: defineTable({
820
+ instanceId: v.string(),
821
+ pid: v.optional(v.number()),
822
+ pidAliveAt: v.optional(v.number()),
823
+ heartbeatTs: v.optional(v.number()),
824
+ heartbeatPid: v.optional(v.number()),
825
+ heartbeatUptimeMs: v.optional(v.number()),
826
+ lockPid: v.optional(v.number()),
827
+ lockPort: v.optional(v.number()),
828
+ lockCreatedAt: v.optional(v.string()),
829
+ lockLeaseUntil: v.optional(v.number()),
830
+ updatedAt: v.number(),
831
+ }).index("by_instance", ["instanceId"]),
832
+
833
+ // ===========================================================================
834
+ // 16. BLOBS (cross-cut — content-addressed bytes via Convex File Storage)
835
+ // ===========================================================================
836
+ brigadeBlobs: defineTable({
837
+ ownerId: v.string(),
838
+ sha256: v.string(),
839
+ storageId: v.id("_storage"),
840
+ mime: v.string(),
841
+ size: v.number(),
842
+ refcount: v.number(),
843
+ lastTouchedAt: v.number(),
844
+ })
845
+ .index("by_sha256", ["sha256"])
846
+ .index("by_owner_storage", ["ownerId", "storageId"]),
847
+ });