@vellumai/assistant 0.5.6 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +1 -1
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +0 -114
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +1 -1
  26. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  27. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  28. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  29. package/src/__tests__/conversation-title-service.test.ts +87 -0
  30. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  31. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  32. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  33. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  34. package/src/__tests__/credentials-cli.test.ts +78 -0
  35. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  36. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  38. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  39. package/src/__tests__/host-shell-tool.test.ts +6 -7
  40. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  42. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  43. package/src/__tests__/intent-routing.test.ts +0 -13
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  45. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  46. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  47. package/src/__tests__/migration-export-http.test.ts +2 -2
  48. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  49. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  50. package/src/__tests__/migration-validate-http.test.ts +2 -2
  51. package/src/__tests__/non-member-access-request.test.ts +0 -5
  52. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  53. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  54. package/src/__tests__/permission-types.test.ts +1 -0
  55. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  56. package/src/__tests__/qdrant-manager.test.ts +28 -2
  57. package/src/__tests__/registry.test.ts +0 -6
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  60. package/src/__tests__/secure-keys.test.ts +83 -263
  61. package/src/__tests__/shell-identity.test.ts +96 -6
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  63. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  64. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  65. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  66. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  67. package/src/__tests__/skill-load-tool.test.ts +0 -2
  68. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  69. package/src/__tests__/skills.test.ts +0 -2
  70. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  71. package/src/__tests__/suggestion-routes.test.ts +1 -32
  72. package/src/__tests__/system-prompt.test.ts +0 -1
  73. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  74. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  75. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  76. package/src/__tests__/update-bulletin.test.ts +0 -2
  77. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  78. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  79. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  80. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  81. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  82. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  83. package/src/calls/audio-store.test.ts +97 -0
  84. package/src/calls/audio-store.ts +205 -0
  85. package/src/calls/call-controller.ts +85 -7
  86. package/src/calls/call-domain.ts +3 -0
  87. package/src/calls/call-store.ts +10 -3
  88. package/src/calls/fish-audio-client.ts +117 -0
  89. package/src/calls/relay-server.ts +27 -0
  90. package/src/calls/twilio-routes.ts +2 -1
  91. package/src/calls/types.ts +1 -0
  92. package/src/calls/voice-ingress-preflight.ts +0 -42
  93. package/src/calls/voice-quality.ts +26 -5
  94. package/src/calls/voice-session-bridge.ts +6 -12
  95. package/src/cli/commands/config.ts +1 -4
  96. package/src/cli/commands/credentials.ts +34 -4
  97. package/src/cli/commands/oauth/index.ts +7 -0
  98. package/src/cli/commands/oauth/platform.ts +179 -0
  99. package/src/cli/commands/platform.ts +3 -3
  100. package/src/config/assistant-feature-flags.ts +186 -5
  101. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  102. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  103. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  104. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  105. package/src/config/bundled-tool-registry.ts +1 -11
  106. package/src/config/env-registry.ts +1 -1
  107. package/src/config/env.ts +8 -14
  108. package/src/config/feature-flag-registry.json +48 -8
  109. package/src/config/loader.ts +98 -31
  110. package/src/config/schema.ts +4 -13
  111. package/src/config/schemas/calls.ts +13 -0
  112. package/src/config/schemas/fish-audio.ts +39 -0
  113. package/src/config/schemas/security.ts +0 -4
  114. package/src/config/types.ts +0 -1
  115. package/src/contacts/contact-store.ts +39 -0
  116. package/src/contacts/types.ts +2 -0
  117. package/src/credential-execution/approval-bridge.ts +1 -0
  118. package/src/credential-execution/executable-discovery.ts +28 -4
  119. package/src/credential-execution/feature-gates.ts +16 -0
  120. package/src/credential-execution/process-manager.ts +38 -0
  121. package/src/daemon/assistant-attachments.ts +9 -0
  122. package/src/daemon/config-watcher.ts +5 -0
  123. package/src/daemon/conversation-tool-setup.ts +0 -105
  124. package/src/daemon/conversation.ts +10 -1
  125. package/src/daemon/handlers/config-vercel.ts +92 -0
  126. package/src/daemon/handlers/skills.ts +2 -15
  127. package/src/daemon/install-symlink.ts +195 -0
  128. package/src/daemon/lifecycle.ts +227 -51
  129. package/src/daemon/message-types/conversations.ts +3 -4
  130. package/src/daemon/message-types/diagnostics.ts +3 -22
  131. package/src/daemon/message-types/messages.ts +0 -2
  132. package/src/daemon/message-types/upgrades.ts +8 -0
  133. package/src/daemon/server.ts +30 -92
  134. package/src/events/domain-events.ts +2 -1
  135. package/src/inbound/platform-callback-registration.ts +3 -3
  136. package/src/instrument.ts +8 -5
  137. package/src/memory/conversation-title-service.ts +50 -1
  138. package/src/memory/db-init.ts +12 -0
  139. package/src/memory/items-extractor.ts +15 -1
  140. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  141. package/src/memory/jobs-store.ts +30 -5
  142. package/src/memory/jobs-worker.ts +31 -7
  143. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  144. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  145. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  146. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  147. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  148. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  149. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  150. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  151. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  152. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  153. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  154. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  155. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  156. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  157. package/src/memory/migrations/116-messages-fts.ts +106 -1
  158. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  160. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  161. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  162. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  163. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  164. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  165. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  166. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  167. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  168. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  169. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  170. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  171. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  172. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  173. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  174. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  175. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  176. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  177. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  178. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  179. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  180. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  181. package/src/memory/migrations/index.ts +4 -0
  182. package/src/memory/migrations/registry.ts +90 -0
  183. package/src/memory/migrations/validate-migration-state.ts +137 -11
  184. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  185. package/src/memory/qdrant-manager.ts +64 -7
  186. package/src/memory/schema/calls.ts +1 -0
  187. package/src/memory/schema/contacts.ts +1 -0
  188. package/src/notifications/decision-engine.ts +4 -1
  189. package/src/oauth/connection-resolver.ts +6 -4
  190. package/src/permissions/checker.ts +0 -38
  191. package/src/permissions/shell-identity.ts +76 -22
  192. package/src/permissions/types.ts +4 -2
  193. package/src/platform/client.ts +35 -7
  194. package/src/prompts/persona-resolver.ts +138 -0
  195. package/src/prompts/system-prompt.ts +36 -4
  196. package/src/prompts/templates/users/default.md +1 -0
  197. package/src/providers/registry.ts +27 -40
  198. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  199. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  200. package/src/runtime/auth/external-assistant-id.ts +13 -59
  201. package/src/runtime/auth/route-policy.ts +15 -1
  202. package/src/runtime/auth/token-service.ts +43 -138
  203. package/src/runtime/channel-readiness-service.ts +1 -16
  204. package/src/runtime/http-server.ts +27 -2
  205. package/src/runtime/middleware/error-handler.ts +1 -9
  206. package/src/runtime/routes/audio-routes.ts +40 -0
  207. package/src/runtime/routes/btw-routes.ts +0 -17
  208. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  209. package/src/runtime/routes/conversation-routes.ts +4 -44
  210. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  211. package/src/runtime/routes/identity-routes.ts +18 -29
  212. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  213. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  214. package/src/runtime/routes/integrations/vercel.ts +89 -0
  215. package/src/runtime/routes/log-export-routes.ts +5 -0
  216. package/src/runtime/routes/memory-item-routes.ts +24 -6
  217. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  218. package/src/runtime/routes/migration-routes.ts +17 -1
  219. package/src/runtime/routes/notification-routes.ts +58 -0
  220. package/src/runtime/routes/schedule-routes.ts +65 -0
  221. package/src/runtime/routes/settings-routes.ts +41 -1
  222. package/src/runtime/routes/tts-routes.ts +86 -0
  223. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  224. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  225. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  226. package/src/runtime/routes/workspace-routes.ts +1 -1
  227. package/src/runtime/routes/workspace-utils.ts +86 -2
  228. package/src/security/ces-credential-client.ts +59 -22
  229. package/src/security/ces-rpc-credential-backend.ts +85 -0
  230. package/src/security/credential-backend.ts +12 -88
  231. package/src/security/keychain-broker-client.ts +10 -2
  232. package/src/security/secure-keys.ts +94 -113
  233. package/src/skills/catalog-install.ts +13 -7
  234. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  235. package/src/tools/calls/call-start.ts +1 -0
  236. package/src/tools/executor.ts +0 -4
  237. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  238. package/src/tools/network/web-fetch.ts +3 -1
  239. package/src/tools/skills/execute.ts +1 -1
  240. package/src/tools/types.ts +0 -8
  241. package/src/util/errors.ts +0 -12
  242. package/src/util/platform.ts +3 -50
  243. package/src/workspace/git-service.ts +5 -2
  244. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  245. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  246. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  247. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  248. package/src/workspace/migrations/006-services-config.ts +49 -0
  249. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  250. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  251. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  252. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  253. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  254. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  255. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  256. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  257. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  258. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  259. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  260. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  261. package/src/workspace/migrations/registry.ts +8 -0
  262. package/src/workspace/migrations/runner.ts +106 -2
  263. package/src/workspace/migrations/types.ts +4 -0
  264. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  265. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  266. package/src/__tests__/diagnostics-export.test.ts +0 -288
  267. package/src/__tests__/local-gateway-health.test.ts +0 -209
  268. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  269. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  270. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  271. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  272. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  273. package/src/__tests__/swarm-recursion.test.ts +0 -197
  274. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  275. package/src/__tests__/swarm-tool.test.ts +0 -185
  276. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  277. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  278. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  279. package/src/commands/cc-command-registry.ts +0 -248
  280. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  281. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  282. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  283. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  284. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  285. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  286. package/src/config/schemas/swarm.ts +0 -82
  287. package/src/logfire.ts +0 -135
  288. package/src/runtime/local-gateway-health.ts +0 -275
  289. package/src/security/secret-ingress.ts +0 -68
  290. package/src/swarm/backend-claude-code.ts +0 -225
  291. package/src/swarm/checkpoint.ts +0 -137
  292. package/src/swarm/graph-utils.ts +0 -53
  293. package/src/swarm/index.ts +0 -55
  294. package/src/swarm/limits.ts +0 -66
  295. package/src/swarm/orchestrator.ts +0 -424
  296. package/src/swarm/plan-validator.ts +0 -117
  297. package/src/swarm/router-planner.ts +0 -162
  298. package/src/swarm/router-prompts.ts +0 -39
  299. package/src/swarm/synthesizer.ts +0 -81
  300. package/src/swarm/types.ts +0 -72
  301. package/src/swarm/worker-backend.ts +0 -131
  302. package/src/swarm/worker-prompts.ts +0 -80
  303. package/src/swarm/worker-runner.ts +0 -170
  304. package/src/tools/claude-code/claude-code.ts +0 -610
  305. package/src/tools/swarm/delegate.ts +0 -205
