@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
@@ -1,3 +1,43 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { downJobDeferrals } from "./001-job-deferrals.js";
3
+ import { downMemoryEntityRelationDedup } from "./004-entity-relation-dedup.js";
4
+ import { downMemoryItemsFingerprintScopeUnique } from "./005-fingerprint-scope-unique.js";
5
+ import { downMemoryItemsScopeSaltedFingerprints } from "./006-scope-salted-fingerprints.js";
6
+ import { downAssistantIdToSelf } from "./007-assistant-id-to-self.js";
7
+ import { downRemoveAssistantIdColumns } from "./008-remove-assistant-id-columns.js";
8
+ import { downLlmUsageEventsDropAssistantId } from "./009-llm-usage-events-drop-assistant-id.js";
9
+ import { downBackfillInboxThreadState } from "./014-backfill-inbox-thread-state.js";
10
+ import { downDropActiveSearchIndex } from "./015-drop-active-search-index.js";
11
+ import { downNotificationTablesSchema } from "./019-notification-tables-schema-migration.js";
12
+ import { downRenameChannelToVellum } from "./020-rename-macos-ios-channel-to-vellum.js";
13
+ import { downEmbeddingVectorBlob } from "./024-embedding-vector-blob.js";
14
+ import { downEmbeddingsNullableVectorJson } from "./026a-embeddings-nullable-vector-json.js";
15
+ import { downNormalizePhoneIdentities } from "./036-normalize-phone-identities.js";
16
+ import { downBackfillGuardianPrincipalId } from "./126-backfill-guardian-principal-id.js";
17
+ import { downGuardianPrincipalIdNotNull } from "./127-guardian-principal-id-not-null.js";
18
+ import { downContactsNotesColumn } from "./134-contacts-notes-column.js";
19
+ import { downBackfillContactInteractionStats } from "./135-backfill-contact-interaction-stats.js";
20
+ import { downDropAssistantIdColumns } from "./136-drop-assistant-id-columns.js";
21
+ import { downBackfillUsageCacheAccounting } from "./140-backfill-usage-cache-accounting.js";
22
+ import { downRenameVerificationTable } from "./141-rename-verification-table.js";
23
+ import { downRenameVerificationSessionIdColumn } from "./142-rename-verification-session-id-column.js";
24
+ import { downRenameGuardianVerificationValues } from "./143-rename-guardian-verification-values.js";
25
+ import { downRenameVoiceToPhone } from "./144-rename-voice-to-phone.js";
26
+ import { migrateDropAccountsTableDown } from "./145-drop-accounts-table.js";
27
+ import { migrateRemindersToSchedulesDown } from "./147-migrate-reminders-to-schedules.js";
28
+ import { migrateDropRemindersTableDown } from "./148-drop-reminders-table.js";
29
+ import { migrateOAuthAppsClientSecretPathDown } from "./150-oauth-apps-client-secret-path.js";
30
+ import {
31
+ migrateGuardianTimestampsEpochMsDown,
32
+ migrateGuardianTimestampsRebuildDown,
33
+ } from "./162-guardian-timestamps-epoch-ms.js";
34
+ import { migrateRenameGmailProviderKeyToGoogleDown } from "./169-rename-gmail-provider-key-to-google.js";
35
+ import { migrateRenameThreadStartersTableDown } from "./174-rename-thread-starters-table.js";
36
+ import { migrateDropCapabilityCardStateDown } from "./176-drop-capability-card-state.js";
37
+ import { migrateBackfillInlineAttachmentsToDiskDown } from "./180-backfill-inline-attachments-to-disk.js";
38
+ import { migrateRenameThreadStartersCheckpointsDown } from "./181-rename-thread-starters-checkpoints.js";
39
+ import { migrateBackfillAudioAttachmentMimeTypesDown } from "./191-backfill-audio-attachment-mime-types.js";
40
+
1
41
  export interface MigrationRegistryEntry {
2
42
  /** The checkpoint key written to memory_checkpoints on completion. */
3
43
  key: string;
@@ -7,6 +47,8 @@ export interface MigrationRegistryEntry {
7
47
  dependsOn?: string[];
8
48
  /** Human-readable description for diagnostics and future authorship guidance. */
9
49
  description: string;
50
+ /** Reverse the migration. Must be idempotent — safe to re-run. */
51
+ down: (database: DrizzleDb) => void;
10
52
  }
11
53
 
12
54
  // ---------------------------------------------------------------------------
@@ -26,18 +68,21 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
26
68
  version: 1,
27
69
  description:
28
70
  "Reconcile legacy deferral history from attempts column into deferrals column",
71
+ down: downJobDeferrals,
29
72
  },
