@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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Route handlers for attachment upload, download, and deletion.
3
3
  */
4
- import { existsSync } from "node:fs";
4
+ import { existsSync, statSync } from "node:fs";
5
5
 
6
6
  import * as attachmentsStore from "../../memory/attachments-store.js";
7
7
  import {
@@ -29,9 +29,10 @@ export async function handleUploadAttachment(req: Request): Promise<Response> {
29
29
  filename?: string;
30
30
  mimeType?: string;
31
31
  data?: string;
32
+ filePath?: string;
32
33
  };
33
34
 
34
- const { filename, mimeType, data } = body;
35
+ const { filename, mimeType, data, filePath } = body;
35
36
 
36
37
  if (!filename || typeof filename !== "string") {
37
38
  return httpError("BAD_REQUEST", "filename is required", 400);
@@ -41,24 +42,49 @@ export async function handleUploadAttachment(req: Request): Promise<Response> {
41
42
  return httpError("BAD_REQUEST", "mimeType is required", 400);
42
43
  }
43
44
 
44
- if (!data || typeof data !== "string") {
45
- return httpError("BAD_REQUEST", "data (base64) is required", 400);
46
- }
47
-
48
45
  const validation = validateAttachmentUpload(filename, mimeType);
49
46
  if (!validation.ok) {
50
47
  return httpError("UNPROCESSABLE_ENTITY", validation.error, 415);
51
48
  }
52
49
 
53
50
  let attachment: attachmentsStore.StoredAttachment;
54
- try {
55
- attachment = attachmentsStore.uploadAttachment(filename, mimeType, data);
56
- } catch (err) {
57
- if (err instanceof AttachmentUploadError) {
58
- const status = err.message.startsWith("Attachment too large") ? 413 : 400;
59
- return httpError("BAD_REQUEST", err.message, status);
51
+
52
+ // File-backed upload: when filePath is provided and data is empty/missing,
53
+ // register the attachment by path reference instead of requiring base64 data.
54
+ // This supports retry of file-backed attachments (e.g. recordings) where the
55
+ // client no longer holds the inline data but the file still exists on disk.
56
+ if (filePath && typeof filePath === "string" && (!data || data === "")) {
57
+ if (!existsSync(filePath)) {
58
+ return httpError("BAD_REQUEST", "filePath does not exist on disk", 400);
59
+ }
60
+ const sizeBytes = statSync(filePath).size;
61
+ attachment = attachmentsStore.uploadFileBackedAttachment(
62
+ filename,
63
+ mimeType,
64
+ filePath,
65
+ sizeBytes,
66
+ );
67
+ } else {
68
+ if (!data || typeof data !== "string") {
69
+ return httpError("BAD_REQUEST", "data (base64) is required", 400);
70
+ }
71
+
72
+ try {
73
+ attachment = attachmentsStore.uploadAttachment(
74
+ filename,
75
+ mimeType,
76
+ data,
77
+ filePath ?? undefined,
78
+ );
79
+ } catch (err) {
80
+ if (err instanceof AttachmentUploadError) {
81
+ const status = err.message.startsWith("Attachment too large")
82
+ ? 413
83
+ : 400;
84
+ return httpError("BAD_REQUEST", err.message, status);
85
+ }
86
+ throw err;
60
87
  }
61
- throw err;
62
88
  }
63
89
 
64
90
  return Response.json({
@@ -102,7 +128,9 @@ export async function handleDeleteAttachment(req: Request): Promise<Response> {
102
128
  }
103
129
 
104
130
  function handleGetAttachment(attachmentId: string): Response {
105
- const attachment = attachmentsStore.getAttachmentById(attachmentId);
131
+ const attachment = attachmentsStore.getAttachmentById(attachmentId, {
132
+ hydrateFileData: true,
133
+ });
106
134
  if (!attachment) {
107
135
  return httpError("NOT_FOUND", "Attachment not found", 404);
108
136
  }
@@ -132,7 +160,9 @@ export function handleGetAttachmentContent(
132
160
  attachmentId: string,
133
161
  req: Request,
134
162
  ): Response {
135
- const attachment = attachmentsStore.getAttachmentById(attachmentId);
163
+ const attachment = attachmentsStore.getAttachmentById(attachmentId, {
164
+ hydrateFileData: true,
165
+ });
136
166
  if (!attachment) {
137
167
  return httpError("NOT_FOUND", "Attachment not found", 404);
138
168
  }
@@ -14,17 +14,12 @@
14
14
 
15
15
  import { existsSync, readFileSync } from "node:fs";
16
16
 
17
- import { buildToolDefinitions } from "../../daemon/conversation-tool-setup.js";
18
17
  import { getConversationByKey } from "../../memory/conversation-key-store.js";
19
- import { buildSystemPrompt } from "../../prompts/system-prompt.js";
20
- import {
21
- createTimeout,
22
- userMessage,
23
- } from "../../providers/provider-send-message.js";
24
18
  import { checkIngressForSecrets } from "../../security/secret-ingress.js";
25
19
  import { getLogger } from "../../util/logger.js";
26
20
  import { getWorkspacePromptPath } from "../../util/platform.js";
27
21
  import type { AuthContext } from "../auth/types.js";
22
+ import { runBtwSidechain } from "../btw-sidechain.js";
28
23
  import { httpError } from "../http-errors.js";
29
24
  import type { RouteDefinition } from "../http-router.js";
30
25
  import type { SendMessageDeps } from "../http-types.js";
@@ -159,73 +154,42 @@ async function handleBtw(
159
154
  const conversation =
160
155
  await deps.sendMessageDeps.getOrCreateConversation(conversationId);
161
156
 
162
- const messages = [...conversation.getMessages(), userMessage(trimmedContent)];
163
- const tools = buildToolDefinitions();
164
- const { signal: timeoutSignal, cleanup: cleanupTimeout } =
165
- createTimeout(30_000);
166
-
167
- // Combine the timeout signal with the request's abort signal so that
168
- // disconnection or timeout both cancel the provider call.
169
- const combinedController = new AbortController();
170
- const onTimeoutAbort = () => combinedController.abort();
171
- const onRequestAbort = () => combinedController.abort();
172
- timeoutSignal.addEventListener("abort", onTimeoutAbort, { once: true });
173
- req.signal.addEventListener("abort", onRequestAbort, { once: true });
174
- const combinedSignal = combinedController.signal;
175
-
176
157
  const encoder = new TextEncoder();
177
158
 
178
159
  const stream = new ReadableStream({
179
160
  start(controller) {
180
161
  (async () => {
181
162
  try {
182
- // Preserve any conversation-specific systemPromptOverride so
183
- // side-chain responses match the active conversation contract. Only
184
- // fall back to a fresh prompt (excluding BOOTSTRAP.md) for
185
- // default conversations where no override was provided.
186
- const systemPrompt = conversation.hasSystemPromptOverride
187
- ? conversation.systemPrompt
188
- : buildSystemPrompt({ excludeBootstrap: true });
189
-
190
163
  const isIntroRequest = conversationKey === IDENTITY_INTRO_KEY;
191
- let textDeltaCount = 0;
192
- let collectedText = "";
193
- await conversation.provider.sendMessage(
194
- messages,
195
- tools,
196
- systemPrompt,
197
- {
198
- config: {
199
- max_tokens: 1024,
200
- tool_choice: { type: "none" },
201
- modelIntent: "latency-optimized",
202
- },
203
- onEvent: (event) => {
204
- if (event.type === "text_delta") {
205
- textDeltaCount++;
206
- if (isIntroRequest) collectedText += event.text;
207
- controller.enqueue(
208
- encoder.encode(
209
- `event: btw_text_delta\ndata: ${JSON.stringify({ text: event.text })}\n\n`,
210
- ),
211
- );
212
- }
213
- },
214
- signal: combinedSignal,
164
+ const result = await runBtwSidechain({
165
+ content: trimmedContent,
166
+ conversation,
167
+ signal: req.signal,
168
+ onEvent: (event) => {
169
+ if (event.type === "text_delta") {
170
+ controller.enqueue(
171
+ encoder.encode(
172
+ `event: btw_text_delta\ndata: ${JSON.stringify({ text: event.text })}\n\n`,
173
+ ),
174
+ );
175
+ }
215
176
  },
216
- );
177
+ });
217
178
 
218
- if (textDeltaCount === 0) {
179
+ if (!result.hadTextDeltas) {
219
180
  log.warn(
220
- { conversationKey, messageCount: messages.length },
181
+ {
182
+ conversationKey,
183
+ messageCount: conversation.getMessages().length + 1,
184
+ },
221
185
  "btw side-chain completed with no text deltas",
222
186
  );
223
187
  }
224
188
 
225
189
  // Cache the generated identity intro for subsequent requests.
226
- if (isIntroRequest && collectedText.trim()) {
190
+ if (isIntroRequest && result.text) {
227
191
  try {
228
- setCachedIntro(collectedText.trim());
192
+ setCachedIntro(result.text);
229
193
  log.debug("Cached identity intro text");
230
194
  } catch {
231
195
  // Non-fatal — next request will regenerate.
@@ -249,10 +213,6 @@ async function handleBtw(
249
213
  } catch {
250
214
  /* stream already closed */
251
215
  }
252
- } finally {
253
- cleanupTimeout();
254
- timeoutSignal.removeEventListener("abort", onTimeoutAbort);
255
- req.signal.removeEventListener("abort", onRequestAbort);
256
216
  }
257
217
  })();
258
218
  },
@@ -2,6 +2,7 @@
2
2
  * Route handlers for conversation management operations.
3
3
  *
4
4
  * POST /v1/conversations/switch — switch to an existing conversation
5
+ * POST /v1/conversations/fork — fork an existing conversation
5
6
  * PATCH /v1/conversations/:id/name — rename a conversation
6
7
  * DELETE /v1/conversations — clear all conversations
7
8
  * POST /v1/conversations/:id/wipe — wipe conversation and revert memory
@@ -15,6 +16,7 @@
15
16
  import {
16
17
  batchSetDisplayOrders,
17
18
  deleteConversation,
19
+ PRIVATE_CONVERSATION_FORK_ERROR,
18
20
  wipeConversation,
19
21
  } from "../../memory/conversation-crud.js";
20
22
  import {
@@ -22,6 +24,7 @@ import {
22
24
  setConversationKeyIfAbsent,
23
25
  } from "../../memory/conversation-key-store.js";
24
26
  import { enqueueMemoryJob } from "../../memory/jobs-store.js";
27
+ import { UserError } from "../../util/errors.js";
25
28
  import { getLogger } from "../../util/logger.js";
26
29
  import { httpError } from "../http-errors.js";
27
30
  import type { RouteDefinition } from "../http-router.js";
@@ -33,6 +36,10 @@ const log = getLogger("conversation-management-routes");
33
36
  // ---------------------------------------------------------------------------
34
37
 
35
38
  export interface ConversationManagementDeps {
39
+ forkConversation?: (params: {
40
+ conversationId: string;
41
+ throughMessageId?: string;
42
+ }) => Promise<Record<string, unknown>> | Record<string, unknown>;
36
43
  switchConversation: (conversationId: string) => Promise<{
37
44
  conversationId: string;
38
45
  title: string;
@@ -59,6 +66,67 @@ export function conversationManagementRouteDefinitions(
59
66
  deps: ConversationManagementDeps,
60
67
  ): RouteDefinition[] {
61
68
  return [
69
+ {
70
+ endpoint: "conversations/fork",
71
+ method: "POST",
72
+ policyKey: "conversations/fork",
73
+ handler: async ({ req }) => {
74
+ if (!deps.forkConversation) {
75
+ return httpError(
76
+ "INTERNAL_ERROR",
77
+ "Conversation forking not available",
78
+ 500,
79
+ );
80
+ }
81
+
82
+ const rawBody = (await req.json()) as unknown;
83
+ if (
84
+ rawBody == null ||
85
+ typeof rawBody !== "object" ||
86
+ Array.isArray(rawBody)
87
+ ) {
88
+ return httpError("BAD_REQUEST", "Invalid request body", 400);
89
+ }
90
+
91
+ const body = rawBody as {
92
+ conversationId?: string;
93
+ throughMessageId?: string;
94
+ };
95
+ const conversationId = body.conversationId;
96
+ if (!conversationId || typeof conversationId !== "string") {
97
+ return httpError("BAD_REQUEST", "Missing conversationId", 400);
98
+ }
99
+ if (
100
+ body.throughMessageId !== undefined &&
101
+ typeof body.throughMessageId !== "string"
102
+ ) {
103
+ return httpError(
104
+ "BAD_REQUEST",
105
+ "throughMessageId must be a string",
106
+ 400,
107
+ );
108
+ }
109
+
110
+ const resolvedConversationId =
111
+ resolveConversationId(conversationId) ?? conversationId;
112
+
113
+ try {
114
+ const conversation = await deps.forkConversation({
115
+ conversationId: resolvedConversationId,
116
+ throughMessageId: body.throughMessageId,
117
+ });
118
+ return Response.json({ conversation });
119
+ } catch (err) {
120
+ if (err instanceof UserError) {
121
+ if (err.message === PRIVATE_CONVERSATION_FORK_ERROR) {
122
+ return httpError("FORBIDDEN", err.message, 403);
123
+ }
124
+ return httpError("NOT_FOUND", err.message, 404);
125
+ }
126
+ throw err;
127
+ }
128
+ },
129
+ },
62
130
  {
63
131
  endpoint: "conversations/switch",
64
132
  method: "POST",
@@ -1,17 +1,27 @@
1
1
  /**
2
- * HTTP route definitions for model configuration, conversation search,
3
- * message content, and queued message deletion.
2
+ * HTTP route definitions for model configuration, embedding configuration,
3
+ * conversation search, message content, LLM context inspection, and queued
4
+ * message deletion.
4
5
  *
5
6
  * These routes expose conversation query functionality over the HTTP API.
6
7
  *
7
- * GET /v1/model — current model info
8
- * PUT /v1/model — set model
9
- * PUT /v1/model/image-gen — set image-gen model
10
- * GET /v1/conversations/search search conversations
11
- * GET /v1/messages/:id/content full message content
12
- * DELETE /v1/messages/queued/:id delete queued message
8
+ * GET /v1/model — current model info
9
+ * PUT /v1/model — set model
10
+ * PUT /v1/model/image-gen — set image-gen model
11
+ * GET /v1/config/embeddings current embedding config
12
+ * PUT /v1/config/embeddings set embedding provider/model
13
+ * GET /v1/conversations/search search conversations
14
+ * GET /v1/messages/:id/content — full message content
15
+ * GET /v1/messages/:id/llm-context — LLM request logs for a message
16
+ * DELETE /v1/messages/queued/:id — delete queued message
13
17
  */
14
18
 
19
+ import { VALID_MEMORY_EMBEDDING_PROVIDERS } from "../../config/schemas/memory-storage.js";
20
+ import { VALID_INFERENCE_PROVIDERS } from "../../config/schemas/services.js";
21
+ import {
22
+ getEmbeddingConfigInfo,
23
+ setEmbeddingConfig,
24
+ } from "../../daemon/handlers/config-embeddings.js";
15
25
  import {
16
26
  getModelInfo,
17
27
  type ModelSetContext,
@@ -23,8 +33,45 @@ import {
23
33
  performConversationSearch,
24
34
  } from "../../daemon/handlers/conversation-history.js";
25
35
  import { deleteQueuedMessage } from "../../daemon/handlers/conversations.js";
36
+ import { getRequestLogsByMessageId } from "../../memory/llm-request-log-store.js";
26
37
  import { httpError } from "../http-errors.js";
27
38
  import type { RouteDefinition } from "../http-router.js";
39
+ import { normalizeLlmContextPayloads } from "./llm-context-normalization.js";
40
+
41
+ const validProviderSet = new Set<string>(VALID_INFERENCE_PROVIDERS);
42
+ const validEmbeddingProviderSet = new Set<string>(
43
+ VALID_MEMORY_EMBEDDING_PROVIDERS,
44
+ );
45
+
46
+ type LlmContextNormalizationResult = ReturnType<
47
+ typeof normalizeLlmContextPayloads
48
+ >;
49
+
50
+ type LlmContextSummaryResponse = NonNullable<
51
+ Omit<NonNullable<LlmContextNormalizationResult["summary"]>, "provider">
52
+ > & {
53
+ provider: string;
54
+ };
55
+
56
+ type LlmContextRouteResult = Omit<LlmContextNormalizationResult, "summary"> & {
57
+ summary?: LlmContextSummaryResponse;
58
+ };
59
+
60
+ function applyStoredProviderToLlmContextResult(
61
+ normalized: LlmContextNormalizationResult,
62
+ provider: string | null,
63
+ ): LlmContextRouteResult {
64
+ if (!provider) {
65
+ return normalized as LlmContextRouteResult;
66
+ }
67
+
68
+ return {
69
+ ...normalized,
70
+ summary: normalized.summary
71
+ ? { ...normalized.summary, provider }
72
+ : { provider },
73
+ };
74
+ }
28
75
 
29
76
  // ---------------------------------------------------------------------------
30
77
  // Dependency interfaces
@@ -65,7 +112,10 @@ export function conversationQueryRouteDefinitions(
65
112
  if (!deps.getModelSetContext) {
66
113
  return httpError("INTERNAL_ERROR", "Model set not available", 500);
67
114
  }
68
- const body = (await req.json()) as { modelId?: string };
115
+ const body = (await req.json()) as {
116
+ modelId?: string;
117
+ provider?: string;
118
+ };
69
119
  if (!body.modelId || typeof body.modelId !== "string") {
70
120
  return httpError(
71
121
  "BAD_REQUEST",
@@ -73,8 +123,23 @@ export function conversationQueryRouteDefinitions(
73
123
  400,
74
124
  );
75
125
  }
126
+ if (
127
+ body.provider !== undefined &&
128
+ (typeof body.provider !== "string" ||
129
+ !validProviderSet.has(body.provider))
130
+ ) {
131
+ return httpError(
132
+ "BAD_REQUEST",
133
+ `Invalid provider "${body.provider}". Valid providers: ${[...validProviderSet].join(", ")}`,
134
+ 400,
135
+ );
136
+ }
76
137
  try {
77
- const info = await setModel(body.modelId, deps.getModelSetContext());
138
+ const info = await setModel(
139
+ body.modelId,
140
+ deps.getModelSetContext(),
141
+ body.provider,
142
+ );
78
143
  return Response.json(info);
79
144
  } catch (err) {
80
145
  const message = err instanceof Error ? err.message : String(err);
@@ -120,6 +185,64 @@ export function conversationQueryRouteDefinitions(
120
185
  },
121
186
  },
122
187
 
188
+ // ── Embedding config ─────────────────────────────────────────────
189
+ {
190
+ endpoint: "config/embeddings",
191
+ method: "GET",
192
+ policyKey: "config/embeddings",
193
+ handler: async () => {
194
+ const info = await getEmbeddingConfigInfo();
195
+ return Response.json(info);
196
+ },
197
+ },
198
+ {
199
+ endpoint: "config/embeddings",
200
+ method: "PUT",
201
+ policyKey: "config/embeddings",
202
+ handler: async ({ req }) => {
203
+ if (!deps.getModelSetContext) {
204
+ return httpError(
205
+ "INTERNAL_ERROR",
206
+ "Embedding config not available",
207
+ 500,
208
+ );
209
+ }
210
+ const body = (await req.json()) as {
211
+ provider?: string;
212
+ model?: string;
213
+ };
214
+ if (!body.provider || typeof body.provider !== "string") {
215
+ return httpError(
216
+ "BAD_REQUEST",
217
+ "Missing required field: provider",
218
+ 400,
219
+ );
220
+ }
221
+ if (!validEmbeddingProviderSet.has(body.provider)) {
222
+ return httpError(
223
+ "BAD_REQUEST",
224
+ `Invalid provider "${body.provider}". Valid providers: ${[...validEmbeddingProviderSet].join(", ")}`,
225
+ 400,
226
+ );
227
+ }
228
+ try {
229
+ const info = await setEmbeddingConfig(
230
+ body.provider,
231
+ body.model,
232
+ deps.getModelSetContext(),
233
+ );
234
+ return Response.json(info);
235
+ } catch (err) {
236
+ const message = err instanceof Error ? err.message : String(err);
237
+ return httpError(
238
+ "INTERNAL_ERROR",
239
+ `Failed to set embedding config: ${message}`,
240
+ 500,
241
+ );
242
+ }
243
+ },
244
+ },
245
+
123
246
  // ── Conversation search ───────────────────────────────────────────
124
247
  {
125
248
  endpoint: "conversations/search",
@@ -167,6 +290,53 @@ export function conversationQueryRouteDefinitions(
167
290
  },
168
291
  },
169
292
 
293
+ // ── LLM context (request logs) for a message ───────────────────────
294
+ {
295
+ endpoint: "messages/:id/llm-context",
296
+ method: "GET",
297
+ policyKey: "messages/llm-context",
298
+ handler: ({ params }) => {
299
+ const messageId = params.id;
300
+ if (!messageId) {
301
+ return httpError("BAD_REQUEST", "message id is required", 400);
302
+ }
303
+ const logs = getRequestLogsByMessageId(messageId);
304
+ return Response.json({
305
+ messageId,
306
+ logs: logs.map((log) => {
307
+ let requestPayload: unknown;
308
+ try {
309
+ requestPayload = JSON.parse(log.requestPayload);
310
+ } catch {
311
+ requestPayload = log.requestPayload;
312
+ }
313
+ let responsePayload: unknown;
314
+ try {
315
+ responsePayload = JSON.parse(log.responsePayload);
316
+ } catch {
317
+ responsePayload = log.responsePayload;
318
+ }
319
+ const normalized = normalizeLlmContextPayloads({
320
+ requestPayload,
321
+ responsePayload,
322
+ createdAt: log.createdAt,
323
+ });
324
+ const result = applyStoredProviderToLlmContextResult(
325
+ normalized,
326
+ log.provider,
327
+ );
328
+ return {
329
+ id: log.id,
330
+ requestPayload,
331
+ responsePayload,
332
+ createdAt: log.createdAt,
333
+ ...result,
334
+ };
335
+ }),
336
+ });
337
+ },
338
+ },
339
+
170
340
  // ── Delete queued message ─────────────────────────────────────────
171
341
  {
172
342
  endpoint: "messages/queued/:id",