@vellumai/assistant 0.3.3 → 0.3.5

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 (163) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +45 -18
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +391 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +397 -135
  12. package/src/__tests__/channel-approvals.test.ts +99 -3
  13. package/src/__tests__/channel-delivery-store.test.ts +30 -4
  14. package/src/__tests__/channel-guardian.test.ts +261 -22
  15. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  16. package/src/__tests__/config-schema.test.ts +2 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-lifecycle.test.ts +636 -0
  19. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  20. package/src/__tests__/entity-search.test.ts +615 -0
  21. package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
  22. package/src/__tests__/handlers-twilio-config.test.ts +480 -0
  23. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  24. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  25. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  26. package/src/__tests__/run-orchestrator.test.ts +22 -0
  27. package/src/__tests__/secret-scanner.test.ts +223 -0
  28. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  29. package/src/__tests__/shell-parser-property.test.ts +357 -2
  30. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  31. package/src/__tests__/system-prompt.test.ts +25 -1
  32. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  33. package/src/__tests__/twilio-routes.test.ts +39 -3
  34. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  35. package/src/__tests__/user-reference.test.ts +68 -0
  36. package/src/__tests__/web-search.test.ts +1 -1
  37. package/src/__tests__/work-item-output.test.ts +110 -0
  38. package/src/calls/call-domain.ts +8 -5
  39. package/src/calls/call-orchestrator.ts +85 -22
  40. package/src/calls/twilio-config.ts +17 -11
  41. package/src/calls/twilio-rest.ts +276 -0
  42. package/src/calls/twilio-routes.ts +39 -1
  43. package/src/cli/map.ts +6 -0
  44. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  45. package/src/commands/cc-command-registry.ts +14 -1
  46. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  47. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  48. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  49. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  50. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  51. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  52. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  53. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  54. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  55. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  56. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  57. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  58. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  59. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  60. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  61. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  62. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  63. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  64. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  65. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  66. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  67. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  68. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  69. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  70. package/src/config/bundled-skills/messaging/SKILL.md +24 -5
  71. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  72. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  73. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  74. package/src/config/defaults.ts +2 -1
  75. package/src/config/schema.ts +9 -3
  76. package/src/config/skills.ts +5 -32
  77. package/src/config/system-prompt.ts +40 -0
  78. package/src/config/templates/IDENTITY.md +2 -2
  79. package/src/config/user-reference.ts +29 -0
  80. package/src/config/vellum-skills/catalog.json +58 -0
  81. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  82. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  83. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  84. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  85. package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
  86. package/src/daemon/auth-manager.ts +103 -0
  87. package/src/daemon/computer-use-session.ts +8 -1
  88. package/src/daemon/config-watcher.ts +253 -0
  89. package/src/daemon/handlers/config.ts +819 -22
  90. package/src/daemon/handlers/dictation.ts +182 -0
  91. package/src/daemon/handlers/identity.ts +14 -23
  92. package/src/daemon/handlers/index.ts +2 -0
  93. package/src/daemon/handlers/sessions.ts +2 -0
  94. package/src/daemon/handlers/shared.ts +3 -0
  95. package/src/daemon/handlers/skills.ts +6 -7
  96. package/src/daemon/handlers/work-items.ts +15 -7
  97. package/src/daemon/ipc-contract-inventory.json +10 -0
  98. package/src/daemon/ipc-contract.ts +114 -4
  99. package/src/daemon/ipc-handler.ts +87 -0
  100. package/src/daemon/lifecycle.ts +18 -4
  101. package/src/daemon/ride-shotgun-handler.ts +11 -1
  102. package/src/daemon/server.ts +111 -504
  103. package/src/daemon/session-agent-loop.ts +10 -15
  104. package/src/daemon/session-runtime-assembly.ts +115 -44
  105. package/src/daemon/session-tool-setup.ts +2 -0
  106. package/src/daemon/session.ts +19 -2
  107. package/src/inbound/public-ingress-urls.ts +3 -3
  108. package/src/memory/channel-guardian-store.ts +2 -1
  109. package/src/memory/db-connection.ts +28 -0
  110. package/src/memory/db-init.ts +1163 -0
  111. package/src/memory/db.ts +2 -2007
  112. package/src/memory/embedding-backend.ts +79 -11
  113. package/src/memory/indexer.ts +2 -0
  114. package/src/memory/job-handlers/media-processing.ts +100 -0
  115. package/src/memory/job-utils.ts +64 -4
  116. package/src/memory/jobs-store.ts +2 -1
  117. package/src/memory/jobs-worker.ts +11 -1
  118. package/src/memory/media-store.ts +759 -0
  119. package/src/memory/recall-cache.ts +107 -0
  120. package/src/memory/retriever.ts +36 -2
  121. package/src/memory/schema-migration.ts +984 -0
  122. package/src/memory/schema.ts +99 -0
  123. package/src/memory/search/entity.ts +208 -25
  124. package/src/memory/search/ranking.ts +6 -1
  125. package/src/memory/search/types.ts +26 -0
  126. package/src/messaging/provider-types.ts +2 -0
  127. package/src/messaging/providers/sms/adapter.ts +204 -0
  128. package/src/messaging/providers/sms/client.ts +93 -0
  129. package/src/messaging/providers/sms/types.ts +7 -0
  130. package/src/permissions/checker.ts +16 -2
  131. package/src/permissions/prompter.ts +14 -3
  132. package/src/permissions/trust-store.ts +7 -0
  133. package/src/runtime/approval-message-composer.ts +143 -0
  134. package/src/runtime/channel-approvals.ts +29 -7
  135. package/src/runtime/channel-guardian-service.ts +44 -18
  136. package/src/runtime/channel-readiness-service.ts +292 -0
  137. package/src/runtime/channel-readiness-types.ts +29 -0
  138. package/src/runtime/gateway-client.ts +2 -1
  139. package/src/runtime/http-server.ts +65 -28
  140. package/src/runtime/http-types.ts +3 -0
  141. package/src/runtime/routes/call-routes.ts +2 -1
  142. package/src/runtime/routes/channel-routes.ts +237 -103
  143. package/src/runtime/routes/run-routes.ts +7 -1
  144. package/src/runtime/run-orchestrator.ts +43 -3
  145. package/src/security/secret-scanner.ts +218 -0
  146. package/src/skills/frontmatter.ts +63 -0
  147. package/src/skills/slash-commands.ts +23 -0
  148. package/src/skills/vellum-catalog-remote.ts +107 -0
  149. package/src/tools/assets/materialize.ts +2 -2
  150. package/src/tools/browser/auto-navigate.ts +132 -24
  151. package/src/tools/browser/browser-manager.ts +67 -61
  152. package/src/tools/calls/call-start.ts +1 -0
  153. package/src/tools/claude-code/claude-code.ts +55 -3
  154. package/src/tools/credentials/vault.ts +1 -1
  155. package/src/tools/execution-target.ts +11 -1
  156. package/src/tools/executor.ts +10 -2
  157. package/src/tools/network/web-search.ts +1 -1
  158. package/src/tools/skills/vellum-catalog.ts +61 -156
  159. package/src/tools/terminal/parser.ts +21 -5
  160. package/src/tools/types.ts +2 -0
  161. package/src/twitter/router.ts +1 -1
  162. package/src/util/platform.ts +43 -1
  163. package/src/util/retry.ts +4 -4