30
73
  {
31
74
  key: "migration_memory_entity_relations_dedup_v1",
32
75
  version: 2,
33
76
  description:
34
77
  "Deduplicate entity relation edges before enforcing the (source, target, relation) unique index",
78
+ down: downMemoryEntityRelationDedup,
35
79
  },
36
80
  {
37
81
  key: "migration_memory_items_fingerprint_scope_unique_v1",
38
82
  version: 3,
39
83
  description:
40
84
  "Replace column-level UNIQUE on fingerprint with compound (fingerprint, scope_id) unique index",
85
+ down: downMemoryItemsFingerprintScopeUnique,
41
86
  },
42
87
  {
43
88
  key: "migration_memory_items_scope_salted_fingerprints_v1",
@@ -45,12 +90,14 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
45
90
  dependsOn: ["migration_memory_items_fingerprint_scope_unique_v1"],
46
91
  description:
47
92
  "Recompute memory item fingerprints to include scope_id prefix after schema change",
93
+ down: downMemoryItemsScopeSaltedFingerprints,
48
94
  },
49
95
  {
50
96
  key: "migration_normalize_assistant_id_to_self_v1",
51
97
  version: 5,
52
98
  description:
53
99
  'Normalize all assistant_id values in scoped tables to the implicit "self" single-tenant identity',
100
+ down: downAssistantIdToSelf,
54
101
  },
55
102
  {
56
103
  key: "migration_remove_assistant_id_columns_v1",
@@ -58,6 +105,7 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
58
105
  dependsOn: ["migration_normalize_assistant_id_to_self_v1"],
59
106
  description:
60
107
  "Rebuild four tables to drop the assistant_id column after normalization",
108
+ down: downRemoveAssistantIdColumns,
61
109
  },
62
110
  {
63
111
  key: "migration_remove_assistant_id_lue_v1",
@@ -65,36 +113,42 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
65
113
  dependsOn: ["migration_normalize_assistant_id_to_self_v1"],
66
114
  description:
67
115
  "Remove assistant_id column from llm_usage_events (separate checkpoint from the four-table migration)",
116
+ down: downLlmUsageEventsDropAssistantId,
68
117
  },
69
118
  {
70
119
  key: "backfill_inbox_thread_state_from_bindings",
71
120
  version: 8,
72
121
  description:
73
122
  "Seed assistant_inbox_thread_state from external_conversation_bindings",
123
+ down: downBackfillInboxThreadState,
74
124
  },
75
125
  {
76
126
  key: "drop_active_search_index_v1",
77
127
  version: 9,
78
128
  description:
79
129
  "Drop old idx_memory_items_active_search so it can be recreated with updated covering columns",
130
+ down: downDropActiveSearchIndex,
80
131
  },
81
132
  {
82
133
  key: "migration_notification_tables_schema_v1",
83
134
  version: 10,
84
135
  description:
85
136
  "Drop legacy enum-based notification tables so they can be recreated with the new signal-contract schema",
137
+ down: downNotificationTablesSchema,
86
138
  },
87
139
  {
88
140
  key: "migration_rename_macos_ios_channel_to_vellum_v1",
89
141
  version: 11,
90
142
  description:
91
143
  "Rename macos and ios channel identifiers to vellum across all tables",
144
+ down: downRenameChannelToVellum,
92
145
  },
93
146
  {
94
147
  key: "migration_embedding_vector_blob_v1",
95
148
  version: 12,
96
149
  description:
97
150
  "Add vector_blob BLOB column to memory_embeddings and backfill from vector_json for compact binary storage",
151
+ down: downEmbeddingVectorBlob,
98
152
  },
99
153
  {
100
154
  key: "migration_embeddings_nullable_vector_json_v1",
@@ -102,18 +156,21 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
102
156
  dependsOn: ["migration_embedding_vector_blob_v1"],
103
157
  description:
104
158
  "Rebuild memory_embeddings to make vector_json nullable (pre-100 DBs had NOT NULL)",
159
+ down: downEmbeddingsNullableVectorJson,
105
160
  },
