@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
@@ -491,12 +491,17 @@ describe("repairHistory", () => {
491
491
  expect(assistantMsg.content[1]).toMatchObject({
492
492
  type: "web_search_tool_result",
493
493
  tool_use_id: "stu_1",
494
- content: { type: "web_search_tool_result_error", error_code: "unavailable" },
494
+ content: {
495
+ type: "web_search_tool_result_error",
496
+ error_code: "unavailable",
497
+ },
495
498
  });
496
499
 
497
500
  // User message has no web_search_tool_result
498
501
  const userMsg = repaired[2];
499
- expect(userMsg.content.every((b) => b.type !== "web_search_tool_result")).toBe(true);
502
+ expect(
503
+ userMsg.content.every((b) => b.type !== "web_search_tool_result"),
504
+ ).toBe(true);
500
505
  });
501
506
 
502
507
  test("migrates legacy web_search_tool_result from user message to assistant message", () => {
@@ -527,7 +532,9 @@ describe("repairHistory", () => {
527
532
  {
528
533
  type: "web_search_tool_result",
529
534
  tool_use_id: "srvtoolu_abc",
530
- content: [{ type: "web_search_result", url: "https://example.com" }],
535
+ content: [
536
+ { type: "web_search_result", url: "https://example.com" },
537
+ ],
531
538
  },
532
539
  { type: "tool_result", tool_use_id: "tu_1", content: "files" },
533
540
  ],
@@ -545,8 +552,12 @@ describe("repairHistory", () => {
545
552
 
546
553
  // The assistant message now has the server pair + client tool_use
547
554
  const assistantMsg = repaired[1];
548
- const serverToolUse = assistantMsg.content.find((b) => b.type === "server_tool_use");
549
- const webSearchResult = assistantMsg.content.find((b) => b.type === "web_search_tool_result");
555
+ const serverToolUse = assistantMsg.content.find(
556
+ (b) => b.type === "server_tool_use",
557
+ );
558
+ const webSearchResult = assistantMsg.content.find(
559
+ (b) => b.type === "web_search_tool_result",
560
+ );
550
561
  expect(serverToolUse).toBeDefined();
551
562
  expect(webSearchResult).toBeDefined();
552
563
 
@@ -554,7 +565,9 @@ describe("repairHistory", () => {
554
565
  const userMsg = repaired[2];
555
566
  expect(stats.orphanToolResultsDowngraded).toBe(1);
556
567
  expect(userMsg.content.some((b) => b.type === "tool_result")).toBe(true);
557
- expect(userMsg.content.every((b) => b.type !== "web_search_tool_result")).toBe(true);
568
+ expect(
569
+ userMsg.content.every((b) => b.type !== "web_search_tool_result"),
570
+ ).toBe(true);
558
571
  });
559
572
 
560
573
  test("trailing server_tool_use gets synthetic result in same assistant message", () => {
@@ -584,7 +597,10 @@ describe("repairHistory", () => {
584
597
  expect(repaired[1].content[1]).toMatchObject({
585
598
  type: "web_search_tool_result",
586
599
  tool_use_id: "stu_1",
587
- content: { type: "web_search_tool_result_error", error_code: "unavailable" },
600
+ content: {
601
+ type: "web_search_tool_result_error",
602
+ error_code: "unavailable",
603
+ },
588
604
  });
589
605
  });
590
606
 
@@ -696,11 +712,15 @@ describe("repairHistory", () => {
696
712
 
697
713
  // Assistant message has the server pair
698
714
  const assistantMsg = repaired[1];
699
- expect(assistantMsg.content.some((b) => b.type === "web_search_tool_result")).toBe(true);
715
+ expect(
716
+ assistantMsg.content.some((b) => b.type === "web_search_tool_result"),
717
+ ).toBe(true);
700
718
 
701
719
  // User message has no web_search_tool_result — the tool_result was downgraded to text
702
720
  const userMsg = repaired[2];
703
- expect(userMsg.content.every((b) => b.type !== "web_search_tool_result")).toBe(true);
721
+ expect(
722
+ userMsg.content.every((b) => b.type !== "web_search_tool_result"),
723
+ ).toBe(true);
704
724
  expect(userMsg.content.every((b) => b.type !== "tool_result")).toBe(true);
705
725
  });
706
726
 
@@ -720,7 +740,9 @@ describe("repairHistory", () => {
720
740
  {
721
741
  type: "web_search_tool_result",
722
742
  tool_use_id: "tu_1",
723
- content: [{ type: "web_search_result", url: "https://example.com" }],
743
+ content: [
744
+ { type: "web_search_result", url: "https://example.com" },
745
+ ],
724
746
  },
725
747
  ],
726
748
  },
@@ -0,0 +1,251 @@
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(), "http-conversation-lineage-test-")),
8
+ );
9
+
10
+ mock.module("../util/platform.js", () => ({
11
+ getRootDir: () => join(testDir, ".vellum"),
12
+ getDataDir: () => join(testDir, ".vellum", "workspace", "data"),
13
+ getWorkspaceDir: () => join(testDir, ".vellum", "workspace"),
14
+ getConversationsDir: () =>
15
+ join(testDir, ".vellum", "workspace", "conversations"),
16
+ isMacOS: () => process.platform === "darwin",
17
+ isLinux: () => process.platform === "linux",
18
+ isWindows: () => process.platform === "win32",
19
+ getPidPath: () => join(testDir, "test.pid"),
20
+ getDbPath: () => join(testDir, "test.db"),
21
+ getLogPath: () => join(testDir, "test.log"),
22
+ ensureDataDir: () => {},
23
+ }));
24
+
25
+ mock.module("../util/logger.js", () => ({
26
+ getLogger: () =>
27
+ new Proxy({} as Record<string, unknown>, {
28
+ get: () => () => {},
29
+ }),
30
+ }));
31
+
32
+ mock.module("../config/env.js", () => ({
33
+ isHttpAuthDisabled: () => true,
34
+ hasUngatedHttpAuthDisabled: () => false,
35
+ getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
36
+ getGatewayPort: () => 7830,
37
+ getRuntimeHttpPort: () => 7821,
38
+ getRuntimeHttpHost: () => "127.0.0.1",
39
+ getRuntimeGatewayOriginSecret: () => undefined,
40
+ getIngressPublicBaseUrl: () => undefined,
41
+ setIngressPublicBaseUrl: () => {},
42
+ }));
43
+
44
+ mock.module("../config/loader.js", () => ({
45
+ getConfig: () => ({
46
+ ui: {},
47
+ model: "test",
48
+ provider: "test",
49
+ memory: { enabled: false },
50
+ rateLimit: { maxRequestsPerMinute: 0 },
51
+ secretDetection: { enabled: false },
52
+ }),
53
+ }));
54
+
55
+ import {
56
+ batchSetDisplayOrders,
57
+ createConversation,
58
+ updateConversationTitle,
59
+ } from "../memory/conversation-crud.js";
60
+ import { getDb, initializeDb, rawRun, resetDb } from "../memory/db.js";
61
+ import { RuntimeHttpServer } from "../runtime/http-server.js";
62
+
63
+ initializeDb();
64
+
65
+ type ConversationSummary = {
66
+ id: string;
67
+ title: string;
68
+ displayOrder?: number | null;
69
+ isPinned?: boolean;
70
+ forkParent?: {
71
+ conversationId: string;
72
+ messageId: string;
73
+ title: string;
74
+ };
75
+ };
76
+
77
+ describe("conversation lineage in HTTP reads", () => {
78
+ let server: RuntimeHttpServer | null = null;
79
+
80
+ beforeEach(async () => {
81
+ await server?.stop();
82
+ server = null;
83
+ clearTables();
84
+ });
85
+
86
+ afterAll(async () => {
87
+ await server?.stop();
88
+ resetDb();
89
+ try {
90
+ rmSync(testDir, { recursive: true, force: true });
91
+ } catch {
92
+ /* best effort */
93
+ }
94
+ });
95
+
96
+ test("GET /v1/conversations returns forkParent for surviving parents", async () => {
97
+ const { child, parent } = seedForkedConversation();
98
+ await startServer();
99
+
100
+ const response = await fetch(url("/conversations"));
101
+ expect(response.status).toBe(200);
102
+
103
+ const body = (await response.json()) as {
104
+ conversations: ConversationSummary[];
105
+ hasMore: boolean;
106
+ };
107
+ const listedChild = body.conversations.find((item) => item.id === child.id);
108
+
109
+ expect(listedChild).toMatchObject({
110
+ id: child.id,
111
+ title: child.title ?? "Untitled",
112
+ forkParent: {
113
+ conversationId: parent.id,
114
+ messageId: "parent-msg-1",
115
+ title: parent.title ?? "Untitled",
116
+ },
117
+ });
118
+ expect(body.hasMore).toBe(false);
119
+ });
120
+
121
+ test("GET /v1/conversations/:id returns forkParent for surviving parents", async () => {
122
+ const { child, parent } = seedForkedConversation();
123
+ await startServer();
124
+
125
+ const response = await fetch(url(`/conversations/${child.id}`));
126
+ expect(response.status).toBe(200);
127
+
128
+ const body = (await response.json()) as {
129
+ conversation: ConversationSummary;
130
+ };
131
+
132
+ expect(body.conversation).toMatchObject({
133
+ id: child.id,
134
+ title: child.title ?? "Untitled",
135
+ forkParent: {
136
+ conversationId: parent.id,
137
+ messageId: "parent-msg-1",
138
+ title: parent.title ?? "Untitled",
139
+ },
140
+ });
141
+ });
142
+
143
+ test("GET /v1/conversations/:id includes pin metadata when present", async () => {
144
+ const conversation = createConversation("Pinned conversation");
145
+ batchSetDisplayOrders([
146
+ { id: conversation.id, displayOrder: 7, isPinned: true },
147
+ ]);
148
+ await startServer();
149
+
150
+ const response = await fetch(url(`/conversations/${conversation.id}`));
151
+ expect(response.status).toBe(200);
152
+
153
+ const body = (await response.json()) as {
154
+ conversation: ConversationSummary;
155
+ };
156
+
157
+ expect(body.conversation).toMatchObject({
158
+ id: conversation.id,
159
+ title: conversation.title ?? "Untitled",
160
+ displayOrder: 7,
161
+ isPinned: true,
162
+ });
163
+ });
164
+
165
+ test("GET /v1/conversations/:id resolves the parent's current title at read time", async () => {
166
+ const { child, parent } = seedForkedConversation({
167
+ parentTitle: "Original parent title",
168
+ });
169
+ updateConversationTitle(parent.id, "Renamed parent title", 0);
170
+ await startServer();
171
+
172
+ const response = await fetch(url(`/conversations/${child.id}`));
173
+ expect(response.status).toBe(200);
174
+
175
+ const body = (await response.json()) as {
176
+ conversation: ConversationSummary;
177
+ };
178
+
179
+ expect(body.conversation.forkParent).toEqual({
180
+ conversationId: parent.id,
181
+ messageId: "parent-msg-1",
182
+ title: "Renamed parent title",
183
+ });
184
+ });
185
+
186
+ test("deleted parents are omitted from list and detail responses", async () => {
187
+ const { child, parent } = seedForkedConversation();
188
+ rawRun("DELETE FROM conversations WHERE id = ?", parent.id);
189
+ await startServer();
190
+
191
+ const listResponse = await fetch(url("/conversations"));
192
+ expect(listResponse.status).toBe(200);
193
+ const listBody = (await listResponse.json()) as {
194
+ conversations: ConversationSummary[];
195
+ };
196
+ const listedChild = listBody.conversations.find(
197
+ (item) => item.id === child.id,
198
+ );
199
+ expect(listedChild).toBeDefined();
200
+ expect(listedChild?.forkParent).toBeUndefined();
201
+
202
+ const detailResponse = await fetch(url(`/conversations/${child.id}`));
203
+ expect(detailResponse.status).toBe(200);
204
+ const detailBody = (await detailResponse.json()) as {
205
+ conversation: ConversationSummary;
206
+ };
207
+ expect(detailBody.conversation.forkParent).toBeUndefined();
208
+ });
209
+
210
+ function clearTables(): void {
211
+ const db = getDb();
212
+ db.run("DELETE FROM conversation_assistant_attention_state");
213
+ db.run("DELETE FROM external_conversation_bindings");
214
+ db.run("DELETE FROM conversation_keys");
215
+ db.run("DELETE FROM messages");
216
+ db.run("DELETE FROM conversations");
217
+ }
218
+
219
+ function seedForkedConversation(opts?: { parentTitle?: string }) {
220
+ const parent = createConversation(
221
+ opts?.parentTitle ?? "Parent conversation",
222
+ );
223
+ const child = createConversation("Forked conversation");
224
+
225
+ rawRun(
226
+ `
227
+ UPDATE conversations
228
+ SET fork_parent_conversation_id = ?, fork_parent_message_id = ?
229
+ WHERE id = ?
230
+ `,
231
+ parent.id,
232
+ "parent-msg-1",
233
+ child.id,
234
+ );
235
+
236
+ return { parent, child };
237
+ }
238
+
239
+ async function startServer(): Promise<void> {
240
+ server = new RuntimeHttpServer({
241
+ port: 0,
242
+ bearerToken: "test-bearer-token",
243
+ });
244
+ await server.start();
245
+ }
246
+
247
+ function url(pathname: string): string {
248
+ if (!server) throw new Error("server not started");
249
+ return `http://127.0.0.1:${server.actualPort}/v1${pathname}`;
250
+ }
251
+ });
@@ -0,0 +1,136 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { reinjectImageSourcePaths } from "../daemon/conversation-lifecycle.js";
4
+ import type { ContentBlock } from "../providers/types.js";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // reinjectImageSourcePaths — re-inject [Attached image source: /path]
8
+ // annotations when loading conversation history from DB
9
+ // ---------------------------------------------------------------------------
10
+
11
+ describe("reinjectImageSourcePaths", () => {
12
+ const baseContent: ContentBlock[] = [
13
+ { type: "text", text: "what is this?" },
14
+ {
15
+ type: "image",
16
+ source: { type: "base64", media_type: "image/jpeg", data: "base64img" },
17
+ },
18
+ ];
19
+
20
+ test("adds annotation when user message has imageSourcePaths in metadata", () => {
21
+ const metadata = JSON.stringify({
22
+ imageSourcePaths: { "photo.jpg": "/Users/me/Desktop/photo.jpg" },
23
+ });
24
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
25
+
26
+ expect(result).toHaveLength(3);
27
+ const annotation = result[2] as { type: "text"; text: string };
28
+ expect(annotation.type).toBe("text");
29
+ expect(annotation.text).toBe(
30
+ "[Attached image source: /Users/me/Desktop/photo.jpg]",
31
+ );
32
+ });
33
+
34
+ test("does NOT annotate assistant messages even if metadata has imageSourcePaths", () => {
35
+ const metadata = JSON.stringify({
36
+ imageSourcePaths: { "photo.jpg": "/Users/me/Desktop/photo.jpg" },
37
+ });
38
+ const result = reinjectImageSourcePaths(baseContent, "assistant", metadata);
39
+
40
+ // Should return the original content unchanged
41
+ expect(result).toBe(baseContent);
42
+ expect(result).toHaveLength(2);
43
+ });
44
+
45
+ test("returns content unchanged when metadata is null", () => {
46
+ const result = reinjectImageSourcePaths(baseContent, "user", null);
47
+ expect(result).toBe(baseContent);
48
+ expect(result).toHaveLength(2);
49
+ });
50
+
51
+ test("returns content unchanged when metadata has no imageSourcePaths", () => {
52
+ const metadata = JSON.stringify({
53
+ userMessageChannel: "desktop",
54
+ });
55
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
56
+ expect(result).toBe(baseContent);
57
+ expect(result).toHaveLength(2);
58
+ });
59
+
60
+ test("returns content unchanged when imageSourcePaths is empty object", () => {
61
+ const metadata = JSON.stringify({
62
+ imageSourcePaths: {},
63
+ });
64
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
65
+ expect(result).toBe(baseContent);
66
+ expect(result).toHaveLength(2);
67
+ });
68
+
69
+ test("handles multiple image source paths", () => {
70
+ const metadata = JSON.stringify({
71
+ imageSourcePaths: {
72
+ "a.jpg": "/path/to/a.jpg",
73
+ "b.png": "/path/to/b.png",
74
+ },
75
+ });
76
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
77
+
78
+ expect(result).toHaveLength(3);
79
+ const annotation = result[2] as { type: "text"; text: string };
80
+ expect(annotation.type).toBe("text");
81
+ expect(annotation.text).toBe(
82
+ "[Attached image source: /path/to/a.jpg]\n[Attached image source: /path/to/b.png]",
83
+ );
84
+ });
85
+
86
+ test("gracefully handles malformed metadata JSON", () => {
87
+ const result = reinjectImageSourcePaths(
88
+ baseContent,
89
+ "user",
90
+ "not-valid-json{{{",
91
+ );
92
+ // Should return original content, not throw
93
+ expect(result).toBe(baseContent);
94
+ expect(result).toHaveLength(2);
95
+ });
96
+
97
+ test("filters out non-string values in imageSourcePaths", () => {
98
+ const metadata = JSON.stringify({
99
+ imageSourcePaths: {
100
+ "photo.jpg": "/Users/me/Desktop/photo.jpg",
101
+ "bad.jpg": 42,
102
+ "also_bad.jpg": null,
103
+ },
104
+ });
105
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
106
+
107
+ expect(result).toHaveLength(3);
108
+ const annotation = result[2] as { type: "text"; text: string };
109
+ expect(annotation.text).toBe(
110
+ "[Attached image source: /Users/me/Desktop/photo.jpg]",
111
+ );
112
+ });
113
+
114
+ test("returns content unchanged when imageSourcePaths has only non-string values", () => {
115
+ const metadata = JSON.stringify({
116
+ imageSourcePaths: {
117
+ "bad.jpg": 42,
118
+ "also_bad.jpg": null,
119
+ },
120
+ });
121
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
122
+ expect(result).toBe(baseContent);
123
+ expect(result).toHaveLength(2);
124
+ });
125
+
126
+ test("preserves original content blocks in returned array", () => {
127
+ const metadata = JSON.stringify({
128
+ imageSourcePaths: { "photo.jpg": "/path/photo.jpg" },
129
+ });
130
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
131
+
132
+ // First two blocks should be identical to the originals
133
+ expect(result[0]).toEqual(baseContent[0]);
134
+ expect(result[1]).toEqual(baseContent[1]);
135
+ });
136
+ });