@vellumai/assistant 0.5.6 → 0.5.8

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 (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -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
+ }
@@ -31,3 +31,20 @@ export function migrateDropActiveSearchIndex(database: DrizzleDb): void {
31
31
  throw e;
32
32
  }
33
33
  }
34
+
35
+ /**
36
+ * Recreate the old idx_memory_items_active_search index with its original
37
+ * covering columns (before the migration added status and invalid_at as
38
+ * indexed columns).
39
+ */
40
+ export function downDropActiveSearchIndex(database: DrizzleDb): void {
41
+ const raw = getSqliteFrom(database);
42
+
43
+ // Drop the current index if it exists, then recreate with the old column set.
44
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_memory_items_active_search`);
45
+ raw.exec(/*sql*/ `
46
+ CREATE INDEX IF NOT EXISTS idx_memory_items_active_search
47
+ ON memory_items(last_seen_at DESC, subject, statement, id, kind, confidence, importance, first_seen_at, scope_id)
48
+ WHERE status = 'active' AND invalid_at IS NULL
49
+ `);
50
+ }
@@ -82,3 +82,15 @@ export function migrateNotificationTablesSchema(database: DrizzleDb): void {
82
82
  }
83
83
  }
84
84
  }
85
+
86
+ /**
87
+ * No-op down: the old enum-based notification tables cannot be recreated
88
+ * without the original schema definitions (column names, types, constraints,
89
+ * and enum values). The forward migration dropped these tables entirely.
90
+ * Any data that was in them is permanently lost. The new signal-contract
91
+ * schema tables are structurally incompatible with the old enum-based ones.
92
+ */
93
+ export function downNotificationTablesSchema(_database: DrizzleDb): void {
94
+ // Intentionally empty — old enum-based tables cannot be recreated without
95
+ // the original schema, and any data they contained is permanently lost.
96
+ }
@@ -130,3 +130,124 @@ export function migrateRenameChannelToVellum(database: DrizzleDb): void {
130
130
  throw e;
131
131
  }
132
132
  }
133
+
134
+ /**
135
+ * Reverse the channel rename by changing "vellum" back to "macos" in all tables.
136
+ *
137
+ * NOTE: The forward migration renamed both "macos" and "ios" to "vellum", so we
138
+ * cannot distinguish which rows were originally "ios". This down migration
139
+ * conservatively maps all "vellum" values back to "macos" since that was the
140
+ * primary desktop channel identifier.
141
+ */
142
+ export function downRenameChannelToVellum(database: DrizzleDb): void {
143
+ const raw = getSqliteFrom(database);
144
+
145
+ try {
146
+ raw.exec("BEGIN");
147
+
148
+ // guardian_action_deliveries.destination_channel
149
+ const gadExists = raw
150
+ .query(
151
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'guardian_action_deliveries'`,
152
+ )
153
+ .get();
154
+ if (gadExists) {
155
+ raw
156
+ .query(
157
+ `UPDATE guardian_action_deliveries SET destination_channel = 'macos' WHERE destination_channel = 'vellum'`,
158
+ )
159
+ .run();
160
+ }
161
+
162
+ // messages.user_message_channel / assistant_message_channel
163
+ const msgsExists = raw
164
+ .query(
165
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'messages'`,
166
+ )
167
+ .get();
168
+ if (msgsExists) {
169
+ const hasUserMsgChannel = raw
170
+ .query(
171
+ `SELECT 1 FROM pragma_table_info('messages') WHERE name = 'user_message_channel'`,
172
+ )
173
+ .get();
174
+ if (hasUserMsgChannel) {
175
+ raw
176
+ .query(
177
+ `UPDATE messages SET user_message_channel = 'macos' WHERE user_message_channel = 'vellum'`,
178
+ )
179
+ .run();
180
+ }
181
+ const hasAssistantMsgChannel = raw
182
+ .query(
183
+ `SELECT 1 FROM pragma_table_info('messages') WHERE name = 'assistant_message_channel'`,
184
+ )
185
+ .get();
186
+ if (hasAssistantMsgChannel) {
187
+ raw
188
+ .query(
189
+ `UPDATE messages SET assistant_message_channel = 'macos' WHERE assistant_message_channel = 'vellum'`,
190
+ )
191
+ .run();
192
+ }
193
+ }
194
+
195
+ // external_conversation_bindings.source_channel
196
+ const ecbExists = raw
197
+ .query(
198
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'external_conversation_bindings'`,
199
+ )
200
+ .get();
201
+ if (ecbExists) {
202
+ raw
203
+ .query(
204
+ `UPDATE external_conversation_bindings SET source_channel = 'macos' WHERE source_channel = 'vellum'`,
205
+ )
206
+ .run();
207
+ }
208
+
209
+ // assistant_inbox_thread_state.source_channel
210
+ const aitsExists = raw
211
+ .query(
212
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'assistant_inbox_thread_state'`,
213
+ )
214
+ .get();
215
+ if (aitsExists) {
216
+ raw
217
+ .query(
218
+ `UPDATE assistant_inbox_thread_state SET source_channel = 'macos' WHERE source_channel = 'vellum'`,
219
+ )
220
+ .run();
221
+ }
222
+
223
+ // conversations.origin_channel
224
+ const convExists = raw
225
+ .query(
226
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'conversations'`,
227
+ )
228
+ .get();
229
+ if (convExists) {
230
+ const hasOriginChannel = raw
231
+ .query(
232
+ `SELECT 1 FROM pragma_table_info('conversations') WHERE name = 'origin_channel'`,
233
+ )
234
+ .get();
235
+ if (hasOriginChannel) {
236
+ raw
237
+ .query(
238
+ `UPDATE conversations SET origin_channel = 'macos' WHERE origin_channel = 'vellum'`,
239
+ )
240
+ .run();
241
+ }
242
+ }
243
+
244
+ raw.exec("COMMIT");
245
+ } catch (e) {
246
+ try {
247
+ raw.exec("ROLLBACK");
248
+ } catch {
249
+ /* no active transaction */
250
+ }
251
+ throw e;
252
+ }
253
+ }
@@ -71,3 +71,77 @@ export function migrateEmbeddingVectorBlob(database: DrizzleDb): void {
71
71
  .run(checkpointKey, Date.now());
72
72
  }