106
161
  {
107
162
  key: "migration_normalize_phone_identities_v1",
108
163
  version: 14,
109
164
  description:
110
165
  "Normalize phone-like identity fields to E.164 format across guardian bindings, verification challenges, canonical requests, ingress members, and rate limits",
166
+ down: downNormalizePhoneIdentities,
111
167
  },
112
168
  {
113
169
  key: "migration_backfill_guardian_principal_id_v3",
114
170
  version: 15,
115
171
  description:
116
172
  "Backfill guardianPrincipalId for existing channel_guardian_bindings and canonical_guardian_requests rows, expire unresolvable pending requests",
173
+ down: downBackfillGuardianPrincipalId,
117
174
  },
118
175
  {
119
176
  key: "migration_guardian_principal_id_not_null_v1",
@@ -121,18 +178,21 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
121
178
  dependsOn: ["migration_backfill_guardian_principal_id_v3"],
122
179
  description:
123
180
  "Enforce NOT NULL on channel_guardian_bindings.guardian_principal_id after backfill",
181
+ down: downGuardianPrincipalIdNotNull,
124
182
  },
125
183
  {
126
184
  key: "migration_contacts_notes_column_v1",
127
185
  version: 17,
128
186
  description:
129
187
  "Consolidate relationship/importance/response_expectation/preferred_tone into a single notes TEXT column, then drop the legacy columns",
188
+ down: downContactsNotesColumn,
130
189
  },
131
190
  {
132
191
  key: "backfill_contact_interaction_stats",
133
192
  version: 18,
134
193
  description:
135
194
  "Backfill contacts.last_interaction from the max lastSeenAt across each contact's channels",
195
+ down: downBackfillContactInteractionStats,
136
196
  },
137
197
  {
138
198
  key: "migration_drop_assistant_id_columns_v1",
@@ -140,48 +200,56 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
140
200
  dependsOn: ["migration_normalize_assistant_id_to_self_v1"],
141
201
  description:
142
202
  "Drop assistant_id columns from all 16 daemon tables after normalization to single-tenant identity",
203
+ down: downDropAssistantIdColumns,
143
204
  },
144
205
  {
145
206
  key: "migration_backfill_usage_cache_accounting_v1",
146
207
  version: 20,
147
208
  description:
148
209
  "Backfill historical Anthropic llm_usage_events rows from llm_request_logs with cache-aware pricing",
210
+ down: downBackfillUsageCacheAccounting,
149
211
  },
150
212
  {
151
213
  key: "migration_rename_verification_table_v1",
152
214
  version: 21,
153
215
  description:
154
216
  "Rename channel_guardian_verification_challenges table to channel_verification_sessions and update indexes",
217
+ down: downRenameVerificationTable,
155
218
  },
156
219
  {
157
220
  key: "migration_rename_verification_session_id_column_v1",
158
221
  version: 22,
159
222
  description:
160
223
  "Rename guardian_verification_session_id column in call_sessions to verification_session_id",
224
+ down: downRenameVerificationSessionIdColumn,
161
225
  },
162
226
  {
163
227
  key: "migration_rename_guardian_verification_values_v1",
164
228
  version: 23,
165
229
  description:
166
230
  "Rename persisted guardian_verification call_mode and guardian_voice_verification_* event_type values to drop the guardian_ prefix",
231
+ down: downRenameGuardianVerificationValues,
167
232
  },
168
233
  {
169
234
  key: "migration_rename_voice_to_phone_v1",
170
235
  version: 24,
171
236
  description:
172
237
  'Rename stored "voice" channel values to "phone" across all tables with channel text columns',
238
+ down: downRenameVoiceToPhone,
173
239
  },
174
240
  {
175
241
  key: "migration_drop_accounts_table_v1",
176
242
  version: 25,
177
243
  description:
178
244
  "Drop the unused legacy accounts table and its leftover indexes after account_manage removal",
245
+ down: migrateDropAccountsTableDown,
179
246
  },
180
247
  {
181
248
  key: "migration_reminders_to_schedules_v1",
182
249
  version: 26,
183
250
  description:
184
251
  "Copy all existing reminders into cron_jobs as one-shot schedules with correct status and field mapping",
252
+ down: migrateRemindersToSchedulesDown,
185
253
  },
