@vellumai/assistant 0.5.1 → 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 (338) 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__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -0,0 +1,243 @@
1
+ import { mkdtempSync, realpathSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ const testDir = realpathSync(
7
+ mkdtempSync(join(tmpdir(), "turn-boundary-resolution-test-")),
8
+ );
9
+ const workspaceDir = join(testDir, ".vellum", "workspace");
10
+ const conversationsDir = join(workspaceDir, "conversations");
11
+
12
+ mock.module("../util/platform.js", () => ({
13
+ getRootDir: () => join(testDir, ".vellum"),
14
+ getDataDir: () => join(workspaceDir, "data"),
15
+ getWorkspaceDir: () => workspaceDir,
16
+ getConversationsDir: () => conversationsDir,
17
+ isMacOS: () => process.platform === "darwin",
18
+ isLinux: () => process.platform === "linux",
19
+ isWindows: () => process.platform === "win32",
20
+ getPidPath: () => join(testDir, "test.pid"),
21
+ getDbPath: () => join(testDir, "test.db"),
22
+ getLogPath: () => join(testDir, "test.log"),
23
+ ensureDataDir: () => {},
24
+ }));
25
+
26
+ mock.module("../util/logger.js", () => ({
27
+ getLogger: () =>
28
+ new Proxy({} as Record<string, unknown>, {
29
+ get: () => () => {},
30
+ }),
31
+ }));
32
+
33
+ mock.module("../config/loader.js", () => ({
34
+ getConfig: () => ({
35
+ ui: {},
36
+ model: "test",
37
+ provider: "test",
38
+ memory: { enabled: false },
39
+ rateLimit: { maxRequestsPerMinute: 0 },
40
+ secretDetection: { enabled: false },
41
+ }),
42
+ }));
43
+
44
+ import {
45
+ addMessage,
46
+ createConversation,
47
+ getAssistantMessageIdsInTurn,
48
+ } from "../memory/conversation-crud.js";
49
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
50
+ import { llmRequestLogs, toolInvocations } from "../memory/schema.js";
51
+
52
+ initializeDb();
53
+
54
+ function resetTables(): void {
55
+ const db = getDb();
56
+ db.delete(llmRequestLogs).run();
57
+ db.delete(toolInvocations).run();
58
+ db.run("DELETE FROM message_attachments");
59
+ db.run("DELETE FROM attachments");
60
+ db.run("DELETE FROM messages");
61
+ db.run("DELETE FROM conversations");
62
+ }
63
+
64
+ function toolResultContent(toolUseIds: string[]): string {
65
+ return JSON.stringify(
66
+ toolUseIds.map((id) => ({
67
+ type: "tool_result",
68
+ tool_use_id: id,
69
+ content: "ok",
70
+ is_error: false,
71
+ })),
72
+ );
73
+ }
74
+
75
+ afterAll(() => {
76
+ resetDb();
77
+ try {
78
+ rmSync(testDir, { recursive: true, force: true });
79
+ } catch {
80
+ /* best effort */
81
+ }
82
+ });
83
+
84
+ describe("getAssistantMessageIdsInTurn", () => {
85
+ beforeEach(() => {
86
+ resetTables();
87
+ });
88
+
89
+ test("single-step turn: returns only the one assistant message", async () => {
90
+ const conv = createConversation("single-step");
91
+ await addMessage(conv.id, "user", "Hello", undefined, {
92
+ skipIndexing: true,
93
+ });
94
+ const a1 = await addMessage(conv.id, "assistant", "Hi there!", undefined, {
95
+ skipIndexing: true,
96
+ });
97
+
98
+ const result = getAssistantMessageIdsInTurn(a1.id);
99
+ expect(result).toEqual([a1.id]);
100
+ });
101
+
102
+ test("multi-step turn: user → A1 → tool_result → A2 → query A2 → returns [A1, A2]", async () => {
103
+ const conv = createConversation("multi-step");
104
+ await addMessage(conv.id, "user", "Do the thing", undefined, {
105
+ skipIndexing: true,
106
+ });
107
+ const a1 = await addMessage(
108
+ conv.id,
109
+ "assistant",
110
+ "Using tool...",
111
+ undefined,
112
+ { skipIndexing: true },
113
+ );
114
+ await addMessage(
115
+ conv.id,
116
+ "user",
117
+ toolResultContent(["tool-1"]),
118
+ undefined,
119
+ { skipIndexing: true },
120
+ );
121
+ const a2 = await addMessage(conv.id, "assistant", "Done!", undefined, {
122
+ skipIndexing: true,
123
+ });
124
+
125
+ const result = getAssistantMessageIdsInTurn(a2.id);
126
+ expect(result).toEqual([a1.id, a2.id]);
127
+ });
128
+
129
+ test("3-step turn: user → A1 → tool_result → A2 → tool_result → A3 → query A3 → returns [A1, A2, A3]", async () => {
130
+ const conv = createConversation("three-step");
131
+ await addMessage(conv.id, "user", "Complex task", undefined, {
132
+ skipIndexing: true,
133
+ });
134
+ const a1 = await addMessage(conv.id, "assistant", "Step 1...", undefined, {
135
+ skipIndexing: true,
136
+ });
137
+ await addMessage(
138
+ conv.id,
139
+ "user",
140
+ toolResultContent(["tool-1"]),
141
+ undefined,
142
+ { skipIndexing: true },
143
+ );
144
+ const a2 = await addMessage(conv.id, "assistant", "Step 2...", undefined, {
145
+ skipIndexing: true,
146
+ });
147
+ await addMessage(
148
+ conv.id,
149
+ "user",
150
+ toolResultContent(["tool-2"]),
151
+ undefined,
152
+ { skipIndexing: true },
153
+ );
154
+ const a3 = await addMessage(conv.id, "assistant", "All done!", undefined, {
155
+ skipIndexing: true,
156
+ });
157
+
158
+ const result = getAssistantMessageIdsInTurn(a3.id);
159
+ expect(result).toEqual([a1.id, a2.id, a3.id]);
160
+ });
161
+
162
+ test("query intermediate message: query A1 in a 2-step turn → returns [A1, A2]", async () => {
163
+ const conv = createConversation("intermediate");
164
+ await addMessage(conv.id, "user", "Start task", undefined, {
165
+ skipIndexing: true,
166
+ });
167
+ const a1 = await addMessage(
168
+ conv.id,
169
+ "assistant",
170
+ "Using tool...",
171
+ undefined,
172
+ { skipIndexing: true },
173
+ );
174
+ await addMessage(
175
+ conv.id,
176
+ "user",
177
+ toolResultContent(["tool-1"]),
178
+ undefined,
179
+ { skipIndexing: true },
180
+ );
181
+ const a2 = await addMessage(conv.id, "assistant", "Done!", undefined, {
182
+ skipIndexing: true,
183
+ });
184
+
185
+ const result = getAssistantMessageIdsInTurn(a1.id);
186
+ expect(result).toEqual([a1.id, a2.id]);
187
+ });
188
+
189
+ test("message not found: returns [messageId]", () => {
190
+ const result = getAssistantMessageIdsInTurn("nonexistent-id");
191
+ expect(result).toEqual(["nonexistent-id"]);
192
+ });
193
+
194
+ test("consecutive turns: query message in second turn → returns only that turn's messages", async () => {
195
+ const conv = createConversation("consecutive");
196
+
197
+ // First turn
198
+ await addMessage(conv.id, "user", "First question", undefined, {
199
+ skipIndexing: true,
200
+ });
201
+ const a1 = await addMessage(
202
+ conv.id,
203
+ "assistant",
204
+ "First answer",
205
+ undefined,
206
+ { skipIndexing: true },
207
+ );
208
+
209
+ // Second turn
210
+ await addMessage(conv.id, "user", "Second question", undefined, {
211
+ skipIndexing: true,
212
+ });
213
+ const a2 = await addMessage(
214
+ conv.id,
215
+ "assistant",
216
+ "Using tool...",
217
+ undefined,
218
+ { skipIndexing: true },
219
+ );
220
+ await addMessage(
221
+ conv.id,
222
+ "user",
223
+ toolResultContent(["tool-1"]),
224
+ undefined,
225
+ { skipIndexing: true },
226
+ );
227
+ const a3 = await addMessage(
228
+ conv.id,
229
+ "assistant",
230
+ "Done with second",
231
+ undefined,
232
+ { skipIndexing: true },
233
+ );
234
+
235
+ // Query second turn → should NOT include a1
236
+ const result = getAssistantMessageIdsInTurn(a3.id);
237
+ expect(result).toEqual([a2.id, a3.id]);
238
+
239
+ // Query first turn → should only include a1
240
+ const firstTurnResult = getAssistantMessageIdsInTurn(a1.id);
241
+ expect(firstTurnResult).toEqual([a1.id]);
242
+ });
243
+ });
@@ -1641,7 +1641,9 @@ describe("web_fetch tool", () => {
1641
1641
  );
1642
1642
 
1643
1643
  expect(result.isError).toBe(false);
1644
- expect(result.content).not.toContain("Extracted text content is very short");
1644
+ expect(result.content).not.toContain(
1645
+ "Extracted text content is very short",
1646
+ );
1645
1647
  });