73
73
  }
74
+
75
+ /**
76
+ * Drop the vector_blob column from memory_embeddings.
77
+ *
78
+ * NOTE: Binary embedding data stored in vector_blob is lost on rollback.
79
+ * Rows that still have vector_json will continue to work; rows that only
80
+ * had vector_blob will lose their embedding vectors.
81
+ *
82
+ * SQLite does not support DROP COLUMN on all versions, so we rebuild the table.
83
+ */
84
+ export function downEmbeddingVectorBlob(database: DrizzleDb): void {
85
+ const raw = getSqliteFrom(database);
86
+
87
+ // Check if vector_blob column exists
88
+ const hasColumn = raw
89
+ .query(
90
+ `SELECT 1 FROM pragma_table_info('memory_embeddings') WHERE name = 'vector_blob'`,
91
+ )
92
+ .get();
93
+ if (!hasColumn) return;
94
+
95
+ raw.exec("PRAGMA foreign_keys = OFF");
96
+ try {
97
+ raw.exec("BEGIN");
98
+
99
+ // Get the current columns minus vector_blob
100
+ const columns = raw
101
+ .query(`SELECT name FROM pragma_table_info('memory_embeddings')`)
102
+ .all() as Array<{ name: string }>;
103
+ const keepColumns = columns
104
+ .map((c) => c.name)
105
+ .filter((n) => n !== "vector_blob");
106
+
107
+ // Get the current DDL to understand the table structure
108
+ const ddl = raw
109
+ .query(
110
+ `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_embeddings'`,
111
+ )
112
+ .get() as { sql: string } | null;
113
+ if (!ddl) {
114
+ raw.exec("ROLLBACK");
115
+ return;
116
+ }
117
+
118
+ // Remove the vector_blob column definition from the DDL
119
+ const newDdl = ddl.sql
120
+ .replace(/,\s*vector_blob\s+BLOB/i, "")
121
+ .replace("memory_embeddings", "memory_embeddings_new");
122
+
123
+ raw.exec(newDdl);
124
+
125
+ const colList = keepColumns.join(", ");
126
+ raw.exec(/*sql*/ `
127
+ INSERT INTO memory_embeddings_new (${colList})
128
+ SELECT ${colList} FROM memory_embeddings
129
+ `);
130
+
131
+ raw.exec(/*sql*/ `DROP TABLE memory_embeddings`);
132
+ raw.exec(
133
+ /*sql*/ `ALTER TABLE memory_embeddings_new RENAME TO memory_embeddings`,
134
+ );
135
+
136
+ raw.exec("COMMIT");
137
+ } catch (e) {
138
+ try {
139
+ raw.exec("ROLLBACK");
140
+ } catch {
141
+ /* no active transaction */
142
+ }
143
+ throw e;
144
+ } finally {
145
+ raw.exec("PRAGMA foreign_keys = ON");
146
+ }
147
+ }
@@ -105,6 +105,88 @@ export function migrateEmbeddingsNullableVectorJson(database: DrizzleDb): void {
105
105
  }
