@vellumai/assistant 0.5.6 → 0.5.7

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 (305) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +1 -1
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +0 -114
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +1 -1
  26. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  27. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  28. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  29. package/src/__tests__/conversation-title-service.test.ts +87 -0
  30. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  31. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  32. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  33. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  34. package/src/__tests__/credentials-cli.test.ts +78 -0
  35. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  36. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  38. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  39. package/src/__tests__/host-shell-tool.test.ts +6 -7
  40. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  42. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  43. package/src/__tests__/intent-routing.test.ts +0 -13
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  45. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  46. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  47. package/src/__tests__/migration-export-http.test.ts +2 -2
  48. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  49. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  50. package/src/__tests__/migration-validate-http.test.ts +2 -2
  51. package/src/__tests__/non-member-access-request.test.ts +0 -5
  52. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  53. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  54. package/src/__tests__/permission-types.test.ts +1 -0
  55. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  56. package/src/__tests__/qdrant-manager.test.ts +28 -2
  57. package/src/__tests__/registry.test.ts +0 -6
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  60. package/src/__tests__/secure-keys.test.ts +83 -263
  61. package/src/__tests__/shell-identity.test.ts +96 -6
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  63. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  64. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  65. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  66. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  67. package/src/__tests__/skill-load-tool.test.ts +0 -2
  68. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  69. package/src/__tests__/skills.test.ts +0 -2
  70. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  71. package/src/__tests__/suggestion-routes.test.ts +1 -32
  72. package/src/__tests__/system-prompt.test.ts +0 -1
  73. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  74. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  75. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  76. package/src/__tests__/update-bulletin.test.ts +0 -2
  77. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  78. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  79. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  80. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  81. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  82. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  83. package/src/calls/audio-store.test.ts +97 -0
  84. package/src/calls/audio-store.ts +205 -0
  85. package/src/calls/call-controller.ts +85 -7
  86. package/src/calls/call-domain.ts +3 -0
  87. package/src/calls/call-store.ts +10 -3
  88. package/src/calls/fish-audio-client.ts +117 -0
  89. package/src/calls/relay-server.ts +27 -0
  90. package/src/calls/twilio-routes.ts +2 -1
  91. package/src/calls/types.ts +1 -0
  92. package/src/calls/voice-ingress-preflight.ts +0 -42
  93. package/src/calls/voice-quality.ts +26 -5
  94. package/src/calls/voice-session-bridge.ts +6 -12
  95. package/src/cli/commands/config.ts +1 -4
  96. package/src/cli/commands/credentials.ts +34 -4
  97. package/src/cli/commands/oauth/index.ts +7 -0
  98. package/src/cli/commands/oauth/platform.ts +179 -0
  99. package/src/cli/commands/platform.ts +3 -3
  100. package/src/config/assistant-feature-flags.ts +186 -5
  101. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  102. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  103. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  104. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  105. package/src/config/bundled-tool-registry.ts +1 -11
  106. package/src/config/env-registry.ts +1 -1
  107. package/src/config/env.ts +8 -14
  108. package/src/config/feature-flag-registry.json +48 -8
  109. package/src/config/loader.ts +98 -31
  110. package/src/config/schema.ts +4 -13
  111. package/src/config/schemas/calls.ts +13 -0
  112. package/src/config/schemas/fish-audio.ts +39 -0
  113. package/src/config/schemas/security.ts +0 -4
  114. package/src/config/types.ts +0 -1
  115. package/src/contacts/contact-store.ts +39 -0
  116. package/src/contacts/types.ts +2 -0
  117. package/src/credential-execution/approval-bridge.ts +1 -0
  118. package/src/credential-execution/executable-discovery.ts +28 -4
  119. package/src/credential-execution/feature-gates.ts +16 -0
  120. package/src/credential-execution/process-manager.ts +38 -0
  121. package/src/daemon/assistant-attachments.ts +9 -0
  122. package/src/daemon/config-watcher.ts +5 -0
  123. package/src/daemon/conversation-tool-setup.ts +0 -105
  124. package/src/daemon/conversation.ts +10 -1
  125. package/src/daemon/handlers/config-vercel.ts +92 -0
  126. package/src/daemon/handlers/skills.ts +2 -15
  127. package/src/daemon/install-symlink.ts +195 -0
  128. package/src/daemon/lifecycle.ts +227 -51
  129. package/src/daemon/message-types/conversations.ts +3 -4
  130. package/src/daemon/message-types/diagnostics.ts +3 -22
  131. package/src/daemon/message-types/messages.ts +0 -2
  132. package/src/daemon/message-types/upgrades.ts +8 -0
  133. package/src/daemon/server.ts +30 -92
  134. package/src/events/domain-events.ts +2 -1
  135. package/src/inbound/platform-callback-registration.ts +3 -3
  136. package/src/instrument.ts +8 -5
  137. package/src/memory/conversation-title-service.ts +50 -1
  138. package/src/memory/db-init.ts +12 -0
  139. package/src/memory/items-extractor.ts +15 -1
  140. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  141. package/src/memory/jobs-store.ts +30 -5
  142. package/src/memory/jobs-worker.ts +31 -7
  143. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  144. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  145. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  146. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  147. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  148. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  149. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  150. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  151. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  152. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  153. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  154. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  155. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  156. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  157. package/src/memory/migrations/116-messages-fts.ts +106 -1
  158. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  160. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  161. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  162. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  163. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  164. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  165. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  166. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  167. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  168. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  169. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  170. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  171. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  172. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  173. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  174. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  175. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  176. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  177. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  178. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  179. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  180. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  181. package/src/memory/migrations/index.ts +4 -0
  182. package/src/memory/migrations/registry.ts +90 -0
  183. package/src/memory/migrations/validate-migration-state.ts +137 -11
  184. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  185. package/src/memory/qdrant-manager.ts +64 -7
  186. package/src/memory/schema/calls.ts +1 -0
  187. package/src/memory/schema/contacts.ts +1 -0
  188. package/src/notifications/decision-engine.ts +4 -1
  189. package/src/oauth/connection-resolver.ts +6 -4
  190. package/src/permissions/checker.ts +0 -38
  191. package/src/permissions/shell-identity.ts +76 -22
  192. package/src/permissions/types.ts +4 -2
  193. package/src/platform/client.ts +35 -7
  194. package/src/prompts/persona-resolver.ts +138 -0
  195. package/src/prompts/system-prompt.ts +36 -4
  196. package/src/prompts/templates/users/default.md +1 -0
  197. package/src/providers/registry.ts +27 -40
  198. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  199. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  200. package/src/runtime/auth/external-assistant-id.ts +13 -59
  201. package/src/runtime/auth/route-policy.ts +15 -1
  202. package/src/runtime/auth/token-service.ts +43 -138
  203. package/src/runtime/channel-readiness-service.ts +1 -16
  204. package/src/runtime/http-server.ts +27 -2
  205. package/src/runtime/middleware/error-handler.ts +1 -9
  206. package/src/runtime/routes/audio-routes.ts +40 -0
  207. package/src/runtime/routes/btw-routes.ts +0 -17
  208. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  209. package/src/runtime/routes/conversation-routes.ts +4 -44
  210. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  211. package/src/runtime/routes/identity-routes.ts +18 -29
  212. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  213. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  214. package/src/runtime/routes/integrations/vercel.ts +89 -0
  215. package/src/runtime/routes/log-export-routes.ts +5 -0
  216. package/src/runtime/routes/memory-item-routes.ts +24 -6
  217. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  218. package/src/runtime/routes/migration-routes.ts +17 -1
  219. package/src/runtime/routes/notification-routes.ts +58 -0
  220. package/src/runtime/routes/schedule-routes.ts +65 -0
  221. package/src/runtime/routes/settings-routes.ts +41 -1
  222. package/src/runtime/routes/tts-routes.ts +86 -0
  223. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  224. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  225. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  226. package/src/runtime/routes/workspace-routes.ts +1 -1
  227. package/src/runtime/routes/workspace-utils.ts +86 -2
  228. package/src/security/ces-credential-client.ts +59 -22
  229. package/src/security/ces-rpc-credential-backend.ts +85 -0
  230. package/src/security/credential-backend.ts +12 -88
  231. package/src/security/keychain-broker-client.ts +10 -2
  232. package/src/security/secure-keys.ts +94 -113
  233. package/src/skills/catalog-install.ts +13 -7
  234. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  235. package/src/tools/calls/call-start.ts +1 -0
  236. package/src/tools/executor.ts +0 -4
  237. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  238. package/src/tools/network/web-fetch.ts +3 -1
  239. package/src/tools/skills/execute.ts +1 -1
  240. package/src/tools/types.ts +0 -8
  241. package/src/util/errors.ts +0 -12
  242. package/src/util/platform.ts +3 -50
  243. package/src/workspace/git-service.ts +5 -2
  244. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  245. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  246. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  247. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  248. package/src/workspace/migrations/006-services-config.ts +49 -0
  249. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  250. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  251. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  252. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  253. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  254. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  255. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  256. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  257. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  258. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  259. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  260. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  261. package/src/workspace/migrations/registry.ts +8 -0
  262. package/src/workspace/migrations/runner.ts +106 -2
  263. package/src/workspace/migrations/types.ts +4 -0
  264. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  265. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  266. package/src/__tests__/diagnostics-export.test.ts +0 -288
  267. package/src/__tests__/local-gateway-health.test.ts +0 -209
  268. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  269. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  270. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  271. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  272. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  273. package/src/__tests__/swarm-recursion.test.ts +0 -197
  274. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  275. package/src/__tests__/swarm-tool.test.ts +0 -185
  276. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  277. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  278. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  279. package/src/commands/cc-command-registry.ts +0 -248
  280. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  281. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  282. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  283. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  284. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  285. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  286. package/src/config/schemas/swarm.ts +0 -82
  287. package/src/logfire.ts +0 -135
  288. package/src/runtime/local-gateway-health.ts +0 -275
  289. package/src/security/secret-ingress.ts +0 -68
  290. package/src/swarm/backend-claude-code.ts +0 -225
  291. package/src/swarm/checkpoint.ts +0 -137
  292. package/src/swarm/graph-utils.ts +0 -53
  293. package/src/swarm/index.ts +0 -55
  294. package/src/swarm/limits.ts +0 -66
  295. package/src/swarm/orchestrator.ts +0 -424
  296. package/src/swarm/plan-validator.ts +0 -117
  297. package/src/swarm/router-planner.ts +0 -162
  298. package/src/swarm/router-prompts.ts +0 -39
  299. package/src/swarm/synthesizer.ts +0 -81
  300. package/src/swarm/types.ts +0 -72
  301. package/src/swarm/worker-backend.ts +0 -131
  302. package/src/swarm/worker-prompts.ts +0 -80
  303. package/src/swarm/worker-runner.ts +0 -170
  304. package/src/tools/claude-code/claude-code.ts +0 -610
  305. package/src/tools/swarm/delegate.ts +0 -205