@@ -0,0 +1,1163 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { getDb } from './db-connection.js';
3
+ import { getLogger } from '../util/logger.js';
4
+ import {
5
+ migrateJobDeferrals,
6
+ migrateToolInvocationsFk,
7
+ migrateMemoryEntityRelationDedup,
8
+ migrateMemoryItemsFingerprintScopeUnique,
9
+ migrateMemoryItemsScopeSaltedFingerprints,
10
+ migrateAssistantIdToSelf,
11
+ migrateRemoveAssistantIdColumns,
12
+ migrateLlmUsageEventsDropAssistantId,
13
+ migrateExtConvBindingsChannelChatUnique,
14
+ migrateCallSessionsProviderSidDedup,
15
+ migrateMemoryFtsBackfill,
16
+ } from './schema-migration.js';
17
+
18
+ const log = getLogger('memory-db');
19
+
20
+ export function initializeDb(): void {
21
+ const database = getDb();
22
+
23
+ database.run(/*sql*/ `
24
+ CREATE TABLE IF NOT EXISTS conversations (
25
+ id TEXT PRIMARY KEY,
26
+ title TEXT,
27
+ created_at INTEGER NOT NULL,
28
+ updated_at INTEGER NOT NULL,
29
+ total_input_tokens INTEGER NOT NULL DEFAULT 0,
30
+ total_output_tokens INTEGER NOT NULL DEFAULT 0,
31
+ total_estimated_cost REAL NOT NULL DEFAULT 0,
32
+ context_summary TEXT,
33
+ context_compacted_message_count INTEGER NOT NULL DEFAULT 0,
34
+ context_compacted_at INTEGER
35
+ )
36
+ `);
37
+
38
+ database.run(/*sql*/ `
39
+ CREATE TABLE IF NOT EXISTS messages (
40
+ id TEXT PRIMARY KEY,
41
+ conversation_id TEXT NOT NULL REFERENCES conversations(id),
42
+ role TEXT NOT NULL,
43
+ content TEXT NOT NULL,
44
+ created_at INTEGER NOT NULL
45
+ )
46
+ `);
47
+
48
+ database.run(/*sql*/ `
49
+ CREATE TABLE IF NOT EXISTS tool_invocations (
50
+ id TEXT PRIMARY KEY,
51
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
52
+ tool_name TEXT NOT NULL,
53
+ input TEXT NOT NULL,
54
+ result TEXT NOT NULL,
55
+ decision TEXT NOT NULL,
56
+ risk_level TEXT NOT NULL,
57
+ duration_ms INTEGER NOT NULL,
58
+ created_at INTEGER NOT NULL
59
+ )
60
+ `);
61
+
62
+ database.run(/*sql*/ `
63
+ CREATE TABLE IF NOT EXISTS memory_segments (
64
+ id TEXT PRIMARY KEY,
65
+ message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
66
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
67
+ role TEXT NOT NULL,
68
+ segment_index INTEGER NOT NULL,
69
+ text TEXT NOT NULL,
70
+ token_estimate INTEGER NOT NULL,
71
+ created_at INTEGER NOT NULL,
72
+ updated_at INTEGER NOT NULL
73
+ )
74
+ `);
75
+
76
+ database.run(/*sql*/ `
77
+ CREATE TABLE IF NOT EXISTS memory_items (
78
+ id TEXT PRIMARY KEY,
79
+ kind TEXT NOT NULL,
80
+ subject TEXT NOT NULL,
81
+ statement TEXT NOT NULL,
82
+ status TEXT NOT NULL,
83
+ confidence REAL NOT NULL,
84
+ fingerprint TEXT NOT NULL,
85
+ first_seen_at INTEGER NOT NULL,
86
+ last_seen_at INTEGER NOT NULL,
87
+ last_used_at INTEGER
88
+ )
89
+ `);
90
+
91
+ database.run(/*sql*/ `
92
+ CREATE TABLE IF NOT EXISTS memory_item_sources (
93
+ memory_item_id TEXT NOT NULL REFERENCES memory_items(id) ON DELETE CASCADE,
94
+ message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
95
+ evidence TEXT,
96
+ created_at INTEGER NOT NULL,
97
+ PRIMARY KEY (memory_item_id, message_id)
98
+ )
99
+ `);
100
+
101
+ database.run(/*sql*/ `
102
+ CREATE TABLE IF NOT EXISTS memory_item_conflicts (
103
+ id TEXT PRIMARY KEY,
104
+ scope_id TEXT NOT NULL DEFAULT 'default',
105
+ existing_item_id TEXT NOT NULL REFERENCES memory_items(id) ON DELETE CASCADE,
106
+ candidate_item_id TEXT NOT NULL REFERENCES memory_items(id) ON DELETE CASCADE,
107
+ relationship TEXT NOT NULL,
108
+ status TEXT NOT NULL,
109
+ clarification_question TEXT,
110
+ resolution_note TEXT,
111
+ last_asked_at INTEGER,
112
+ resolved_at INTEGER,
113
+ created_at INTEGER NOT NULL,
114
+ updated_at INTEGER NOT NULL
115
+ )
116
+ `);
117
+
118
+ database.run(/*sql*/ `
119
+ CREATE TABLE IF NOT EXISTS memory_summaries (
120
+ id TEXT PRIMARY KEY,
121
+ scope TEXT NOT NULL,
122
+ scope_key TEXT NOT NULL,
123
+ summary TEXT NOT NULL,
124
+ token_estimate INTEGER NOT NULL,
125
+ start_at INTEGER NOT NULL,
126
+ end_at INTEGER NOT NULL,
127
+ created_at INTEGER NOT NULL,
128
+ updated_at INTEGER NOT NULL,
129
+ UNIQUE (scope, scope_key)
130
+ )
131
+ `);
132
+
133
+ database.run(/*sql*/ `
134
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
135
+ id TEXT PRIMARY KEY,
136
+ target_type TEXT NOT NULL,
137
+ target_id TEXT NOT NULL,
138
+ provider TEXT NOT NULL,
139
+ model TEXT NOT NULL,
140
+ dimensions INTEGER NOT NULL,
141
+ vector_json TEXT NOT NULL,
142
+ created_at INTEGER NOT NULL,
143
+ updated_at INTEGER NOT NULL,
144
+ UNIQUE (target_type, target_id, provider, model)
145
+ )
146
+ `);
147
+
148
+ database.run(/*sql*/ `
149
+ CREATE TABLE IF NOT EXISTS memory_jobs (
150
+ id TEXT PRIMARY KEY,
151
+ type TEXT NOT NULL,
152
+ payload TEXT NOT NULL,
153
+ status TEXT NOT NULL,
154
+ attempts INTEGER NOT NULL DEFAULT 0,
155
+ run_after INTEGER NOT NULL,
156
+ last_error TEXT,
157
+ created_at INTEGER NOT NULL,
158
+ updated_at INTEGER NOT NULL
159
+ )
160
+ `);
161
+
162
+ database.run(/*sql*/ `
163
+ CREATE TABLE IF NOT EXISTS memory_checkpoints (
164
+ key TEXT PRIMARY KEY,
165
+ value TEXT NOT NULL,
166
+ updated_at INTEGER NOT NULL
167
+ )
168
+ `);
169
+
170
+ database.run(/*sql*/ `
171
+ CREATE TABLE IF NOT EXISTS attachments (
172
+ id TEXT PRIMARY KEY,
173
+ original_filename TEXT NOT NULL,
174
+ mime_type TEXT NOT NULL,
175
+ size_bytes INTEGER NOT NULL,
176
+ kind TEXT NOT NULL,
177
+ data_base64 TEXT NOT NULL,
178
+ created_at INTEGER NOT NULL
179
+ )
180
+ `);
181
+
182
+ database.run(/*sql*/ `
183
+ CREATE TABLE IF NOT EXISTS message_attachments (
184
+ id TEXT PRIMARY KEY,
185
+ message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
186
+ attachment_id TEXT NOT NULL REFERENCES attachments(id) ON DELETE CASCADE,
187
+ position INTEGER NOT NULL DEFAULT 0,
188
+ created_at INTEGER NOT NULL
189
+ )
190
+ `);
191
+
192
+ database.run(/*sql*/ `
193
+ CREATE TABLE IF NOT EXISTS message_surfaces (
194
+ id TEXT PRIMARY KEY,
195
+ message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
196
+ surface_id TEXT NOT NULL,
197
+ surface_type TEXT NOT NULL,
198
+ title TEXT,
199
+ data TEXT NOT NULL,
200
+ actions TEXT,
201
+ surface_message TEXT,
202
+ display TEXT,
203
+ position INTEGER NOT NULL DEFAULT 0,
204
+ created_at INTEGER NOT NULL
205
+ )
206
+ `);
207
+
208
+ database.run(/*sql*/ `
209
+ CREATE TABLE IF NOT EXISTS channel_inbound_events (
210
+ id TEXT PRIMARY KEY,
211
+ source_channel TEXT NOT NULL,
212
+ external_chat_id TEXT NOT NULL,
213
+ external_message_id TEXT NOT NULL,
214
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
215
+ message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
216
+ delivery_status TEXT NOT NULL DEFAULT 'pending',
217
+ created_at INTEGER NOT NULL,
218
+ updated_at INTEGER NOT NULL,
219
+ UNIQUE (source_channel, external_chat_id, external_message_id)
220
+ )
221
+ `);
222
+
223
+ database.run(/*sql*/ `
224
+ CREATE TABLE IF NOT EXISTS message_runs (
225
+ id TEXT PRIMARY KEY,
226
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
227
+ message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
228
+ status TEXT NOT NULL DEFAULT 'running',
229
+ pending_confirmation TEXT,
230
+ pending_secret TEXT,
231
+ input_tokens INTEGER NOT NULL DEFAULT 0,
232
+ output_tokens INTEGER NOT NULL DEFAULT 0,
233
+ estimated_cost REAL NOT NULL DEFAULT 0,
234
+ error TEXT,
235
+ created_at INTEGER NOT NULL,
236
+ updated_at INTEGER NOT NULL
237
+ )
238
+ `);
239
+
240
+ try { database.run(/*sql*/ `ALTER TABLE message_runs ADD COLUMN pending_secret TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE message_runs ADD COLUMN pending_secret (likely already exists)'); }
241
+
242
+ database.run(/*sql*/ `
243
+ CREATE TABLE IF NOT EXISTS reminders (
244
+ id TEXT PRIMARY KEY,
245
+ label TEXT NOT NULL,
246
+ message TEXT NOT NULL,
247
+ fire_at INTEGER NOT NULL,
248
+ mode TEXT NOT NULL,
249
+ status TEXT NOT NULL,
250
+ fired_at INTEGER,
251
+ conversation_id TEXT,
252
+ created_at INTEGER NOT NULL,
253
+ updated_at INTEGER NOT NULL
254
+ )
255
+ `);
256
+
257
+ database.run(/*sql*/ `
258
+ CREATE TABLE IF NOT EXISTS cron_jobs (
259
+ id TEXT PRIMARY KEY,
260
+ name TEXT NOT NULL,
261
+ enabled INTEGER NOT NULL DEFAULT 1,
262
+ cron_expression TEXT NOT NULL,
263
+ schedule_syntax TEXT NOT NULL DEFAULT 'cron',
264
+ timezone TEXT,
265
+ message TEXT NOT NULL,
266
+ next_run_at INTEGER NOT NULL,
267
+ last_run_at INTEGER,
268
+ last_status TEXT,
269
+ retry_count INTEGER NOT NULL DEFAULT 0,
270
+ created_by TEXT NOT NULL,
271
+ created_at INTEGER NOT NULL,
272
+ updated_at INTEGER NOT NULL
273
+ )
274
+ `);
275
+
276
+ database.run(/*sql*/ `
277
+ CREATE TABLE IF NOT EXISTS cron_runs (
278
+ id TEXT PRIMARY KEY,
279
+ job_id TEXT NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE,
280
+ status TEXT NOT NULL,
281
+ started_at INTEGER NOT NULL,
282
+ finished_at INTEGER,
283
+ duration_ms INTEGER,
284
+ output TEXT,
285
+ error TEXT,
286
+ conversation_id TEXT,
287
+ created_at INTEGER NOT NULL
288
+ )
289
+ `);
290
+
291
+ database.run(/*sql*/ `
292
+ CREATE TABLE IF NOT EXISTS accounts (
293
+ id TEXT PRIMARY KEY,
294
+ service TEXT NOT NULL,
295
+ username TEXT,
296
+ email TEXT,
297
+ display_name TEXT,
298
+ status TEXT NOT NULL DEFAULT 'active',
299
+ credential_ref TEXT,
300
+ metadata_json TEXT,
301
+ created_at INTEGER NOT NULL,
302
+ updated_at INTEGER NOT NULL
303
+ )
304
+ `);
305
+
306
+ database.run(/*sql*/ `
307
+ CREATE TABLE IF NOT EXISTS documents (
308
+ surface_id TEXT PRIMARY KEY,
309
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
310
+ title TEXT NOT NULL,
311
+ content TEXT NOT NULL,
312
+ word_count INTEGER NOT NULL DEFAULT 0,
313
+ created_at INTEGER NOT NULL,
314
+ updated_at INTEGER NOT NULL
315
+ )
316
+ `);
317
+
318
+ database.run(/*sql*/ `
319
+ CREATE TABLE IF NOT EXISTS published_pages (
320
+ id TEXT PRIMARY KEY,
321
+ deployment_id TEXT NOT NULL UNIQUE,
322
+ public_url TEXT NOT NULL,
323
+ page_title TEXT,
324
+ html_hash TEXT NOT NULL,
325
+ published_at INTEGER NOT NULL,
326
+ status TEXT NOT NULL DEFAULT 'active'
327
+ )
328
+ `);
329
+
330
+ try { database.run(/*sql*/ `ALTER TABLE published_pages ADD COLUMN app_id TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE published_pages ADD COLUMN app_id (likely already exists)'); }
331
+ try { database.run(/*sql*/ `ALTER TABLE published_pages ADD COLUMN project_slug TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE published_pages ADD COLUMN project_slug (likely already exists)'); }
332
+
333
+ database.run(/*sql*/ `
334
+ CREATE TABLE IF NOT EXISTS shared_app_links (
335
+ id TEXT PRIMARY KEY,
336
+ share_token TEXT NOT NULL UNIQUE,
337
+ bundle_data BLOB NOT NULL,
338
+ bundle_size_bytes INTEGER NOT NULL,
339
+ manifest_json TEXT NOT NULL,
340
+ download_count INTEGER NOT NULL DEFAULT 0,
341
+ created_at INTEGER NOT NULL,
342
+ expires_at INTEGER
343
+ )
344
+ `);
345
+
346
+ database.run(/*sql*/ `
347
+ CREATE TABLE IF NOT EXISTS home_base_app_links (
348
+ id TEXT PRIMARY KEY,
349
+ app_id TEXT NOT NULL,
350
+ source TEXT NOT NULL,
351
+ created_at INTEGER NOT NULL,
352
+ updated_at INTEGER NOT NULL
353
+ )
354
+ `);
355
+
356
+ // ── Watchers ─────────────────────────────────────────────────────────
357
+
358
+ database.run(/*sql*/ `
359
+ CREATE TABLE IF NOT EXISTS watchers (
360
+ id TEXT PRIMARY KEY,
361
+ name TEXT NOT NULL,
362
+ provider_id TEXT NOT NULL,
363
+ enabled INTEGER NOT NULL DEFAULT 1,
364
+ poll_interval_ms INTEGER NOT NULL DEFAULT 60000,
365
+ action_prompt TEXT NOT NULL,
366
+ watermark TEXT,
367
+ conversation_id TEXT,
368
+ status TEXT NOT NULL DEFAULT 'idle',
369
+ consecutive_errors INTEGER NOT NULL DEFAULT 0,
370
+ last_error TEXT,
371
+ last_poll_at INTEGER,
372
+ next_poll_at INTEGER NOT NULL,
373
+ config_json TEXT,
374
+ credential_service TEXT NOT NULL,
375
+ created_at INTEGER NOT NULL,
376
+ updated_at INTEGER NOT NULL
377
+ )
378
+ `);
379
+
380
+ database.run(/*sql*/ `
381
+ CREATE TABLE IF NOT EXISTS watcher_events (
382
+ id TEXT PRIMARY KEY,
383
+ watcher_id TEXT NOT NULL REFERENCES watchers(id) ON DELETE CASCADE,
384
+ external_id TEXT NOT NULL,
385
+ event_type TEXT NOT NULL,
386
+ summary TEXT NOT NULL,
387
+ payload_json TEXT NOT NULL,
388
+ disposition TEXT NOT NULL DEFAULT 'pending',
389
+ llm_action TEXT,
390
+ processed_at INTEGER,
391
+ created_at INTEGER NOT NULL,
392
+ UNIQUE (watcher_id, external_id)
393
+ )
394
+ `);
395
+
396
+ database.run(/*sql*/ `
397
+ CREATE TABLE IF NOT EXISTS llm_request_logs (
398
+ id TEXT PRIMARY KEY,
399
+ conversation_id TEXT NOT NULL,
400
+ request_payload TEXT NOT NULL,
401
+ response_payload TEXT NOT NULL,
402
+ created_at INTEGER NOT NULL
403
+ )
404
+ `);
405
+
406
+ database.run(/*sql*/ `
407
+ CREATE TABLE IF NOT EXISTS llm_usage_events (
408
+ id TEXT PRIMARY KEY,
409
+ created_at INTEGER NOT NULL,
410
+ conversation_id TEXT,
411
+ run_id TEXT,
412
+ request_id TEXT,
413
+ actor TEXT NOT NULL,
414
+ provider TEXT NOT NULL,
415
+ model TEXT NOT NULL,
416
+ input_tokens INTEGER NOT NULL,
417
+ output_tokens INTEGER NOT NULL,
418
+ cache_creation_input_tokens INTEGER,
419
+ cache_read_input_tokens INTEGER,
420
+ estimated_cost_usd REAL,
421
+ pricing_status TEXT NOT NULL,
422
+ metadata_json TEXT
423
+ )
424
+ `);
425
+
426
+ database.run(/*sql*/ `
427
+ CREATE TABLE IF NOT EXISTS memory_entities (
428
+ id TEXT PRIMARY KEY,
429
+ name TEXT NOT NULL,
430
+ type TEXT NOT NULL,
431
+ aliases TEXT,
432
+ description TEXT,
433
+ first_seen_at INTEGER NOT NULL,
434
+ last_seen_at INTEGER NOT NULL,
435
+ mention_count INTEGER NOT NULL DEFAULT 1
436
+ )
437
+ `);
438
+
439
+ database.run(/*sql*/ `
440
+ CREATE TABLE IF NOT EXISTS memory_entity_relations (
441
+ id TEXT PRIMARY KEY,
442
+ source_entity_id TEXT NOT NULL,
443
+ target_entity_id TEXT NOT NULL,
444
+ relation TEXT NOT NULL,
445
+ evidence TEXT,
446
+ first_seen_at INTEGER NOT NULL,
447
+ last_seen_at INTEGER NOT NULL
448
+ )
449
+ `);
450
+
451
+ database.run(/*sql*/ `
452
+ CREATE TABLE IF NOT EXISTS memory_item_entities (
453
+ memory_item_id TEXT NOT NULL,
454
+ entity_id TEXT NOT NULL,
455
+ PRIMARY KEY (memory_item_id, entity_id)
456
+ )
457
+ `);
458
+
459
+ // FTS table for lexical retrieval over memory_segments.text.
460
+ database.run(/*sql*/ `
461
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_segment_fts USING fts5(
462
+ segment_id UNINDEXED,
463
+ text
464
+ )
465
+ `);
466
+
467
+ database.run(/*sql*/ `
468
+ CREATE TRIGGER IF NOT EXISTS memory_segments_ai
469
+ AFTER INSERT ON memory_segments
470
+ BEGIN
471
+ INSERT INTO memory_segment_fts(segment_id, text) VALUES (new.id, new.text);
472
+ END
473
+ `);
474
+
475
+ database.run(/*sql*/ `
476
+ CREATE TRIGGER IF NOT EXISTS memory_segments_ad
477
+ AFTER DELETE ON memory_segments
478
+ BEGIN
479
+ DELETE FROM memory_segment_fts WHERE segment_id = old.id;
480
+ END
481
+ `);
482
+
483
+ database.run(/*sql*/ `
484
+ CREATE TRIGGER IF NOT EXISTS memory_segments_au
485
+ AFTER UPDATE ON memory_segments
486
+ BEGIN
487
+ DELETE FROM memory_segment_fts WHERE segment_id = old.id;
488
+ INSERT INTO memory_segment_fts(segment_id, text) VALUES (new.id, new.text);
489
+ END
490
+ `);
491
+
492
+ database.run(/*sql*/ `
493
+ CREATE TABLE IF NOT EXISTS conversation_keys (
494
+ id TEXT PRIMARY KEY,
495
+ conversation_key TEXT NOT NULL UNIQUE,
496
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
497
+ created_at INTEGER NOT NULL
498
+ )
499
+ `);
500
+
501
+ // Migrations — ALTER TABLE ADD COLUMN throws if column already exists
502
+ try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN total_input_tokens INTEGER NOT NULL DEFAULT 0`); } catch { /* already exists */ }
503
+ try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN total_output_tokens INTEGER NOT NULL DEFAULT 0`); } catch { /* already exists */ }
504
+ try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN total_estimated_cost REAL NOT NULL DEFAULT 0`); } catch { /* already exists */ }
505
+ try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN context_summary TEXT`); } catch { /* already exists */ }
506
+ try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN context_compacted_message_count INTEGER NOT NULL DEFAULT 0`); } catch { /* already exists */ }
507
+ try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN context_compacted_at INTEGER`); } catch { /* already exists */ }
508
+ try { database.run(/*sql*/ `ALTER TABLE memory_items ADD COLUMN importance REAL`); } catch { /* already exists */ }
509
+ try { database.run(/*sql*/ `ALTER TABLE memory_items ADD COLUMN access_count INTEGER NOT NULL DEFAULT 0`); } catch { /* already exists */ }
510
+ try { database.run(/*sql*/ `ALTER TABLE memory_summaries ADD COLUMN version INTEGER NOT NULL DEFAULT 1`); } catch { /* already exists */ }
511
+ try { database.run(/*sql*/ `ALTER TABLE memory_items ADD COLUMN valid_from INTEGER`); } catch { /* already exists */ }
512
+ try { database.run(/*sql*/ `ALTER TABLE memory_items ADD COLUMN invalid_at INTEGER`); } catch { /* already exists */ }
513
+ try { database.run(/*sql*/ `ALTER TABLE memory_items ADD COLUMN verification_state TEXT NOT NULL DEFAULT 'assistant_inferred'`); } catch { /* already exists */ }
514
+ try { database.run(/*sql*/ `ALTER TABLE memory_jobs ADD COLUMN deferrals INTEGER NOT NULL DEFAULT 0`); } catch { /* already exists */ }
515
+ try { database.run(/*sql*/ `ALTER TABLE memory_segments ADD COLUMN scope_id TEXT NOT NULL DEFAULT 'default'`); } catch { /* already exists */ }
516
+ try { database.run(/*sql*/ `ALTER TABLE memory_items ADD COLUMN scope_id TEXT NOT NULL DEFAULT 'default'`); } catch { /* already exists */ }
517
+ try { database.run(/*sql*/ `ALTER TABLE memory_summaries ADD COLUMN scope_id TEXT NOT NULL DEFAULT 'default'`); } catch { /* already exists */ }
518
+ try { database.run(/*sql*/ `ALTER TABLE memory_segments ADD COLUMN content_hash TEXT`); } catch { /* already exists */ }
519
+ try { database.run(/*sql*/ `ALTER TABLE channel_inbound_events ADD COLUMN source_message_id TEXT`); } catch { /* already exists */ }
520
+ try { database.run(/*sql*/ `ALTER TABLE attachments ADD COLUMN content_hash TEXT`); } catch { /* already exists */ }
521
+ try { database.run(/*sql*/ `ALTER TABLE channel_inbound_events ADD COLUMN processing_status TEXT NOT NULL DEFAULT 'pending'`); } catch { /* already exists */ }
522
+ try { database.run(/*sql*/ `ALTER TABLE channel_inbound_events ADD COLUMN processing_attempts INTEGER NOT NULL DEFAULT 0`); } catch { /* already exists */ }
523
+ try { database.run(/*sql*/ `ALTER TABLE channel_inbound_events ADD COLUMN last_processing_error TEXT`); } catch { /* already exists */ }
524
+ try { database.run(/*sql*/ `ALTER TABLE channel_inbound_events ADD COLUMN retry_after INTEGER`); } catch { /* already exists */ }
525
+ try { database.run(/*sql*/ `ALTER TABLE channel_inbound_events ADD COLUMN raw_payload TEXT`); } catch { /* already exists */ }
526
+ try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN thread_type TEXT NOT NULL DEFAULT 'standard'`); } catch { /* already exists */ }
527
+ try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN memory_scope_id TEXT NOT NULL DEFAULT 'default'`); } catch { /* already exists */ }
528
+ try { database.run(/*sql*/ `ALTER TABLE attachments ADD COLUMN thumbnail_base64 TEXT`); } catch { /* already exists */ }
529
+ try { database.run(/*sql*/ `ALTER TABLE cron_jobs ADD COLUMN schedule_syntax TEXT NOT NULL DEFAULT 'cron'`); } catch { /* already exists */ }
530
+ try { database.run(/*sql*/ `ALTER TABLE messages ADD COLUMN metadata TEXT`); } catch { /* already exists */ }
531
+ try { database.run(/*sql*/ `ALTER TABLE memory_embeddings ADD COLUMN content_hash TEXT`); } catch { /* already exists */ }
532
+
533
+ migrateJobDeferrals(database);
534
+ migrateToolInvocationsFk(database);
535
+ migrateMemoryEntityRelationDedup(database);
536
+ migrateMemoryItemsFingerprintScopeUnique(database);
537
+ migrateMemoryItemsScopeSaltedFingerprints(database);
538
+ migrateAssistantIdToSelf(database);
539
+ migrateRemoveAssistantIdColumns(database);
540
+ migrateLlmUsageEventsDropAssistantId(database);
541
+
542
+ // Indexes for query performance on large datasets
543
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_request_logs_conv_created ON llm_request_logs(conversation_id, created_at)`);
544
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id)`);
545
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_tool_invocations_conversation_id ON tool_invocations(conversation_id)`);
546
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at)`);
547
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_segments_message_segment ON memory_segments(message_id, segment_index)`);
548
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_segments_conversation_created ON memory_segments(conversation_id, created_at DESC)`);
549
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_item_sources_message_id ON memory_item_sources(message_id)`);
550
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_item_conflicts_status_created ON memory_item_conflicts(status, created_at)`);
551
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_item_conflicts_status_resolved_at ON memory_item_conflicts(status, resolved_at)`);
552
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_item_conflicts_scope_status ON memory_item_conflicts(scope_id, status)`);
553
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_item_conflicts_existing_item_id ON memory_item_conflicts(existing_item_id)`);
554
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_item_conflicts_candidate_item_id ON memory_item_conflicts(candidate_item_id)`);
555
+ database.run(/*sql*/ `
556
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_item_conflicts_pending_pair_unique
557
+ ON memory_item_conflicts(scope_id, existing_item_id, candidate_item_id)
558
+ WHERE status = 'pending_clarification'
559
+ `);
560
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_fingerprint_scope ON memory_items(fingerprint, scope_id)`);
561
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_items_kind_status ON memory_items(kind, status)`);
562
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_items_status_invalid_at ON memory_items(status, invalid_at)`);
563
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_items_scope_status_kind ON memory_items(scope_id, status, kind)`);
564
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_items_last_seen_at ON memory_items(last_seen_at)`);
565
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_embeddings_target ON memory_embeddings(target_type, target_id)`);
566
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_embeddings_provider_model ON memory_embeddings(provider, model)`);
567
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_embeddings_content_hash ON memory_embeddings(content_hash, provider, model)`);
568
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_jobs_status_run_after ON memory_jobs(status, run_after)`);
569
+ database.run(/*sql*/ `
570
+ CREATE INDEX IF NOT EXISTS idx_memory_jobs_conflict_resolve_dedupe
571
+ ON memory_jobs(
572
+ type,
573
+ status,
574
+ json_extract(payload, '$.messageId'),
575
+ COALESCE(json_extract(payload, '$.scopeId'), 'default')
576
+ )
577
+ `);
578
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_summaries_scope_time ON memory_summaries(scope, end_at DESC)`);
579
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_segments_scope_id ON memory_segments(scope_id)`);
580
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_items_scope_id ON memory_items(scope_id)`);
581
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_summaries_scope_id ON memory_summaries(scope_id)`);
582
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conversation_keys_key ON conversation_keys(conversation_key)`);
583
+ // Deduplicate before creating unique index — existing DBs may have duplicate content_hash values.
584
+ // Re-point message_attachments to the survivor (MIN rowid per content_hash), then delete dupes.
585
+ {
586
+ const raw = (database as unknown as { $client: Database }).$client;
587
+ raw.exec(/*sql*/ `
588
+ UPDATE message_attachments
589
+ SET attachment_id = (
590
+ SELECT a_survivor.id
591
+ FROM attachments a_survivor
592
+ WHERE a_survivor.content_hash = (
593
+ SELECT a_dup.content_hash FROM attachments a_dup
594
+ WHERE a_dup.id = message_attachments.attachment_id
595
+ )
596
+ ORDER BY a_survivor.rowid
597
+ LIMIT 1
598
+ )
599
+ WHERE attachment_id IN (
600
+ SELECT id FROM attachments
601
+ WHERE content_hash IS NOT NULL
602
+ AND rowid NOT IN (
603
+ SELECT MIN(rowid) FROM attachments
604
+ WHERE content_hash IS NOT NULL
605
+ GROUP BY content_hash
606
+ )
607
+ )
608
+ `);
609
+ raw.exec(/*sql*/ `
610
+ DELETE FROM attachments
611
+ WHERE content_hash IS NOT NULL
612
+ AND rowid NOT IN (
613
+ SELECT MIN(rowid) FROM attachments
614
+ WHERE content_hash IS NOT NULL
615
+ GROUP BY content_hash
616
+ )
617
+ `);
618
+ }
619
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_attachments_content_dedup ON attachments(content_hash) WHERE content_hash IS NOT NULL`);
620
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_attachments_message_id ON message_attachments(message_id)`);
621
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_attachments_attachment_id ON message_attachments(attachment_id)`);
622
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_lookup ON channel_inbound_events(source_channel, external_chat_id, external_message_id)`);
623
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_conversation ON channel_inbound_events(conversation_id)`);
624
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_source_msg ON channel_inbound_events(source_channel, external_chat_id, source_message_id)`);
625
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_inbound_events_processing_retry ON channel_inbound_events(processing_status, retry_after)`);
626
+
627
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_runs_status ON message_runs(status)`);
628
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_runs_conversation ON message_runs(conversation_id)`);
629
+
630
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_reminders_status_fire_at ON reminders(status, fire_at)`);
631
+
632
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled_next_run ON cron_jobs(enabled, next_run_at)`);
633
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_cron_jobs_syntax_enabled_next_run ON cron_jobs(schedule_syntax, enabled, next_run_at)`);
634
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_cron_runs_job_id ON cron_runs(job_id)`);
635
+
636
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_accounts_service ON accounts(service)`);
637
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status)`);
638
+
639
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_created_at ON llm_usage_events(created_at)`);
640
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_provider ON llm_usage_events(provider)`);
641
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_model ON llm_usage_events(model)`);
642
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_actor ON llm_usage_events(actor)`);
643
+
644
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_shared_app_links_share_token ON shared_app_links(share_token)`);
645
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_home_base_app_links_app_id ON home_base_app_links(app_id)`);
646
+
647
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_published_pages_html_hash ON published_pages(html_hash)`);
648
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_published_pages_status ON published_pages(status)`);
649
+
650
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_watchers_enabled_next_poll ON watchers(enabled, next_poll_at)`);
651
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_watchers_status ON watchers(status)`);
652
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_watcher_events_watcher_id ON watcher_events(watcher_id)`);
653
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_watcher_events_disposition ON watcher_events(disposition)`);
654
+
655
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_entities_name ON memory_entities(name)`);
656
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_entities_type ON memory_entities(type)`);
657
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_entity_relations_unique_edge ON memory_entity_relations(source_entity_id, target_entity_id, relation)`);
658
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_entity_relations_source ON memory_entity_relations(source_entity_id)`);
659
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_entity_relations_target ON memory_entity_relations(target_entity_id)`);
660
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_item_entities_memory_item ON memory_item_entities(memory_item_id)`);
661
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_item_entities_entity ON memory_item_entities(entity_id)`);
662
+
663
+ // ── Contacts ────────────────────────────────────────────────────────
664
+
665
+ database.run(/*sql*/ `
666
+ CREATE TABLE IF NOT EXISTS contacts (
667
+ id TEXT PRIMARY KEY,
668
+ display_name TEXT NOT NULL,
669
+ relationship TEXT,
670
+ importance REAL NOT NULL DEFAULT 0.5,
671
+ response_expectation TEXT,
672
+ preferred_tone TEXT,
673
+ last_interaction INTEGER,
674
+ interaction_count INTEGER NOT NULL DEFAULT 0,
675
+ created_at INTEGER NOT NULL,
676
+ updated_at INTEGER NOT NULL
677
+ )
678
+ `);
679
+
680
+ database.run(/*sql*/ `
681
+ CREATE TABLE IF NOT EXISTS contact_channels (
682
+ id TEXT PRIMARY KEY,
683
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
684
+ type TEXT NOT NULL,
685
+ address TEXT NOT NULL,
686
+ is_primary INTEGER NOT NULL DEFAULT 0,
687
+ created_at INTEGER NOT NULL
688
+ )
689
+ `);
690
+
691
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_contacts_display_name ON contacts(display_name)`);
692
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_contacts_importance ON contacts(importance DESC)`);
693
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_contacts_last_interaction ON contacts(last_interaction DESC)`);
694
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_contact_channels_contact_id ON contact_channels(contact_id)`);
695
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_contact_channels_type_address ON contact_channels(type, address)`);
696
+
697
+ // ── Triage Results ─────────────────────────────────────────────────
698
+
699
+ database.run(/*sql*/ `
700
+ CREATE TABLE IF NOT EXISTS triage_results (
701
+ id TEXT PRIMARY KEY,
702
+ channel TEXT NOT NULL,
703
+ sender TEXT NOT NULL,
704
+ category TEXT NOT NULL,
705
+ confidence REAL NOT NULL,
706
+ suggested_action TEXT NOT NULL,
707
+ matched_playbook_ids TEXT,
708
+ message_id TEXT,
709
+ created_at INTEGER NOT NULL
710
+ )
711
+ `);
712
+
713
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_triage_results_channel ON triage_results(channel)`);
714
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_triage_results_category ON triage_results(category)`);
715
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_triage_results_sender ON triage_results(sender)`);
716
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_triage_results_created_at ON triage_results(created_at DESC)`);
717
+
718
+ // ── Call Sessions (outgoing AI phone calls) ────────────────────────
719
+
720
+ database.run(/*sql*/ `
721
+ CREATE TABLE IF NOT EXISTS call_sessions (
722
+ id TEXT PRIMARY KEY,
723
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
724
+ provider TEXT NOT NULL,
725
+ provider_call_sid TEXT,
726
+ from_number TEXT NOT NULL,
727
+ to_number TEXT NOT NULL,
728
+ task TEXT,
729
+ status TEXT NOT NULL DEFAULT 'initiated',
730
+ started_at INTEGER,
731
+ ended_at INTEGER,
732
+ last_error TEXT,
733
+ created_at INTEGER NOT NULL,
734
+ updated_at INTEGER NOT NULL
735
+ )
736
+ `);
737
+
738
+ database.run(/*sql*/ `
739
+ CREATE TABLE IF NOT EXISTS call_events (
740
+ id TEXT PRIMARY KEY,
741
+ call_session_id TEXT NOT NULL REFERENCES call_sessions(id) ON DELETE CASCADE,
742
+ event_type TEXT NOT NULL,
743
+ payload_json TEXT NOT NULL DEFAULT '{}',
744
+ created_at INTEGER NOT NULL
745
+ )
746
+ `);
747
+
748
+ database.run(/*sql*/ `
749
+ CREATE TABLE IF NOT EXISTS call_pending_questions (
750
+ id TEXT PRIMARY KEY,
751
+ call_session_id TEXT NOT NULL REFERENCES call_sessions(id) ON DELETE CASCADE,
752
+ question_text TEXT NOT NULL,
753
+ status TEXT NOT NULL DEFAULT 'pending',
754
+ asked_at INTEGER NOT NULL,
755
+ answered_at INTEGER,
756
+ answer_text TEXT
757
+ )
758
+ `);
759
+
760
+ database.run(/*sql*/ `
761
+ CREATE TABLE IF NOT EXISTS processed_callbacks (
762
+ id TEXT PRIMARY KEY,
763
+ dedupe_key TEXT NOT NULL UNIQUE,
764
+ call_session_id TEXT NOT NULL REFERENCES call_sessions(id) ON DELETE CASCADE,
765
+ created_at INTEGER NOT NULL
766
+ )
767
+ `);
768
+
769
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_sessions_conversation_id ON call_sessions(conversation_id)`);
770
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_sessions_provider_call_sid ON call_sessions(provider_call_sid)`);
771
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_sessions_status ON call_sessions(status)`);
772
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_events_call_session_id ON call_events(call_session_id)`);
773
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_pending_questions_call_session_id ON call_pending_questions(call_session_id)`);
774
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_pending_questions_status ON call_pending_questions(status)`);
775
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_processed_callbacks_dedupe_key ON processed_callbacks(dedupe_key)`);
776
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_processed_callbacks_call_session_id ON processed_callbacks(call_session_id)`);
777
+
778
+ // Add claim ownership token to prevent cross-handler claim interference
779
+ try { database.run(/*sql*/ `ALTER TABLE processed_callbacks ADD COLUMN claim_id TEXT`); } catch { /* already exists */ }
780
+
781
+ // Caller identity persistence for auditability
782
+ try { database.run(/*sql*/ `ALTER TABLE call_sessions ADD COLUMN caller_identity_mode TEXT`); } catch { /* already exists */ }
783
+ try { database.run(/*sql*/ `ALTER TABLE call_sessions ADD COLUMN caller_identity_source TEXT`); } catch { /* already exists */ }
784
+
785
+ // Unique constraint: at most one non-null provider_call_sid per (provider, provider_call_sid).
786
+ // On upgraded databases that pre-date this constraint, duplicate rows may exist; deduplicate
787
+ // them first to avoid a UNIQUE constraint failure that would prevent startup.
788
+ migrateCallSessionsProviderSidDedup(database);
789
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_call_sessions_provider_sid_unique ON call_sessions(provider, provider_call_sid) WHERE provider_call_sid IS NOT NULL`);
790
+
791
+ // ── Follow-ups ─────────────────────────────────────────────────────
792
+
793
+ database.run(/*sql*/ `
794
+ CREATE TABLE IF NOT EXISTS followups (
795
+ id TEXT PRIMARY KEY,
796
+ channel TEXT NOT NULL,
797
+ thread_id TEXT NOT NULL,
798
+ contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
799
+ sent_at INTEGER NOT NULL,
800
+ expected_response_by INTEGER,
801
+ status TEXT NOT NULL DEFAULT 'pending',
802
+ reminder_cron_id TEXT,
803
+ created_at INTEGER NOT NULL,
804
+ updated_at INTEGER NOT NULL
805
+ )
806
+ `);
807
+
808
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_followups_status ON followups(status)`);
809
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_followups_channel ON followups(channel)`);
810
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_followups_contact_id ON followups(contact_id)`);
811
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_followups_channel_thread ON followups(channel, thread_id)`);
812
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_followups_status_expected ON followups(status, expected_response_by)`);
813
+
814
+ // ── Tasks ─────────────────────────────────────────────────────────
815
+
816
+ database.run(/*sql*/ `
817
+ CREATE TABLE IF NOT EXISTS tasks (
818
+ id TEXT PRIMARY KEY,
819
+ title TEXT NOT NULL,
820
+ template TEXT NOT NULL,
821
+ input_schema TEXT,
822
+ context_flags TEXT,
823
+ required_tools TEXT,
824
+ created_from_conversation_id TEXT,
825
+ status TEXT NOT NULL DEFAULT 'active',
826
+ created_at INTEGER NOT NULL,
827
+ updated_at INTEGER NOT NULL
828
+ )
829
+ `);
830
+
831
+ database.run(/*sql*/ `
832
+ CREATE TABLE IF NOT EXISTS task_runs (
833
+ id TEXT PRIMARY KEY,
834
+ task_id TEXT NOT NULL REFERENCES tasks(id),
835
+ conversation_id TEXT,
836
+ status TEXT NOT NULL DEFAULT 'pending',
837
+ started_at INTEGER,
838
+ finished_at INTEGER,
839
+ error TEXT,
840
+ principal_id TEXT,
841
+ memory_scope_id TEXT,
842
+ created_at INTEGER NOT NULL
843
+ )
844
+ `);
845
+
846
+ database.run(/*sql*/ `
847
+ CREATE TABLE IF NOT EXISTS task_candidates (
848
+ id TEXT PRIMARY KEY,
849
+ source_conversation_id TEXT NOT NULL,
850
+ compiled_template TEXT NOT NULL,
851
+ confidence REAL,
852
+ required_tools TEXT,
853
+ created_at INTEGER NOT NULL,
854
+ promoted_task_id TEXT
855
+ )
856
+ `);
857
+
858
+ // ── Work Items (Tasks) ──────────────────────────────────────────────
859
+
860
+ database.run(/*sql*/ `
861
+ CREATE TABLE IF NOT EXISTS work_items (
862
+ id TEXT PRIMARY KEY,
863
+ task_id TEXT NOT NULL REFERENCES tasks(id),
864
+ title TEXT NOT NULL,
865
+ notes TEXT,
866
+ status TEXT NOT NULL DEFAULT 'queued',
867
+ priority_tier INTEGER NOT NULL DEFAULT 1,
868
+ sort_index INTEGER,
869
+ last_run_id TEXT,
870
+ last_run_conversation_id TEXT,
871
+ last_run_status TEXT,
872
+ source_type TEXT,
873
+ source_id TEXT,
874
+ created_at INTEGER NOT NULL,
875
+ updated_at INTEGER NOT NULL
876
+ )
877
+ `);
878
+
879
+ // Work item run contract snapshot
880
+ try { database.run(/*sql*/ `ALTER TABLE work_items ADD COLUMN required_tools TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE work_items ADD COLUMN required_tools (likely already exists)'); }
881
+
882
+ // Work item permission preflight columns
883
+ try { database.run(/*sql*/ `ALTER TABLE work_items ADD COLUMN approved_tools TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE work_items ADD COLUMN approved_tools (likely already exists)'); }
884
+ try { database.run(/*sql*/ `ALTER TABLE work_items ADD COLUMN approval_status TEXT DEFAULT 'none'`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE work_items ADD COLUMN approval_status (likely already exists)'); }
885
+
886
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status)`);
887
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_work_items_task_id ON work_items(task_id)`);
888
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_work_items_priority_sort ON work_items(priority_tier, sort_index)`);
889
+
890
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)`);
891
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_task_runs_task_id ON task_runs(task_id)`);
892
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status)`);
893
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_task_candidates_promoted ON task_candidates(promoted_task_id)`);
894
+
895
+ // ── External Conversation Bindings ──────────────────────────────────
896
+
897
+ database.run(/*sql*/ `
898
+ CREATE TABLE IF NOT EXISTS external_conversation_bindings (
899
+ conversation_id TEXT PRIMARY KEY REFERENCES conversations(id) ON DELETE CASCADE,
900
+ source_channel TEXT NOT NULL,
901
+ external_chat_id TEXT NOT NULL,
902
+ external_user_id TEXT,
903
+ display_name TEXT,
904
+ username TEXT,
905
+ created_at INTEGER NOT NULL,
906
+ updated_at INTEGER NOT NULL,
907
+ last_inbound_at INTEGER,
908
+ last_outbound_at INTEGER
909
+ )
910
+ `);
911
+
912
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_ext_conv_bindings_channel_chat ON external_conversation_bindings(source_channel, external_chat_id)`);
913
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_ext_conv_bindings_channel ON external_conversation_bindings(source_channel)`);
914
+
915
+ migrateExtConvBindingsChannelChatUnique(database);
916
+
917
+ // ── Channel Guardian ───────────────────────────────────────────────
918
+
919
+ database.run(/*sql*/ `
920
+ CREATE TABLE IF NOT EXISTS channel_guardian_bindings (
921
+ id TEXT PRIMARY KEY,
922
+ assistant_id TEXT NOT NULL,
923
+ channel TEXT NOT NULL,
924
+ guardian_external_user_id TEXT NOT NULL,
925
+ guardian_delivery_chat_id TEXT NOT NULL,
926
+ status TEXT NOT NULL DEFAULT 'active',
927
+ verified_at INTEGER NOT NULL,
928
+ verified_via TEXT NOT NULL DEFAULT 'challenge',
929
+ metadata_json TEXT,
930
+ created_at INTEGER NOT NULL,
931
+ updated_at INTEGER NOT NULL
932
+ )
933
+ `);
934
+
935
+ database.run(/*sql*/ `
936
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_guardian_bindings_active
937
+ ON channel_guardian_bindings(assistant_id, channel)
938
+ WHERE status = 'active'
939
+ `);
940
+
941
+ database.run(/*sql*/ `
942
+ CREATE TABLE IF NOT EXISTS channel_guardian_verification_challenges (
943
+ id TEXT PRIMARY KEY,
944
+ assistant_id TEXT NOT NULL,
945
+ channel TEXT NOT NULL,
946
+ challenge_hash TEXT NOT NULL,
947
+ expires_at INTEGER NOT NULL,
948
+ status TEXT NOT NULL DEFAULT 'pending',
949
+ created_by_session_id TEXT,
950
+ consumed_by_external_user_id TEXT,
951
+ consumed_by_chat_id TEXT,
952
+ created_at INTEGER NOT NULL,
953
+ updated_at INTEGER NOT NULL
954
+ )
955
+ `);
956
+
957
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_guardian_challenges_lookup ON channel_guardian_verification_challenges(assistant_id, channel, challenge_hash, status)`);
958
+
959
+ database.run(/*sql*/ `
960
+ CREATE TABLE IF NOT EXISTS channel_guardian_approval_requests (
961
+ id TEXT PRIMARY KEY,
962
+ run_id TEXT NOT NULL,
963
+ conversation_id TEXT NOT NULL,
964
+ channel TEXT NOT NULL,
965
+ requester_external_user_id TEXT NOT NULL,
966
+ requester_chat_id TEXT NOT NULL,
967
+ guardian_external_user_id TEXT NOT NULL,
968
+ guardian_chat_id TEXT NOT NULL,
969
+ tool_name TEXT NOT NULL,
970
+ risk_level TEXT,
971
+ reason TEXT,
972
+ status TEXT NOT NULL DEFAULT 'pending',
973
+ decided_by_external_user_id TEXT,
974
+ expires_at INTEGER NOT NULL,
975
+ created_at INTEGER NOT NULL,
976
+ updated_at INTEGER NOT NULL
977
+ )
978
+ `);
979
+
980
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_guardian_approval_run ON channel_guardian_approval_requests(run_id, status)`);
981
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_guardian_approval_status ON channel_guardian_approval_requests(status)`);
982
+
983
+ // Migration: add assistant_id column to scope approval requests by assistant.
984
+ // Existing rows default to 'self' for backward compatibility.
985
+ try { database.run(/*sql*/ `ALTER TABLE channel_guardian_approval_requests ADD COLUMN assistant_id TEXT NOT NULL DEFAULT 'self'`); } catch { /* already exists */ }
986
+
987
+ // ── Channel Guardian Verification Rate Limits ─────────────────────
988
+
989
+ database.run(/*sql*/ `
990
+ CREATE TABLE IF NOT EXISTS channel_guardian_rate_limits (
991
+ id TEXT PRIMARY KEY,
992
+ assistant_id TEXT NOT NULL,
993
+ channel TEXT NOT NULL,
994
+ actor_external_user_id TEXT NOT NULL,
995
+ actor_chat_id TEXT NOT NULL,
996
+ invalid_attempts INTEGER NOT NULL DEFAULT 0,
997
+ window_started_at INTEGER NOT NULL DEFAULT 0,
998
+ attempt_timestamps_json TEXT NOT NULL DEFAULT '[]',
999
+ locked_until INTEGER,
1000
+ created_at INTEGER NOT NULL,
1001
+ updated_at INTEGER NOT NULL
1002
+ )
1003
+ `);
1004
+
1005
+ // Migration: add attempt_timestamps_json column for true sliding-window rate limiting.
1006
+ // The old invalid_attempts / window_started_at columns are left in place (SQLite
1007
+ // doesn't support DROP COLUMN in older versions) but are no longer read by the app.
1008
+ try { database.run(/*sql*/ `ALTER TABLE channel_guardian_rate_limits ADD COLUMN attempt_timestamps_json TEXT NOT NULL DEFAULT '[]'`); } catch { /* already exists */ }
1009
+
1010
+ // Migration: re-add legacy columns for databases created during the brief window when
1011
+ // PR #6748 was live (columns were absent from CREATE TABLE). These columns are not read
1012
+ // by app logic but must exist so drizzle inserts don't fail.
1013
+ try { database.run(/*sql*/ `ALTER TABLE channel_guardian_rate_limits ADD COLUMN invalid_attempts INTEGER NOT NULL DEFAULT 0`); } catch { /* already exists */ }
1014
+ try { database.run(/*sql*/ `ALTER TABLE channel_guardian_rate_limits ADD COLUMN window_started_at INTEGER NOT NULL DEFAULT 0`); } catch { /* already exists */ }
1015
+
1016
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_guardian_rate_limits_actor ON channel_guardian_rate_limits(assistant_id, channel, actor_external_user_id, actor_chat_id)`);
1017
+
1018
+ // ── Media Assets ───────────────────────────────────────────────────
1019
+
1020
+ database.run(/*sql*/ `
1021
+ CREATE TABLE IF NOT EXISTS media_assets (
1022
+ id TEXT PRIMARY KEY,
1023
+ title TEXT NOT NULL,
1024
+ file_path TEXT NOT NULL,
1025
+ mime_type TEXT NOT NULL,
1026
+ duration_seconds REAL,
1027
+ file_hash TEXT NOT NULL,
1028
+ status TEXT NOT NULL DEFAULT 'registered',
1029
+ media_type TEXT NOT NULL,
1030
+ metadata TEXT,
1031
+ created_at INTEGER NOT NULL,
1032
+ updated_at INTEGER NOT NULL
1033
+ )
1034
+ `);
1035
+
1036
+ // Drop the old non-unique index so it can be recreated as UNIQUE (migration for existing databases)
1037
+ database.run(/*sql*/ `DROP INDEX IF EXISTS idx_media_assets_file_hash`);
1038
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_media_assets_file_hash ON media_assets(file_hash)`);
1039
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_assets_status ON media_assets(status)`);
1040
+
1041
+ database.run(/*sql*/ `
1042
+ CREATE TABLE IF NOT EXISTS processing_stages (
1043
+ id TEXT PRIMARY KEY,
1044
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1045
+ stage TEXT NOT NULL,
1046
+ status TEXT NOT NULL DEFAULT 'pending',
1047
+ progress INTEGER NOT NULL DEFAULT 0,
1048
+ last_error TEXT,
1049
+ started_at INTEGER,
1050
+ completed_at INTEGER
1051
+ )
1052
+ `);
1053
+
1054
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_processing_stages_asset_id ON processing_stages(asset_id)`);
1055
+
1056
+ // ── Media Keyframes ─────────────────────────────────────────────────
1057
+
1058
+ database.run(/*sql*/ `
1059
+ CREATE TABLE IF NOT EXISTS media_keyframes (
1060
+ id TEXT PRIMARY KEY,
1061
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1062
+ timestamp REAL NOT NULL,
1063
+ file_path TEXT NOT NULL,
1064
+ metadata TEXT,
1065
+ created_at INTEGER NOT NULL
1066
+ )
1067
+ `);
1068
+
1069
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_keyframes_asset_id ON media_keyframes(asset_id)`);
1070
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_keyframes_asset_timestamp ON media_keyframes(asset_id, timestamp)`);
1071
+
1072
+ // ── Media Vision Outputs ────────────────────────────────────────────
1073
+
1074
+ database.run(/*sql*/ `
1075
+ CREATE TABLE IF NOT EXISTS media_vision_outputs (
1076
+ id TEXT PRIMARY KEY,
1077
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1078
+ keyframe_id TEXT NOT NULL REFERENCES media_keyframes(id) ON DELETE CASCADE,
1079
+ analysis_type TEXT NOT NULL,
1080
+ output TEXT NOT NULL,
1081
+ confidence REAL,
1082
+ created_at INTEGER NOT NULL
1083
+ )
1084
+ `);
1085
+
1086
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_vision_outputs_asset_id ON media_vision_outputs(asset_id)`);
1087
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_vision_outputs_keyframe_id ON media_vision_outputs(keyframe_id)`);
1088
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_vision_outputs_asset_type ON media_vision_outputs(asset_id, analysis_type)`);
1089
+
1090
+ // ── Media Timelines ─────────────────────────────────────────────────
1091
+
1092
+ database.run(/*sql*/ `
1093
+ CREATE TABLE IF NOT EXISTS media_timelines (
1094
+ id TEXT PRIMARY KEY,
1095
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1096
+ start_time REAL NOT NULL,
1097
+ end_time REAL NOT NULL,
1098
+ segment_type TEXT NOT NULL,
1099
+ attributes TEXT,
1100
+ confidence REAL,
1101
+ created_at INTEGER NOT NULL
1102
+ )
1103
+ `);
1104
+
1105
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_timelines_asset_id ON media_timelines(asset_id)`);
1106
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_timelines_asset_time ON media_timelines(asset_id, start_time)`);
1107
+
1108
+ // ── Media Events ──────────────────────────────────────────────────
1109
+
1110
+ database.run(/*sql*/ `
1111
+ CREATE TABLE IF NOT EXISTS media_events (
1112
+ id TEXT PRIMARY KEY,
1113
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1114
+ event_type TEXT NOT NULL,
1115
+ start_time REAL NOT NULL,
1116
+ end_time REAL NOT NULL,
1117
+ confidence REAL NOT NULL,
1118
+ reasons TEXT NOT NULL,
1119
+ metadata TEXT,
1120
+ created_at INTEGER NOT NULL
1121
+ )
1122
+ `);
1123
+
1124
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_events_asset_id ON media_events(asset_id)`);
1125
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_events_asset_type ON media_events(asset_id, event_type)`);
1126
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_events_confidence ON media_events(confidence DESC)`);
1127
+
1128
+ // ── Media Tracking Profiles ─────────────────────────────────────────
1129
+
1130
+ database.run(/*sql*/ `
1131
+ CREATE TABLE IF NOT EXISTS media_tracking_profiles (
1132
+ id TEXT PRIMARY KEY,
1133
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1134
+ capabilities TEXT NOT NULL,
1135
+ created_at INTEGER NOT NULL
1136
+ )
1137
+ `);
1138
+
1139
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_tracking_profiles_asset_id ON media_tracking_profiles(asset_id)`);
1140
+
1141
+ // ── Media Event Feedback ──────────────────────────────────────────
1142
+
1143
+ database.run(/*sql*/ `
1144
+ CREATE TABLE IF NOT EXISTS media_event_feedback (
1145
+ id TEXT PRIMARY KEY,
1146
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1147
+ event_id TEXT NOT NULL REFERENCES media_events(id) ON DELETE CASCADE,
1148
+ feedback_type TEXT NOT NULL,
1149
+ original_start_time REAL,
1150
+ original_end_time REAL,
1151
+ corrected_start_time REAL,
1152
+ corrected_end_time REAL,
1153
+ notes TEXT,
1154
+ created_at INTEGER NOT NULL
1155
+ )
1156
+ `);
1157
+
1158
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_event_feedback_asset_id ON media_event_feedback(asset_id)`);
1159
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_event_feedback_event_id ON media_event_feedback(event_id)`);
1160
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_event_feedback_type ON media_event_feedback(asset_id, feedback_type)`);
1161
+
1162
+ migrateMemoryFtsBackfill(database);
1163
+ }