1646
1648
 
1647
1649
  test("does not suggest JS rendering notice in raw mode even for sparse HTML", async () => {
@@ -1660,6 +1662,8 @@ describe("web_fetch tool", () => {
1660
1662
  );
1661
1663
 
1662
1664
  expect(result.isError).toBe(false);
1663
- expect(result.content).not.toContain("Extracted text content is very short");
1665
+ expect(result.content).not.toContain(
1666
+ "Extracted text content is very short",
1667
+ );
1664
1668
  });
1665
1669
  });
@@ -0,0 +1,335 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Mocks — must precede the migration import
14
+ // ---------------------------------------------------------------------------
15
+
16
+ mock.module("../security/secure-keys.js", () => ({
17
+ getProviderKeyAsync: async () => null,
18
+ getSecureKeyAsync: async () => null,
19
+ }));
20
+
21
+ import { servicesConfigMigration } from "../workspace/migrations/006-services-config.js";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ let workspaceDir: string;
28
+
29
+ function freshWorkspace(): void {
30
+ workspaceDir = join(
31
+ tmpdir(),
32
+ `vellum-migration-006-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
33
+ );
34
+ mkdirSync(workspaceDir, { recursive: true });
35
+ }
36
+
37
+ function writeConfig(data: Record<string, unknown>): void {
38
+ writeFileSync(
39
+ join(workspaceDir, "config.json"),
40
+ JSON.stringify(data, null, 2) + "\n",
41
+ );
42
+ }
43
+
44
+ function readConfig(): Record<string, unknown> {
45
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Setup / teardown
50
+ // ---------------------------------------------------------------------------
51
+
52
+ beforeEach(() => {
53
+ freshWorkspace();
54
+ });
55
+
56
+ afterEach(() => {
57
+ if (existsSync(workspaceDir)) {
58
+ rmSync(workspaceDir, { recursive: true, force: true });
59
+ }
60
+ });
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Tests
64
+ // ---------------------------------------------------------------------------
65
+
66
+ describe("006-services-config migration", () => {
67
+ test("migrates all legacy fields into services object", async () => {
68
+ writeConfig({
69
+ provider: "openai",
70
+ model: "gpt-4o",
71
+ imageGenModel: "dall-e-3",
72
+ webSearchProvider: "brave",
73
+ otherSetting: true,
74
+ });
75
+
76
+ await servicesConfigMigration.run(workspaceDir);
77
+
78
+ const config = readConfig();
79
+
80
+ // Legacy fields removed
81
+ expect(config.provider).toBeUndefined();
82
+ expect(config.model).toBeUndefined();
83
+ expect(config.imageGenModel).toBeUndefined();
84
+ expect(config.webSearchProvider).toBeUndefined();
85
+
86
+ // Non-legacy fields preserved
87
+ expect(config.otherSetting).toBe(true);
88
+
89
+ // Services populated correctly
90
+ const services = config.services as Record<string, Record<string, unknown>>;
91
+ expect(services.inference).toEqual({
92
+ mode: "your-own",
93
+ provider: "openai",
94
+ model: "gpt-4o",
95
+ });
96
+ expect(services["image-generation"]).toEqual({
97
+ mode: "your-own",
98
+ provider: "openai",
99
+ model: "dall-e-3",
100
+ });
101
+ expect(services["web-search"]).toEqual({
102
+ mode: "your-own",
103
+ provider: "brave",
104
+ });
105
+ });
106
+
107
+ test("uses anthropic defaults when legacy fields are non-string", async () => {
108
+ // Legacy fields present but not strings (e.g. null or number) — fall
109
+ // through to defaults
110
+ writeConfig({
111
+ provider: null,
112
+ model: 123,
113
+ });
114
+
115
+ await servicesConfigMigration.run(workspaceDir);
116
+
117
+ const config = readConfig();
118
+ const services = config.services as Record<string, Record<string, unknown>>;
119
+ expect(services.inference.provider).toBe("anthropic");
120
+ expect(services.inference.model).toBe("claude-opus-4-6");
121
+ });
122
+
123
+ test("no-op when config.json does not exist", async () => {
124
+ // No config file — should return without error
125
+ await servicesConfigMigration.run(workspaceDir);
126
+ expect(existsSync(join(workspaceDir, "config.json"))).toBe(false);
127
+ });
128
+
129
+ test("no-op when no legacy fields are present (fresh install)", async () => {
130
+ const original = {
131
+ services: {
132
+ inference: {
133
+ mode: "your-own",
134
+ provider: "anthropic",
135
+ model: "claude-opus-4-6",
136
+ },
137
+ },
138
+ };
139
+ writeConfig(original);
140
+
141
+ await servicesConfigMigration.run(workspaceDir);
142
+
143
+ // Config should be unchanged — migration skipped entirely
144
+ const config = readConfig();
145
+ expect(config).toEqual(original);
146
+ });
147
+
148
+ test("no-op when no legacy fields are present (already migrated)", async () => {
149
+ const alreadyMigrated = {
150
+ services: {
151
+ inference: { mode: "your-own", provider: "openai", model: "gpt-4o" },
152
+ "image-generation": {
153
+ mode: "your-own",
154
+ provider: "gemini",
155
+ model: "gemini-2.5-flash-image",
156
+ },
157
+ "web-search": { mode: "your-own", provider: "anthropic-native" },
158
+ },
159
+ };
160
+ writeConfig(alreadyMigrated);
161
+
162
+ await servicesConfigMigration.run(workspaceDir);
163
+
164
+ const config = readConfig();
165
+ expect(config).toEqual(alreadyMigrated);
166
+ });
167
+
168
+ test("idempotency: running migration twice produces same result", async () => {
169
+ writeConfig({
170
+ provider: "openai",
171
+ model: "gpt-4o",
172
+ imageGenModel: "dall-e-3",
173
+ webSearchProvider: "brave",
174
+ });
175
+
176
+ await servicesConfigMigration.run(workspaceDir);
177
+ const afterFirst = readConfig();
178
+
179
+ await servicesConfigMigration.run(workspaceDir);
180
+ const afterSecond = readConfig();
181
+
182
+ expect(afterSecond).toEqual(afterFirst);
183
+ });
184
+
185
+ test("merges with existing backfilled services object", async () => {
186
+ // Simulates the scenario where backfillConfigDefaults wrote a default
187
+ // services object before migrations run, and legacy fields coexist.
188
+ writeConfig({
189
+ provider: "openai",
190
+ model: "gpt-4o",
191
+ services: {
192
+ inference: {
193
+ mode: "your-own",
194
+ provider: "anthropic",
195
+ model: "claude-opus-4-6",
196
+ },
197
+ "image-generation": {
198
+ mode: "your-own",
199
+ provider: "gemini",
200
+ model: "gemini-2.5-flash-image",
201
+ },
202
+ "web-search": {
203
+ mode: "your-own",
204
+ provider: "anthropic-native",
205
+ },
206
+ },
207
+ });
208
+
209
+ await servicesConfigMigration.run(workspaceDir);
210
+
211
+ const config = readConfig();
212
+ const services = config.services as Record<string, Record<string, unknown>>;
213
+
214
+ // Legacy fields should win over backfilled defaults
215
+ expect(services.inference.provider).toBe("openai");
216
+ expect(services.inference.model).toBe("gpt-4o");
217
+
218
+ // Legacy fields removed
219
+ expect(config.provider).toBeUndefined();
220
+ expect(config.model).toBeUndefined();
221
+ });
222
+
223
+ test("preserves extra keys from existing services during merge", async () => {
224
+ // If backfill or future code added extra keys in services, the spread
225
+ // operator should preserve them.
226
+ writeConfig({
227
+ provider: "openai",
228
+ services: {
229
+ inference: {
230
+ mode: "your-own",
231
+ provider: "anthropic",
232
+ model: "claude-opus-4-6",
233
+ extraKey: "should-survive",
234
+ },
235
+ "custom-service": {
236
+ foo: "bar",
237
+ },
238
+ },
239
+ });
240
+
241
+ await servicesConfigMigration.run(workspaceDir);
242
+
243
+ const config = readConfig();
244
+ const services = config.services as Record<string, Record<string, unknown>>;
245
+
246
+ // Extra key preserved from spread
247
+ expect(services.inference.extraKey).toBe("should-survive");
248
+
249
+ // Custom service section preserved from top-level spread
250
+ expect(services["custom-service"]).toEqual({ foo: "bar" });
251
+ });
252
+
253
+ test("gemini image model sets provider to gemini", async () => {
254
+ writeConfig({
255
+ imageGenModel: "gemini-2.5-flash-image",
256
+ });
257
+
258
+ await servicesConfigMigration.run(workspaceDir);
259
+
260
+ const config = readConfig();
261
+ const services = config.services as Record<string, Record<string, unknown>>;
262
+ expect(services["image-generation"].provider).toBe("gemini");
263
+ expect(services["image-generation"].model).toBe("gemini-2.5-flash-image");
264
+ });
265
+
266
+ test("dall-e image model sets provider to openai", async () => {
267
+ writeConfig({
268
+ imageGenModel: "dall-e-3",
269
+ });
270
+
271
+ await servicesConfigMigration.run(workspaceDir);
272
+
273
+ const config = readConfig();
274
+ const services = config.services as Record<string, Record<string, unknown>>;
275
+ expect(services["image-generation"].provider).toBe("openai");
276
+ expect(services["image-generation"].model).toBe("dall-e-3");
277
+ });
278
+
279
+ test("gracefully handles invalid JSON in config file", async () => {
280
+ writeFileSync(join(workspaceDir, "config.json"), "not-valid-json");
281
+
282
+ // Should return without error
283
+ await servicesConfigMigration.run(workspaceDir);
284
+
285
+ // File should be unchanged
286
+ expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
287
+ "not-valid-json",
288
+ );
289
+ });
290
+
291
+ test("gracefully handles array config", async () => {
292
+ writeFileSync(join(workspaceDir, "config.json"), JSON.stringify([1, 2, 3]));
293
+
294
+ // Should return without error
295
+ await servicesConfigMigration.run(workspaceDir);
296
+
297
+ // File should be unchanged
298
+ const raw = JSON.parse(
299
+ readFileSync(join(workspaceDir, "config.json"), "utf-8"),
300
+ );
301
+ expect(raw).toEqual([1, 2, 3]);
302
+ });
303
+
304
+ test("falls back to existing services values when legacy fields are absent for that service", async () => {
305
+ // Only provider legacy field present; imageGenModel and webSearchProvider
306
+ // are missing. Existing services should be used for image-generation and
307
+ // web-search.
308
+ writeConfig({
309
+ provider: "openai",
310
+ services: {
311
+ "image-generation": {
312
+ mode: "your-own",
313
+ provider: "openai",
314
+ model: "dall-e-2",
315
+ },
316
+ "web-search": {
317
+ mode: "your-own",
318
+ provider: "brave",
319
+ },
320
+ },
321
+ });
322
+
323
+ await servicesConfigMigration.run(workspaceDir);
324
+
325
+ const config = readConfig();
326
+ const services = config.services as Record<string, Record<string, unknown>>;
327
+
328
+ // image-generation: falls back to existing services model value
329
+ expect(services["image-generation"].model).toBe("dall-e-2");
330
+ expect(services["image-generation"].provider).toBe("openai");
331
+
332
+ // web-search: falls back to existing services provider value
333
+ expect(services["web-search"].provider).toBe("brave");
334
+ });
335
+ });