@@ -25,7 +25,8 @@ export interface ToolDomainEvents {
25
25
  | "always_allow_high_risk"
26
26
  | "deny"
27
27
  | "always_deny"
28
- | "temporary_override";
28
+ | "temporary_override"
29
+ | "dangerously_skip_permissions";
29
30
  riskLevel: string;
30
31
  decidedAtMs: number;
31
32
  };
@@ -2,7 +2,7 @@
2
2
  * Platform callback route registration for containerized deployments.
3
3
  *
4
4
  * When the assistant daemon runs inside a container (IS_CONTAINERIZED=true)
5
- * with a configured PLATFORM_BASE_URL and PLATFORM_ASSISTANT_ID, external
5
+ * with a configured VELLUM_PLATFORM_URL and PLATFORM_ASSISTANT_ID, external
6
6
  * service callbacks (Twilio webhooks, OAuth redirects, Telegram webhooks, etc.)
7
7
  * must route through the platform's gateway proxy instead of hitting the
8
8
  * assistant directly.
@@ -12,7 +12,7 @@
12
12
  * webhooks to the correct containerized assistant instance.
13
13
  *
14
14
  * The platform endpoint is:
15
- * POST {PLATFORM_BASE_URL}/v1/internal/gateway/callback-routes/register/
15
+ * POST {VELLUM_PLATFORM_URL}/v1/internal/gateway/callback-routes/register/
16
16
  *
