@vellumai/assistant 0.5.0 → 0.5.2

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 (347) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -9
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-queue.test.ts +36 -1
  37. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  38. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  39. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  41. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  42. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  43. package/src/__tests__/conversation-store.test.ts +24 -21
  44. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  45. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  46. package/src/__tests__/conversation-title-service.test.ts +137 -0
  47. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  48. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  49. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  51. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  52. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  53. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  54. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  55. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  56. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  57. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  58. package/src/__tests__/diagnostics-export.test.ts +70 -1
  59. package/src/__tests__/filesystem-tools.test.ts +4 -2
  60. package/src/__tests__/first-greeting.test.ts +80 -0
  61. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  63. package/src/__tests__/history-repair.test.ts +103 -10
  64. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  65. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  66. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  67. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  68. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  69. package/src/__tests__/media-generate-image.test.ts +47 -94
  70. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  71. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  72. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  73. package/src/__tests__/migration-export-http.test.ts +3 -1
  74. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  75. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  76. package/src/__tests__/mime-builder.test.ts +3 -2
  77. package/src/__tests__/non-member-access-request.test.ts +12 -1
  78. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  80. package/src/__tests__/oauth-store.test.ts +115 -0
  81. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  82. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  83. package/src/__tests__/recording-handler.test.ts +17 -0
  84. package/src/__tests__/registry.test.ts +3 -8
  85. package/src/__tests__/relay-server.test.ts +1 -1
  86. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  87. package/src/__tests__/schema-transforms.test.ts +165 -5
  88. package/src/__tests__/server-history-render.test.ts +2 -2
  89. package/src/__tests__/skill-feature-flags-integration.test.ts +18 -17
  90. package/src/__tests__/skill-feature-flags.test.ts +13 -13
  91. package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
  92. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  93. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  94. package/src/__tests__/starter-task-flow.test.ts +1 -0
  95. package/src/__tests__/suggestion-routes.test.ts +443 -0
  96. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  97. package/src/__tests__/swarm-recursion.test.ts +1 -0
  98. package/src/__tests__/swarm-tool.test.ts +1 -0
  99. package/src/__tests__/system-prompt.test.ts +8 -0
  100. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  101. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  102. package/src/__tests__/top-level-renderer.test.ts +22 -0
  103. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  104. package/src/__tests__/web-fetch.test.ts +6 -2
  105. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  106. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  107. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  108. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  109. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  110. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  111. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  112. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  113. package/src/agent/attachments.ts +27 -1
  114. package/src/agent/loop.ts +29 -1
  115. package/src/avatar/traits-png-sync.ts +80 -25
  116. package/src/bundler/app-bundler.ts +4 -4
  117. package/src/calls/call-domain.ts +1 -0
  118. package/src/calls/voice-session-bridge.ts +1 -0
  119. package/src/cli/commands/auth.ts +92 -0
  120. package/src/cli/commands/avatar.ts +7 -6
  121. package/src/cli/commands/config.ts +2 -0
  122. package/src/cli/commands/oauth/providers.ts +29 -0
  123. package/src/cli/program.ts +12 -0
  124. package/src/cli.ts +15 -48
  125. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  126. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  127. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  128. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  129. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  130. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  131. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  135. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  136. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  137. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  139. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  140. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  141. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  142. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  143. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  144. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  145. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  146. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  148. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  149. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  150. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  151. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  152. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  153. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  154. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  155. package/src/config/bundled-tool-registry.ts +2 -14
  156. package/src/config/feature-flag-registry.json +16 -0
  157. package/src/config/loader.ts +64 -0
  158. package/src/config/raw-config-utils.ts +30 -0
  159. package/src/config/schema-utils.ts +28 -7
  160. package/src/config/schema.ts +8 -0
  161. package/src/config/schemas/elevenlabs.ts +18 -0
  162. package/src/config/schemas/memory-lifecycle.ts +4 -2
  163. package/src/config/schemas/memory-storage.ts +1 -1
  164. package/src/config/schemas/services.ts +8 -6
  165. package/src/contacts/contact-store.ts +13 -6
  166. package/src/contacts/contacts-write.ts +0 -1
  167. package/src/context/window-manager.ts +13 -2
  168. package/src/daemon/conversation-agent-loop-handlers.ts +46 -42
  169. package/src/daemon/conversation-agent-loop.ts +56 -19
  170. package/src/daemon/conversation-attachments.ts +18 -36
  171. package/src/daemon/conversation-error.ts +2 -1
  172. package/src/daemon/conversation-history.ts +18 -4
  173. package/src/daemon/conversation-lifecycle.ts +39 -15
  174. package/src/daemon/conversation-messaging.ts +70 -26
  175. package/src/daemon/conversation-process.ts +58 -34
  176. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  177. package/src/daemon/conversation-slash.ts +121 -256
  178. package/src/daemon/conversation-surfaces.ts +143 -20
  179. package/src/daemon/conversation-tool-setup.ts +0 -6
  180. package/src/daemon/conversation-workspace.ts +21 -1
  181. package/src/daemon/conversation.ts +51 -29
  182. package/src/daemon/first-greeting.ts +35 -0
  183. package/src/daemon/handlers/config-embeddings.ts +148 -0
  184. package/src/daemon/handlers/config-model.ts +71 -26
  185. package/src/daemon/handlers/conversations.ts +0 -23
  186. package/src/daemon/handlers/recording.ts +26 -21
  187. package/src/daemon/history-repair.ts +28 -8
  188. package/src/daemon/host-cu-proxy.ts +2 -2
  189. package/src/daemon/lifecycle.ts +106 -64
  190. package/src/daemon/message-protocol.ts +3 -0
  191. package/src/daemon/message-types/conversations.ts +19 -0
  192. package/src/daemon/message-types/messages.ts +1 -0
  193. package/src/daemon/message-types/shared.ts +2 -0
  194. package/src/daemon/message-types/surfaces.ts +2 -0
  195. package/src/daemon/message-types/upgrades.ts +23 -0
  196. package/src/daemon/server.ts +83 -12
  197. package/src/daemon/shutdown-handlers.ts +8 -5
  198. package/src/daemon/startup-error.ts +9 -0
  199. package/src/daemon/tool-side-effects.ts +11 -28
  200. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  201. package/src/instrument.ts +0 -4
  202. package/src/media/app-icon-generator.ts +2 -2
  203. package/src/memory/app-git-service.ts +28 -16
  204. package/src/memory/app-store.ts +230 -41
  205. package/src/memory/attachments-store.ts +558 -130
  206. package/src/memory/conversation-attention-store.ts +70 -0
  207. package/src/memory/conversation-crud.ts +442 -3
  208. package/src/memory/conversation-directories.ts +125 -0
  209. package/src/memory/conversation-disk-view.ts +390 -0
  210. package/src/memory/conversation-key-store.ts +17 -5
  211. package/src/memory/conversation-queries.ts +5 -1
  212. package/src/memory/conversation-title-service.ts +21 -49
  213. package/src/memory/db-init.ts +28 -0
  214. package/src/memory/embedding-backend.ts +42 -53
  215. package/src/memory/embedding-gemini.test.ts +4 -4
  216. package/src/memory/embedding-local.ts +1 -3
  217. package/src/memory/embedding-ollama.ts +1 -3
  218. package/src/memory/embedding-openai.ts +1 -3
  219. package/src/memory/indexer.ts +9 -7
  220. package/src/memory/items-extractor.ts +42 -13
  221. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  222. package/src/memory/job-handlers/embedding.test.ts +1 -4
  223. package/src/memory/llm-request-log-store.ts +100 -1
  224. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  225. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  226. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  227. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  228. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  229. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  230. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  231. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  232. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  233. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  234. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  235. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  236. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  237. package/src/memory/migrations/index.ts +7 -0
  238. package/src/memory/migrations/registry.ts +13 -0
  239. package/src/memory/retriever.test.ts +601 -2
  240. package/src/memory/retriever.ts +85 -9
  241. package/src/memory/schema/conversations.ts +6 -0
  242. package/src/memory/schema/infrastructure.ts +13 -7
  243. package/src/memory/schema/oauth.ts +6 -0
  244. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  245. package/src/notifications/copy-composer.ts +26 -0
  246. package/src/notifications/decision-engine.ts +14 -1
  247. package/src/notifications/emit-signal.ts +1 -1
  248. package/src/notifications/signal.ts +36 -0
  249. package/src/oauth/byo-connection.test.ts +1 -45
  250. package/src/oauth/byo-connection.ts +2 -8
  251. package/src/oauth/connect-orchestrator.ts +15 -11
  252. package/src/oauth/connection-resolver.test.ts +191 -0
  253. package/src/oauth/connection-resolver.ts +66 -38
  254. package/src/oauth/connection.ts +0 -1
  255. package/src/oauth/oauth-store.ts +97 -47
  256. package/src/oauth/platform-connection.test.ts +0 -1
  257. package/src/oauth/platform-connection.ts +11 -3
  258. package/src/oauth/seed-providers.ts +78 -3
  259. package/src/oauth/token-persistence.ts +16 -10
  260. package/src/permissions/checker.ts +62 -19
  261. package/src/prompts/system-prompt.ts +2 -0
  262. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  263. package/src/providers/anthropic/client.ts +8 -1
  264. package/src/providers/failover.ts +4 -1
  265. package/src/providers/gemini/client.ts +50 -0
  266. package/src/providers/model-catalog.ts +92 -0
  267. package/src/providers/model-intents.ts +29 -20
  268. package/src/providers/openai/client.ts +49 -0
  269. package/src/providers/types.ts +2 -0
  270. package/src/runtime/access-request-helper.ts +16 -7
  271. package/src/runtime/auth/credential-service.ts +3 -1
  272. package/src/runtime/auth/route-policy.ts +14 -1
  273. package/src/runtime/btw-sidechain.ts +101 -0
  274. package/src/runtime/channel-reply-delivery.ts +17 -1
  275. package/src/runtime/http-router.ts +3 -1
  276. package/src/runtime/http-server.ts +196 -141
  277. package/src/runtime/http-types.ts +1 -0
  278. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  279. package/src/runtime/routes/access-request-decision.ts +41 -0
  280. package/src/runtime/routes/app-management-routes.ts +6 -3
  281. package/src/runtime/routes/app-routes.ts +7 -3
  282. package/src/runtime/routes/approval-routes.ts +1 -0
  283. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  284. package/src/runtime/routes/attachment-routes.ts +45 -15
  285. package/src/runtime/routes/btw-routes.ts +21 -61
  286. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  287. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  288. package/src/runtime/routes/conversation-routes.ts +222 -28
  289. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  290. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  291. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  292. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  293. package/src/runtime/routes/log-export-routes.ts +3 -0
  294. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  295. package/src/runtime/routes/memory-item-routes.ts +4 -0
  296. package/src/runtime/routes/migration-routes.ts +4 -1
  297. package/src/runtime/routes/oauth-apps.ts +291 -0
  298. package/src/runtime/routes/secret-routes.ts +28 -1
  299. package/src/runtime/routes/settings-routes.ts +14 -0
  300. package/src/runtime/routes/trace-event-routes.ts +4 -1
  301. package/src/schedule/schedule-store.ts +9 -21
  302. package/src/security/secure-keys.ts +21 -0
  303. package/src/signals/bash.ts +1 -1
  304. package/src/swarm/backend-claude-code.ts +3 -6
  305. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  306. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  307. package/src/tools/AGENTS.md +6 -10
  308. package/src/tools/apps/executors.ts +17 -232
  309. package/src/tools/claude-code/claude-code.ts +2 -3
  310. package/src/tools/credentials/vault.ts +7 -12
  311. package/src/tools/host-filesystem/read.ts +13 -10
  312. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  313. package/src/tools/schedule/list.ts +2 -7
  314. package/src/tools/schema-transforms.ts +5 -0
  315. package/src/tools/shared/filesystem/format-diff.ts +4 -21
  316. package/src/tools/skills/execute.ts +1 -1
  317. package/src/tools/tool-manifest.ts +0 -6
  318. package/src/tools/ui-surface/definitions.ts +2 -2
  319. package/src/util/device-id.ts +28 -5
  320. package/src/util/platform.ts +6 -0
  321. package/src/util/pricing.ts +1 -0
  322. package/src/util/retry.ts +1 -3
  323. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  324. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  325. package/src/workspace/migrations/006-services-config.ts +5 -0
  326. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  327. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  328. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  329. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  330. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  331. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  332. package/src/workspace/migrations/registry.ts +10 -0
  333. package/src/workspace/top-level-renderer.ts +12 -0
  334. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  335. package/src/__tests__/asset-search-tool.test.ts +0 -536
  336. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  337. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  338. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  339. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  340. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  341. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  342. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  343. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  344. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  345. package/src/daemon/media-visibility-policy.ts +0 -59
  346. package/src/tools/assets/materialize.ts +0 -248
  347. package/src/tools/assets/search.ts +0 -400