186
254
  {
187
255
  key: "migration_drop_reminders_table_v1",
@@ -189,18 +257,21 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
189
257
  dependsOn: ["migration_reminders_to_schedules_v1"],
190
258
  description:
191
259
  "Drop the legacy reminders table and its index after data migration to cron_jobs",
260
+ down: migrateDropRemindersTableDown,
192
261
  },
193
262
  {
194
263
  key: "migration_oauth_apps_client_secret_path_v1",
195
264
  version: 28,
196
265
  description:
197
266
  "Add client_secret_credential_path column to oauth_apps and backfill existing rows with convention-based paths",
267
+ down: migrateOAuthAppsClientSecretPathDown,
198
268
  },
199
269
  {
200
270
  key: "migration_guardian_timestamps_epoch_ms_v1",
201
271
  version: 29,
202
272
  description:
203
273
  "Convert guardian table timestamps from ISO 8601 text to epoch ms integers for consistency with all other tables",
274
+ down: migrateGuardianTimestampsEpochMsDown,
204
275
  },
205
276
  {
206
277
  key: "migration_guardian_timestamps_rebuild_v1",
@@ -208,18 +279,21 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
208
279
  dependsOn: ["migration_guardian_timestamps_epoch_ms_v1"],
209
280
  description:
210
281
  "Rebuild guardian tables so timestamp columns have INTEGER affinity instead of TEXT",
282
+ down: migrateGuardianTimestampsRebuildDown,
211
283
  },
212
284
  {
213
285
  key: "migration_rename_gmail_provider_key_to_google_v1",
214
286
  version: 31,
215
287
  description:
216
288
  "Rename integration:gmail provider key to integration:google across oauth_providers, oauth_apps, and oauth_connections",
289
+ down: migrateRenameGmailProviderKeyToGoogleDown,
217
290
  },
218
291
  {
219
292
  key: "migration_rename_thread_starters_table_v1",
220
293
  version: 32,
221
294
  description:
222
295
  "Rename thread_starters table to conversation_starters and recreate indexes with new names",
296
+ down: migrateRenameThreadStartersTableDown,
223
297
  },
224
298
  {
225
299
  key: "migration_drop_capability_card_state_v1",
@@ -227,12 +301,14 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
227
301
  dependsOn: ["migration_rename_thread_starters_table_v1"],
228
302
  description:
229
303
  "Remove deleted capability-card rows, jobs, checkpoints, and category state",
304
+ down: migrateDropCapabilityCardStateDown,
230
305
  },
231
306
  {
232
307
  key: "migration_backfill_inline_attachments_v1",
233
308
  version: 34,
234
309
  description:
235
310
  "Backfill existing inline base64 attachments to on-disk storage and clear dataBase64",
311
+ down: migrateBackfillInlineAttachmentsToDiskDown,
236
312
  },
237
313
  {
238
314
  key: "migration_rename_thread_starters_checkpoints_v1",
@@ -240,12 +316,26 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
240
316
  dependsOn: ["migration_rename_thread_starters_table_v1"],
241
317
  description:
242
318
  "Rename checkpoint keys from thread_starters: to conversation_starters: prefix so renamed code paths find existing generation state",
319
+ down: migrateRenameThreadStartersCheckpointsDown,
320
+ },
321
+ {
322
+ key: "migration_backfill_audio_attachment_mime_types_v1",
323
+ version: 36,
324
+ description:
325
+ "Backfill correct MIME types for audio attachments stored as application/octet-stream due to missing extension map entries",
326
+ down: migrateBackfillAudioAttachmentMimeTypesDown,
243
327
  },
244
328
  ];
245
329
 
330
+ export function getMaxMigrationVersion(): number {
331
+ return Math.max(...MIGRATION_REGISTRY.map((e) => e.version));
332
+ }
333
+
246
334
  export interface MigrationValidationResult {
247
335
  /** Keys of migrations whose checkpoint has value 'started' — started but never completed. */
248
336
  crashed: string[];
249
337
  /** Pairs where a completed migration's declared prerequisite is missing from checkpoints. */
250
338
  dependencyViolations: Array<{ migration: string; missingDependency: string }>;
339
+ /** Checkpoint keys present in the database but absent from the migration registry — likely from a newer version. */
340
+ unknownCheckpoints: string[];
251
341
  }
