@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
@@ -9,7 +9,6 @@ import { buildMemoryRecall } from "../../memory/retriever.js";
9
9
  import { memoryItems } from "../../memory/schema.js";
10
10
  import type { ScopePolicyOverride } from "../../memory/search/types.js";
11
11
  import { getLogger } from "../../util/logger.js";
12
- import { truncate } from "../../util/truncate.js";
13
12
  import type { ToolExecutionResult } from "../types.js";
14
13
 
15
14
  const log = getLogger("memory-tools");
@@ -39,6 +38,7 @@ export async function handleMemorySave(
39
38
  "decision",
40
39
  "constraint",
41
40
  "event",
41
+ "journal",
42
42
  ]);
43
43
  if (typeof rawKind !== "string") {
44
44
  return {
@@ -60,14 +60,14 @@ export async function handleMemorySave(
60
60
 
61
61
  const subject =
62
62
  typeof args.subject === "string" && args.subject.trim().length > 0
63
- ? truncate(args.subject.trim(), 80, "")
63
+ ? args.subject.trim()
64
64
  : inferSubjectFromStatement(statement.trim());
65
65
 
66
66
  try {
67
67
  const db = getDb();
68
68
  const id = uuid();
69
69
  const now = Date.now();
70
- const trimmedStatement = truncate(statement.trim(), 500, "");
70
+ const trimmedStatement = statement.trim();
71
71
 
72
72
  const fingerprint = computeMemoryFingerprint(
73
73
  scopeId,
@@ -93,6 +93,7 @@ export async function handleMemorySave(
93
93
  status: "active",
94
94
  importance: 0.8,
95
95
  lastSeenAt: now,
96
+ sourceType: "tool",
96
97
  verificationState: "user_confirmed",
97
98
  })
98
99
  .where(eq(memoryItems.id, existing.id))
@@ -115,6 +116,7 @@ export async function handleMemorySave(
115
116
  confidence: 0.95, // explicit saves have high confidence
116
117
  importance: 0.8, // explicit saves are high importance
117
118
  fingerprint,
119
+ sourceType: "tool",
118
120
  verificationState: "user_confirmed",
119
121
  scopeId,
120
122
  firstSeenAt: now,
@@ -187,7 +189,7 @@ export async function handleMemoryUpdate(
187
189
  }
188
190
 
189
191
  const now = Date.now();
190
- const trimmedStatement = truncate(statement.trim(), 500, "");
192
+ const trimmedStatement = statement.trim();
191
193
 
192
194
  const fingerprint = computeMemoryFingerprint(
193
195
  scopeId,
@@ -221,6 +223,7 @@ export async function handleMemoryUpdate(
221
223
  fingerprint,
222
224
  lastSeenAt: now,
223
225
  importance: 0.8,
226
+ sourceType: "tool",
224
227
  verificationState: "user_confirmed",
225
228
  })
226
229
  .where(eq(memoryItems.id, existing.id))
@@ -308,7 +311,7 @@ export async function handleMemoryRecall(
308
311
  items: [],
309
312
  sources: {
310
313
  semantic: recall.semanticHits,
311
- recency: recall.recencyHits,
314
+ recency: 0,
312
315
  },
313
316
  };
314
317
  return {
@@ -328,7 +331,7 @@ export async function handleMemoryRecall(
328
331
  })),
329
332
  sources: {
330
333
  semantic: recall.semanticHits,
331
- recency: recall.recencyHits,
334
+ recency: 0,
332
335
  },
333
336
  };
334
337
 
@@ -416,7 +419,7 @@ export async function handleMemoryDelete(
416
419
  function inferSubjectFromStatement(statement: string): string {
417
420
  // Take first few words as a subject label
418
421
  const words = statement.split(/\s+/).slice(0, 6).join(" ");
419
- return truncate(words, 80, "");
422
+ return words;
420
423
  }
421
424
 
422
425
  /**
@@ -61,10 +61,25 @@ const store = new SessionStore();
61
61
  /**
62
62
  * Host patterns that are allowed by default through the proxy policy engine,
63
63
  * regardless of session configuration. Supports exact matches (e.g.
64
- * `"localhost"`) and wildcard subdomain patterns (e.g. `"*.vellum.ai"`
65
- * matches `platform.vellum.ai`, `dev-platform.vellum.ai`, etc.).
64
+ * `"localhost"`) and wildcard subdomain patterns (e.g. `"*.example.com"`
65
+ * matches `api.example.com`, `dev.example.com`, etc.).
66
+ *
67
+ * Additional patterns can be added via the `PROXY_ALLOWED_HOSTS` env var
68
+ * (comma-separated, e.g. `"*.example.com,api.foo.bar"`).
66
69
  */
67
- const ALLOWED_HOST_PATTERNS: readonly string[] = ["*.vellum.ai", "localhost"];
70
+ const ALLOWED_HOST_PATTERNS: readonly string[] = (() => {
71
+ const extra = process.env.PROXY_ALLOWED_HOSTS?.trim();
72
+ const defaults = ["localhost"];
73
+ if (extra) {
74
+ defaults.push(
75
+ ...extra
76
+ .split(",")
77
+ .map((h) => h.trim())
78
+ .filter(Boolean),
79
+ );
80
+ }
81
+ return defaults;
82
+ })();
68
83
 
69
84
  /**
70
85
  * Returns `true` when `hostname` matches any entry in
@@ -73,7 +88,7 @@ const ALLOWED_HOST_PATTERNS: readonly string[] = ["*.vellum.ai", "localhost"];
73
88
  function isAllowedHost(hostname: string): boolean {
74
89
  for (const pattern of ALLOWED_HOST_PATTERNS) {
75
90
  if (pattern.startsWith("*.")) {
76
- const suffix = pattern.slice(1); // e.g. ".vellum.ai"
91
+ const suffix = pattern.slice(1); // e.g. ".example.com"
77
92
  if (hostname.endsWith(suffix) || hostname === pattern.slice(2)) {
78
93
  return true;
79
94
  }
@@ -573,7 +573,9 @@ export async function executeWebFetch(
573
573
  Accept:
574
574
  "text/markdown, text/html;q=0.9, application/xhtml+xml;q=0.9, text/plain;q=0.8, application/json;q=0.7, */*;q=0.6",
575
575
  "Accept-Encoding": "identity",
576
- "User-Agent": "VellumAssistant/1.0 (+https://vellum.ai)",
576
+ "User-Agent":
577
+ process.env.HTTP_USER_AGENT ||
578
+ "VellumAssistant/1.0 (+https://vellum.ai)",
577
579
  };
578
580
 
579
581
  let currentUrl = new URL(requestedUrl);
@@ -30,7 +30,7 @@ export class SkillExecuteTool implements Tool {
30
30
  activity: {
31
31
  type: "string",
32
32
  description:
33
- "Brief non-technical explanation of what you are doing and why, shown to the user as a status update.",
33
+ "Brief non-technical explanation of what you are doing and why, shown to the user as a progress update.",
34
34
  },
35
35
  },
36
36
  required: ["tool", "input", "activity"],
@@ -28,6 +28,7 @@ const SAFE_ENV_VARS = [
28
28
  "GPG_TTY",
29
29
  "GNUPGHOME",
30
30
  "VELLUM_DEV",
31
+ "VELLUM_WORKSPACE_DIR",
31
32
  ] as const;
32
33
 
33
34
  export function buildSanitizedEnv(): Record<string, string> {
@@ -118,14 +118,6 @@ export interface ToolContext {
118
118
  proxyToolResolver?: ProxyToolResolver;
119
119
  /** When set, only tools in this set may execute. Tools outside the set are blocked with an error. */
120
120
  allowedToolNames?: Set<string>;
121
- /** Request user confirmation for a sub-tool operation (used by claude_code tool). */
122
- requestConfirmation?: (req: {
123
- toolName: string;
124
- input: Record<string, unknown>;
125
- riskLevel: string;
126
- executionTarget?: ExecutionTarget;
127
- principal?: string;
128
- }) => Promise<{ decision: "allow" | "deny" }>;
129
121
  /** Prompt the user for a secret value via native SecureField UI. */
130
122
  requestSecret?: (params: {
131
123
  service: string;
@@ -0,0 +1,15 @@
1
+ import { isLinux, isMacOS } from "./platform.js";
2
+
3
+ /**
4
+ * Open a URL in the user's default browser, falling back to printing the URL
5
+ * to stderr on unsupported platforms.
6
+ */
7
+ export function openInBrowser(url: string): void {
8
+ if (isMacOS()) {
9
+ Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
10
+ } else if (isLinux()) {
11
+ Bun.spawn(["xdg-open", url], { stdout: "ignore", stderr: "ignore" });
12
+ } else {
13
+ process.stderr.write(`Open this URL to authorize:\n\n${url}\n`);
14
+ }
15
+ }
@@ -20,9 +20,6 @@ export enum ErrorCode {
20
20
  // WASM integrity check failures
21
21
  INTEGRITY_ERROR = "INTEGRITY_ERROR",
22
22
 
23
- // Secret detected in inbound content
24
- INGRESS_BLOCKED = "INGRESS_BLOCKED",
25
-
26
23
  // Internal/unexpected errors
27
24
  INTERNAL_ERROR = "INTERNAL_ERROR",
28
25
  }
@@ -178,12 +175,3 @@ export class IntegrityError extends AssistantError {
178
175
  }
179
176
  }
180
177
 
181
- export class IngressBlockedError extends AssistantError {
182
- constructor(
183
- message: string,
184
- public readonly detectedTypes: string[],
185
- ) {
186
- super(message, ErrorCode.INGRESS_BLOCKED);
187
- this.name = "IngressBlockedError";
188
- }
189
- }
@@ -3,7 +3,6 @@ import {
3
3
  existsSync,
4
4
  mkdirSync,
5
5
  readFileSync,
6
- writeFileSync,
7
6
  } from "node:fs";
8
7
  import { homedir } from "node:os";
9
8
  import { join } from "node:path";
@@ -45,25 +44,6 @@ export function getClipboardCommand(): string | null {
45
44
  return null;
46
45
  }
47
46
 
48
- /**
49
- * Read and parse the lockfile (~/.vellum.lock.json).
50
- * Respects BASE_DATA_DIR for non-standard home directories.
51
- * Returns null if the file doesn't exist or is malformed.
52
- */
53
- export function readLockfile(): Record<string, unknown> | null {
54
- const base = getBaseDataDir() || homedir();
55
- const lockPath = join(base, ".vellum.lock.json");
56
- if (!existsSync(lockPath)) return null;
57
- try {
58
- const raw = JSON.parse(readFileSync(lockPath, "utf-8"));
59
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
60
- return raw as Record<string, unknown>;
61
- }
62
- } catch {
63
- // malformed JSON
64
- }
65
- return null;
66
- }
67
47
 
68
48
  /**
69
49
  * Resolve the instance data directory from the lockfile.
@@ -155,45 +135,18 @@ export function resolveInstanceDataDir(): string | undefined {
155
135
  * (see migration 007-assistant-id-to-self). However, the desktop UI
156
136
  * sends the real assistant ID (e.g., "vellum-true-eel") while the
157
137
  * inbound call path resolves phone numbers to config keys (typically
158
- * "self"). This function maps any known lockfile assistant ID to "self"
138
+ * "self"). This function maps the current assistant's ID to "self"
159
139
  * so both sides use a consistent DB key.
160
- *
161
- * Multi-instance safety: each daemon process runs with a scoped
162
- * BASE_DATA_DIR, so readLockfile() only sees the lockfile for this
163
- * instance. The mapping to "self" is correct because each daemon is
164
- * single-tenant — it only manages its own instance's data.
165
140
  */
166
141
  export function normalizeAssistantId(assistantId: string): string {
167
142
  if (assistantId === "self") return "self";
168
143
 
169
- try {
170
- const lockData = readLockfile();
171
- const assistants = lockData?.assistants as
172
- | Array<Record<string, unknown>>
173
- | undefined;
174
- if (assistants) {
175
- for (const entry of assistants) {
176
- if (entry.assistantId === assistantId) return "self";
177
- }
178
- }
179
- } catch {
180
- // lockfile unreadable — return as-is
181
- }
144
+ const ownName = process.env.VELLUM_ASSISTANT_NAME;
145
+ if (ownName && assistantId === ownName) return "self";
182
146
 
183
147
  return assistantId;
184
148
  }
185
149
 
186
- /**
187
- * Write data to the primary lockfile (~/.vellum.lock.json).
188
- * Respects BASE_DATA_DIR for non-standard home directories.
189
- */
190
- export function writeLockfile(data: Record<string, unknown>): void {
191
- const base = getBaseDataDir() || homedir();
192
- writeFileSync(
193
- join(base, ".vellum.lock.json"),
194
- JSON.stringify(data, null, 2) + "\n",
195
- );
196
- }
197
150
 
198
151
  /**
199
152
  * Returns the root ~/.vellum directory. User-facing files (config, prompt
@@ -371,7 +324,7 @@ export function getSignalsDir(): string {
371
324
  /**
372
325
  * Returns the workspace root for user-facing state.
373
326
  *
374
- * When the WORKSPACE_DIR env var is set, returns that value (used in
327
+ * When the VELLUM_WORKSPACE_DIR env var is set, returns that value (used in
375
328
  * containerized deployments where the workspace is a separate volume).
376
329
  * Otherwise falls back to ~/.vellum/workspace.
377
330
  *
@@ -753,8 +753,11 @@ export class WorkspaceGitService {
753
753
  * Must be called with the mutex lock held.
754
754
  */
755
755
  private async ensureCommitIdentityLocked(): Promise<void> {
756
- await this.execGit(["config", "user.name", "Vellum Assistant"]);
757
- await this.execGit(["config", "user.email", "assistant@vellum.ai"]);
756
+ const gitName = process.env.ASSISTANT_GIT_USER_NAME || "Vellum Assistant";
757
+ const gitEmail =
758
+ process.env.ASSISTANT_GIT_USER_EMAIL || "assistant@vellum.ai";
759
+ await this.execGit(["config", "user.name", gitName]);
760
+ await this.execGit(["config", "user.email", gitEmail]);
758
761
  }
759
762
 
760
763
  /**
@@ -22,4 +22,19 @@ export const avatarRenameMigration: WorkspaceMigration = {
22
22
  renameSync(oldTraits, newTraits);
23
23
  }
24
24
  },
25
+ down(workspaceDir: string): void {
26
+ const avatarDir = join(workspaceDir, "data", "avatar");
27
+
28
+ const newImage = join(avatarDir, "avatar-image.png");
29
+ const oldImage = join(avatarDir, "custom-avatar.png");
30
+ if (existsSync(newImage) && !existsSync(oldImage)) {
31
+ renameSync(newImage, oldImage);
32
+ }
33
+
34
+ const newTraits = join(avatarDir, "character-traits.json");
35
+ const oldTraits = join(avatarDir, "avatar-components.json");
36
+ if (existsSync(newTraits) && !existsSync(oldTraits)) {
37
+ renameSync(newTraits, oldTraits);
38
+ }
39
+ },
25
40
  };
@@ -1,4 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ unlinkSync,
6
+ writeFileSync,
7
+ } from "node:fs";
2
8
  import { join } from "node:path";
3
9
 
4
10
  import { getDeviceIdBaseDir } from "../../util/device-id.js";
@@ -97,4 +103,14 @@ export const seedDeviceIdMigration: WorkspaceMigration = {
97
103
  // Best-effort — getDeviceId() will generate a new one if this fails.
98
104
  }
99
105
  },
106
+ down(_workspaceDir: string): void {
107
+ // The forward migration seeds deviceId in ~/.vellum/device.json from the
108
+ // lockfile. Reverse by removing device.json entirely — getDeviceId() will
109
+ // generate a fresh one on next startup if needed.
110
+ const base = getDeviceIdBaseDir();
111
+ const devicePath = join(base, ".vellum", "device.json");
112
+ if (existsSync(devicePath)) {
113
+ unlinkSync(devicePath);
114
+ }
115
+ },
100
116
  };
@@ -45,6 +45,39 @@ export const extractCollectUsageDataMigration: WorkspaceMigration = {
45
45
  delete config.assistantFeatureFlagValues;
46
46
  }
47
47
 
48
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
49
+ },
50
+ down(workspaceDir: string): void {
51
+ const configPath = join(workspaceDir, "config.json");
52
+ if (!existsSync(configPath)) return;
53
+
54
+ let config: Record<string, unknown>;
55
+ try {
56
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
57
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
58
+ config = raw as Record<string, unknown>;
59
+ } catch {
60
+ return;
61
+ }
62
+
63
+ // Only reverse if collectUsageData was explicitly set to false
64
+ // (the forward migration only persisted false).
65
+ if (!("collectUsageData" in config)) return;
66
+ const value = config.collectUsageData;
67
+ if (typeof value !== "boolean") return;
68
+
69
+ // Restore the feature flag value
70
+ const FLAG_KEY = "feature_flags.collect-usage-data.enabled";
71
+ const flagValues = (config.assistantFeatureFlagValues ?? {}) as Record<
72
+ string,
73
+ unknown
74
+ >;
75
+ flagValues[FLAG_KEY] = value;
76
+ config.assistantFeatureFlagValues = flagValues;
77
+
78
+ // Remove the extracted top-level key
79
+ delete config.collectUsageData;
80
+
48
81
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
49
82
  },
50
83
  };
@@ -9,4 +9,7 @@ export const addSendDiagnosticsMigration: WorkspaceMigration = {
9
9
  // will sync the UserDefaults value on first startup. This migration exists
10
10
  // as a checkpoint marker for future reference.
11
11
  },
12
+ down(_workspaceDir: string): void {
13
+ // No-op — the forward migration is a checkpoint marker with no data changes.
14
+ },
12
15
  };
@@ -132,6 +132,55 @@ export const servicesConfigMigration: WorkspaceMigration = {
132
132
  delete config.imageGenModel;
133
133
  delete config.webSearchProvider;
134
134
 
135
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
136
+ },
137
+ down(workspaceDir: string): void {
138
+ const configPath = join(workspaceDir, "config.json");
139
+ if (!existsSync(configPath)) return;
140
+
141
+ let config: Record<string, unknown>;
142
+ try {
143
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
144
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
145
+ config = raw as Record<string, unknown>;
146
+ } catch {
147
+ return;
148
+ }
149
+
150
+ const services = config.services;
151
+ if (!services || typeof services !== "object" || Array.isArray(services))
152
+ return;
153
+
154
+ const svc = services as Record<string, Record<string, unknown>>;
155
+
156
+ // Extract inference provider and model back to top-level fields.
157
+ // Note: inferenceMode is lost in this rollback — the original config did
158
+ // not store a mode field. This is an accepted lossy reversal.
159
+ if (svc.inference) {
160
+ if (typeof svc.inference.provider === "string") {
161
+ config.provider = svc.inference.provider;
162
+ }
163
+ if (typeof svc.inference.model === "string") {
164
+ config.model = svc.inference.model;
165
+ }
166
+ }
167
+
168
+ // Extract image generation model back to top-level
169
+ if (svc["image-generation"]) {
170
+ if (typeof svc["image-generation"].model === "string") {
171
+ config.imageGenModel = svc["image-generation"].model;
172
+ }
173
+ }
174
+
175
+ // Extract web search provider back to top-level
176
+ if (svc["web-search"]) {
177
+ if (typeof svc["web-search"].provider === "string") {
178
+ config.webSearchProvider = svc["web-search"].provider;
179
+ }
180
+ }
181
+
182
+ delete config.services;
183
+
135
184
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
136
185
  },
137
186
  };
@@ -34,4 +34,31 @@ export const webSearchProviderRenameMigration: WorkspaceMigration = {
34
34
  ws.provider = "inference-provider-native";
35
35
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
36
36
  },
37
+ down(workspaceDir: string): void {
38
+ const configPath = join(workspaceDir, "config.json");
39
+ if (!existsSync(configPath)) return;
40
+
41
+ let config: Record<string, unknown>;
42
+ try {
43
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
44
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
45
+ config = raw as Record<string, unknown>;
46
+ } catch {
47
+ return;
48
+ }
49
+
50
+ const services = config.services;
51
+ if (!services || typeof services !== "object" || Array.isArray(services))
52
+ return;
53
+
54
+ const webSearch = (services as Record<string, unknown>)["web-search"];
55
+ if (!webSearch || typeof webSearch !== "object" || Array.isArray(webSearch))
56
+ return;
57
+
58
+ const ws = webSearch as Record<string, unknown>;
59
+ if (ws.provider !== "inference-provider-native") return;
60
+
61
+ ws.provider = "anthropic-native";
62
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
63
+ },
37
64
  };
@@ -9,4 +9,7 @@ export const voiceTimeoutAndMaxStepsMigration: WorkspaceMigration = {
9
9
  // Existing users: macOS client will sync UserDefaults values
10
10
  // to config on next startup via settings sync endpoints.
11
11
  },
12
+ down(_workspaceDir: string): void {
13
+ // No-op — the forward migration is a checkpoint marker with no data changes.
14
+ },
12
15
  };
@@ -7,4 +7,8 @@ export const backfillConversationDiskViewMigration: WorkspaceMigration = {
7
7
  run(_workspaceDir: string): void {
8
8
  rebuildConversationDiskViewFromDb();
9
9
  },
10
+ // No-op: the disk view is a derived cache that can be regenerated from the
11
+ // database at any time. Removing it would only cause unnecessary I/O churn
12
+ // since the next forward migration (or startup rebuild) will recreate it.
13
+ down(_workspaceDir: string): void {},
10
14
  };
@@ -76,6 +76,84 @@ export const appDirRenameMigration: WorkspaceMigration = {
76
76
  description:
77
77
  "Rename UUID-based app directories and files to human-readable slugified names",
78
78
 
79
+ down(workspaceDir: string): void {
80
+ const appsDir = join(workspaceDir, "data", "apps");
81
+ if (!existsSync(appsDir)) return;
82
+
83
+ const jsonFiles = readdirSync(appsDir)
84
+ .filter((f) => f.endsWith(".json"))
85
+ .sort();
86
+
87
+ if (jsonFiles.length === 0) return;
88
+
89
+ for (const jsonFile of jsonFiles) {
90
+ const jsonPath = join(appsDir, jsonFile);
91
+ let raw: string;
92
+ try {
93
+ raw = readFileSync(jsonPath, "utf-8");
94
+ } catch {
95
+ continue;
96
+ }
97
+
98
+ let parsed: {
99
+ id?: string;
100
+ name?: string;
101
+ dirName?: string;
102
+ };
103
+ try {
104
+ parsed = JSON.parse(raw);
105
+ } catch {
106
+ continue;
107
+ }
108
+
109
+ const appId = parsed.id;
110
+ if (!appId || !parsed.dirName || !isValidDirName(parsed.dirName)) {
111
+ continue;
112
+ }
113
+
114
+ const dirName = parsed.dirName;
115
+
116
+ // 1. Rename the app directory: {dirName}/ -> {appId}/
117
+ const slugDir = join(appsDir, dirName);
118
+ const uuidDir = join(appsDir, appId);
119
+ if (existsSync(slugDir) && !existsSync(uuidDir) && slugDir !== uuidDir) {
120
+ renameSync(slugDir, uuidDir);
121
+ }
122
+
123
+ // 2. Rename the preview file: {dirName}.preview -> {appId}.preview
124
+ const slugPreview = join(appsDir, `${dirName}.preview`);
125
+ const uuidPreview = join(appsDir, `${appId}.preview`);
126
+ if (
127
+ existsSync(slugPreview) &&
128
+ !existsSync(uuidPreview) &&
129
+ slugPreview !== uuidPreview
130
+ ) {
131
+ renameSync(slugPreview, uuidPreview);
132
+ }
133
+
134
+ // 3. Remove dirName from JSON and rename file: {dirName}.json -> {appId}.json
135
+ const updatedParsed = { ...parsed };
136
+ delete updatedParsed.dirName;
137
+ const updatedJson = JSON.stringify(updatedParsed, null, 2);
138
+
139
+ const uuidJsonFile = `${appId}.json`;
140
+ const uuidJsonPath = join(appsDir, uuidJsonFile);
141
+
142
+ if (jsonFile !== uuidJsonFile) {
143
+ writeFileSync(uuidJsonPath, updatedJson, "utf-8");
144
+ if (existsSync(jsonPath) && jsonPath !== uuidJsonPath) {
145
+ try {
146
+ unlinkSync(jsonPath);
147
+ } catch {
148
+ // Old file cleanup is best-effort
149
+ }
150
+ }
151
+ } else {
152
+ writeFileSync(uuidJsonPath, updatedJson, "utf-8");
153
+ }
154
+ }
155
+ },
156
+
79
157
  run(workspaceDir: string): void {
80
158
  const appsDir = join(workspaceDir, "data", "apps");
81
159
  if (!existsSync(appsDir)) return;
@@ -14,6 +14,17 @@ export const backfillInstallationIdMigration: WorkspaceMigration = {
14
14
  id: "011-backfill-installation-id",
15
15
  description:
16
16
  "Backfill installationId into lockfile from SQLite checkpoint and clean up stale row",
17
+
18
+ down(_workspaceDir: string): void {
19
+ // The forward migration moved an installationId from a SQLite checkpoint
20
+ // into the lockfile entry. Rolling back by removing installationId from
21
+ // the lockfile would break telemetry continuity and the field is harmless
22
+ // to leave in place. The SQLite checkpoint was already deleted and
23
+ // cannot be restored.
24
+ //
25
+ // No-op: leaving installationId in the lockfile is safe and non-disruptive.
26
+ },
27
+
17
28
  run(_workspaceDir: string): void {
18
29
  // a. Read existing installation ID from SQLite, or generate a new one.
19
30
  // On fresh installs the memory_checkpoints table may not exist yet,