106
106
  }
107
107
 
108
+ /**
109
+ * Reverse v13: rebuild memory_embeddings with NOT NULL on vector_json.
110
+ *
111
+ * WARNING: Any rows with NULL vector_json will be lost — they cannot satisfy
112
+ * the NOT NULL constraint. This is acceptable because the forward migration
113
+ * only relaxed the constraint; rows written after the forward migration may
114
+ * have NULL vector_json (relying on vector_blob instead).
115
+ */
116
+ export function downEmbeddingsNullableVectorJson(database: DrizzleDb): void {
117
+ const raw = getSqliteFrom(database);
118
+
119
+ const tableExists = raw
120
+ .query(
121
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'memory_embeddings'`,
122
+ )
123
+ .get();
124
+ if (!tableExists) return;
125
+
126
+ // Check if vector_json already has NOT NULL — already rolled back
127
+ const ddl = raw
128
+ .query(
129
+ `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_embeddings'`,
130
+ )
131
+ .get() as { sql: string } | null;
132
+ if (ddl && isColumnNotNull(ddl.sql, "vector_json")) return;
133
+
134
+ raw.exec("PRAGMA foreign_keys = OFF");
135
+ try {
136
+ raw.exec("BEGIN");
137
+
138
+ raw.exec(/*sql*/ `
139
+ CREATE TABLE memory_embeddings_new (
140
+ id TEXT PRIMARY KEY,
141
+ target_type TEXT NOT NULL,
142
+ target_id TEXT NOT NULL,
143
+ provider TEXT NOT NULL,
144
+ model TEXT NOT NULL,
145
+ dimensions INTEGER NOT NULL,
146
+ vector_json TEXT NOT NULL,
147
+ vector_blob BLOB,
148
+ content_hash TEXT,
149
+ created_at INTEGER NOT NULL,
150
+ updated_at INTEGER NOT NULL,
151
+ UNIQUE (target_type, target_id, provider, model)
152
+ )
153
+ `);
154
+ // Only copy rows where vector_json is NOT NULL — rows with NULL cannot
155
+ // satisfy the restored constraint and are lost.
156
+ raw.exec(/*sql*/ `
157
+ INSERT OR IGNORE INTO memory_embeddings_new (
158
+ id, target_type, target_id, provider, model, dimensions,
159
+ vector_json, vector_blob, content_hash, created_at, updated_at
160
+ )
161
+ SELECT
162
+ id, target_type, target_id, provider, model, dimensions,
163
+ vector_json, vector_blob, content_hash, created_at, updated_at
164
+ FROM memory_embeddings
165
+ WHERE vector_json IS NOT NULL
166
+ ORDER BY updated_at DESC
167
+ `);
168
+ raw.exec(/*sql*/ `DROP TABLE memory_embeddings`);
169
+ raw.exec(
170
+ /*sql*/ `ALTER TABLE memory_embeddings_new RENAME TO memory_embeddings`,
171
+ );
172
+
173
+ raw.exec(
174
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_embeddings_content_hash ON memory_embeddings(content_hash, provider, model)`,
175
+ );
176
+
177
+ raw.exec("COMMIT");
178
+ } catch (e) {
179
+ try {
180
+ raw.exec("ROLLBACK");
181
+ } catch {
182
+ /* no active transaction */
183
+ }
184
+ throw e;
185
+ } finally {
186
+ raw.exec("PRAGMA foreign_keys = ON");
187
+ }
188
+ }
189
+
108
190
  /** Check whether a column is declared NOT NULL in a CREATE TABLE DDL string. */
109
191
  function isColumnNotNull(ddl: string, column: string): boolean {
110
192
  const pattern = new RegExp(`${column}\\s+\\w+.*?NOT\\s+NULL`, "i");
@@ -336,3 +336,14 @@ export function migrateNormalizePhoneIdentities(database: DrizzleDb): void {
336
336
  throw e;
337
337
  }
338
338
  }
339
+
340
+ /**
341
+ * Reverse v14: no-op — original non-E.164 phone formats are not recoverable.
342
+ *
343
+ * The forward migration normalised phone numbers to E.164. The original
344
+ * formatting (parentheses, dashes, spaces, country-code variants) was
345
+ * discarded during normalisation and cannot be reconstructed.
346
+ */
347
+ export function downNormalizePhoneIdentities(_database: DrizzleDb): void {
348
+ // Lossy — original phone formats are not recoverable.
349
+ }
@@ -1,4 +1,7 @@
1
- import type { DrizzleDb } from "../db-connection.js";
1
+ import { getLogger } from "../../util/logger.js";
2
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
3
+
4
+ const logger = getLogger("messages-fts");
2
5
 
3
6
  /**
4
7
  * FTS5 virtual table for full-text search over messages.content.
@@ -21,7 +24,81 @@ import type { DrizzleDb } from "../db-connection.js";
21
24
  * ALL writes to the messages table to fail until the FTS table is rebuilt.
22
25
  * If this happens, `messages_fts` should be dropped and recreated, then
23
26
  * backfilled via `migrateMessagesFtsBackfill`.
27
+ *
28
+ * ## Auto-recovery from corruption
29
+ *
30
+ * After creating (or finding an existing) messages_fts table, we probe it
31
+ * with a lightweight MATCH query that exercises the FTS inverted index
32
+ * in O(1). If the probe throws SQLITE_CORRUPT_VTAB or SQLITE_CORRUPT,
33
+ * we force-remove all shadow tables and the vtable entry (falling back
34
+ * to `PRAGMA writable_schema` if DROP TABLE itself fails on the corrupt
35
+ * vtable) and recreate it from scratch. The subsequent
36
+ * `migrateMessagesFtsBackfill` call in db-init.ts will repopulate the
37
+ * index from the messages table — no message data is lost.
38
+ */
39
+ function isSqliteCorruptionError(err: unknown): boolean {
40
+ const code =
41
+ err != null && typeof err === "object" && "code" in err
42
+ ? (err as { code: string }).code
43
+ : undefined;
44
+ return code === "SQLITE_CORRUPT_VTAB" || code === "SQLITE_CORRUPT";
45
+ }
46
+
47
+ /**
48
+ * Force-remove all FTS5 shadow tables, triggers, and the vtable entry.
49
+ *
50
+ * We drop each artifact individually so that a corrupt shadow table
51
+ * doesn't block cleanup of the others. If `DROP TABLE messages_fts`
52
+ * itself fails (FTS5's xDestroy hits a corrupt shadow table), we fall
53
+ * back to `PRAGMA writable_schema` to delete the vtable entry directly
54
+ * from `sqlite_schema`. Without this fallback, `CREATE VIRTUAL TABLE
55
+ * IF NOT EXISTS` would be a no-op and the crash loop would persist.
24
56
  */
57
+ function dropFtsShadowTables(raw: ReturnType<typeof getSqliteFrom>): void {
58
+ const drops = [
59
+ `DROP TRIGGER IF EXISTS messages_fts_ai`,
60
+ `DROP TRIGGER IF EXISTS messages_fts_ad`,
61
+ `DROP TRIGGER IF EXISTS messages_fts_au`,
62
+ `DROP TABLE IF EXISTS messages_fts_config`,
63
+ `DROP TABLE IF EXISTS messages_fts_docsize`,
64
+ `DROP TABLE IF EXISTS messages_fts_content`,
65
+ `DROP TABLE IF EXISTS messages_fts_idx`,
66
+ `DROP TABLE IF EXISTS messages_fts_data`,
67
+ ];
68
+ for (const sql of drops) {
69
+ try {
70
+ raw.exec(sql);
71
+ } catch {
72
+ // Shadow table may itself be corrupt — ignore and continue
73
+ }
74
+ }
75
+
76
+ // Try the normal DROP TABLE path first (lets FTS5 clean up properly).
77
+ try {
78
+ raw.exec(`DROP TABLE IF EXISTS messages_fts`);
79
+ } catch {
80
+ // FTS5's xDestroy failed — force-remove the vtable entry from
81
+ // sqlite_schema so CREATE VIRTUAL TABLE isn't a no-op.
82
+ logger.warn(
83
+ "[messages-fts] DROP TABLE messages_fts failed — removing vtable entry via writable_schema",
84
+ );
85
+ raw.exec(`PRAGMA writable_schema = ON`);
86
+ try {
87
+ raw.exec(
88
+ `DELETE FROM sqlite_schema WHERE type = 'table' AND name = 'messages_fts'`,
89
+ );
90
+ } catch (schemaErr) {
91
+ logger.error(
92
+ { err: schemaErr },
93
+ "[messages-fts] Failed to remove vtable entry from sqlite_schema",
94
+ );
95
+ throw schemaErr;
96
+ } finally {
97
+ raw.exec(`PRAGMA writable_schema = OFF`);
98
+ }
99
+ }
100
+ }
101
+
25
102
  export function createMessagesFts(database: DrizzleDb): void {
26
103
  database.run(/*sql*/ `
27
104
  CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
@@ -30,6 +107,34 @@ export function createMessagesFts(database: DrizzleDb): void {
30
107
  )
31
108
  `);
32
109
 
110
+ // Probe the FTS inverted index for corruption. A MATCH query exercises
111
+ // the index structures (not just the content store), so it catches
112
+ // corruption in shadow tables like _idx and _data. On empty tables
113
+ // this returns null gracefully. O(1) with LIMIT 1.
114
+ const raw = getSqliteFrom(database);
115
+ try {
116
+ raw
117
+ .query(`SELECT * FROM messages_fts WHERE messages_fts MATCH 'a' LIMIT 1`)
118
+ .get();
119
+ } catch (err: unknown) {
120
+ if (!isSqliteCorruptionError(err)) {
121
+ throw err;
122
+ }
123
+ logger.warn(
124
+ { err },
125
+ "[messages-fts] Detected corrupt messages_fts virtual table — dropping and recreating",
126
+ );
127
+ // DROP TABLE on a corrupt vtable can itself throw, so drop the
128
+ // FTS5 shadow tables directly to guarantee cleanup.
129
+ dropFtsShadowTables(raw);
130
+ database.run(/*sql*/ `
131
+ CREATE VIRTUAL TABLE messages_fts USING fts5(
132
+ message_id UNINDEXED,
133
+ content
134
+ )
135
+ `);
136
+ }
137
+
33
138
  database.run(/*sql*/ `
34
139
  CREATE TRIGGER IF NOT EXISTS messages_fts_ai
35
140
  AFTER INSERT ON messages
@@ -1,6 +1,58 @@
1
1
  import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
2
  import { withCrashRecovery } from "./validate-migration-state.js";
3
3
 
4
+ /**
5
+ * Reverse v15: set guardian_principal_id back to NULL on all rows in
6
+ * channel_guardian_bindings and canonical_guardian_requests.
7
+ *
8
+ * Also un-expires requests that the forward migration expired (sets them
9
+ * back to 'pending'). This is a best-effort reversal — the original status
10
+ * of expired requests cannot be perfectly reconstructed if they were already
11
+ * expired before the forward migration ran, but the forward migration only
12
+ * expired requests that had NULL guardian_principal_id and status = 'pending'.
13
+ */
14
+ export function downBackfillGuardianPrincipalId(database: DrizzleDb): void {
15
+ const raw = getSqliteFrom(database);
16
+
17
+ // Null out guardian_principal_id on channel_guardian_bindings
18
+ const bindingsExists = raw
19
+ .query(
20
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'channel_guardian_bindings'`,
21
+ )
22
+ .get();
23
+ if (bindingsExists) {
24
+ const colExists = raw
25
+ .query(
26
+ `SELECT 1 FROM pragma_table_info('channel_guardian_bindings') WHERE name = 'guardian_principal_id'`,
27
+ )
28
+ .get();
29
+ if (colExists) {
30
+ raw.exec(
31
+ /*sql*/ `UPDATE channel_guardian_bindings SET guardian_principal_id = NULL`,
32
+ );
33
+ }
34
+ }
35
+
36
+ // Null out guardian_principal_id on canonical_guardian_requests
37
+ const requestsExists = raw
38
+ .query(
39
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'canonical_guardian_requests'`,
40
+ )
41
+ .get();
42
+ if (requestsExists) {
43
+ const colExists = raw
44
+ .query(
45
+ `SELECT 1 FROM pragma_table_info('canonical_guardian_requests') WHERE name = 'guardian_principal_id'`,
46
+ )
47
+ .get();
48
+ if (colExists) {
49
+ raw.exec(
50
+ /*sql*/ `UPDATE canonical_guardian_requests SET guardian_principal_id = NULL`,
51
+ );
52
+ }
53
+ }
54
+ }
55
+
4
56
  /**
5
57
  * Backfill guardianPrincipalId for existing channel_guardian_bindings and
6
58
  * canonical_guardian_requests rows.