17
17
  * It accepts { assistant_id, callback_path, type } and returns a stable
18
18
  * callback_url that external services should use.
@@ -30,7 +30,7 @@ const log = getLogger("platform-callback-registration");
30
30
 
31
31
  /**
32
32
  * Whether the daemon should register callback routes with the platform.
33
- * True when IS_CONTAINERIZED, PLATFORM_BASE_URL, and PLATFORM_ASSISTANT_ID
33
+ * True when IS_CONTAINERIZED, VELLUM_PLATFORM_URL, and PLATFORM_ASSISTANT_ID
34
34
  * are all set. Intentionally does **not** require the managed proxy API key
35
35
  * so that callback-only flows (OAuth transport, Telegram/Twilio callback
36
36
  * registration) work during partial bootstrap before the key is injected.
package/src/instrument.ts CHANGED
@@ -39,14 +39,17 @@ function redactObject(obj: unknown): unknown {
39
39
  }
40
40
 
41
41
  /**
42
- * Call after dotenv has loaded so SENTRY_DSN is available.
43
- * Always initializes Sentry to capture early startup crashes. If the user
44
- * later opts out via the sendDiagnostics config key (or VELLUM_DEV=1),
45
- * call closeSentry() after config is loaded to stop future event capturing.
42
+ * Call after dotenv has loaded so SENTRY_DSN_ASSISTANT is available.
43
+ * Initializes Sentry when the DSN is set; no-ops when empty/unset so
44
+ * local dev builds don't send crash reports. If the user later opts out
45
+ * via the sendDiagnostics config key (or VELLUM_DEV=1), call closeSentry()
46
+ * after config is loaded to stop future event capturing.
46
47
  */