@@ -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,
@@ -17,6 +17,10 @@ import type { WorkspaceMigration } from "./types.js";
17
17
  const LEGACY_CONVERSATION_DIR_PATTERN =
18
18
  /^(.*)_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z)$/;
19
19
 
20
+ /** Matches the new timestamp-first format: {timestamp}_{conversationId} */
21
+ const NEW_CONVERSATION_DIR_PATTERN =
22
+ /^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z)_(.+)$/;
23
+
20
24
  function parseLegacyConversationDirName(
21
25
  dirName: string,
22
26
  ): { conversationId: string; timestamp: string } | null {
@@ -29,11 +33,51 @@ function parseLegacyConversationDirName(
29
33
  };
30
34
  }
31
35
 
36
+ function parseNewConversationDirName(
37
+ dirName: string,
38
+ ): { timestamp: string; conversationId: string } | null {
39
+ const match = dirName.match(NEW_CONVERSATION_DIR_PATTERN);
40
+ if (!match) return null;
41
+
42
+ return {
43
+ timestamp: match[1],
44
+ conversationId: match[2],
45
+ };
46
+ }
47
+
32
48
  export const renameConversationDiskViewDirsMigration: WorkspaceMigration = {
33
49
  id: "012-rename-conversation-disk-view-dirs",
34
50
  description:
35
51
  "Rename legacy conversation disk-view directories to timestamp-first names",
36
52
 
53
+ down(workspaceDir: string): void {
54
+ const conversationsDir = join(workspaceDir, "conversations");
55
+ if (!existsSync(conversationsDir)) return;
56
+
57
+ const entries = readdirSync(conversationsDir, { withFileTypes: true })
58
+ .filter((entry) => entry.isDirectory())
59
+ .map((entry) => entry.name)
60
+ .sort();
61
+
62
+ for (const dirName of entries) {
63
+ const parsed = parseNewConversationDirName(dirName);
64
+ if (!parsed) continue;
65
+
66
+ const sourcePath = join(conversationsDir, dirName);
67
+ const targetName = `${parsed.conversationId}_${parsed.timestamp}`;
68
+ const targetPath = join(conversationsDir, targetName);
69
+
70
+ if (sourcePath === targetPath) continue;
71
+ if (existsSync(targetPath)) continue;
72
+
73
+ try {
74
+ renameSync(sourcePath, targetPath);
75
+ } catch {
76
+ // Best-effort: leave the directory in place if a single rename fails.
77
+ }
78
+ }
79
+ },
80
+
37
81
  run(workspaceDir: string): void {
38
82
  const conversationsDir = join(workspaceDir, "conversations");
39
83
  if (!existsSync(conversationsDir)) return;
@@ -8,4 +8,9 @@ export const repairConversationDiskViewMigration: WorkspaceMigration = {
8
8
  run(_workspaceDir: string): void {
9
9
  rebuildConversationDiskViewFromDb();
10
10
  },
11
+ // No-op: this is a repair migration that rebuilds derived disk-view data
12
+ // from the database. There is no meaningful reverse operation — the data
13
+ // is a cache that can be regenerated, and removing it would just cause
14
+ // unnecessary churn on the next forward run.
15
+ down(_workspaceDir: string): void {},
11
16
  };
@@ -0,0 +1,153 @@
1
+ import { getLogger } from "../../util/logger.js";
2
+ import type { WorkspaceMigration } from "./types.js";
3
+
4
+ const log = getLogger("workspace-migrations");
5
+
6
+ const BROKER_WAIT_INTERVAL_MS = 500;
7
+ const BROKER_WAIT_MAX_ATTEMPTS = 10; // 5 seconds total
8
+
9
+ export const migrateCredentialsToKeychainMigration: WorkspaceMigration = {
10
+ id: "015-migrate-credentials-to-keychain",
11
+ description:
12
+ "Copy encrypted store credentials to keychain for single-backend migration",
13
+
14
+ async down(_workspaceDir: string): Promise<void> {
15
+ // Reverse: copy credentials from keychain back to encrypted store.
16
+ // Mirrors the forward logic of 016-migrate-credentials-from-keychain.
17
+ if (
18
+ process.env.VELLUM_DESKTOP_APP !== "1" ||
19
+ process.env.VELLUM_DEV === "1"
20
+ ) {
21
+ return;
22
+ }
23
+
24
+ const { createBrokerClient } =
25
+ await import("../../security/keychain-broker-client.js");
26
+ const client = createBrokerClient();
27
+
28
+ let brokerAvailable = false;
29
+ for (let i = 0; i < BROKER_WAIT_MAX_ATTEMPTS; i++) {
30
+ if (client.isAvailable()) {
31
+ brokerAvailable = true;
32
+ break;
33
+ }
34
+ await new Promise((r) => setTimeout(r, BROKER_WAIT_INTERVAL_MS));
35
+ }
36
+
37
+ if (!brokerAvailable) {
38
+ throw new Error(
39
+ "Keychain broker not available after waiting — credential rollback " +
40
+ "will be retried on next startup",
41
+ );
42
+ }
43
+
44
+ const { setKey } = await import("../../security/encrypted-store.js");
45
+
46
+ const accounts = await client.list();
47
+ if (accounts.length === 0) return;
48
+
49
+ let rolledBackCount = 0;
50
+ let failedCount = 0;
51
+
52
+ for (const account of accounts) {
53
+ const result = await client.get(account);
54
+ if (!result || !result.found || result.value === undefined) {
55
+ log.warn(
56
+ { account },
57
+ "Failed to read key from keychain during rollback — skipping",
58
+ );
59
+ failedCount++;
60
+ continue;
61
+ }
62
+
63
+ const written = setKey(account, result.value);
64
+ if (written) {
65
+ await client.del(account);
66
+ rolledBackCount++;
67
+ } else {
68
+ log.warn(
69
+ { account },
70
+ "Failed to write key to encrypted store during rollback — skipping",
71
+ );
72
+ failedCount++;
73
+ }
74
+ }
75
+
76
+ log.info(
77
+ { rolledBackCount, failedCount },
78
+ "Credential rollback from keychain to encrypted store complete",
79
+ );
80
+ },
81
+
82
+ async run(_workspaceDir: string): Promise<void> {
83
+ // Only run on mac production builds (desktop app, non-dev).
84
+ if (
85
+ process.env.VELLUM_DESKTOP_APP !== "1" ||
86
+ process.env.VELLUM_DEV === "1"
87
+ ) {
88
+ return;
89
+ }
90
+
91
+ const { createBrokerClient } =
92
+ await import("../../security/keychain-broker-client.js");
93
+ const client = createBrokerClient();
94
+
95
+ // Wait for the broker to become available (up to 5 seconds), matching
96
+ // the retry strategy in secure-keys.ts waitForBrokerAvailability().
97
+ let brokerAvailable = false;
98
+ for (let i = 0; i < BROKER_WAIT_MAX_ATTEMPTS; i++) {
99
+ if (client.isAvailable()) {
100
+ brokerAvailable = true;
101
+ break;
102
+ }
103
+ await new Promise((r) => setTimeout(r, BROKER_WAIT_INTERVAL_MS));
104
+ }
105
+
106
+ if (!brokerAvailable) {
107
+ throw new Error(
108
+ "Keychain broker not available after waiting — credential migration " +
109
+ "will be retried on next startup",
110
+ );
111
+ }
112
+
113
+ const { listKeys, getKey, deleteKey } =
114
+ await import("../../security/encrypted-store.js");
115
+
116
+ const accounts = listKeys();
117
+ if (accounts.length === 0) {
118
+ return;
119
+ }
120
+
121
+ let migratedCount = 0;
122
+ let failedCount = 0;
123
+
124
+ for (const account of accounts) {
125
+ const value = getKey(account);
126
+ if (value === undefined) {
127
+ log.warn(
128
+ { account },
129
+ "Failed to read key from encrypted store — skipping",
130
+ );
131
+ failedCount++;
132
+ continue;
133
+ }
134
+
135
+ const result = await client.set(account, value);
136
+ if (result.status === "ok") {
137
+ deleteKey(account);
138
+ migratedCount++;
139
+ } else {
140
+ log.warn(
141
+ { account, status: result.status },
142
+ "Failed to write key to keychain — skipping",
143
+ );
144
+ failedCount++;
145
+ }
146
+ }
147
+
148
+ log.info(
149
+ { migratedCount, failedCount },
150
+ "Credential migration to keychain complete",
151
+ );
152
+ },
153
+ };
@@ -0,0 +1,156 @@
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ import { getRootDir } from "../../util/platform.js";
13
+ import type { WorkspaceMigration } from "./types.js";
14
+
15
+ export const extractFeatureFlagsToProtectedMigration: WorkspaceMigration = {
16
+ id: "016-extract-feature-flags-to-protected",
17
+ description:
18
+ "Move assistantFeatureFlagValues from config.json to ~/.vellum/protected/feature-flags.json",
19
+
20
+ down(workspaceDir: string): void {
21
+ // Reverse: read feature flags from protected directory and write them
22
+ // back to config.json as assistantFeatureFlagValues.
23
+ const protectedDir = join(getRootDir(), "protected");
24
+ const featureFlagsPath = join(protectedDir, "feature-flags.json");
25
+
26
+ if (!existsSync(featureFlagsPath)) return;
27
+
28
+ let flagValues: Record<string, boolean>;
29
+ try {
30
+ const raw = JSON.parse(readFileSync(featureFlagsPath, "utf-8"));
31
+ if (
32
+ !raw ||
33
+ raw.version !== 1 ||
34
+ !raw.values ||
35
+ typeof raw.values !== "object"
36
+ ) {
37
+ return;
38
+ }
39
+ flagValues = raw.values;
40
+ } catch {
41
+ return; // Malformed file — skip
42
+ }
43
+
44
+ if (Object.keys(flagValues).length === 0) return;
45
+
46
+ // Read config.json and restore assistantFeatureFlagValues
47
+ const configPath = join(workspaceDir, "config.json");
48
+ let config: Record<string, unknown> = {};
49
+ if (existsSync(configPath)) {
50
+ try {
51
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
52
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
53
+ config = raw as Record<string, unknown>;
54
+ }
55
+ } catch {
56
+ // Malformed config — start with empty object
57
+ }
58
+ }
59
+
60
+ // Merge into existing assistantFeatureFlagValues if present
61
+ const existing = (config.assistantFeatureFlagValues ?? {}) as Record<
62
+ string,
63
+ boolean
64
+ >;
65
+ config.assistantFeatureFlagValues = { ...existing, ...flagValues };
66
+
67
+ const tmpConfigPath = configPath + ".tmp";
68
+ writeFileSync(
69
+ tmpConfigPath,
70
+ JSON.stringify(config, null, 2) + "\n",
71
+ "utf-8",
72
+ );
73
+ renameSync(tmpConfigPath, configPath);
74
+
75
+ // Remove the protected feature-flags file
76
+ try {
77
+ unlinkSync(featureFlagsPath);
78
+ } catch {
79
+ // Best-effort cleanup
80
+ }
81
+ },
82
+
83
+ run(workspaceDir: string): void {
84
+ const configPath = join(workspaceDir, "config.json");
85
+ if (!existsSync(configPath)) return;
86
+
87
+ let config: Record<string, unknown>;
88
+ try {
89
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
90
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
91
+ config = raw as Record<string, unknown>;
92
+ } catch {
93
+ return; // Malformed config — skip
94
+ }
95
+
96
+ const flagValues = config.assistantFeatureFlagValues as
97
+ | Record<string, boolean>
98
+ | undefined;
99
+ if (
100
+ !flagValues ||
101
+ typeof flagValues !== "object" ||
102
+ Object.keys(flagValues).length === 0
103
+ ) {
104
+ return; // Nothing to migrate
105
+ }
106
+
107
+ // Write feature flags to protected directory
108
+ const protectedDir = join(getRootDir(), "protected");
109
+ mkdirSync(protectedDir, { recursive: true });
110
+
111
+ const featureFlagsPath = join(protectedDir, "feature-flags.json");
112
+
113
+ // Read existing feature-flags.json if present (may have been written by
114
+ // the gateway in a rolling deployment) so we merge rather than overwrite.
115
+ let existingValues: Record<string, boolean> = {};
116
+ if (existsSync(featureFlagsPath)) {
117
+ try {
118
+ const existing = JSON.parse(readFileSync(featureFlagsPath, "utf-8"));
119
+ if (
120
+ existing.version === 1 &&
121
+ existing.values &&
122
+ typeof existing.values === "object"
123
+ ) {
124
+ existingValues = existing.values;
125
+ }
126
+ } catch {
127
+ // Malformed file — start fresh
128
+ }
129
+ }
130
+
131
+ // Merge: config values take precedence, existing keys preserved
132
+ const mergedValues = { ...existingValues, ...flagValues };
133
+
134
+ const featureFlagsContent = JSON.stringify(
135
+ { version: 1, values: mergedValues },
136
+ null,
137
+ 2,
138
+ );
139
+
140
+ const tmpFeatureFlagsPath = featureFlagsPath + ".tmp";
141
+ writeFileSync(tmpFeatureFlagsPath, featureFlagsContent + "\n", "utf-8");
142
+ chmodSync(tmpFeatureFlagsPath, 0o600);
143
+ renameSync(tmpFeatureFlagsPath, featureFlagsPath);
144
+
145
+ // Remove assistantFeatureFlagValues from config.json
146
+ delete config.assistantFeatureFlagValues;
147
+
148
+ const tmpConfigPath = configPath + ".tmp";
149
+ writeFileSync(
150
+ tmpConfigPath,
151
+ JSON.stringify(config, null, 2) + "\n",
152
+ "utf-8",
153
+ );
154
+ renameSync(tmpConfigPath, configPath);
155
+ },
156
+ };