@@ -381,6 +381,11 @@ export function getWorkspaceHooksDir(): string {
381
381
  return join(getWorkspaceDir(), "hooks");
382
382
  }
383
383
 
384
+ /** Returns ~/.vellum/workspace/conversations */
385
+ export function getConversationsDir(): string {
386
+ return join(getWorkspaceDir(), "conversations");
387
+ }
388
+
384
389
  /** Returns the workspace path for a prompt file (e.g. IDENTITY.md, SOUL.md, USER.md). */
385
390
  export function getWorkspacePromptPath(file: string): string {
386
391
  return join(getWorkspaceDir(), file);
@@ -399,6 +404,7 @@ export function ensureDataDir(): void {
399
404
  join(root, "hooks"),
400
405
  join(workspace, "skills"),
401
406
  join(workspace, "embedding-models"),
407
+ join(workspace, "conversations"),
402
408
  // Data sub-dirs under workspace
403
409
  wsData,
404
410
  join(wsData, "db"),
@@ -25,6 +25,7 @@ const PROVIDER_PRICING: Record<string, Record<string, ModelPricing>> = {
25
25
  },
26
26
  openai: {
27
27
  "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15 },
28
+ "gpt-5.4-mini": { inputPer1M: 0.5, outputPer1M: 3 },
28
29
  "gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25 },
29
30
  "gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14 },
30
31
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
package/src/util/retry.ts CHANGED
@@ -108,9 +108,7 @@ export function isRetryableNetworkError(error: unknown): boolean {
108
108
 
109
109
  // Fall back to message-based detection for errors without errno codes
110
110
  // (e.g. Bun's "The socket connection was closed unexpectedly")
111
- if (
112
- RETRYABLE_NETWORK_MESSAGE_PATTERNS.some((p) => p.test(error.message))
113
- ) {
111
+ if (RETRYABLE_NETWORK_MESSAGE_PATTERNS.some((p) => p.test(error.message))) {
114
112
  return true;
115
113
  }
116
114
 
@@ -16,8 +16,8 @@ export const backfillInstallationIdMigration: WorkspaceMigration = {
16
16
  "Backfill installationId into lockfile from SQLite checkpoint and clean up stale row",
17
17
  run(_workspaceDir: string): void {
18
18
  // a. Read existing installation ID from SQLite, or generate a new one.
19
- // On fresh installs the memory_checkpoints table may not exist yet
20
- // (workspace migrations run before initializeDb), so treat errors as null.
19
+ // On fresh installs the memory_checkpoints table may not exist yet,
20
+ // so treat errors as null.
21
21
  let existingId: string | null = null;
22
22
  try {
23
23
  existingId = getMemoryCheckpoint("telemetry:installation_id");
@@ -26,19 +26,30 @@ export const backfillInstallationIdMigration: WorkspaceMigration = {
26
26
  }
27
27
  const installationId = existingId || randomUUID();
28
28
 
29
- // b. Read the lockfile from the standard path
29
+ // b. Read the lockfile check both the current and legacy lockfile paths
30
+ // to support installs that haven't migrated the filename yet.
30
31
  const base = process.env.BASE_DATA_DIR?.trim() || homedir();
31
- const lockPath = join(base, ".vellum.lock.json");
32
- if (!existsSync(lockPath)) return;
32
+ const lockCandidates = [
33
+ join(base, ".vellum.lock.json"),
34
+ join(base, ".vellum.lockfile.json"),
35
+ ];
33
36
 
34
- let lockData: Record<string, unknown>;
35
- try {
36
- const raw = JSON.parse(readFileSync(lockPath, "utf-8"));
37
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
38
- lockData = raw as Record<string, unknown>;
39
- } catch {
40
- return;
37
+ let lockPath: string | undefined;
38
+ let lockData: Record<string, unknown> | undefined;
39
+ for (const candidate of lockCandidates) {
40
+ if (!existsSync(candidate)) continue;
41
+ try {
42
+ const raw = JSON.parse(readFileSync(candidate, "utf-8"));
43
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
44
+ lockPath = candidate;
45
+ lockData = raw as Record<string, unknown>;
46
+ break;
47
+ }
48
+ } catch {
49
+ // Malformed — try next candidate.
50
+ }
41
51
  }
52
+ if (!lockPath || !lockData) return;
42
53
 
43
54
  // c. Find the assistant entry that corresponds to this daemon instance
44
55
  const assistants = lockData.assistants as
@@ -1,16 +1,15 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { join } from "node:path";
4
3
 
5
- import { getBaseDataDir } from "../../config/env-registry.js";
4
+ import { getDeviceIdBaseDir } from "../../util/device-id.js";
6
5
  import type { WorkspaceMigration } from "./types.js";
7
6
 
8
7
  export const seedDeviceIdMigration: WorkspaceMigration = {
9
8
  id: "003-seed-device-id",
10
9
  description:
11
- "Seed ~/.vellum/device.json deviceId from the most recent lockfile installationId for continuity",
10
+ "Seed device.json deviceId from the most recent lockfile installationId for continuity",
12
11
  run(_workspaceDir: string): void {
13
- const base = getBaseDataDir() || homedir();
12
+ const base = getDeviceIdBaseDir();
14
13
  const vellumDir = join(base, ".vellum");
15
14
  const devicePath = join(vellumDir, "device.json");
16
15
 
@@ -83,6 +83,11 @@ export const servicesConfigMigration: WorkspaceMigration = {
83
83
  ...existingServices,
84
84
  };
85
85
 
86
+ // Legacy top-level fields (provider, model) are the user's actual
87
+ // configuration from before the services structure existed. If they're
88
+ // present as strings they take precedence over any `existingServices`
89
+ // values, which are just defaults written by backfillConfigDefaults().
90
+ // The spread preserves any extra keys that future backfills may add.
86
91
  services.inference = {
87
92
  ...(existingServices.inference ?? {}),
88
93
  mode: inferenceMode,
@@ -0,0 +1,12 @@
1
+ import type { WorkspaceMigration } from "./types.js";
2
+
3
+ export const voiceTimeoutAndMaxStepsMigration: WorkspaceMigration = {
4
+ id: "008-voice-timeout-and-max-steps",
5
+ description:
6
+ "Add elevenlabs.conversationTimeoutSeconds and maxStepsPerSession to config schema (defaults handle new installs; macOS client syncs existing UserDefaults values on startup)",
7
+ run(_workspaceDir: string): void {
8
+ // No-op — schema defaults handle new installs.
9
+ // Existing users: macOS client will sync UserDefaults values
10
+ // to config on next startup via settings sync endpoints.
11
+ },
12
+ };
@@ -0,0 +1,10 @@
1
+ import { rebuildConversationDiskViewFromDb } from "./rebuild-conversation-disk-view.js";
2
+ import type { WorkspaceMigration } from "./types.js";
3
+
4
+ export const backfillConversationDiskViewMigration: WorkspaceMigration = {
5
+ id: "009-backfill-conversation-disk-view",
6
+ description: "Rebuild conversation disk view for existing conversations",
7
+ run(_workspaceDir: string): void {
8
+ rebuildConversationDiskViewFromDb();
9
+ },
10
+ };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Workspace migration 010: Rename UUID-based app directories and files to
3
+ * human-readable slugified names.
4
+ *
5
+ * Inline slugify + dedup logic (not imported from app-store) so the migration
6
+ * remains stable even if runtime code changes in the future.
7
+ *
8
+ * Idempotent: safe to re-run after interruption at any point. Handles
9
+ * partially-renamed states (crash between JSON write and file rename).
10
+ */
11
+
12
+ import { execSync } from "node:child_process";
13
+ import { randomUUID } from "node:crypto";
14
+ import {
15
+ existsSync,
16
+ mkdirSync,
17
+ readdirSync,
18
+ readFileSync,
19
+ renameSync,
20
+ unlinkSync,
21
+ writeFileSync,
22
+ } from "node:fs";
23
+ import { join } from "node:path";
24
+
25
+ import type { WorkspaceMigration } from "./types.js";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Self-contained slug generation (do NOT import from app-store)
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function slugify(name: string): string {
32
+ let slug = name
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9-]/g, "-")
35
+ .replace(/-{2,}/g, "-")
36
+ .replace(/^-+|-+$/g, "");
37
+
38
+ if (slug.length > 60) {
39
+ slug = slug.slice(0, 60).replace(/-+$/, "");
40
+ }
41
+
42
+ if (!slug) {
43
+ slug = `app-${randomUUID().slice(0, 8)}`;
44
+ }
45
+
46
+ return slug;
47
+ }
48
+
49
+ function generateUniqueDirName(name: string, usedNames: Set<string>): string {
50
+ const base = slugify(name);
51
+ if (!usedNames.has(base)) return base;
52
+ let counter = 2;
53
+ while (usedNames.has(`${base}-${counter}`)) {
54
+ counter++;
55
+ }
56
+ return `${base}-${counter}`;
57
+ }
58
+
59
+ /** Defense-in-depth: reject dirNames that could cause path traversal. */
60
+ function isValidDirName(dirName: string): boolean {
61
+ return (
62
+ !!dirName &&
63
+ !dirName.includes("/") &&
64
+ !dirName.includes("\\") &&
65
+ !dirName.includes("..") &&
66
+ dirName === dirName.trim()
67
+ );
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Migration
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export const appDirRenameMigration: WorkspaceMigration = {
75
+ id: "010-app-dir-rename",
76
+ description:
77
+ "Rename UUID-based app directories and files to human-readable slugified names",
78
+
79
+ run(workspaceDir: string): void {
80
+ const appsDir = join(workspaceDir, "data", "apps");
81
+ if (!existsSync(appsDir)) return;
82
+
83
+ // Read all JSON files (sorted for deterministic ordering)
84
+ const jsonFiles = readdirSync(appsDir)
85
+ .filter((f) => f.endsWith(".json"))
86
+ .sort();
87
+
88
+ if (jsonFiles.length === 0) return;
89
+
90
+ const usedNames = new Set<string>();
91
+
92
+ for (const jsonFile of jsonFiles) {
93
+ const jsonPath = join(appsDir, jsonFile);
94
+ let raw: string;
95
+ try {
96
+ raw = readFileSync(jsonPath, "utf-8");
97
+ } catch {
98
+ continue; // skip unreadable files
99
+ }
100
+
101
+ let parsed: {
102
+ id?: string;
103
+ name?: string;
104
+ dirName?: string;
105
+ };
106
+ try {
107
+ parsed = JSON.parse(raw);
108
+ } catch {
109
+ continue; // skip malformed JSON
110
+ }
111
+
112
+ const appId = parsed.id;
113
+ const appName = parsed.name ?? "untitled";
114
+ if (!appId) continue;
115
+
116
+ // Check if already migrated: has dirName AND filesystem matches
117
+ if (parsed.dirName && isValidDirName(parsed.dirName)) {
118
+ const expectedJsonFile = `${parsed.dirName}.json`;
119
+ if (
120
+ jsonFile === expectedJsonFile &&
121
+ existsSync(join(appsDir, parsed.dirName))
122
+ ) {
123
+ // Already fully migrated -- just track the name
124
+ usedNames.add(parsed.dirName);
125
+ continue;
126
+ }
127
+
128
+ // Partially renamed: JSON has dirName but files may still be at old paths.
129
+ // Use the dirName from JSON but rename from wherever the files actually are.
130
+ const dirName = parsed.dirName;
131
+ usedNames.add(dirName);
132
+ renameAppFiles(appsDir, jsonFile, appId, dirName, parsed, raw);
133
+ continue;
134
+ }
135
+
136
+ // No dirName yet -- generate one
137
+ const dirName = generateUniqueDirName(appName, usedNames);
138
+ if (!isValidDirName(dirName)) continue; // safety check
139
+ usedNames.add(dirName);
140
+ renameAppFiles(appsDir, jsonFile, appId, dirName, parsed, raw);
141
+ }
142
+
143
+ // Best-effort git commit
144
+ try {
145
+ const gitDir = join(appsDir, ".git");
146
+ if (existsSync(gitDir)) {
147
+ execSync(
148
+ "git add -A && git commit -m 'Migration 010: rename app dirs to slugified names' --allow-empty",
149
+ {
150
+ cwd: appsDir,
151
+ stdio: "ignore",
152
+ timeout: 10_000,
153
+ },
154
+ );
155
+ }
156
+ } catch {
157
+ // Git failure is non-fatal -- log nothing since we don't have
158
+ // the logger available in migrations. The next commitAppChange()
159
+ // call will pick up the renamed files naturally.
160
+ }
161
+ },
162
+ };
163
+
164
+ /**
165
+ * Rename app files from their current location to dirName-based paths.
166
+ * Each step checks existence to handle partial completion.
167
+ */
168
+ function renameAppFiles(
169
+ appsDir: string,
170
+ currentJsonFile: string,
171
+ appId: string,
172
+ dirName: string,
173
+ parsed: Record<string, unknown>,
174
+ _rawJson: string,
175
+ ): void {
176
+ const targetJsonFile = `${dirName}.json`;
177
+ const targetPreviewFile = `${dirName}.preview`;
178
+
179
+ // 1. Rename the app directory: {appId}/ -> {dirName}/
180
+ const oldDir = join(appsDir, appId);
181
+ const newDir = join(appsDir, dirName);
182
+ if (existsSync(oldDir) && !existsSync(newDir) && oldDir !== newDir) {
183
+ renameSync(oldDir, newDir);
184
+ } else if (!existsSync(newDir)) {
185
+ // Directory doesn't exist at either location -- create it
186
+ mkdirSync(newDir, { recursive: true });
187
+ }
188
+
189
+ // 2. Rename the preview file: {appId}.preview -> {dirName}.preview
190
+ const oldPreview = join(appsDir, `${appId}.preview`);
191
+ const newPreview = join(appsDir, targetPreviewFile);
192
+ if (
193
+ existsSync(oldPreview) &&
194
+ !existsSync(newPreview) &&
195
+ oldPreview !== newPreview
196
+ ) {
197
+ renameSync(oldPreview, newPreview);
198
+ }
199
+
200
+ // 3. Rename the JSON file: {currentFilename} -> {dirName}.json
201
+ // Also update the dirName field in the JSON content.
202
+ const currentJsonPath = join(appsDir, currentJsonFile);
203
+ const targetJsonPath = join(appsDir, targetJsonFile);
204
+
205
+ // Update the JSON with dirName field
206
+ const updatedParsed = { ...parsed, dirName };
207
+ const updatedJson = JSON.stringify(updatedParsed, null, 2);
208
+
209
+ if (currentJsonFile !== targetJsonFile) {
210
+ // Write to new location, then remove old
211
+ writeFileSync(targetJsonPath, updatedJson, "utf-8");
212
+ if (existsSync(currentJsonPath) && currentJsonPath !== targetJsonPath) {
213
+ try {
214
+ unlinkSync(currentJsonPath);
215
+ } catch {
216
+ // Old file cleanup is best-effort
217
+ }
218
+ }
219
+ } else {
220
+ // Just update the content in place
221
+ writeFileSync(targetJsonPath, updatedJson, "utf-8");
222
+ }
223
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Workspace migration 012: Rename legacy conversation disk-view directories
3
+ * from `{conversationId}_{timestamp}` to `{timestamp}_{conversationId}`.
4
+ *
5
+ * Idempotent and conservative:
6
+ * - skips directories that already use the new format
7
+ * - skips non-matching directories
8
+ * - leaves an existing target directory alone rather than clobbering it
9
+ * - continues past per-directory rename failures
10
+ */
11
+
12
+ import { existsSync, readdirSync, renameSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
15
+ import type { WorkspaceMigration } from "./types.js";
16
+
17
+ const LEGACY_CONVERSATION_DIR_PATTERN =
18
+ /^(.*)_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z)$/;
19
+
20
+ function parseLegacyConversationDirName(
21
+ dirName: string,
22
+ ): { conversationId: string; timestamp: string } | null {
23
+ const match = dirName.match(LEGACY_CONVERSATION_DIR_PATTERN);
24
+ if (!match) return null;
25
+
26
+ return {
27
+ conversationId: match[1],
28
+ timestamp: match[2],
29
+ };
30
+ }
31
+
32
+ export const renameConversationDiskViewDirsMigration: WorkspaceMigration = {
33
+ id: "012-rename-conversation-disk-view-dirs",
34
+ description:
35
+ "Rename legacy conversation disk-view directories to timestamp-first names",
36
+
37
+ run(workspaceDir: string): void {
38
+ const conversationsDir = join(workspaceDir, "conversations");
39
+ if (!existsSync(conversationsDir)) return;
40
+
41
+ const entries = readdirSync(conversationsDir, { withFileTypes: true })
42
+ .filter((entry) => entry.isDirectory())
43
+ .map((entry) => entry.name)
44
+ .sort();
45
+
46
+ for (const dirName of entries) {
47
+ const parsed = parseLegacyConversationDirName(dirName);
48
+ if (!parsed) continue;
49
+
50
+ const sourcePath = join(conversationsDir, dirName);
51
+ const targetName = `${parsed.timestamp}_${parsed.conversationId}`;
52
+ const targetPath = join(conversationsDir, targetName);
53
+
54
+ if (sourcePath === targetPath) continue;
55
+ if (existsSync(targetPath)) continue;
56
+
57
+ try {
58
+ renameSync(sourcePath, targetPath);
59
+ } catch {
60
+ // Best-effort: leave the old directory in place if a single rename fails.
61
+ }
62
+ }
63
+ },
64
+ };
@@ -0,0 +1,11 @@
1
+ import { rebuildConversationDiskViewFromDb } from "./rebuild-conversation-disk-view.js";
2
+ import type { WorkspaceMigration } from "./types.js";
3
+
4
+ export const repairConversationDiskViewMigration: WorkspaceMigration = {
5
+ id: "013-repair-conversation-disk-view",
6
+ description:
7
+ "Repair missing conversation disk-view folders skipped by the conversationKey creation path",
8
+ run(_workspaceDir: string): void {
9
+ rebuildConversationDiskViewFromDb();
10
+ },
11
+ };
@@ -0,0 +1,186 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ import { asc, eq } from "drizzle-orm";
12
+
13
+ import { resolveConversationDirectoryPaths } from "../../memory/conversation-directories.js";
14
+ import {
15
+ initConversationDir,
16
+ syncMessageToDisk,
17
+ updateMetaFile,
18
+ } from "../../memory/conversation-disk-view.js";
19
+ import { getDb } from "../../memory/db.js";
20
+ import { conversations, messages } from "../../memory/schema.js";
21
+ import { getLogger } from "../../util/logger.js";
22
+
23
+ const log = getLogger("workspace-migrations");
24
+
25
+ function hasExpectedDiskViewArtifacts(
26
+ conv: { updatedAt: number },
27
+ dirPath: string,
28
+ ): boolean {
29
+ const metaPath = join(dirPath, "meta.json");
30
+ const messagesPath = join(dirPath, "messages.jsonl");
31
+ const attachDir = join(dirPath, "attachments");
32
+ if (
33
+ !existsSync(metaPath) ||
34
+ !existsSync(messagesPath) ||
35
+ !existsSync(attachDir)
36
+ )
37
+ return false;
38
+
39
+ try {
40
+ const existing = JSON.parse(readFileSync(metaPath, "utf-8"));
41
+ const expectedUpdatedAt = new Date(conv.updatedAt).toISOString();
42
+ return existing.updatedAt === expectedUpdatedAt;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function convergeDualConversationDirsToCanonical(
49
+ conv: { updatedAt: number },
50
+ canonicalDirPath: string,
51
+ legacyDirPath: string,
52
+ ): void {
53
+ if (!existsSync(canonicalDirPath) || !existsSync(legacyDirPath)) return;
54
+ if (!hasExpectedDiskViewArtifacts(conv, canonicalDirPath)) return;
55
+ rmSync(legacyDirPath, { recursive: true, force: true });
56
+ }
57
+
58
+ function getProjectedAttachmentFilenames(messagesPath: string): Set<string> {
59
+ const filenames = new Set<string>();
60
+ if (!existsSync(messagesPath)) return filenames;
61
+
62
+ const raw = readFileSync(messagesPath, "utf-8");
63
+ for (const line of raw.split("\n")) {
64
+ if (!line.trim()) continue;
65
+ try {
66
+ const parsed = JSON.parse(line) as { attachments?: unknown };
67
+ if (!Array.isArray(parsed.attachments)) continue;
68
+ for (const attachment of parsed.attachments) {
69
+ if (typeof attachment === "string") {
70
+ filenames.add(attachment);
71
+ }
72
+ }
73
+ } catch {
74
+ // Ignore malformed lines. A later replay will rewrite them.
75
+ }
76
+ }
77
+
78
+ return filenames;
79
+ }
80
+
81
+ function pruneUnreferencedProjectedAttachments(
82
+ attachDir: string,
83
+ messagesPath: string,
84
+ ): void {
85
+ if (!existsSync(attachDir)) return;
86
+
87
+ const referenced = getProjectedAttachmentFilenames(messagesPath);
88
+ for (const entry of readdirSync(attachDir)) {
89
+ if (referenced.has(entry)) continue;
90
+ rmSync(join(attachDir, entry), { recursive: true, force: true });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Rebuild the conversation disk view for all persisted conversations.
96
+ *
97
+ * Conversations are processed by ascending createdAt so replay ordering is
98
+ * stable and deterministic across runs.
99
+ */
100
+ export function rebuildConversationDiskViewFromDb(): void {
101
+ const db = getDb();
102
+
103
+ const allConversations = db
104
+ .select()
105
+ .from(conversations)
106
+ .orderBy(asc(conversations.createdAt))
107
+ .all();
108
+
109
+ const total = allConversations.length;
110
+ let processed = 0;
111
+
112
+ for (const conv of allConversations) {
113
+ const {
114
+ canonicalDirPath,
115
+ legacyDirPath,
116
+ resolvedDirPath: dirPath,
117
+ } = resolveConversationDirectoryPaths(conv.id, conv.createdAt);
118
+ const metaPath = join(dirPath, "meta.json");
119
+ const messagesPath = join(dirPath, "messages.jsonl");
120
+ const attachDir = join(dirPath, "attachments");
121
+
122
+ // Check if already migrated (idempotent)
123
+ if (existsSync(metaPath) && hasExpectedDiskViewArtifacts(conv, dirPath)) {
124
+ // Prefer the timestamp-first canonical directory whenever both sibling
125
+ // directories exist and the canonical projection is complete.
126
+ convergeDualConversationDirsToCanonical(
127
+ conv,
128
+ canonicalDirPath,
129
+ legacyDirPath,
130
+ );
131
+ processed++;
132
+ if (processed % 50 === 0) {
133
+ log.info(`Backfilled ${processed}/${total} conversations to disk`);
134
+ }
135
+ continue;
136
+ }
137
+
138
+ // Create dir + meta.json (initConversationDir sets updatedAt = createdAt)
139
+ initConversationDir(conv);
140
+
141
+ // Clear stale data from any previous interrupted run so append-only
142
+ // syncMessageToDisk calls below don't produce duplicates.
143
+ if (existsSync(messagesPath)) {
144
+ rmSync(messagesPath, { force: true });
145
+ }
146
+ writeFileSync(messagesPath, "");
147
+
148
+ // Preserve already materialized attachment files across repair replay.
149
+ // Some rows have data_base64 compacted away and only retain their
150
+ // conversation-scoped file_path, so removing attachments/ here would
151
+ // make the content unrecoverable.
152
+ mkdirSync(attachDir, { recursive: true });
153
+
154
+ // Query all messages for this conversation and sync each to disk
155
+ const convMessages = db
156
+ .select()
157
+ .from(messages)
158
+ .where(eq(messages.conversationId, conv.id))
159
+ .orderBy(asc(messages.createdAt))
160
+ .all();
161
+
162
+ for (const msg of convMessages) {
163
+ syncMessageToDisk(conv.id, msg.id, conv.createdAt);
164
+ }
165
+
166
+ // Write the real updatedAt only AFTER all messages are synced so the
167
+ // idempotency check won't skip a conversation with incomplete messages
168
+ // if the migration is interrupted mid-loop.
169
+ updateMetaFile(conv);
170
+ pruneUnreferencedProjectedAttachments(attachDir, messagesPath);
171
+ convergeDualConversationDirsToCanonical(
172
+ conv,
173
+ canonicalDirPath,
174
+ legacyDirPath,
175
+ );
176
+
177
+ processed++;
178
+ if (processed % 50 === 0) {
179
+ log.info(`Backfilled ${processed}/${total} conversations to disk`);
180
+ }
181
+ }
182
+
183
+ if (total > 0) {
184
+ log.info(`Backfilled ${processed}/${total} conversations to disk`);
185
+ }
186
+ }
@@ -5,6 +5,11 @@ import { extractCollectUsageDataMigration } from "./004-extract-collect-usage-da
5
5
  import { addSendDiagnosticsMigration } from "./005-add-send-diagnostics.js";
6
6
  import { servicesConfigMigration } from "./006-services-config.js";
7
7
  import { webSearchProviderRenameMigration } from "./007-web-search-provider-rename.js";
8
+ import { voiceTimeoutAndMaxStepsMigration } from "./008-voice-timeout-and-max-steps.js";
9
+ import { backfillConversationDiskViewMigration } from "./009-backfill-conversation-disk-view.js";
10
+ import { appDirRenameMigration } from "./010-app-dir-rename.js";
11
+ import { renameConversationDiskViewDirsMigration } from "./012-rename-conversation-disk-view-dirs.js";
12
+ import { repairConversationDiskViewMigration } from "./013-repair-conversation-disk-view.js";
8
13
  import type { WorkspaceMigration } from "./types.js";
9
14
 
10
15
  /**
@@ -19,4 +24,9 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
19
24
  addSendDiagnosticsMigration,
20
25
  servicesConfigMigration,
21
26
  webSearchProviderRenameMigration,
27
+ voiceTimeoutAndMaxStepsMigration,
28
+ backfillConversationDiskViewMigration,
29
+ appDirRenameMigration,
30
+ renameConversationDiskViewDirsMigration,
31
+ repairConversationDiskViewMigration,
22
32
  ];