47
48
  export function initSentry(): void {
49
+ const dsn = getSentryDsn();
50
+ if (!dsn) return;
48
51
  Sentry.init({
49
- dsn: getSentryDsn(),
52
+ dsn,
50
53
  release: `vellum-assistant@${APP_VERSION}`,
51
54
  dist: COMMIT_SHA,
52
55
  environment: APP_VERSION === "0.0.0-dev" ? "development" : "production",
@@ -358,12 +358,61 @@ function deriveFallbackTitle(context?: TitleContext): string | null {
358
358
  return null;
359
359
  }
360
360
 
361
+ /**
362
+ * Extract only human-authored text from stored message content for title
363
+ * generation. Unlike extractTextFromStoredMessageContent (which includes
364
+ * tool metadata like "Tool use (...): {...}"), this only extracts:
365
+ * - `text` blocks (the actual conversation content)
366
+ * - `tool_result` string content (topical signal from tool responses)
367
+ * — web_search_tool_result is skipped (structured search data, not topical)
368
+ *
369
+ * Returns empty string for content-block arrays with no extractable text,
370
+ * preventing raw JSON from polluting the title prompt.
371
+ */
372
+ function extractTextForTitle(raw: string): string {
373
+ try {
374
+ const parsed = JSON.parse(raw);
375
+ if (typeof parsed === "string") return parsed;
376
+ if (!Array.isArray(parsed)) return raw;
377
+ const texts: string[] = [];
378
+ for (const block of parsed) {
379
+ if (!block || typeof block !== "object") continue;
380
+ if (block.type === "text" && typeof block.text === "string") {
381
+ texts.push(block.text);
382
+ // guard:allow-tool-result-only — web_search_tool_result has structured
383
+ // search result arrays, not useful for title generation; only plain
384
+ // tool_result string content carries topical signal.
385
+ } else if (block.type === "tool_result") {
386
+ if (typeof block.content === "string") {
387
+ texts.push(block.content);
388
+ } else if (Array.isArray(block.content)) {
389
+ for (const nested of block.content) {
390
+ if (
391
+ nested &&
392
+ typeof nested === "object" &&
393
+ nested.type === "text" &&
394
+ typeof nested.text === "string"
395
+ ) {
396
+ texts.push(nested.text);
397
+ }
398
+ }
399
+ }
400
+ }
401
+ }
402
+ return texts.join("\n");
403
+ } catch {
404
+ return raw;
405
+ }
406
+ }
407
+
361
408
  function buildRegenerationPrompt(recentMessages: MessageRow[]): string {
362
409
  const parts: string[] = ["Recent messages:"];
363
410
 
364
411
  for (const msg of recentMessages) {
412
+ const text = extractTextForTitle(msg.content);
413
+ if (!text) continue;
365
414
  const role = msg.role === "user" ? "User" : "Assistant";
366
- parts.push(`${role}: ${truncate(msg.content, 200, "")}`);
415
+ parts.push(`${role}: ${truncate(text, 200, "")}`);
367
416
  }
368
417
 
369
418
  return parts.join("\n");
@@ -35,12 +35,14 @@ import {
35
35
  createTasksAndWorkItemsTables,
36
36
  createWatchersAndLogsTables,
37
37
  migrateAssistantContactMetadata,
38
+ migrateBackfillAudioAttachmentMimeTypes,
38
39
  migrateBackfillContactInteractionStats,
39
40
  migrateBackfillGuardianPrincipalId,
40
41
  migrateBackfillInlineAttachmentsToDisk,
41
42
  migrateBackfillUsageCacheAccounting,
42
43
  migrateCallSessionInviteMetadata,
43
44
  migrateCallSessionMode,
45
+ migrateCallSessionSkipDisclosure,
44
46
  migrateCanonicalGuardianDeliveriesDestinationIndex,
45
47
  migrateCanonicalGuardianRequesterChatId,
46
48
  migrateCapabilityCardColumns,
@@ -51,6 +53,7 @@ import {
51
53
  migrateContactsAssistantId,
52
54
  migrateContactsNotesColumn,
53
55
  migrateContactsRolePrincipal,
56
+ migrateContactsUserFileColumn,
54
57
  migrateConversationForkLineage,
55
58
  migrateConversationsThreadTypeIndex,
56
59
  migrateCreateThreadStartersTable,
@@ -492,6 +495,15 @@ export function initializeDb(): void {
492
495
  // 86. Drop simplified-memory tables and reducer checkpoint columns
493
496
  migrateDropSimplifiedMemory(database);
494
497
 
498
+ // 87. Add skip_disclosure column to call_sessions for per-call disclosure control
499
+ migrateCallSessionSkipDisclosure(database);
500
+
501
+ // 88. Backfill correct MIME types for audio attachments stored as application/octet-stream
502
+ migrateBackfillAudioAttachmentMimeTypes(database);
503
+
504
+ // 89. Add user_file column to contacts for per-user persona file mapping
505
+ migrateContactsUserFileColumn(database);
506
+
495
507
  validateMigrationState(database);
496
508
 
497
509
  if (process.env.BUN_TEST === "1") {
@@ -3,6 +3,7 @@ import { v4 as uuid } from "uuid";
3
3
 
4
4
  import { getConfig } from "../config/loader.js";
5
5
  import type { MemoryExtractionConfig } from "../config/types.js";
6
+ import { resolveGuardianPersona } from "../prompts/persona-resolver.js";
6
7
  import { buildCoreIdentityContext } from "../prompts/system-prompt.js";
7
8
  import {
8
9
  createTimeout,
@@ -150,6 +151,7 @@ function buildExtractionSystemPrompt(
150
151
  statement: string;
151
152
  }>,
152
153
  messageRole: string,
154
+ userPersona?: string | null,
153
155
  ): string {
154
156
  // Build the fixed instruction body first so we can measure it and allocate
155
157
  // the remaining budget to identity context.
@@ -206,7 +208,9 @@ IMPORTANT: The message below is from the ASSISTANT, not the user. Do NOT attribu
206
208
  // ceiling, preventing oversized prompts from exceeding the provider input
207
209
  // window (which would cause sendMessage to error and fall back to
208
210
  // lower-quality pattern-based extraction).
209
- const rawIdentityContext = buildCoreIdentityContext();
211
+ const rawIdentityContext = buildCoreIdentityContext(
212
+ userPersona ? { userPersona } : undefined,
213
+ );
210
214
 
211
215
  let prompt = "";
212
216
  if (rawIdentityContext) {
@@ -316,6 +320,7 @@ async function extractItemsWithLLM(
316
320
  extractionConfig: MemoryExtractionConfig,
317
321
  scopeId: string,
318
322
  messageRole: string,
323
+ userPersona?: string | null,
319
324
  ): Promise<ExtractedItem[]> {
320
325
  const provider = await getConfiguredProvider();
321
326
  if (!provider) {
@@ -334,6 +339,7 @@ async function extractItemsWithLLM(
334
339
  const systemPrompt = buildExtractionSystemPrompt(
335
340
  existingItems,
336
341
  messageRole,
342
+ userPersona,
337
343
  );
338
344
 
339
345
  const messagePrefix =
@@ -532,12 +538,20 @@ export async function extractAndUpsertMemoryItemsForMessage(
532
538
  const config = getConfig();
533
539
  const extractionConfig = config.memory.extraction;
534
540
  const effectiveScopeId = scopeId ?? "default";
541
+
542
+ // Resolve the guardian's persona to provide personality-aware extraction
543
+ // context. Currently uses the guardian persona for all conversations —
544
+ // non-guardian conversations are rare and the guardian's persona provides
545
+ // better extraction context than none.
546
+ const userPersona = resolveGuardianPersona();
547
+
535
548
  const extracted = extractionConfig.useLLM
536
549
  ? await extractItemsWithLLM(
537
550
  text,
538
551
  extractionConfig,
539
552
  effectiveScopeId,
540
553
  message.role,
554
+ userPersona,
541
555
  )
542
556
  : extractItemsPatternBased(text, effectiveScopeId);
543
557
 
@@ -9,6 +9,7 @@ import { and, desc, eq } from "drizzle-orm";
9
9
  import { v4 as uuid } from "uuid";
10
10
 
11
11
  import { loadSkillCatalog } from "../../config/skills.js";
12
+ import { resolveGuardianPersona } from "../../prompts/persona-resolver.js";
12
13
  import { buildCoreIdentityContext } from "../../prompts/system-prompt.js";
13
14
  import {
14
15
  createTimeout,
@@ -171,7 +172,9 @@ async function generateStarters(scopeId: string): Promise<GeneratedStarter[]> {
171
172
 
172
173
  // Truncate identity context to prevent oversized prompts when SOUL.md /
173
174
  // IDENTITY.md / USER.md are large.
174
- const rawIdentityContext = buildCoreIdentityContext();
175
+ const rawIdentityContext = buildCoreIdentityContext({
176
+ userPersona: resolveGuardianPersona(),
177
+ });
175
178
  const identityContext = rawIdentityContext
176
179
  ? truncate(rawIdentityContext, 2000, "\n…[truncated]")
177
180
  : null;
@@ -4,6 +4,10 @@ import { v4 as uuid } from "uuid";
4
4
  import { getLogger } from "../util/logger.js";
5
5
  import { truncate } from "../util/truncate.js";
6
6
  import { getDb, rawAll, rawChanges } from "./db.js";
7
+ import {
8
+ isQdrantBreakerOpen,
9
+ shouldAllowQdrantProbe,
10
+ } from "./qdrant-circuit-breaker.js";
7
11
  import { memoryJobs } from "./schema.js";
8
12
 
9
13
  const log = getLogger("memory-jobs-store");
@@ -201,14 +205,33 @@ export function claimMemoryJobs(limit: number): MemoryJob[] {
201
205
  .all();
202
206
 
203
207
  const remainingSlots = limit - nonEmbedCandidates.length;
208
+
209
+ // When the Qdrant circuit breaker is open, skip embed jobs entirely —
210
+ // they would just be claimed → fail → deferred, wasting CPU cycles.
211
+ // Exception: if the cooldown has elapsed (breaker ready for half-open probe),
212
+ // allow exactly 1 embed job through so the breaker can self-heal.
213
+ const breakerOpen = isQdrantBreakerOpen();
214
+ const probeAllowed = breakerOpen && shouldAllowQdrantProbe();
215
+ const skipEmbedJobs = breakerOpen && !probeAllowed;
216
+ const embedLimit = probeAllowed ? 1 : remainingSlots;
217
+
218
+ if (skipEmbedJobs && remainingSlots > 0) {
219
+ log.debug("Skipping embed job claims — Qdrant circuit breaker is open");
220
+ }
221
+ if (probeAllowed && remainingSlots > 0) {
222
+ log.debug(
223
+ "Allowing 1 embed probe job — Qdrant circuit breaker cooldown elapsed",
224
+ );
225
+ }
226
+
204
227
  const embedCandidates =
205
- remainingSlots > 0
228
+ remainingSlots > 0 && !skipEmbedJobs
206
229
  ? db
207
230
  .select()
208
231
  .from(memoryJobs)
209
232
  .where(and(pendingFilter, inArray(memoryJobs.type, EMBED_JOB_TYPES)))
210
233
  .orderBy(asc(memoryJobs.runAfter), asc(memoryJobs.createdAt))
211
- .limit(remainingSlots)
234
+ .limit(embedLimit)
212
235
  .all()
213
236
  : [];
214
237
 
@@ -243,8 +266,8 @@ export function completeMemoryJob(id: string): void {
243
266
 
244
267
  /** Max times a job can be deferred before it is marked as failed. */
245
268
  const MAX_DEFERRALS = 50;
246
- /** Warn when deferrals reach 80% of the limit. */
247
- const DEFER_WARNING_THRESHOLD = Math.floor(MAX_DEFERRALS * 0.8);
269
+ /** Log warnings at these milestone counts to avoid flooding logs. */
270
+ const DEFERRAL_WARN_MILESTONES = [40, 45];
248
271
  /** Base delay in ms for deferred jobs (grows with exponential backoff). */
249
272
  const DEFER_BASE_DELAY_MS = 30_000;
250
273
  /** Maximum delay cap for deferred jobs (5 minutes). */
@@ -286,7 +309,9 @@ export function deferMemoryJob(id: string): "deferred" | "failed" {
286
309
  return "failed";
287
310
  }
288
311
 
289
- if (deferrals >= DEFER_WARNING_THRESHOLD) {
312
+ // Log at milestones only (40, 45) to avoid flooding logs.
313
+ // At 50, the job fails via the check above, so 40 and 45 are the warnings.
314
+ if (DEFERRAL_WARN_MILESTONES.includes(deferrals)) {
290
315
  log.warn(
291
316
  { jobId: id, type: row.type, deferrals, max: MAX_DEFERRALS },
292
317
  "Job approaching max deferral limit",
@@ -44,6 +44,9 @@ import { QdrantCircuitOpenError } from "./qdrant-circuit-breaker.js";
44
44
 
45
45
  const log = getLogger("memory-jobs-worker");
46
46
 
47
+ export const POLL_INTERVAL_MIN_MS = 1_500;
48
+ export const POLL_INTERVAL_MAX_MS = 30_000;
49
+
47
50
  export interface MemoryJobsWorker {
48
51
  runOnce(): Promise<number>;
49
52
  stop(): void;
@@ -57,24 +60,45 @@ export function startMemoryJobsWorker(): MemoryJobsWorker {
57
60
 
58
61
  let stopped = false;
59
62
  let tickRunning = false;
63
+ let timer: ReturnType<typeof setTimeout>;
64
+ let currentIntervalMs = POLL_INTERVAL_MIN_MS;
60
65
 
61
66
  const tick = async () => {
62
67
  if (stopped || tickRunning) return;
63
68
  tickRunning = true;
64
69
  try {
65
- await runMemoryJobsOnce({ enableScheduledCleanup: true });
70
+ const processed = await runMemoryJobsOnce({
71
+ enableScheduledCleanup: true,
72
+ });
73
+ if (processed > 0) {
74
+ currentIntervalMs = POLL_INTERVAL_MIN_MS;
75
+ } else {
76
+ currentIntervalMs = Math.min(
77
+ currentIntervalMs * 2,
78
+ POLL_INTERVAL_MAX_MS,
79
+ );
80
+ }
66
81
  } catch (err) {
67
82
  log.error({ err }, "Memory worker tick failed");
83
+ currentIntervalMs = Math.min(currentIntervalMs * 2, POLL_INTERVAL_MAX_MS);
68
84
  } finally {
69
85
  tickRunning = false;
70
86
  }
71
87
  };
72
88
 
73
- const timer = setInterval(() => {
74
- void tick();
75
- }, 1500);
76
- timer.unref();
77
- void tick();
89
+ const scheduleTick = () => {
90
+ if (stopped) return;
91
+ timer = setTimeout(() => {
92
+ void tick().then(() => {
93
+ if (!stopped) scheduleTick();
94
+ });
95
+ }, currentIntervalMs);
96
+ (timer as NodeJS.Timeout).unref?.();
97
+ };
98
+
99
+ void tick().then(() => {
100
+ if (!stopped) scheduleTick();
101
+ });
78
102
 
79
103
  return {
80
104
  async runOnce(): Promise<number> {
@@ -82,7 +106,7 @@ export function startMemoryJobsWorker(): MemoryJobsWorker {
82
106
  },
83
107
  stop(): void {
84
108
  stopped = true;
85
- clearInterval(timer);
109
+ clearTimeout(timer);
86
110
  },
87
111
  };
88
112
  }
@@ -49,3 +49,22 @@ export function migrateJobDeferrals(database: DrizzleDb): void {
49
49
  throw e;
50
50
  }
51
51
  }
52
+
53
+ /**
54
+ * Reverse the deferral reconciliation by moving `deferrals` back into `attempts`
55
+ * for pending embed jobs. Best-effort: jobs that accumulated real deferral counts
56
+ * after the forward migration ran cannot be distinguished from migrated ones.
57
+ */
58
+ export function downJobDeferrals(database: DrizzleDb): void {
59
+ const raw = getSqliteFrom(database);
60
+ raw.exec(/*sql*/ `
61
+ UPDATE memory_jobs
62
+ SET attempts = deferrals,
63
+ deferrals = 0,
64
+ updated_at = ${Date.now()}
65
+ WHERE status = 'pending'
66
+ AND deferrals > 0
67
+ AND attempts = 0
68
+ AND type IN ('embed_segment', 'embed_item', 'embed_summary')
69
+ `);
70
+ }
@@ -91,3 +91,13 @@ export function migrateMemoryEntityRelationDedup(database: DrizzleDb): void {
91
91
  throw e;
92
92
  }
93
93
  }
94
+
95
+ /**
96
+ * No-op down: deduplication is a lossy operation — deleted duplicate rows
97
+ * cannot be restored. The forward migration merged rows by keeping the most
98
+ * recent evidence per (source, target, relation) triple; the discarded rows
99
+ * are permanently lost.
100
+ */
101
+ export function downMemoryEntityRelationDedup(_database: DrizzleDb): void {
102
+ // Intentionally empty — irreversible lossy migration.
103
+ }
@@ -93,3 +93,79 @@ export function migrateMemoryItemsFingerprintScopeUnique(
93
93
  raw.exec("PRAGMA foreign_keys = ON");
94
94
  }
95
95
  }
96
+
97
+ /**
98
+ * Reverse the compound (fingerprint, scope_id) unique index change by rebuilding
99
+ * memory_items with a column-level UNIQUE on fingerprint.
100
+ *
101
+ * WARNING: This is dangerous if data now relies on the compound constraint
102
+ * (i.e., the same fingerprint exists in multiple scopes). In that case, the
103
+ * rebuild will fail with a UNIQUE constraint violation. This is intentional —
104
+ * it prevents silent data loss on rollback.
105
+ */
106
+ export function downMemoryItemsFingerprintScopeUnique(
107
+ database: DrizzleDb,
108
+ ): void {
109
+ const raw = getSqliteFrom(database);
110
+
111
+ // Check if the column-level UNIQUE already exists — if so, nothing to do.
112
+ const tableDdl = raw
113
+ .query(
114
+ `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_items'`,
115
+ )
116
+ .get() as { sql: string } | null;
117
+ if (
118
+ !tableDdl ||
119
+ tableDdl.sql.match(/fingerprint\s+TEXT\s+NOT\s+NULL\s+UNIQUE/i)
120
+ ) {
121
+ return;
122
+ }
123
+
124
+ raw.exec("PRAGMA foreign_keys = OFF");
125
+ try {
126
+ raw.exec("BEGIN");
127
+
128
+ raw.exec(/*sql*/ `
129
+ CREATE TABLE memory_items_new (
130
+ id TEXT PRIMARY KEY,
131
+ kind TEXT NOT NULL,
132
+ subject TEXT NOT NULL,
133
+ statement TEXT NOT NULL,
134
+ status TEXT NOT NULL,
135
+ confidence REAL NOT NULL,
136
+ fingerprint TEXT NOT NULL UNIQUE,
137
+ first_seen_at INTEGER NOT NULL,
138
+ last_seen_at INTEGER NOT NULL,
139
+ last_used_at INTEGER,
140
+ importance REAL,
141
+ access_count INTEGER NOT NULL DEFAULT 0,
142
+ valid_from INTEGER,
143
+ invalid_at INTEGER,
144
+ verification_state TEXT NOT NULL DEFAULT 'assistant_inferred',
145
+ scope_id TEXT NOT NULL DEFAULT 'default'
146
+ )
147
+ `);
148
+
149
+ raw.exec(/*sql*/ `
150
+ INSERT INTO memory_items_new
151
+ SELECT id, kind, subject, statement, status, confidence, fingerprint,
152
+ first_seen_at, last_seen_at, last_used_at, importance, access_count,
153
+ valid_from, invalid_at, verification_state, scope_id
154
+ FROM memory_items
155
+ `);
156
+
157
+ raw.exec(/*sql*/ `DROP TABLE memory_items`);
158
+ raw.exec(/*sql*/ `ALTER TABLE memory_items_new RENAME TO memory_items`);
159
+
160
+ raw.exec("COMMIT");
161
+ } catch (e) {
162
+ try {
163
+ raw.exec("ROLLBACK");
164
+ } catch {
165
+ /* no active transaction */
166
+ }
167
+ throw e;
168
+ } finally {
169
+ raw.exec("PRAGMA foreign_keys = ON");
170
+ }
171
+ }
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
4
  import { computeMemoryFingerprint } from "../fingerprint.js";
3
5
 
@@ -75,3 +77,51 @@ export function migrateMemoryItemsScopeSaltedFingerprints(
75
77
  throw e;
76
78
  }
77
79
  }
80
+
81
+ /**
82
+ * Reverse the scope-salted fingerprint migration by recomputing fingerprints
83
+ * WITHOUT the scope_id prefix.
84
+ *
85
+ * Old format: sha256(`${kind}|${subject.toLowerCase()}|${statement.toLowerCase()}`)
86
+ */
87
+ export function downMemoryItemsScopeSaltedFingerprints(
88
+ database: DrizzleDb,
89
+ ): void {
90
+ const raw = getSqliteFrom(database);
91
+
92
+ interface ItemRow {
93
+ id: string;
94
+ kind: string;
95
+ subject: string;
96
+ statement: string;
97
+ }
98
+
99
+ const items = raw
100
+ .query(`SELECT id, kind, subject, statement FROM memory_items`)
101
+ .all() as ItemRow[];
102
+
103
+ if (items.length === 0) return;
104
+
105
+ try {
106
+ raw.exec("BEGIN");
107
+
108
+ const updateStmt = raw.prepare(
109
+ `UPDATE memory_items SET fingerprint = ? WHERE id = ?`,
110
+ );
111
+
112
+ for (const item of items) {
113
+ const normalized = `${item.kind}|${item.subject.toLowerCase()}|${item.statement.toLowerCase()}`;
114
+ const fingerprint = createHash("sha256").update(normalized).digest("hex");
115
+ updateStmt.run(fingerprint, item.id);
116
+ }
117
+
118
+ raw.exec("COMMIT");
119
+ } catch (e) {
120
+ try {
121
+ raw.exec("ROLLBACK");
122
+ } catch {
123
+ /* no active transaction */
124
+ }
125
+ throw e;
126
+ }
127
+ }
@@ -265,3 +265,13 @@ export function migrateAssistantIdToSelf(database: DrizzleDb): void {
265
265
  throw e;
266
266
  }
267
267
  }
268
+
269
+ /**
270
+ * No-op down: the original assistant_id values are not recoverable. The forward
271
+ * migration normalized all assistant_id values to "self" and merged/deduplicated
272
+ * rows where the same logical entity existed under both the real assistantId and
273
+ * "self". The original per-assistant IDs are permanently lost.
274
+ */
275
+ export function downAssistantIdToSelf(_database: DrizzleDb): void {
276
+ // Intentionally empty — original assistant_id values cannot be restored.
277
+ }
@@ -228,3 +228,37 @@ export function migrateRemoveAssistantIdColumns(database: DrizzleDb): void {
228
228
  raw.exec("PRAGMA foreign_keys = ON");
229
229
  }
230
230
  }
231
+
232
+ /**
233
+ * Add the assistant_id column back to the 4 tables that had it removed.
234
+ *
235
+ * NOTE: The data previously stored in assistant_id is lost — all rows will
236
+ * have assistant_id = 'self' after this down migration. This only restores
237
+ * the column structure so that older code expecting the column can function.
238
+ */
239
+ export function downRemoveAssistantIdColumns(database: DrizzleDb): void {
240
+ const raw = getSqliteFrom(database);
241
+
242
+ const tables = [
243
+ "conversation_keys",
244
+ "attachments",
245
+ "channel_inbound_events",
246
+ "message_runs",
247
+ ];
248
+
249
+ for (const table of tables) {
250
+ // Check if the table exists and lacks assistant_id
251
+ const ddl = raw
252
+ .query(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`)
253
+ .get(table) as { sql: string } | null;
254
+ if (!ddl || ddl.sql.includes("assistant_id")) continue;
255
+
256
+ try {
257
+ raw.exec(
258
+ /*sql*/ `ALTER TABLE ${table} ADD COLUMN assistant_id TEXT NOT NULL DEFAULT 'self'`,
259
+ );
260
+ } catch {
261
+ /* column already exists */
262
+ }
263
+ }
264
+ }
@@ -95,3 +95,29 @@ export function migrateLlmUsageEventsDropAssistantId(
95
95
  raw.exec("PRAGMA foreign_keys = ON");
96
96
  }
97
97
  }
98
+
99
+ /**
100
+ * Add the assistant_id column back to llm_usage_events.
101
+ *
102
+ * NOTE: The data previously stored in assistant_id is lost — all rows will
103
+ * have assistant_id = NULL after this down migration. This only restores
104
+ * the column structure so that older code expecting the column can function.
105
+ */
106
+ export function downLlmUsageEventsDropAssistantId(database: DrizzleDb): void {
107
+ const raw = getSqliteFrom(database);
108
+
109
+ const ddl = raw
110
+ .query(
111
+ `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'llm_usage_events'`,
112
+ )
113
+ .get() as { sql: string } | null;
114
+ if (!ddl || ddl.sql.includes("assistant_id")) return;
115
+
116
+ try {
117
+ raw.exec(
118
+ /*sql*/ `ALTER TABLE llm_usage_events ADD COLUMN assistant_id TEXT`,
119
+ );
120
+ } catch {
121
+ /* column already exists */
122
+ }
123
+ }
@@ -88,3 +88,13 @@ export function migrateBackfillInboxThreadStateFromBindings(
88
88
  throw e;
89
89
  }
90
90
  }
91
+
92
+ /**
93
+ * No-op down: the seeded inbox thread state rows are expected to remain.
94
+ * The forward migration used INSERT OR IGNORE, so existing rows were never
95
+ * modified. Removing the seeded rows could leave the inbox empty for
96
+ * pre-existing conversations, which is worse than keeping them.
97
+ */
98
+ export function downBackfillInboxThreadState(_database: DrizzleDb): void {
99
+ // Intentionally empty — seeded data is expected to remain.
100
+ }