@vellumai/assistant 0.8.1 → 0.8.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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -14,6 +14,12 @@ import {
14
14
 
15
15
  let rawConfigFixture: Record<string, unknown> = {};
16
16
  let savedRawConfig: Record<string, unknown> | null = null;
17
+ // Counters / spies so tests can assert that `commitConfigWrite` ran its
18
+ // post-write side effects. Each `replaceProfileRoute.handler` call that
19
+ // hits `commitConfigWrite` should bump these once.
20
+ let invalidateConfigCacheCalls = 0;
21
+ let initializeProvidersCalls = 0;
22
+ let clearEmbeddingBackendCacheCalls = 0;
17
23
 
18
24
  mock.module("../../../config/loader.js", () => ({
19
25
  loadRawConfig: () => structuredClone(rawConfigFixture),
@@ -26,6 +32,28 @@ mock.module("../../../config/loader.js", () => ({
26
32
  ) => {
27
33
  Object.assign(target, overrides);
28
34
  },
35
+ // `commitConfigWrite` (used by `handleReplaceInferenceProfile`) pulls
36
+ // in `getConfig` for the provider reinit's config arg and
37
+ // `invalidateConfigCache` so the next caller sees the fresh write.
38
+ // Stub both: getConfig returns whatever was last saved (or the fixture
39
+ // if nothing has been saved yet) and the cache-invalidation function
40
+ // is a counter so we can assert it fired.
41
+ getConfig: () => structuredClone(savedRawConfig ?? rawConfigFixture),
42
+ invalidateConfigCache: () => {
43
+ invalidateConfigCacheCalls += 1;
44
+ },
45
+ }));
46
+
47
+ mock.module("../../../providers/registry.js", () => ({
48
+ initializeProviders: async () => {
49
+ initializeProvidersCalls += 1;
50
+ },
51
+ }));
52
+
53
+ mock.module("../../../memory/embedding-backend.js", () => ({
54
+ clearEmbeddingBackendCache: () => {
55
+ clearEmbeddingBackendCacheCalls += 1;
56
+ },
29
57
  }));
30
58
 
31
59
  import type { ConversationCreateType } from "../../../memory/conversation-crud.js";
@@ -288,6 +316,9 @@ describe("GET /v1/messages/:id/llm-context — conversationTotalEstimatedCostUsd
288
316
  describe("PUT /v1/config/llm/profiles/:name", () => {
289
317
  beforeEach(() => {
290
318
  savedRawConfig = null;
319
+ invalidateConfigCacheCalls = 0;
320
+ initializeProvidersCalls = 0;
321
+ clearEmbeddingBackendCacheCalls = 0;
291
322
  rawConfigFixture = {
292
323
  llm: {
293
324
  profiles: {
@@ -313,8 +344,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
313
344
  };
314
345
  });
315
346
 
316
- test("owns contextWindow maxInputTokens while preserving non-UI profile leaves", () => {
317
- const result = replaceProfileRoute.handler({
347
+ test("owns contextWindow maxInputTokens while preserving non-UI profile leaves", async () => {
348
+ const result = await replaceProfileRoute.handler({
318
349
  pathParams: { name: "custom" },
319
350
  body: {
320
351
  provider: "openai",
@@ -343,8 +374,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
343
374
  expect(savedProfile.openrouter).toEqual({ only: ["anthropic"] });
344
375
  });
345
376
 
346
- test("writes only the replacement contextWindow maxInputTokens override", () => {
347
- const result = replaceProfileRoute.handler({
377
+ test("writes only the replacement contextWindow maxInputTokens override", async () => {
378
+ const result = await replaceProfileRoute.handler({
348
379
  pathParams: { name: "custom" },
349
380
  body: {
350
381
  provider: "openai",
@@ -375,8 +406,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
375
406
  expect(savedProfile.openrouter).toEqual({ only: ["anthropic"] });
376
407
  });
377
408
 
378
- test("writes provider_connection when present in body", () => {
379
- const result = replaceProfileRoute.handler({
409
+ test("writes provider_connection when present in body", async () => {
410
+ const result = await replaceProfileRoute.handler({
380
411
  pathParams: { name: "custom" },
381
412
  body: {
382
413
  provider: "openai",
@@ -396,7 +427,7 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
396
427
  expect(savedProfile.provider_connection).toBe("personal-openai");
397
428
  });
398
429
 
399
- test("clears provider_connection when omitted from body (UI-owned key)", () => {
430
+ test("clears provider_connection when omitted from body (UI-owned key)", async () => {
400
431
  // Seed an existing binding so the test starts from a non-empty state.
401
432
  (
402
433
  rawConfigFixture.llm as {
@@ -404,7 +435,7 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
404
435
  }
405
436
  ).profiles.custom.provider_connection = "stale-openai";
406
437
 
407
- const result = replaceProfileRoute.handler({
438
+ const result = await replaceProfileRoute.handler({
408
439
  pathParams: { name: "custom" },
409
440
  body: {
410
441
  provider: "openai",
@@ -439,8 +470,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
439
470
  };
440
471
  });
441
472
 
442
- test("allows label edit on managed profile, preserving seed fields", () => {
443
- const result = replaceProfileRoute.handler({
473
+ test("allows label edit on managed profile, preserving seed fields", async () => {
474
+ const result = await replaceProfileRoute.handler({
444
475
  pathParams: { name: "balanced" },
445
476
  body: { label: "My Balanced" },
446
477
  });
@@ -459,8 +490,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
459
490
  expect(savedProfile.source).toBe("managed");
460
491
  });
461
492
 
462
- test("allows status edit on managed profile", () => {
463
- const result = replaceProfileRoute.handler({
493
+ test("allows status edit on managed profile", async () => {
494
+ const result = await replaceProfileRoute.handler({
464
495
  pathParams: { name: "balanced" },
465
496
  body: { status: "disabled" },
466
497
  });
@@ -476,8 +507,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
476
507
  expect(savedProfile.provider).toBe("anthropic");
477
508
  });
478
509
 
479
- test("allows label+status edit together", () => {
480
- const result = replaceProfileRoute.handler({
510
+ test("allows label+status edit together", async () => {
511
+ const result = await replaceProfileRoute.handler({
481
512
  pathParams: { name: "balanced" },
482
513
  body: { label: "Renamed", status: "disabled" },
483
514
  });
@@ -493,25 +524,81 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
493
524
  expect(savedProfile.status).toBe("disabled");
494
525
  });
495
526
 
496
- test("rejects provider edit on managed profile with disallowed-keys error", () => {
497
- expect(() =>
527
+ test("rejects provider edit on managed profile with disallowed-keys error", async () => {
528
+ // The handler is `async`, so synchronous BadRequest throws still
529
+ // surface as a rejected promise; assert via `.rejects.toThrow`.
530
+ await expect(
498
531
  replaceProfileRoute.handler({
499
532
  pathParams: { name: "balanced" },
500
533
  body: { provider: "openai", model: "gpt-5" },
501
534
  }),
502
- ).toThrow(/Cannot edit managed profile "balanced" fields \[provider, model\]/);
535
+ ).rejects.toThrow(
536
+ /Cannot edit managed profile "balanced" fields \[provider, model\]/,
537
+ );
503
538
  });
504
539
 
505
- test("rejects mixed allowed+disallowed fields", () => {
540
+ test("rejects mixed allowed+disallowed fields", async () => {
506
541
  // label is allowed but maxTokens is not — must reject without partially
507
542
  // applying label, so saver should never be invoked.
508
- expect(() =>
543
+ await expect(
509
544
  replaceProfileRoute.handler({
510
545
  pathParams: { name: "balanced" },
511
546
  body: { label: "Try", maxTokens: 999 },
512
547
  }),
513
- ).toThrow(/Cannot edit managed profile "balanced" fields \[maxTokens\]/);
548
+ ).rejects.toThrow(
549
+ /Cannot edit managed profile "balanced" fields \[maxTokens\]/,
550
+ );
514
551
  expect(savedRawConfig).toBeNull();
552
+ // Reject path skips commitConfigWrite entirely — no provider reinit
553
+ // or cache invalidation should fire on a guard rejection.
554
+ expect(initializeProvidersCalls).toBe(0);
555
+ expect(invalidateConfigCacheCalls).toBe(0);
556
+ expect(clearEmbeddingBackendCacheCalls).toBe(0);
557
+ });
558
+ });
559
+
560
+ describe("commitConfigWrite side effects", () => {
561
+ test("status flip on managed profile triggers provider reinit + cache invalidation", async () => {
562
+ // Seed a managed profile that the user will disable. commitConfigWrite
563
+ // must reinit the provider registry so the status change is reflected
564
+ // in the running daemon immediately, not at the next watcher tick.
565
+ (rawConfigFixture.llm as { profiles: Record<string, unknown> }).profiles[
566
+ "balanced"
567
+ ] = {
568
+ source: "managed",
569
+ provider: "anthropic",
570
+ model: "claude-sonnet-4-6",
571
+ label: "Balanced",
572
+ status: "active",
573
+ };
574
+
575
+ const result = await replaceProfileRoute.handler({
576
+ pathParams: { name: "balanced" },
577
+ body: { status: "disabled" },
578
+ });
579
+
580
+ expect(result).toEqual({ ok: true });
581
+ expect(initializeProvidersCalls).toBe(1);
582
+ expect(invalidateConfigCacheCalls).toBe(1);
583
+ expect(clearEmbeddingBackendCacheCalls).toBe(1);
584
+ });
585
+
586
+ test("custom profile provider swap triggers provider reinit + cache invalidation", async () => {
587
+ // Custom profile path: provider/model swap on a user-owned profile.
588
+ // Same side-effect contract — registry must reinit so the new
589
+ // provider is wired into the running daemon without restart.
590
+ const result = await replaceProfileRoute.handler({
591
+ pathParams: { name: "custom" },
592
+ body: {
593
+ provider: "openai",
594
+ model: "gpt-5.5",
595
+ },
596
+ });
597
+
598
+ expect(result).toEqual({ ok: true });
599
+ expect(initializeProvidersCalls).toBe(1);
600
+ expect(invalidateConfigCacheCalls).toBe(1);
601
+ expect(clearEmbeddingBackendCacheCalls).toBe(1);
515
602
  });
516
603
  });
517
604
  });
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Tests for the `/v1/question-response` route in `question-routes.ts`.
3
+ *
4
+ * Covers:
5
+ * - kind: "submit" — single-entry happy path (option + free_text).
6
+ * - kind: "submit" — multi-entry batch resolves with the full result.
7
+ * - kind: "close" — every entry reported as skipped, overall="closed".
8
+ * - Validation: missing questionId from the batch → 400.
9
+ * - Validation: unknown questionId → 400.
10
+ * - Validation: option submission with unknown optionId → 400.
11
+ * - Cross-talk safety: a registered "confirmation" requestId returns 404.
12
+ * - Legacy single-question shim: works against a one-element batch,
13
+ * 400s against a multi-element batch.
14
+ * - The pending interaction is removed after a successful resolve.
15
+ */
16
+
17
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
18
+
19
+ import type { QuestionPromptResult } from "../../../permissions/question-prompter.js";
20
+ import * as pendingInteractions from "../../pending-interactions.js";
21
+ import { BadRequestError, NotFoundError } from "../errors.js";
22
+ import { ROUTES as QUESTION_ROUTES } from "../question-routes.js";
23
+ import type { RouteDefinition, RouteHandlerArgs } from "../types.js";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ function findHandler(operationId: string): RouteDefinition["handler"] {
30
+ const route = QUESTION_ROUTES.find((r) => r.operationId === operationId);
31
+ if (!route) throw new Error(`Route ${operationId} not found`);
32
+ return route.handler;
33
+ }
34
+
35
+ const handler = findHandler("question_response");
36
+
37
+ async function call(args: RouteHandlerArgs): Promise<unknown> {
38
+ return await handler(args);
39
+ }
40
+
41
+ /**
42
+ * Register a pending "question" interaction with the metadata the route
43
+ * needs to validate batched submissions. Mirrors what
44
+ * QuestionPrompter.prompt() does internally.
45
+ */
46
+ function registerQuestion(
47
+ requestId: string,
48
+ questions: Array<{ id: string; options: string[] }>,
49
+ rpcResolve: (value: unknown) => void = () => {},
50
+ ): void {
51
+ const optionsById: Record<string, string[]> = {};
52
+ for (const q of questions) optionsById[q.id] = q.options;
53
+ pendingInteractions.register(requestId, {
54
+ conversationId: "conv-1",
55
+ kind: "question",
56
+ rpcResolve,
57
+ metadata: {
58
+ orderedIds: questions.map((q) => q.id),
59
+ optionsById,
60
+ },
61
+ });
62
+ }
63
+
64
+ beforeEach(() => {
65
+ pendingInteractions.clear();
66
+ });
67
+
68
+ afterEach(() => {
69
+ pendingInteractions.clear();
70
+ });
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Tests
74
+ // ---------------------------------------------------------------------------
75
+
76
+ describe("POST /v1/question-response", () => {
77
+ test("submit: resolves a one-question batch with an option entry", async () => {
78
+ const resolved: QuestionPromptResult[] = [];
79
+ registerQuestion(
80
+ "req-1",
81
+ [{ id: "q1", options: ["yes", "no"] }],
82
+ (v) => resolved.push(v as QuestionPromptResult),
83
+ );
84
+
85
+ const result = await call({
86
+ body: {
87
+ requestId: "req-1",
88
+ kind: "submit",
89
+ responses: [{ questionId: "q1", kind: "option", optionId: "yes" }],
90
+ },
91
+ });
92
+
93
+ expect(result).toEqual({ success: true });
94
+ expect(resolved).toEqual([
95
+ {
96
+ entries: [{ questionId: "q1", decision: "option", optionId: "yes" }],
97
+ overall: "completed",
98
+ },
99
+ ]);
100
+ expect(pendingInteractions.get("req-1")).toBeUndefined();
101
+ });
102
+
103
+ test("submit: three-question batch with two options + one free-text", async () => {
104
+ const resolved: QuestionPromptResult[] = [];
105
+ registerQuestion(
106
+ "req-3",
107
+ [
108
+ { id: "q1", options: ["alice_work", "alice_personal"] },
109
+ { id: "q2", options: ["yes", "no"] },
110
+ { id: "q3", options: ["noon", "1pm"] },
111
+ ],
112
+ (v) => resolved.push(v as QuestionPromptResult),
113
+ );
114
+
115
+ const result = await call({
116
+ body: {
117
+ requestId: "req-3",
118
+ kind: "submit",
119
+ responses: [
120
+ { questionId: "q1", kind: "option", optionId: "alice_work" },
121
+ { questionId: "q3", kind: "free_text", text: "noon-ish" },
122
+ { questionId: "q2", kind: "option", optionId: "yes" },
123
+ ],
124
+ },
125
+ });
126
+
127
+ expect(result).toEqual({ success: true });
128
+ expect(resolved[0]?.overall).toBe("completed");
129
+ // Entries are ordered to match the original questions array.
130
+ expect(resolved[0]?.entries).toEqual([
131
+ { questionId: "q1", decision: "option", optionId: "alice_work" },
132
+ { questionId: "q2", decision: "option", optionId: "yes" },
133
+ { questionId: "q3", decision: "free_text", text: "noon-ish" },
134
+ ]);
135
+ });
136
+
137
+ test("submit: all-skip resolves with completed + skipped entries", async () => {
138
+ const resolved: QuestionPromptResult[] = [];
139
+ registerQuestion(
140
+ "req-skip-all",
141
+ [
142
+ { id: "q1", options: ["a", "b"] },
143
+ { id: "q2", options: ["x", "y"] },
144
+ ],
145
+ (v) => resolved.push(v as QuestionPromptResult),
146
+ );
147
+
148
+ await call({
149
+ body: {
150
+ requestId: "req-skip-all",
151
+ kind: "submit",
152
+ responses: [
153
+ { questionId: "q1", kind: "skip" },
154
+ { questionId: "q2", kind: "skip" },
155
+ ],
156
+ },
157
+ });
158
+
159
+ expect(resolved[0]).toEqual({
160
+ entries: [
161
+ { questionId: "q1", decision: "skipped" },
162
+ { questionId: "q2", decision: "skipped" },
163
+ ],
164
+ overall: "completed",
165
+ });
166
+ });
167
+
168
+ test("close: every entry reported as skipped with overall=closed", async () => {
169
+ const resolved: QuestionPromptResult[] = [];
170
+ registerQuestion(
171
+ "req-close",
172
+ [
173
+ { id: "q1", options: ["a", "b"] },
174
+ { id: "q2", options: ["x", "y"] },
175
+ ],
176
+ (v) => resolved.push(v as QuestionPromptResult),
177
+ );
178
+
179
+ const result = await call({
180
+ body: { requestId: "req-close", kind: "close" },
181
+ });
182
+
183
+ expect(result).toEqual({ success: true });
184
+ expect(resolved[0]).toEqual({
185
+ entries: [
186
+ { questionId: "q1", decision: "skipped" },
187
+ { questionId: "q2", decision: "skipped" },
188
+ ],
189
+ overall: "closed",
190
+ });
191
+ expect(pendingInteractions.get("req-close")).toBeUndefined();
192
+ });
193
+
194
+ test("returns 404 when no pending interaction exists for the requestId", async () => {
195
+ let thrown: unknown;
196
+ try {
197
+ await call({
198
+ body: {
199
+ requestId: "missing",
200
+ kind: "submit",
201
+ responses: [{ questionId: "q1", kind: "option", optionId: "a" }],
202
+ },
203
+ });
204
+ } catch (err) {
205
+ thrown = err;
206
+ }
207
+ expect(thrown).toBeInstanceOf(NotFoundError);
208
+ expect((thrown as NotFoundError).statusCode).toBe(404);
209
+ });
210
+
211
+ test("returns 400 when the request body fails schema validation", async () => {
212
+ registerQuestion("req-bad", [{ id: "q1", options: ["a", "b"] }]);
213
+ let thrown: unknown;
214
+ try {
215
+ // Missing `responses` for kind: "submit".
216
+ await call({ body: { requestId: "req-bad", kind: "submit" } });
217
+ } catch (err) {
218
+ thrown = err;
219
+ }
220
+ expect(thrown).toBeInstanceOf(BadRequestError);
221
+ expect((thrown as BadRequestError).statusCode).toBe(400);
222
+ });
223
+
224
+ test("returns 400 when kind is unknown", async () => {
225
+ let thrown: unknown;
226
+ try {
227
+ await call({ body: { requestId: "req-1", kind: "bogus" } });
228
+ } catch (err) {
229
+ thrown = err;
230
+ }
231
+ expect(thrown).toBeInstanceOf(BadRequestError);
232
+ });
233
+
234
+ test("returns 400 when body is missing entirely", async () => {
235
+ let thrown: unknown;
236
+ try {
237
+ await call({});
238
+ } catch (err) {
239
+ thrown = err;
240
+ }
241
+ expect(thrown).toBeInstanceOf(BadRequestError);
242
+ });
243
+
244
+ test("validation: batch missing a questionId from the original set → 400", async () => {
245
+ const resolved: unknown[] = [];
246
+ registerQuestion(
247
+ "req-miss",
248
+ [
249
+ { id: "q1", options: ["a", "b"] },
250
+ { id: "q2", options: ["x", "y"] },
251
+ ],
252
+ (v) => resolved.push(v),
253
+ );
254
+
255
+ let thrown: unknown;
256
+ try {
257
+ await call({
258
+ body: {
259
+ requestId: "req-miss",
260
+ kind: "submit",
261
+ responses: [{ questionId: "q1", kind: "option", optionId: "a" }],
262
+ },
263
+ });
264
+ } catch (err) {
265
+ thrown = err;
266
+ }
267
+ expect(thrown).toBeInstanceOf(BadRequestError);
268
+ // Pending interaction left in place so the user can retry.
269
+ expect(pendingInteractions.get("req-miss")).toBeDefined();
270
+ expect(resolved).toEqual([]);
271
+ });
272
+
273
+ test("validation: unknown questionId → 400", async () => {
274
+ registerQuestion("req-uq", [{ id: "q1", options: ["a", "b"] }]);
275
+
276
+ let thrown: unknown;
277
+ try {
278
+ await call({
279
+ body: {
280
+ requestId: "req-uq",
281
+ kind: "submit",
282
+ responses: [
283
+ { questionId: "q1", kind: "option", optionId: "a" },
284
+ { questionId: "qX", kind: "skip" },
285
+ ],
286
+ },
287
+ });
288
+ } catch (err) {
289
+ thrown = err;
290
+ }
291
+ expect(thrown).toBeInstanceOf(BadRequestError);
292
+ });
293
+
294
+ test("validation: unknown optionId → 400", async () => {
295
+ registerQuestion("req-uo", [{ id: "q1", options: ["a", "b"] }]);
296
+
297
+ let thrown: unknown;
298
+ try {
299
+ await call({
300
+ body: {
301
+ requestId: "req-uo",
302
+ kind: "submit",
303
+ responses: [{ questionId: "q1", kind: "option", optionId: "nope" }],
304
+ },
305
+ });
306
+ } catch (err) {
307
+ thrown = err;
308
+ }
309
+ expect(thrown).toBeInstanceOf(BadRequestError);
310
+ });
311
+
312
+ test("cross-talk safe: confirmation requestId returns 404", async () => {
313
+ const resolved: unknown[] = [];
314
+ pendingInteractions.register("req-confirm", {
315
+ conversationId: "conv-1",
316
+ kind: "confirmation",
317
+ rpcResolve: (value) => resolved.push(value),
318
+ });
319
+
320
+ let thrown: unknown;
321
+ try {
322
+ await call({
323
+ body: {
324
+ requestId: "req-confirm",
325
+ kind: "submit",
326
+ responses: [{ questionId: "q1", kind: "option", optionId: "yes" }],
327
+ },
328
+ });
329
+ } catch (err) {
330
+ thrown = err;
331
+ }
332
+ expect(thrown).toBeInstanceOf(NotFoundError);
333
+ expect(resolved).toEqual([]);
334
+ expect(pendingInteractions.get("req-confirm")?.kind).toBe("confirmation");
335
+ });
336
+
337
+ test("legacy single-question shim: resolves against a one-element batch", async () => {
338
+ const resolved: QuestionPromptResult[] = [];
339
+ registerQuestion(
340
+ "req-legacy",
341
+ [{ id: "q1", options: ["yes", "no"] }],
342
+ (v) => resolved.push(v as QuestionPromptResult),
343
+ );
344
+
345
+ const result = await call({
346
+ body: { requestId: "req-legacy", kind: "option", optionId: "yes" },
347
+ });
348
+
349
+ expect(result).toEqual({ success: true });
350
+ expect(resolved[0]).toEqual({
351
+ entries: [{ questionId: "q1", decision: "option", optionId: "yes" }],
352
+ overall: "completed",
353
+ });
354
+ });
355
+
356
+ test("legacy single-question shim: free-text resolves against a one-element batch", async () => {
357
+ const resolved: QuestionPromptResult[] = [];
358
+ registerQuestion(
359
+ "req-legacy-ft",
360
+ [{ id: "q1", options: ["yes", "no"] }],
361
+ (v) => resolved.push(v as QuestionPromptResult),
362
+ );
363
+
364
+ await call({
365
+ body: { requestId: "req-legacy-ft", kind: "free_text", text: "maybe" },
366
+ });
367
+
368
+ expect(resolved[0]).toEqual({
369
+ entries: [{ questionId: "q1", decision: "free_text", text: "maybe" }],
370
+ overall: "completed",
371
+ });
372
+ });
373
+
374
+ test("legacy single-question shim: rejects against a multi-element batch", async () => {
375
+ registerQuestion("req-legacy-multi", [
376
+ { id: "q1", options: ["a", "b"] },
377
+ { id: "q2", options: ["x", "y"] },
378
+ ]);
379
+
380
+ let thrown: unknown;
381
+ try {
382
+ await call({
383
+ body: { requestId: "req-legacy-multi", kind: "option", optionId: "a" },
384
+ });
385
+ } catch (err) {
386
+ thrown = err;
387
+ }
388
+ expect(thrown).toBeInstanceOf(BadRequestError);
389
+ expect((thrown as BadRequestError).message.toLowerCase()).toContain(
390
+ "multi-question",
391
+ );
392
+ // Pending interaction left in place.
393
+ expect(pendingInteractions.get("req-legacy-multi")).toBeDefined();
394
+ });
395
+ });
@@ -94,7 +94,7 @@ mock.module("../../../tts/synthesize-text.js", () => ({
94
94
  // ---------------------------------------------------------------------------
95
95
 
96
96
  import { RouteError } from "../errors.js";
97
- import { ROUTES } from "../tts-routes.js";
97
+ import { formatTtsFailureMessage, ROUTES } from "../tts-routes.js";
98
98
  import type { RouteHandlerArgs } from "../types.js";
99
99
 
100
100
  // ---------------------------------------------------------------------------
@@ -286,6 +286,69 @@ describe("tts-routes", () => {
286
286
  "BAD_GATEWAY",
287
287
  );
288
288
  });
289
+
290
+ test("propagates the underlying error message into the 502 response", async () => {
291
+ // Mimics what `synthesize-text.ts` re-throws when an ElevenLabs adapter
292
+ // raises ELEVENLABS_TTS_HTTP_ERROR with a parsed upstream message.
293
+ mockSynthesizeError = new MockTtsSynthesisError(
294
+ "TTS_SYNTHESIS_FAILED",
295
+ "TTS synthesis failed (provider: elevenlabs): Free users cannot use library voices via the API. Please upgrade your subscription to use this voice.",
296
+ );
297
+
298
+ const { handler } = getRoute("messages/:messageId/tts");
299
+ const err = await expectRouteError(
300
+ () => handler(makeMessageTtsArgs()),
301
+ 502,
302
+ "BAD_GATEWAY",
303
+ );
304
+ expect(err.message).toContain("Free users cannot use library voices");
305
+ expect(err.message).toContain("Please upgrade your subscription");
306
+ // No double-prefix — message stays as the inner self-describing form.
307
+ expect(err.message.startsWith("TTS synthesis failed")).toBe(true);
308
+ expect(
309
+ err.message.startsWith("TTS synthesis failed: TTS synthesis failed"),
310
+ ).toBe(false);
311
+ });
312
+ });
313
+
314
+ // ---------------------------------------------------------------------------
315
+ // Tests — formatTtsFailureMessage
316
+ // ---------------------------------------------------------------------------
317
+
318
+ describe("formatTtsFailureMessage", () => {
319
+ test("returns the base message when given a non-Error value", () => {
320
+ expect(formatTtsFailureMessage(undefined)).toBe("TTS synthesis failed");
321
+ expect(formatTtsFailureMessage(null)).toBe("TTS synthesis failed");
322
+ expect(formatTtsFailureMessage("oops")).toBe("TTS synthesis failed");
323
+ });
324
+
325
+ test("returns the base message when the error has no message text", () => {
326
+ const err = new Error("");
327
+ expect(formatTtsFailureMessage(err)).toBe("TTS synthesis failed");
328
+ });
329
+
330
+ test("prefixes raw provider error messages with the base", () => {
331
+ const err = new Error("Voice not found");
332
+ expect(formatTtsFailureMessage(err)).toBe(
333
+ "TTS synthesis failed: Voice not found",
334
+ );
335
+ });
336
+
337
+ test("passes pre-prefixed messages through verbatim (no double-prefix)", () => {
338
+ const err = new Error(
339
+ "TTS synthesis failed (provider: elevenlabs): Free users cannot use library voices via the API.",
340
+ );
341
+ expect(formatTtsFailureMessage(err)).toBe(
342
+ "TTS synthesis failed (provider: elevenlabs): Free users cannot use library voices via the API.",
343
+ );
344
+ });
345
+
346
+ test("trims surrounding whitespace from messages", () => {
347
+ const err = new Error(" Quota exceeded ");
348
+ expect(formatTtsFailureMessage(err)).toBe(
349
+ "TTS synthesis failed: Quota exceeded",
350
+ );
351
+ });
289
352
  });
290
353
 
291
354
  // ---------------------------------------------------------------------------