@@ -33,7 +33,9 @@ export function recoverCrashedMigrations(database: DrizzleDb): string[] {
33
33
  return [];
34
34
  }
35
35
 
36
- const crashed = rows.filter((r) => r.value === "started").map((r) => r.key);
36
+ const crashed = rows
37
+ .filter((r) => r.value === "started" || r.value === "rolling_back")
38
+ .map((r) => r.key);
37
39
  if (crashed.length === 0) return [];
38
40
 
39
41
  log.error(
@@ -83,7 +85,12 @@ export function withCrashRecovery(
83
85
  const existing = raw
84
86
  .query(`SELECT value FROM memory_checkpoints WHERE key = ?`)
85
87
  .get(checkpointKey) as { value: string } | null;
86
- if (existing && existing.value !== "started") return;
88
+ if (
89
+ existing &&
90
+ existing.value !== "started" &&
91
+ existing.value !== "rolling_back"
92
+ )
93
+ return;
87
94
 
88
95
  raw
89
96
  .query(
@@ -126,12 +133,14 @@ export function validateMigrationState(
126
133
  .all() as Array<{ key: string; value: string }>;
127
134
  } catch {
128
135
  // memory_checkpoints may not exist on a very old database; skip.
129
- return { crashed: [], dependencyViolations: [] };
136
+ return { crashed: [], dependencyViolations: [], unknownCheckpoints: [] };
130
137
  }
131
138
 
132
- // Any remaining 'started' checkpoints after recovery + migration execution
133
- // indicate a migration that was retried but failed again.
134
- const crashed = rows.filter((r) => r.value === "started").map((r) => r.key);
139
+ // Any remaining 'started' or 'rolling_back' checkpoints after recovery +
140
+ // migration execution indicate a migration that was retried but failed again.
141
+ const crashed = rows
142
+ .filter((r) => r.value === "started" || r.value === "rolling_back")
143
+ .map((r) => r.key);
135
144
  if (crashed.length > 0) {
136
145
  log.error(
137
146
  { crashed },
@@ -149,11 +158,14 @@ export function validateMigrationState(
149
158
  );
150
159
  }
151
160
 
152
- // Only rows whose value is NOT 'started' represent truly completed migrations.
153
- // In-progress/crashed checkpoints (value = 'started') must not count as applied
154
- // dependencies — the migration never finished, so its postconditions are unmet.
161
+ // Only rows whose value is NOT 'started' or 'rolling_back' represent truly
162
+ // completed migrations. In-progress/crashed checkpoints must not count as
163
+ // applied dependencies — the migration never finished, so its postconditions
164
+ // are unmet.
155
165
  const completed = new Set(
156
- rows.filter((r) => r.value !== "started").map((r) => r.key),
166
+ rows
167
+ .filter((r) => r.value !== "started" && r.value !== "rolling_back")
168
+ .map((r) => r.key),
157
169
  );
158
170
 
159
171
  const dependencyViolations: Array<{
@@ -191,5 +203,119 @@ export function validateMigrationState(
191
203
  );
192
204
  }
193
205
 
194
- return { crashed, dependencyViolations };
206
+ // Detect checkpoints that exist in the database but have no corresponding
207
+ // registry entry — these are from a newer version of the daemon.
208
+ //
209
+ // The memory_checkpoints table is a general-purpose key-value store also
210
+ // used by non-migration subsystems (e.g., "identity:intro:text",
211
+ // "conversation_starters:item_count_at_last_gen"). Filter to only keys
212
+ // that follow migration naming conventions before comparing against the
213
+ // registry to avoid false-positive warnings.
214
+ const registryKeys = new Set(MIGRATION_REGISTRY.map((e) => e.key));
215
+ const isMigrationKey = (k: string): boolean =>
216
+ k.startsWith("migration_") ||
217
+ k.startsWith("backfill_") ||
218
+ k.startsWith("drop_");
219
+ const unknownCheckpoints = [...completed].filter(
220
+ (k) => isMigrationKey(k) && !registryKeys.has(k),
221
+ );
222
+
223
+ if (unknownCheckpoints.length > 0) {
224
+ log.warn(
225
+ { unknownCheckpoints },
226
+ `Database contains ${unknownCheckpoints.length} migration checkpoint(s) from a newer version. Data may be incompatible.`,
227
+ );
228
+ }
229
+
230
+ return { crashed, dependencyViolations, unknownCheckpoints };
231
+ }
232
+
233
+ /**
234
+ * Roll back all completed memory (database) migrations with version > targetVersion.
235
+ *
236
+ * Iterates eligible migrations in reverse version order. For each:
237
+ * 1. Marks the checkpoint as `"rolling_back"` for crash recovery.
238
+ * 2. Calls `entry.down(database)` — each down() manages its own transactions.
239
+ * (`down` is required on `MigrationRegistryEntry` at the type level.)
240
+ * 3. Deletes the checkpoint from `memory_checkpoints`.
241
+ *
242
+ * **Usage**: Pass the target version number you want to roll back *to*. All
243
+ * migrations with a higher version number that have been applied will be
244
+ * reversed. For example, `rollbackMemoryMigration(db, 5)` rolls back all
245
+ * applied migrations with version > 5.
246
+ *
247
+ * **Checkpoint state**: Each rolled-back migration's checkpoint is deleted
248
+ * from `memory_checkpoints`. If the process crashes mid-rollback, the
249
+ * `"rolling_back"` marker is detected and cleared by
250
+ * `recoverCrashedMigrations` on the next startup.
251
+ *
252
+ * **Warning — data loss**: Some down() migrations may not fully restore the
253
+ * original state (e.g., DROP TABLE migrations recreate the table but cannot
254
+ * recover the original data). Review each migration's down() implementation
255
+ * before calling this function programmatically.
256
+ *
257
+ * **Important**: Stop the assistant before running rollbacks. Rolling back
258
+ * migrations while the assistant is running may cause schema mismatches,
259
+ * query failures, or data corruption.
260
+ *
261
+ * @param database The Drizzle database instance.
262
+ * @param targetVersion Roll back to this version (exclusive — all migrations
263
+ * with version > targetVersion are reversed).
264
+ * @returns The list of rolled-back migration keys.
265
+ */
266
+ export function rollbackMemoryMigration(
267
+ database: DrizzleDb,
268
+ targetVersion: number,
269
+ ): string[] {
270
+ const raw = getSqliteFrom(database);
271
+
272
+ // Read completed checkpoints to determine which migrations have been applied.
273
+ let rows: Array<{ key: string; value: string }>;
274
+ try {
275
+ rows = raw
276
+ .query(`SELECT key, value FROM memory_checkpoints`)
277
+ .all() as Array<{ key: string; value: string }>;
278
+ } catch {
279
+ return [];
280
+ }
281
+
282
+ const completedKeys = new Set(
283
+ rows
284
+ .filter((r) => r.value !== "started" && r.value !== "rolling_back")
285
+ .map((r) => r.key),
286
+ );
287
+
288
+ // Find registry entries with version > targetVersion that have completed checkpoints.
289
+ const toRollback = MIGRATION_REGISTRY.filter(
290
+ (entry) => entry.version > targetVersion && completedKeys.has(entry.key),
291
+ ).sort((a, b) => b.version - a.version); // reverse version order
292
+
293
+ const rolledBack: string[] = [];
294
+
295
+ for (const entry of toRollback) {
296
+ // Mark as rolling_back for crash recovery — if the process crashes here,
297
+ // recoverCrashedMigrations will clear this checkpoint on next startup.
298
+ raw
299
+ .query(
300
+ `UPDATE memory_checkpoints SET value = 'rolling_back', updated_at = ? WHERE key = ?`,
301
+ )
302
+ .run(Date.now(), entry.key);
303
+
304
+ // Execute the down migration — let it manage its own transaction lifecycle.
305
+ // Many down() functions call BEGIN/COMMIT internally or use PRAGMA statements
306
+ // that are no-ops inside a transaction.
307
+ entry.down(database);
308
+
309
+ // Delete the checkpoint after down() succeeds — outside any transaction
310
+ // so it's not affected by down()'s internal transaction management.
311
+ raw.query(`DELETE FROM memory_checkpoints WHERE key = ?`).run(entry.key);
312
+
313
+ log.info(
314
+ { key: entry.key, version: entry.version },
315
+ `Rolled back migration "${entry.key}" (version ${entry.version})`,
316
+ );
317
+ rolledBack.push(entry.key);
318
+ }
319
+
320
+ return rolledBack;
195
321
  }
@@ -101,6 +101,15 @@ export function isQdrantBreakerOpen(): boolean {
101
101
  return breakerState !== "closed";
102
102
  }
103
103
 
104
+ /**
105
+ * Returns true when the breaker is open and the cooldown has elapsed,
106
+ * meaning the next call to `withQdrantBreaker` will transition to half-open.
107
+ * Use this to allow a single probe job through when embed jobs are otherwise skipped.
108
+ */
109
+ export function shouldAllowQdrantProbe(): boolean {
110
+ return breakerState === "open" && Date.now() - openedAt >= COOLDOWN_MS;
111
+ }
112
+
104
113
  /** @internal Test-only: reset circuit breaker state */
105
114
  export function _resetQdrantBreaker(): void {
106
115
  breakerState = "closed";
@@ -4,6 +4,7 @@ import {
4
4
  existsSync,
5
5
  mkdirSync,
6
6
  readFileSync,
7
+ symlinkSync,
7
8
  unlinkSync,
8
9
  writeFileSync,
9
10
  } from "node:fs";
@@ -12,7 +13,7 @@ import { dirname, join } from "node:path";
12
13
 
13
14
  import type { Subprocess } from "bun";
14
15
 
15
- import { getQdrantUrlEnv } from "../config/env.js";
16
+ import { getQdrantReadyzTimeoutMs, getQdrantUrlEnv } from "../config/env.js";
16
17
  import { getLogger } from "../util/logger.js";
17
18
  import { getDataDir } from "../util/platform.js";
18
19
 
@@ -47,6 +48,8 @@ export interface QdrantManagerConfig {
47
48
  */
48
49
  export class QdrantManager {
49
50
  private process: Subprocess | null = null;
51
+ private stderrBuffer = "";
52
+ private stderrDrained: Promise<void> = Promise.resolve();
50
53
  private readonly url: string;
51
54
  private readonly host: string;
52
55
  private readonly port: number;
@@ -67,7 +70,8 @@ export class QdrantManager {
67
70
 
68
71
  this.readyzPollIntervalMs =
69
72
  config.readyzPollIntervalMs ?? READYZ_POLL_INTERVAL_MS;
70
- this.readyzTimeoutMs = config.readyzTimeoutMs ?? READYZ_TIMEOUT_MS;
73
+ this.readyzTimeoutMs =
74
+ config.readyzTimeoutMs ?? getQdrantReadyzTimeoutMs() ?? READYZ_TIMEOUT_MS;
71
75
  this.shutdownGraceMs = config.shutdownGraceMs ?? SHUTDOWN_GRACE_MS;
72
76
 
73
77
  // External mode only if QDRANT_URL is explicitly set
@@ -92,13 +96,15 @@ export class QdrantManager {
92
96
  await this.installBinary(binaryPath);
93
97
  }
94
98
 
99
+ const spawnPath = this.ensureVellumSymlink(binaryPath);
100
+
95
101
  log.info(
96
- { binaryPath, storagePath: this.storagePath, port: this.port },
102
+ { binaryPath: spawnPath, storagePath: this.storagePath, port: this.port },
97
103
  "Starting Qdrant",
98
104
  );
99
105
 
100
- this.process = Bun.spawn({
101
- cmd: [binaryPath],
106
+ const proc = Bun.spawn({
107
+ cmd: [spawnPath],
102
108
  env: {
103
109
  ...process.env,
104
110
  QDRANT__SERVICE__HOST: this.host,
@@ -109,8 +115,10 @@ export class QdrantManager {
109
115
  QDRANT__LOG_LEVEL: "WARN",
110
116
  },
111
117
  stdout: "ignore",
112
- stderr: "ignore",
118
+ stderr: "pipe",
113
119
  });
120
+ this.process = proc;
121
+ this.drainStderrFrom(proc.stderr);
114
122
 
115
123
  if (this.process.pid) {
116
124
  this.writePid(this.process.pid);
@@ -150,6 +158,7 @@ export class QdrantManager {
150
158
  }
151
159
 
152
160
  this.process = null;
161
+ this.stderrBuffer = "";
153
162
  this.cleanupPid();
154
163
  log.info("Qdrant stopped");
155
164
  }
@@ -259,6 +268,15 @@ export class QdrantManager {
259
268
  private async waitForReady(): Promise<void> {
260
269
  const start = Date.now();
261
270
  while (Date.now() - start < this.readyzTimeoutMs) {
271
+ // Fail fast if the managed process exited before becoming ready
272
+ if (this.process != null && this.process.exitCode != null) {
273
+ await this.stderrDrained;
274
+ const stderr = this.stderrBuffer.trim();
275
+ throw new Error(
276
+ `Qdrant process exited with code ${this.process.exitCode} before becoming ready` +
277
+ (stderr ? `\nstderr:\n${stderr}` : ""),
278
+ );
279
+ }
262
280
  try {
263
281
  const res = await fetch(`${this.url}/readyz`);
264
282
  if (res.ok) return;
@@ -267,11 +285,32 @@ export class QdrantManager {
267
285
  }
268
286
  await Bun.sleep(this.readyzPollIntervalMs);
269
287
  }
288
+ const stderr = this.stderrBuffer.trim();
270
289
  throw new Error(
271
- `Qdrant did not become ready within ${this.readyzTimeoutMs}ms at ${this.url}`,
290
+ `Qdrant did not become ready within ${this.readyzTimeoutMs}ms at ${this.url}` +
291
+ (stderr ? `\nstderr:\n${stderr}` : ""),
272
292
  );
273
293
  }
274
294
 
295
+ private drainStderrFrom(stream: ReadableStream<Uint8Array>): void {
296
+ const reader = stream.getReader();
297
+ const decoder = new TextDecoder();
298
+ this.stderrDrained = (async () => {
299
+ try {
300
+ for (;;) {
301
+ const { done, value } = await reader.read();
302
+ if (done) break;
303
+ this.stderrBuffer += decoder.decode(value, { stream: true });
304
+ if (this.stderrBuffer.length > 4096) {
305
+ this.stderrBuffer = this.stderrBuffer.slice(-4096);
306
+ }
307
+ }
308
+ } catch {
309
+ // Stream closed or error — expected during shutdown
310
+ }
311
+ })();
312
+ }
313
+
275
314
  private getBinaryPath(): string {
276
315
  return join(getDataDir(), "qdrant", "bin", "qdrant");
277
316
  }
@@ -314,4 +353,22 @@ export class QdrantManager {
314
353
  }
315
354
  }
316
355
  }
356
+
357
+ /**
358
+ * Ensures a `vellum-qdrant` symlink exists next to the real binary so that
359
+ * `lsof` reports "vellum-qdrant" in the COMMAND column, making the process
360
+ * discoverable by tools that scan for "vellum" in process names.
361
+ */
362
+ private ensureVellumSymlink(binaryPath: string): string {
363
+ const symlinkPath = join(dirname(binaryPath), "vellum-qdrant");
364
+ if (!existsSync(symlinkPath)) {
365
+ try {
366
+ symlinkSync(binaryPath, symlinkPath);
367
+ } catch {
368
+ // Fall back to the real binary if symlink creation fails
369
+ return binaryPath;
370
+ }
371
+ }
372
+ return symlinkPath;
373
+ }
317
374
  }
@@ -27,6 +27,7 @@ export const callSessions = sqliteTable(
27
27
  inviteGuardianName: text("invite_guardian_name"),
28
28
  callerIdentityMode: text("caller_identity_mode"),
29
29
  callerIdentitySource: text("caller_identity_source"),
30
+ skipDisclosure: integer("skip_disclosure").notNull().default(0),
30
31
  initiatedFromConversationId: text("initiated_from_conversation_id"),
31
32
  startedAt: integer("started_at"),
32
33
  endedAt: integer("ended_at"),
@@ -10,6 +10,7 @@ export const contacts = sqliteTable("contacts", {
10
10
  updatedAt: integer("updated_at").notNull(),
11
11
  role: text("role").notNull().default("contact"), // 'guardian' | 'contact'
12
12
  principalId: text("principal_id"), // internal auth principal (nullable)
13
+ userFile: text("user_file"), // workspace-relative path to per-user persona file
13
14
  contactType: text("contact_type").notNull().default("human"), // 'human' | 'assistant'
14
15
  });
15
16