@vellumai/assistant 0.8.4 → 0.8.5

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 (438) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/browser-use-architecture-phase2.md +1 -1
  3. package/knip.json +2 -1
  4. package/openapi.yaml +809 -11
  5. package/package.json +1 -1
  6. package/src/__tests__/anthropic-provider.test.ts +34 -37
  7. package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
  8. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
  9. package/src/__tests__/audit-log-rotation.test.ts +70 -16
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +3 -3
  11. package/src/__tests__/btw-routes.test.ts +2 -3
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
  14. package/src/__tests__/channel-guardian.test.ts +3 -3
  15. package/src/__tests__/checker.test.ts +6 -15
  16. package/src/__tests__/compaction-events.test.ts +1 -0
  17. package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
  18. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
  19. package/src/__tests__/computer-use-tools.test.ts +2 -4
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  21. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
  22. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop-overflow.test.ts +197 -2
  24. package/src/__tests__/conversation-agent-loop.test.ts +163 -122
  25. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
  26. package/src/__tests__/conversation-clear-safety.test.ts +25 -25
  27. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  29. package/src/__tests__/conversation-error.test.ts +31 -0
  30. package/src/__tests__/conversation-fork-crud.test.ts +178 -15
  31. package/src/__tests__/conversation-lifecycle.test.ts +52 -11
  32. package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +13 -13
  33. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  34. package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
  36. package/src/__tests__/conversation-skill-tools.test.ts +2 -5
  37. package/src/__tests__/conversation-store.test.ts +1 -1
  38. package/src/__tests__/conversation-sync-tags.test.ts +99 -32
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  40. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  42. package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
  43. package/src/__tests__/credential-execution-tools.test.ts +6 -6
  44. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  45. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  46. package/src/__tests__/dynamic-page-surface.test.ts +2 -2
  47. package/src/__tests__/email-html-renderer.test.ts +12 -0
  48. package/src/__tests__/gateway-flag-listener.test.ts +237 -0
  49. package/src/__tests__/gemini-provider.test.ts +78 -0
  50. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  51. package/src/__tests__/guardian-outbound-http.test.ts +7 -5
  52. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  53. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
  54. package/src/__tests__/heartbeat-service.test.ts +4 -0
  55. package/src/__tests__/host-shell-tool.test.ts +1 -1
  56. package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
  57. package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
  58. package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
  59. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
  60. package/src/__tests__/llm-resolver.test.ts +77 -9
  61. package/src/__tests__/llm-usage-store.test.ts +66 -0
  62. package/src/__tests__/logger.test.ts +89 -0
  63. package/src/__tests__/mcp-abort-signal.test.ts +2 -2
  64. package/src/__tests__/media-generate-image.test.ts +31 -0
  65. package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
  66. package/src/__tests__/model-intents.test.ts +2 -4
  67. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  68. package/src/__tests__/onboarding-template-contract.test.ts +1 -1
  69. package/src/__tests__/openai-provider.test.ts +46 -0
  70. package/src/__tests__/openai-responses-provider.test.ts +114 -12
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
  72. package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
  73. package/src/__tests__/platform.test.ts +2 -2
  74. package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
  75. package/src/__tests__/plugin-bootstrap.test.ts +2 -2
  76. package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
  77. package/src/__tests__/plugin-types.test.ts +3 -2
  78. package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
  79. package/src/__tests__/pricing.test.ts +12 -0
  80. package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
  81. package/src/__tests__/registry.test.ts +2 -8
  82. package/src/__tests__/require-fresh-approval.test.ts +2 -2
  83. package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
  84. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  85. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  86. package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
  87. package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
  88. package/src/__tests__/skill-tool-factory.test.ts +1 -1
  89. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  90. package/src/__tests__/suggestion-routes.test.ts +1 -0
  91. package/src/__tests__/sync-message-contract.test.ts +59 -0
  92. package/src/__tests__/system-prompt.test.ts +145 -131
  93. package/src/__tests__/terminal-tools.test.ts +1 -1
  94. package/src/__tests__/tool-approval-handler.test.ts +1 -5
  95. package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
  96. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
  98. package/src/__tests__/tool-executor.test.ts +9 -62
  99. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
  100. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  101. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
  102. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  103. package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
  104. package/src/__tests__/usage-routes.test.ts +3 -0
  105. package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
  106. package/src/__tests__/workspace-git-service.test.ts +6 -5
  107. package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
  108. package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
  109. package/src/acp/prepare-agent-env.ts +78 -0
  110. package/src/acp/session-manager.ts +1 -1
  111. package/src/agent/loop.ts +8 -0
  112. package/src/api/README.md +5 -0
  113. package/src/api/index.ts +4 -0
  114. package/src/api/package.json +10 -0
  115. package/src/background-wake/background-wake-routes.test.ts +233 -0
  116. package/src/background-wake/runtime-registry.ts +24 -0
  117. package/src/cli/commands/__tests__/browser.test.ts +23 -5
  118. package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
  119. package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
  120. package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
  121. package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
  122. package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
  123. package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
  124. package/src/cli/commands/browser.ts +247 -0
  125. package/src/cli/commands/domain.ts +91 -41
  126. package/src/cli/commands/inference.ts +93 -40
  127. package/src/cli/commands/memory-v2-compare-render.ts +115 -0
  128. package/src/cli/commands/memory-v2.ts +176 -1
  129. package/src/cli/commands/memory-v3-render.ts +344 -0
  130. package/src/cli/commands/memory-v3.ts +316 -0
  131. package/src/cli/program.ts +2 -0
  132. package/src/config/assistant-feature-flags.ts +21 -9
  133. package/src/config/bundled-skills/document-editor/SKILL.md +11 -2
  134. package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
  135. package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
  136. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  137. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  138. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
  139. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
  140. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
  141. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
  142. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
  143. package/src/config/bundled-tool-registry.ts +2 -0
  144. package/src/config/call-site-defaults.ts +7 -6
  145. package/src/config/feature-flag-registry.json +16 -0
  146. package/src/config/schemas/__tests__/memory-v2.test.ts +213 -1
  147. package/src/config/schemas/call-site-catalog.ts +21 -7
  148. package/src/config/schemas/llm.ts +12 -1
  149. package/src/config/schemas/memory-v2.ts +246 -0
  150. package/src/config/schemas/memory.ts +2 -1
  151. package/src/context/compactor.ts +52 -0
  152. package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
  153. package/src/conversations/message-consolidation.ts +404 -0
  154. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
  155. package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
  156. package/src/daemon/conversation-agent-loop-handlers.ts +2 -13
  157. package/src/daemon/conversation-agent-loop.ts +126 -76
  158. package/src/daemon/conversation-error.ts +31 -1
  159. package/src/daemon/conversation-lifecycle.ts +27 -22
  160. package/src/daemon/conversation-runtime-assembly.ts +10 -9
  161. package/src/daemon/conversation-tool-setup.ts +63 -3
  162. package/src/daemon/conversation-usage.ts +2 -0
  163. package/src/daemon/conversation.ts +14 -29
  164. package/src/daemon/disk-pressure-guard.ts +14 -2
  165. package/src/daemon/handlers/config-model.test.ts +1 -0
  166. package/src/daemon/handlers/conversations.ts +11 -3
  167. package/src/daemon/host-browser-proxy.ts +5 -5
  168. package/src/daemon/host-cu-proxy.ts +4 -4
  169. package/src/daemon/host-file-proxy.ts +4 -4
  170. package/src/daemon/host-proxy-base.ts +4 -4
  171. package/src/daemon/host-transfer-proxy.ts +10 -10
  172. package/src/daemon/lifecycle.ts +23 -20
  173. package/src/daemon/meet-manifest-loader.ts +1 -7
  174. package/src/daemon/message-types/conversations.ts +6 -9
  175. package/src/daemon/message-types/home.ts +1 -13
  176. package/src/daemon/message-types/messages.ts +6 -14
  177. package/src/daemon/message-types/sync.ts +14 -0
  178. package/src/daemon/shutdown-handlers.ts +24 -5
  179. package/src/daemon/switch-inference-profile-tool.ts +52 -0
  180. package/src/daemon/tool-setup-types.ts +13 -0
  181. package/src/events/relationship-state-updated.ts +25 -0
  182. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  183. package/src/home/home-greeting.ts +0 -9
  184. package/src/home/suggested-prompts.ts +0 -9
  185. package/src/ipc/gateway-flag-listener.ts +123 -0
  186. package/src/ipc/skill-routes/registries.ts +8 -12
  187. package/src/memory/__tests__/db-async-query.test.ts +165 -0
  188. package/src/memory/__tests__/db-maintenance.test.ts +115 -0
  189. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
  190. package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
  191. package/src/memory/__tests__/memory-retrospective-job.test.ts +7 -0
  192. package/src/memory/auto-analysis-enqueue.ts +5 -1
  193. package/src/memory/conversation-crud.ts +71 -70
  194. package/src/memory/conversation-starters-cadence.ts +3 -1
  195. package/src/memory/conversation-title-service.ts +19 -3
  196. package/src/memory/db-async-query.ts +214 -0
  197. package/src/memory/db-init.ts +10 -0
  198. package/src/memory/db-maintenance.ts +30 -21
  199. package/src/memory/graph/bootstrap.ts +8 -1
  200. package/src/memory/graph/capability-seed.ts +7 -3
  201. package/src/memory/graph/conversation-graph-memory.ts +100 -17
  202. package/src/memory/graph/extraction.ts +1 -5
  203. package/src/memory/graph/graph-search.ts +7 -1
  204. package/src/memory/indexer.ts +28 -18
  205. package/src/memory/job-handlers/cleanup.ts +76 -18
  206. package/src/memory/job-handlers/conversation-starters.ts +1 -4
  207. package/src/memory/jobs/embed-pkb-file.ts +6 -1
  208. package/src/memory/jobs-store.ts +14 -0
  209. package/src/memory/jobs-worker.ts +55 -22
  210. package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
  211. package/src/memory/llm-request-log-source-local.ts +7 -0
  212. package/src/memory/llm-request-log-source.ts +9 -2
  213. package/src/memory/llm-request-log-store.ts +43 -1
  214. package/src/memory/llm-usage-store.ts +24 -0
  215. package/src/memory/memory-retrospective-enqueue.ts +8 -1
  216. package/src/memory/memory-retrospective-job.ts +5 -0
  217. package/src/memory/memory-v2-activation-log-store.ts +15 -6
  218. package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
  219. package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
  220. package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
  221. package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
  222. package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
  223. package/src/memory/migrations/index.ts +17 -0
  224. package/src/memory/migrations/registry.ts +33 -0
  225. package/src/memory/schema/conversations.ts +1 -1
  226. package/src/memory/schema/infrastructure.ts +21 -0
  227. package/src/memory/tool-usage-store.ts +36 -8
  228. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
  229. package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
  230. package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
  231. package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
  232. package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
  233. package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
  234. package/src/memory/v2/__tests__/injection.test.ts +127 -98
  235. package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
  236. package/src/memory/v2/__tests__/router.test.ts +171 -3
  237. package/src/memory/v2/harness/compare.ts +57 -0
  238. package/src/memory/v2/harness/metrics.ts +124 -0
  239. package/src/memory/v2/harness/oracle.ts +145 -0
  240. package/src/memory/v2/harness/replay-input.ts +224 -0
  241. package/src/memory/v2/harness/retriever.ts +74 -0
  242. package/src/memory/v2/harness/router-retriever.ts +43 -0
  243. package/src/memory/v2/harness/runner.ts +106 -0
  244. package/src/memory/v2/harness/trace.ts +58 -0
  245. package/src/memory/v2/injection.ts +21 -15
  246. package/src/memory/v2/prompts/router.ts +26 -1
  247. package/src/memory/v2/qdrant.ts +14 -2
  248. package/src/memory/v2/router.ts +171 -18
  249. package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
  250. package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
  251. package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
  252. package/src/memory/v3/__tests__/edges.test.ts +563 -0
  253. package/src/memory/v3/__tests__/filter.test.ts +512 -0
  254. package/src/memory/v3/__tests__/gate.test.ts +574 -0
  255. package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
  256. package/src/memory/v3/__tests__/loop.test.ts +530 -0
  257. package/src/memory/v3/__tests__/retriever.test.ts +226 -0
  258. package/src/memory/v3/__tests__/scouts.test.ts +440 -0
  259. package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
  260. package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
  261. package/src/memory/v3/__tests__/traversal.test.ts +469 -0
  262. package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
  263. package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
  264. package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
  265. package/src/memory/v3/__tests__/validate.test.ts +245 -0
  266. package/src/memory/v3/auto-edges.ts +223 -0
  267. package/src/memory/v3/coactivation-store.ts +124 -0
  268. package/src/memory/v3/consolidation-job.ts +323 -0
  269. package/src/memory/v3/edge-learning-job.ts +160 -0
  270. package/src/memory/v3/edges.ts +249 -0
  271. package/src/memory/v3/filter.ts +281 -0
  272. package/src/memory/v3/gate.ts +334 -0
  273. package/src/memory/v3/index-composition.ts +113 -0
  274. package/src/memory/v3/llm-capture.ts +46 -0
  275. package/src/memory/v3/loop.ts +382 -0
  276. package/src/memory/v3/maintenance.ts +144 -0
  277. package/src/memory/v3/prompt-context.ts +33 -0
  278. package/src/memory/v3/prompts/consolidation.ts +458 -0
  279. package/src/memory/v3/prompts/system-prompts.ts +196 -0
  280. package/src/memory/v3/retriever.ts +33 -0
  281. package/src/memory/v3/scouts.ts +420 -0
  282. package/src/memory/v3/shadow-middleware.ts +305 -0
  283. package/src/memory/v3/traversal.ts +206 -0
  284. package/src/memory/v3/tree-index.ts +237 -0
  285. package/src/memory/v3/tree-store.ts +394 -0
  286. package/src/memory/v3/tree-walk.ts +351 -0
  287. package/src/memory/v3/types.ts +65 -0
  288. package/src/memory/v3/validate.ts +300 -0
  289. package/src/notifications/adapters/macos.ts +18 -1
  290. package/src/notifications/adapters/platform.ts +1 -1
  291. package/src/notifications/decision-engine.ts +1 -4
  292. package/src/notifications/emit-signal.ts +29 -49
  293. package/src/permissions/prompter.ts +3 -3
  294. package/src/permissions/question-prompter.ts +5 -2
  295. package/src/permissions/secret-prompter.ts +2 -2
  296. package/src/plugin-api/index.ts +4 -0
  297. package/src/plugin-api/types.ts +7 -33
  298. package/src/plugins/defaults/index.ts +6 -0
  299. package/src/plugins/defaults/injectors.ts +18 -11
  300. package/src/plugins/external-plugin-loader.ts +5 -68
  301. package/src/plugins/types.ts +11 -16
  302. package/src/proactive-artifact/aux-message-injector.ts +17 -4
  303. package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
  304. package/src/prompts/persona-resolver.ts +36 -21
  305. package/src/prompts/sections.ts +39 -7
  306. package/src/prompts/system-prompt.ts +50 -185
  307. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  308. package/src/prompts/templates/system-sections.ts +230 -8
  309. package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
  310. package/src/providers/__tests__/retry-callsite.test.ts +85 -5
  311. package/src/providers/anthropic/client.ts +32 -66
  312. package/src/providers/call-site-routing.ts +14 -2
  313. package/src/providers/connection-model-compat.ts +38 -0
  314. package/src/providers/connection-resolution.ts +16 -2
  315. package/src/providers/gemini/client.ts +49 -6
  316. package/src/providers/inference/adapter-factory.ts +3 -0
  317. package/src/providers/minimax/client.ts +106 -0
  318. package/src/providers/model-catalog.ts +43 -0
  319. package/src/providers/model-intents.ts +1 -1
  320. package/src/providers/openai/chat-completions-provider.ts +6 -3
  321. package/src/providers/openai/codex-models.ts +18 -0
  322. package/src/providers/openai/responses-provider.ts +78 -21
  323. package/src/providers/provider-send-message.ts +7 -1
  324. package/src/providers/retry.ts +34 -3
  325. package/src/providers/thinking-config.ts +26 -1
  326. package/src/providers/usage-tracking.ts +2 -0
  327. package/src/runtime/AGENTS.md +2 -2
  328. package/src/runtime/agent-wake.ts +1 -0
  329. package/src/runtime/assistant-event-hub.ts +76 -6
  330. package/src/runtime/auth/route-policy.ts +36 -0
  331. package/src/runtime/btw-sidechain.ts +0 -6
  332. package/src/runtime/http-types.ts +0 -2
  333. package/src/runtime/migrations/vbundle-builder.ts +10 -3
  334. package/src/runtime/pending-interactions.ts +0 -1
  335. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +106 -0
  336. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +25 -6
  337. package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
  338. package/src/runtime/routes/acp-routes.test.ts +255 -6
  339. package/src/runtime/routes/acp-routes.ts +8 -1
  340. package/src/runtime/routes/avatar-routes.ts +10 -10
  341. package/src/runtime/routes/background-wake-routes.ts +188 -0
  342. package/src/runtime/routes/browser-tabs-routes.ts +200 -0
  343. package/src/runtime/routes/btw-routes.ts +0 -6
  344. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  345. package/src/runtime/routes/conversation-list-routes.ts +12 -4
  346. package/src/runtime/routes/conversation-management-routes.ts +77 -20
  347. package/src/runtime/routes/conversation-query-routes.ts +142 -36
  348. package/src/runtime/routes/conversation-routes.ts +252 -410
  349. package/src/runtime/routes/conversation-starter-routes.ts +6 -3
  350. package/src/runtime/routes/disk-pressure-routes.ts +1 -1
  351. package/src/runtime/routes/domain-routes.ts +60 -10
  352. package/src/runtime/routes/email-routes.ts +5 -2
  353. package/src/runtime/routes/events-routes.ts +54 -10
  354. package/src/runtime/routes/group-routes.ts +24 -8
  355. package/src/runtime/routes/host-browser-routes.ts +10 -2
  356. package/src/runtime/routes/host-cu-routes.ts +2 -2
  357. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
  358. package/src/runtime/routes/index.ts +8 -0
  359. package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
  360. package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
  361. package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
  362. package/src/runtime/routes/memory-item-routes.ts +8 -3
  363. package/src/runtime/routes/memory-v2-routes.ts +215 -5
  364. package/src/runtime/routes/memory-v3-routes.ts +316 -0
  365. package/src/runtime/routes/migration-routes.ts +21 -24
  366. package/src/runtime/routes/plugins-routes.ts +337 -0
  367. package/src/runtime/routes/rename-conversation-routes.ts +6 -2
  368. package/src/runtime/routes/secret-routes.ts +25 -5
  369. package/src/runtime/routes/settings-routes.ts +12 -11
  370. package/src/runtime/routes/slack-channel-routes.ts +5 -4
  371. package/src/runtime/routes/workspace-routes.ts +25 -10
  372. package/src/runtime/sync/resource-sync-events.ts +106 -38
  373. package/src/runtime/sync/sync-publisher.test.ts +49 -0
  374. package/src/runtime/sync/sync-publisher.ts +2 -1
  375. package/src/runtime/verification-outbound-actions.ts +73 -1
  376. package/src/telemetry/types.ts +12 -0
  377. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  378. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  379. package/src/tools/acp/spawn.test.ts +119 -0
  380. package/src/tools/acp/spawn.ts +15 -2
  381. package/src/tools/apps/definitions.ts +2 -8
  382. package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
  383. package/src/tools/ask-question/ask-question-tool.ts +38 -45
  384. package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
  385. package/src/tools/browser/browser-execution.ts +16 -3
  386. package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
  387. package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
  388. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
  389. package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
  390. package/src/tools/browser/cdp-client/factory.ts +100 -17
  391. package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
  392. package/src/tools/browser/cdp-client/types.ts +65 -0
  393. package/src/tools/browser/pinned-tabs.ts +96 -40
  394. package/src/tools/computer-use/definitions.ts +22 -78
  395. package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
  396. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
  397. package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
  398. package/src/tools/credentials/vault.ts +3 -9
  399. package/src/tools/document/document-tool.ts +59 -0
  400. package/src/tools/execution-target.ts +21 -23
  401. package/src/tools/executor.ts +6 -1
  402. package/src/tools/filesystem/edit.ts +3 -9
  403. package/src/tools/filesystem/list.ts +3 -9
  404. package/src/tools/filesystem/read.ts +3 -9
  405. package/src/tools/filesystem/write.ts +3 -9
  406. package/src/tools/host-filesystem/edit.ts +3 -9
  407. package/src/tools/host-filesystem/read.ts +3 -9
  408. package/src/tools/host-filesystem/transfer.ts +3 -9
  409. package/src/tools/host-filesystem/write.ts +3 -9
  410. package/src/tools/host-terminal/host-shell.ts +3 -9
  411. package/src/tools/mcp/mcp-tool-factory.ts +1 -8
  412. package/src/tools/memory/register.test.ts +1 -1
  413. package/src/tools/memory/register.ts +4 -9
  414. package/src/tools/network/web-fetch.ts +3 -9
  415. package/src/tools/network/web-search.ts +25 -32
  416. package/src/tools/registry.ts +7 -23
  417. package/src/tools/schema-transforms.ts +1 -1
  418. package/src/tools/skills/execute.ts +3 -9
  419. package/src/tools/skills/load.ts +3 -9
  420. package/src/tools/skills/skill-tool-factory.ts +1 -8
  421. package/src/tools/subagent/notify-parent.ts +3 -9
  422. package/src/tools/system/request-permission.ts +3 -9
  423. package/src/tools/terminal/shell.ts +3 -9
  424. package/src/tools/tool-defaults.ts +94 -0
  425. package/src/tools/types.ts +27 -98
  426. package/src/tools/ui-surface/definitions.ts +6 -22
  427. package/src/usage/pricing.ts +23 -0
  428. package/src/usage/types.ts +12 -0
  429. package/src/util/logger.ts +16 -7
  430. package/src/util/platform.ts +7 -2
  431. package/src/util/sqlite3-runtime.ts +65 -0
  432. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
  433. package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
  434. package/src/workspace/migrations/registry.ts +2 -0
  435. package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
  436. package/src/__tests__/message-complete-display-id.test.ts +0 -175
  437. package/src/daemon/query-complexity-router.ts +0 -75
  438. package/src/prompts/cache-boundary.ts +0 -8
@@ -20,6 +20,10 @@ import {
20
20
  } from "../../channels/types.js";
21
21
  import { isHttpAuthDisabled } from "../../config/env.js";
22
22
  import { getConfig } from "../../config/loader.js";
23
+ import {
24
+ mergeConsecutiveAssistantMessages,
25
+ mergeToolResultsIntoAssistantMessages,
26
+ } from "../../conversations/message-consolidation.js";
23
27
  import { createApprovalConversationGenerator } from "../../daemon/approval-generators.js";
24
28
  import type { Conversation } from "../../daemon/conversation.js";
25
29
  import {
@@ -121,7 +125,12 @@ import {
121
125
  resolveTrustContext,
122
126
  withSourceChannel,
123
127
  } from "../trust-context-resolver.js";
124
- import { BadRequestError, InternalError, RouteError } from "./errors.js";
128
+ import {
129
+ BadRequestError,
130
+ InternalError,
131
+ NotFoundError,
132
+ RouteError,
133
+ } from "./errors.js";
125
134
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
126
135
  import { RouteResponse } from "./types.js";
127
136
 
@@ -142,6 +151,34 @@ function isValidRiskThreshold(value: unknown): value is RiskThreshold {
142
151
  );
143
152
  }
144
153
 
154
+ // ---------------------------------------------------------------------------
155
+ // Temporary fix — remove when #31994 lands
156
+ // ---------------------------------------------------------------------------
157
+ //
158
+ // The canned-response paths in this file (canned greeting, inline approval
159
+ // reply, slash command, /compact, /clean) bypass the agent loop and so don't
160
+ // pick up the per-turn anchor id allocated in conversation-agent-loop.ts.
161
+ // Their `message_complete` events therefore went out without `messageId`,
162
+ // and the macOS client filter at ChatActionHandler.swift:507 dropped those
163
+ // events when they raced past the 50 ms streaming-buffer flush — leaving
164
+ // `isSending` stuck for the full 60 s watchdog window.
165
+ //
166
+ // Centralized so the patch surface is one helper + N one-line callers rather
167
+ // than N duplicated literals. When #31994 lands and stamps these sites with
168
+ // `state.assistantTurnId` directly, grep for `emitCannedMessageComplete` to
169
+ // find every call site and inline-then-delete.
170
+ function emitCannedMessageComplete(
171
+ send: (msg: ServerMessage) => void,
172
+ conversationId: string,
173
+ persistedAssistantId: string,
174
+ ): void {
175
+ send({
176
+ type: "message_complete",
177
+ conversationId,
178
+ messageId: persistedAssistantId,
179
+ });
180
+ }
181
+
145
182
  /**
146
183
  * True when a message's persisted metadata explicitly flags it as hidden.
147
184
  * Used to suppress internal scaffolding messages from UI history while
@@ -283,6 +320,8 @@ async function tryConsumeCanonicalGuardianReply(params: {
283
320
  verifiedActorExternalUserId?: string;
284
321
  /** Verified actor principal ID for principal-based authorization. */
285
322
  verifiedActorPrincipalId?: string;
323
+ /** Originating client identifier for sync_changed self-echo suppression. */
324
+ originClientId?: string;
286
325
  }): Promise<{ consumed: boolean; messageId?: string }> {
287
326
  const {
288
327
  conversationId,
@@ -295,6 +334,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
295
334
  approvalConversationGenerator,
296
335
  verifiedActorExternalUserId,
297
336
  verifiedActorPrincipalId,
337
+ originClientId,
298
338
  } = params;
299
339
  const trimmedContent = content.trim();
300
340
 
@@ -392,7 +432,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
392
432
  ? "Decision applied."
393
433
  : "Request already resolved.");
394
434
  const assistantMessage = createAssistantMessage(replyText);
395
- await addMessage(
435
+ const persistedAssistant = await addMessage(
396
436
  conversationId,
397
437
  "assistant",
398
438
  JSON.stringify(assistantMessage.content),
@@ -407,9 +447,9 @@ async function tryConsumeCanonicalGuardianReply(params: {
407
447
  text: replyText,
408
448
  conversationId: conversationId,
409
449
  });
410
- onEvent({ type: "message_complete", conversationId: conversationId });
450
+ emitCannedMessageComplete(onEvent, conversationId, persistedAssistant.id);
411
451
  }
412
- publishConversationMessagesChanged(conversationId);
452
+ publishConversationMessagesChanged(conversationId, originClientId);
413
453
  } catch (err) {
414
454
  log.warn(
415
455
  { err, conversationId },
@@ -792,14 +832,8 @@ export function handleListMessages({
792
832
  // on createdAt. The mismatch is benign — it may return slightly extra
793
833
  // data on a page boundary but never loses messages.
794
834
  const displayTimestamp = m.sentAt ?? m.timestamp;
795
- const mergedMessageIds = mergedIdMap.get(m.id) ?? [];
796
- const daemonMessageId =
797
- m.role === "assistant"
798
- ? (mergedMessageIds[mergedMessageIds.length - 1] ?? m.id)
799
- : undefined;
800
835
  return {
801
836
  id: m.id ?? "",
802
- ...(daemonMessageId ? { daemonMessageId } : {}),
803
837
  role: m.role,
804
838
  content: m.text,
805
839
  timestamp: new Date(displayTimestamp).toISOString(),
@@ -849,305 +883,6 @@ export function handleListMessages({
849
883
  return { messages };
850
884
  }
851
885
 
852
- // ── Tool-result merging ─────────────────────────────────────────────
853
-
854
- function isToolResultType(type: string): boolean {
855
- return type === "tool_result" || type === "web_search_tool_result";
856
- }
857
-
858
- function isSystemNoticeText(block: Record<string, unknown>): boolean {
859
- if (block.type !== "text") return false;
860
- const text = typeof block.text === "string" ? block.text : "";
861
- return (
862
- text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
863
- );
864
- }
865
-
866
- /**
867
- * Merge tool_result blocks from user messages into the preceding assistant
868
- * message's content array. This lets renderHistoryContent's pendingToolUses
869
- * map pair tool_use and tool_result blocks, preventing "unknown" tool names.
870
- *
871
- * User messages that consist entirely of tool_result blocks (and optional
872
- * system_notice text) are removed from the output. Mixed messages (tool_result
873
- * + real user text) keep only the non-tool-result blocks.
874
- */
875
- function mergeToolResultsIntoAssistantMessages(
876
- messages: MessageRow[],
877
- ): MessageRow[] {
878
- // Index of the most recent assistant message in the output array.
879
- let lastAssistantIdx = -1;
880
- // Parsed content caches — lazily populated per assistant message.
881
- const parsedAssistantContent = new Map<number, unknown[]>();
882
-
883
- const result: MessageRow[] = [];
884
-
885
- for (const msg of messages) {
886
- if (msg.role === "assistant") {
887
- lastAssistantIdx = result.length;
888
- result.push(msg);
889
- continue;
890
- }
891
-
892
- // Only process user messages — other roles pass through.
893
- if (msg.role !== "user") {
894
- result.push(msg);
895
- continue;
896
- }
897
-
898
- let blocks: unknown[];
899
- try {
900
- const parsed = JSON.parse(msg.content);
901
- if (!Array.isArray(parsed)) {
902
- result.push(msg);
903
- continue;
904
- }
905
- blocks = parsed;
906
- } catch {
907
- result.push(msg);
908
- continue;
909
- }
910
-
911
- // Separate tool-result blocks from real user content.
912
- const toolResultBlocks: unknown[] = [];
913
- const otherBlocks: unknown[] = [];
914
- for (const block of blocks) {
915
- if (
916
- typeof block === "object" &&
917
- block !== null &&
918
- typeof (block as Record<string, unknown>).type === "string"
919
- ) {
920
- const rec = block as Record<string, unknown>;
921
- if (isToolResultType(rec.type as string)) {
922
- toolResultBlocks.push(block);
923
- } else if (isSystemNoticeText(rec)) {
924
- // System notices don't count as user content — drop them when
925
- // the message is otherwise tool-result-only.
926
- otherBlocks.push(block);
927
- } else {
928
- otherBlocks.push(block);
929
- }
930
- } else {
931
- otherBlocks.push(block);
932
- }
933
- }
934
-
935
- // No tool results → pass through unchanged. System notices are only
936
- // injected alongside tool results in the agent loop, so a pure user
937
- // message (no tool_result blocks) should never be filtered — even if
938
- // the user's text happens to look like a system_notice tag.
939
- if (toolResultBlocks.length === 0) {
940
- result.push(msg);
941
- continue;
942
- }
943
-
944
- // Append tool_result blocks to the preceding assistant message's content.
945
- if (lastAssistantIdx >= 0) {
946
- const assistant = result[lastAssistantIdx];
947
- let assistantContent = parsedAssistantContent.get(lastAssistantIdx);
948
- if (!assistantContent) {
949
- try {
950
- const parsed = JSON.parse(assistant.content);
951
- assistantContent = Array.isArray(parsed) ? parsed : [parsed];
952
- } catch {
953
- assistantContent = [];
954
- }
955
- parsedAssistantContent.set(lastAssistantIdx, assistantContent);
956
- }
957
- assistantContent.push(...toolResultBlocks);
958
- } else {
959
- // No preceding assistant message (pagination boundary) — keep the
960
- // original message as-is to avoid permanent data loss. The preceding
961
- // assistant tool_use lives in the previous page; dropping the result
962
- // here would be unrecoverable.
963
- // Still strip system notices so internal prompt text isn't exposed.
964
- const filteredBlocks = blocks.filter(
965
- (b) =>
966
- !(
967
- typeof b === "object" &&
968
- b !== null &&
969
- isSystemNoticeText(b as Record<string, unknown>)
970
- ),
971
- );
972
- result.push({
973
- ...msg,
974
- content:
975
- filteredBlocks.length === blocks.length
976
- ? msg.content
977
- : JSON.stringify(filteredBlocks),
978
- });
979
- continue;
980
- }
981
-
982
- // If the user message had only tool_result (+ system_notice) blocks,
983
- // suppress it entirely. Otherwise keep the non-tool-result content.
984
- const realUserContent = otherBlocks.filter(
985
- (b) =>
986
- !(
987
- typeof b === "object" &&
988
- b !== null &&
989
- isSystemNoticeText(b as Record<string, unknown>)
990
- ),
991
- );
992
- if (realUserContent.length > 0) {
993
- result.push({ ...msg, content: JSON.stringify(otherBlocks) });
994
- }
995
- // else: tool-result-only → suppressed (results already merged above)
996
- }
997
-
998
- // Write back any modified assistant message content.
999
- for (const [idx, content] of parsedAssistantContent) {
1000
- result[idx] = { ...result[idx], content: JSON.stringify(content) };
1001
- }
1002
-
1003
- return result;
1004
- }
1005
-
1006
- // ── Consecutive assistant message merging ────────────────────────────
1007
-
1008
- /** Parse a message's JSON content into an array of content blocks. */
1009
- function parseContentBlocks(content: string): unknown[] {
1010
- try {
1011
- const parsed = JSON.parse(content);
1012
- return Array.isArray(parsed) ? parsed : [parsed];
1013
- } catch (err) {
1014
- log.warn(
1015
- { err },
1016
- "Failed to parse content blocks during assistant message merge",
1017
- );
1018
- return [];
1019
- }
1020
- }
1021
-
1022
- /**
1023
- * Append content blocks from a donor message onto a target block array.
1024
- * Parses the donor's JSON content and pushes each block into `target`.
1025
- */
1026
- function appendContentBlocks(target: unknown[], donorContent: string): void {
1027
- try {
1028
- const parsed = JSON.parse(donorContent);
1029
- if (Array.isArray(parsed)) {
1030
- target.push(...parsed);
1031
- } else {
1032
- target.push(parsed);
1033
- }
1034
- } catch (err) {
1035
- log.warn(
1036
- { err },
1037
- "Failed to parse donor content blocks during assistant message merge",
1038
- );
1039
- }
1040
- }
1041
-
1042
- /**
1043
- * Promote metadata fields from a donor message to the surviving message
1044
- * when the survivor lacks them. Currently promotes `subagentNotification`.
1045
- * Returns a new MessageRow if promotion occurred, otherwise the original.
1046
- */
1047
- function promoteMetadata(survivor: MessageRow, donor: MessageRow): MessageRow {
1048
- if (donor.metadata && survivor.metadata) {
1049
- try {
1050
- const survivorMeta = JSON.parse(survivor.metadata);
1051
- const donorMeta = JSON.parse(donor.metadata);
1052
- if (
1053
- !survivorMeta.subagentNotification &&
1054
- donorMeta.subagentNotification
1055
- ) {
1056
- survivorMeta.subagentNotification = donorMeta.subagentNotification;
1057
- return { ...survivor, metadata: JSON.stringify(survivorMeta) };
1058
- }
1059
- } catch (err) {
1060
- log.warn(
1061
- { err },
1062
- "Failed to parse metadata during assistant message merge",
1063
- );
1064
- }
1065
- } else if (donor.metadata && !survivor.metadata) {
1066
- return { ...survivor, metadata: donor.metadata };
1067
- }
1068
- return survivor;
1069
- }
1070
-
1071
- /**
1072
- * Merge consecutive assistant messages into a single message at query time.
1073
- *
1074
- * During streaming, all assistant turns within one agent loop accumulate on
1075
- * a single client-side ChatMessage. In the DB, each API turn is stored as a
1076
- * separate assistant row (consolidation is deferred to compaction for
1077
- * prefix-cache stability). This produces N separate assistant messages that
1078
- * the client renders as N individual bubbles — each showing "Completed 1
1079
- * step" instead of one grouped "Completed N steps" accordion.
1080
- *
1081
- * This function concatenates the content block arrays of consecutive
1082
- * assistant messages (no intervening user messages after tool-result
1083
- * merging) into the first message of each run. The merged messages are
1084
- * removed from the output. This is query-time only — the DB is not
1085
- * modified.
1086
- *
1087
- * The first message in each run keeps its id, createdAt, and metadata so
1088
- * that attachment lookups, display timestamps, and subagent notifications
1089
- * continue to work. Metadata from later messages in the run (e.g.
1090
- * subagentNotification) is preserved by promoting it to the surviving
1091
- * message when the surviving message has no metadata of its own for that
1092
- * field.
1093
- */
1094
- function mergeConsecutiveAssistantMessages(messages: MessageRow[]): {
1095
- messages: MessageRow[];
1096
- /** Maps each surviving message ID → all original message IDs merged into it. */
1097
- mergedIdMap: Map<string, string[]>;
1098
- } {
1099
- const result: MessageRow[] = [];
1100
- // Key = index in `result`, value = accumulated content blocks.
1101
- const pendingMerges = new Map<number, unknown[]>();
1102
- // Key = index in `result`, value = IDs of messages merged into the target.
1103
- const mergedIds = new Map<number, string[]>();
1104
-
1105
- for (const msg of messages) {
1106
- const lastIdx = result.length - 1;
1107
- const isConsecutiveAssistant =
1108
- msg.role === "assistant" &&
1109
- lastIdx >= 0 &&
1110
- result[lastIdx].role === "assistant";
1111
-
1112
- if (!isConsecutiveAssistant) {
1113
- result.push(msg);
1114
- continue;
1115
- }
1116
-
1117
- // Track the donor message ID.
1118
- let ids = mergedIds.get(lastIdx);
1119
- if (!ids) {
1120
- ids = [];
1121
- mergedIds.set(lastIdx, ids);
1122
- }
1123
- ids.push(msg.id);
1124
-
1125
- // Lazily parse the target's content on first merge.
1126
- let targetContent = pendingMerges.get(lastIdx);
1127
- if (!targetContent) {
1128
- targetContent = parseContentBlocks(result[lastIdx].content);
1129
- pendingMerges.set(lastIdx, targetContent);
1130
- }
1131
-
1132
- appendContentBlocks(targetContent, msg.content);
1133
- result[lastIdx] = promoteMetadata(result[lastIdx], msg);
1134
- }
1135
-
1136
- // Write back merged content for any messages that were targets.
1137
- for (const [idx, content] of pendingMerges) {
1138
- result[idx] = { ...result[idx], content: JSON.stringify(content) };
1139
- }
1140
-
1141
- // Build the merged ID map keyed by surviving message ID.
1142
- const mergedIdMap = new Map<string, string[]>();
1143
- for (const [idx, ids] of mergedIds) {
1144
- mergedIdMap.set(result[idx].id, ids);
1145
- }
1146
-
1147
- return { messages: result, mergedIdMap };
1148
- }
1149
-
1150
- /**
1151
886
  /**
1152
887
  * Persist the pre-chat onboarding payload to disk.
1153
888
  *
@@ -1240,6 +975,7 @@ export async function handleSendMessage(
1240
975
  ): Promise<unknown> {
1241
976
  const body = (rawBody ?? {}) as {
1242
977
  conversationKey?: string;
978
+ conversationId?: string;
1243
979
  content?: string;
1244
980
  attachmentIds?: string[];
1245
981
  sourceChannel?: string;
@@ -1271,8 +1007,14 @@ export async function handleSendMessage(
1271
1007
 
1272
1008
  const actorPrincipalId = headers?.["x-vellum-actor-principal-id"];
1273
1009
  const principalType = headers?.["x-vellum-principal-type"];
1010
+ const originClientId =
1011
+ headers?.["x-vellum-client-id"]?.trim() || undefined;
1274
1012
 
1275
1013
  const { conversationKey, content, attachmentIds } = body;
1014
+ const inboundConversationId =
1015
+ typeof body.conversationId === "string" && body.conversationId.length > 0
1016
+ ? body.conversationId
1017
+ : undefined;
1276
1018
  const clientMessageId =
1277
1019
  typeof body.clientMessageId === "string" ? body.clientMessageId : undefined;
1278
1020
  const requestedInferenceProfile =
@@ -1340,12 +1082,6 @@ export async function handleSendMessage(
1340
1082
  ? (canonicalizeTimeZone(body.clientTimezone) ?? undefined)
1341
1083
  : undefined;
1342
1084
 
1343
- // When conversationKey is omitted, derive a stable default from
1344
- // sourceChannel + sourceInterface so that repeated calls from the same
1345
- // channel/interface pair share a single conversation thread.
1346
- const resolvedConversationKey =
1347
- conversationKey ?? `default:${sourceChannel}:${sourceInterface}`;
1348
-
1349
1085
  // Reject non-string content values (numbers, objects, etc.)
1350
1086
  if (content != null && typeof content !== "string") {
1351
1087
  throw new BadRequestError("content must be a string");
@@ -1409,9 +1145,40 @@ export async function handleSendMessage(
1409
1145
  // timer so the next heartbeat is a full interval after this interaction.
1410
1146
  HeartbeatService.getInstance()?.resetTimer();
1411
1147
 
1412
- const mapping = getOrCreateConversation(resolvedConversationKey, {
1413
- conversationType: "standard",
1414
- });
1148
+ // Resolve the target conversation. Fetch by `conversationId` (the
1149
+ // assistant-minted internal id) when the client supplies it — clients
1150
+ // must obtain this id from a prior daemon response, so a missing row
1151
+ // is a 404. Otherwise fall through to the external-key path: the
1152
+ // client-supplied `conversationKey` (used by non-vellum channels and
1153
+ // the web idempotency flow) or, when neither is provided, a stable
1154
+ // default keyed on sourceChannel + sourceInterface so repeated calls
1155
+ // from the same channel/interface share a single thread.
1156
+ let mapping: {
1157
+ conversationId: string;
1158
+ conversationType: string;
1159
+ created: boolean;
1160
+ };
1161
+ if (inboundConversationId !== undefined) {
1162
+ const existing = getConversation(inboundConversationId);
1163
+ if (!existing) {
1164
+ throw new NotFoundError(
1165
+ `Conversation ${inboundConversationId} not found`,
1166
+ );
1167
+ }
1168
+ mapping = {
1169
+ conversationId: existing.id,
1170
+ conversationType: existing.conversationType,
1171
+ created: false,
1172
+ };
1173
+ } else {
1174
+ const resolvedConversationKey =
1175
+ conversationKey && conversationKey.length > 0
1176
+ ? conversationKey
1177
+ : `default:${sourceChannel}:${sourceInterface}`;
1178
+ mapping = getOrCreateConversation(resolvedConversationKey, {
1179
+ conversationType: "standard",
1180
+ });
1181
+ }
1415
1182
 
1416
1183
  if (requestedRiskThreshold !== undefined) {
1417
1184
  const result = await ipcCall("set_conversation_threshold", {
@@ -1445,6 +1212,7 @@ export async function handleSendMessage(
1445
1212
  publishConversationListAndMetadataChanged(
1446
1213
  "created",
1447
1214
  mapping.conversationId,
1215
+ originClientId,
1448
1216
  );
1449
1217
  }
1450
1218
  }
@@ -1664,7 +1432,7 @@ export async function handleSendMessage(
1664
1432
  const conversationId = mapping.conversationId;
1665
1433
 
1666
1434
  const assistantMsg = createAssistantMessage(cannedGreeting);
1667
- await addMessage(
1435
+ const persistedAssistant = await addMessage(
1668
1436
  mapping.conversationId,
1669
1437
  "assistant",
1670
1438
  JSON.stringify(assistantMsg.content),
@@ -1708,8 +1476,12 @@ export async function handleSendMessage(
1708
1476
  text: cannedGreeting,
1709
1477
  conversationId,
1710
1478
  });
1711
- broadcastMessage({ type: "message_complete", conversationId });
1712
- publishConversationMessagesChanged(conversationId);
1479
+ emitCannedMessageComplete(
1480
+ broadcastMessage,
1481
+ conversationId,
1482
+ persistedAssistant.id,
1483
+ );
1484
+ publishConversationMessagesChanged(conversationId, originClientId);
1713
1485
  conversation.processing = false;
1714
1486
  silentlyWithLog(
1715
1487
  conversation.drainQueue(),
@@ -1787,6 +1559,7 @@ export async function handleSendMessage(
1787
1559
  : deps.approvalConversationGenerator,
1788
1560
  verifiedActorExternalUserId,
1789
1561
  verifiedActorPrincipalId,
1562
+ originClientId,
1790
1563
  });
1791
1564
  if (inlineReplyResult.consumed) {
1792
1565
  return {
@@ -1975,7 +1748,7 @@ export async function handleSendMessage(
1975
1748
  conversation.getMessages().push(llmMsg);
1976
1749
 
1977
1750
  const assistantMsg = createAssistantMessage(slashResult.message);
1978
- await addMessage(
1751
+ const persistedAssistant = await addMessage(
1979
1752
  mapping.conversationId,
1980
1753
  "assistant",
1981
1754
  JSON.stringify(assistantMsg.content),
@@ -2030,11 +1803,12 @@ export async function handleSendMessage(
2030
1803
  text: message,
2031
1804
  conversationId,
2032
1805
  });
2033
- broadcastMessage({
2034
- type: "message_complete",
2035
- conversationId: conversationId,
2036
- });
2037
- publishConversationMessagesChanged(conversationId);
1806
+ emitCannedMessageComplete(
1807
+ broadcastMessage,
1808
+ conversationId,
1809
+ persistedAssistant.id,
1810
+ );
1811
+ publishConversationMessagesChanged(conversationId, originClientId);
2038
1812
  conversation.processing = false;
2039
1813
  silentlyWithLog(conversation.drainQueue(), "slash-command queue drain");
2040
1814
  }, 0);
@@ -2062,12 +1836,22 @@ export async function handleSendMessage(
2062
1836
  assistantMessageInterface: sourceInterface,
2063
1837
  };
2064
1838
  const cleanMsg = createUserMessage(rawContent, attachments);
2065
- const persisted = await addMessage(
2066
- mapping.conversationId,
2067
- "user",
2068
- JSON.stringify(cleanMsg.content),
2069
- channelMeta,
2070
- );
1839
+ let persisted: Awaited<ReturnType<typeof addMessage>>;
1840
+ try {
1841
+ persisted = await addMessage(
1842
+ mapping.conversationId,
1843
+ "user",
1844
+ JSON.stringify(cleanMsg.content),
1845
+ channelMeta,
1846
+ );
1847
+ } catch (err) {
1848
+ // The fire-and-forget compaction below owns clearing `processing`, but a
1849
+ // throw from this initial persist never reaches it — reset here so the
1850
+ // conversation isn't stranded in queued mode.
1851
+ conversation.processing = false;
1852
+ silentlyWithLog(conversation.drainQueue(), "compact-command queue drain");
1853
+ throw err;
1854
+ }
2071
1855
  conversation.getMessages().push(cleanMsg);
2072
1856
 
2073
1857
  const conversationId = mapping.conversationId;
@@ -2085,7 +1869,7 @@ export async function handleSendMessage(
2085
1869
  messageId: persisted.id,
2086
1870
  clientMessageId,
2087
1871
  });
2088
- publishConversationMessagesChanged(conversationId);
1872
+ publishConversationMessagesChanged(conversationId, originClientId);
2089
1873
  conversation.emitActivityState(
2090
1874
  "thinking",
2091
1875
  "context_compacting",
@@ -2097,7 +1881,7 @@ export async function handleSendMessage(
2097
1881
  const responseText = formatCompactResult(result);
2098
1882
 
2099
1883
  const assistantMsg = createAssistantMessage(responseText);
2100
- await addMessage(
1884
+ const persistedAssistant = await addMessage(
2101
1885
  conversationId,
2102
1886
  "assistant",
2103
1887
  JSON.stringify(assistantMsg.content),
@@ -2111,11 +1895,15 @@ export async function handleSendMessage(
2111
1895
  text: responseText,
2112
1896
  conversationId,
2113
1897
  });
2114
- broadcastMessage({ type: "message_complete", conversationId });
2115
- publishConversationMessagesChanged(conversationId);
1898
+ emitCannedMessageComplete(
1899
+ broadcastMessage,
1900
+ conversationId,
1901
+ persistedAssistant.id,
1902
+ );
1903
+ publishConversationMessagesChanged(conversationId, originClientId);
2116
1904
  } catch (err) {
2117
1905
  if (assistantMessagePersisted) {
2118
- publishConversationMessagesChanged(conversationId);
1906
+ publishConversationMessagesChanged(conversationId, originClientId);
2119
1907
  }
2120
1908
  log.error({ err, conversationId }, "Compact command failed");
2121
1909
  broadcastMessage({
@@ -2143,78 +1931,87 @@ export async function handleSendMessage(
2143
1931
 
2144
1932
  if (slashResult.kind === "clean") {
2145
1933
  conversation.processing = true;
2146
- const provenance = provenanceFromTrustContext(conversation.trustContext);
2147
- const channelMeta = {
2148
- ...provenance,
2149
- userMessageChannel: sourceChannel,
2150
- assistantMessageChannel: sourceChannel,
2151
- userMessageInterface: sourceInterface,
2152
- assistantMessageInterface: sourceInterface,
2153
- };
2154
- const cleanMsg = createUserMessage(rawContent, attachments);
2155
- const persisted = await addMessage(
2156
- mapping.conversationId,
2157
- "user",
2158
- JSON.stringify(cleanMsg.content),
2159
- channelMeta,
2160
- );
2161
- conversation.getMessages().push(cleanMsg);
2162
-
2163
1934
  const conversationId = mapping.conversationId;
2164
-
2165
- let assistantMessagePersisted = false;
1935
+ // Outer try/finally guarantees the processing flag is cleared (and the
1936
+ // queue drained) on every failure path — including a throw from the
1937
+ // initial user-message persist below, which would otherwise leave the
1938
+ // conversation stuck in queued mode indefinitely.
2166
1939
  try {
2167
- broadcastMessage({
2168
- type: "user_message_echo",
2169
- text: rawContent,
2170
- conversationId,
2171
- messageId: persisted.id,
2172
- clientMessageId,
2173
- });
2174
- publishConversationMessagesChanged(conversationId);
2175
-
2176
- const result = await conversation.forceClean();
2177
- const responseText = formatCleanResult(result);
2178
-
2179
- const assistantMsg = createAssistantMessage(responseText);
2180
- await addMessage(
2181
- conversationId,
2182
- "assistant",
2183
- JSON.stringify(assistantMsg.content),
1940
+ const provenance = provenanceFromTrustContext(conversation.trustContext);
1941
+ const channelMeta = {
1942
+ ...provenance,
1943
+ userMessageChannel: sourceChannel,
1944
+ assistantMessageChannel: sourceChannel,
1945
+ userMessageInterface: sourceInterface,
1946
+ assistantMessageInterface: sourceInterface,
1947
+ };
1948
+ const cleanMsg = createUserMessage(rawContent, attachments);
1949
+ const persisted = await addMessage(
1950
+ mapping.conversationId,
1951
+ "user",
1952
+ JSON.stringify(cleanMsg.content),
2184
1953
  channelMeta,
2185
1954
  );
2186
- assistantMessagePersisted = true;
2187
- conversation.getMessages().push(assistantMsg);
1955
+ conversation.getMessages().push(cleanMsg);
2188
1956
 
2189
- broadcastMessage({
2190
- type: "assistant_text_delta",
2191
- text: responseText,
2192
- conversationId,
2193
- });
2194
- broadcastMessage({ type: "message_complete", conversationId });
2195
- publishConversationMessagesChanged(conversationId);
2196
- } catch (err) {
2197
- if (assistantMessagePersisted) {
2198
- publishConversationMessagesChanged(conversationId);
1957
+ let assistantMessagePersisted = false;
1958
+ try {
1959
+ broadcastMessage({
1960
+ type: "user_message_echo",
1961
+ text: rawContent,
1962
+ conversationId,
1963
+ messageId: persisted.id,
1964
+ clientMessageId,
1965
+ });
1966
+ publishConversationMessagesChanged(conversationId, originClientId);
1967
+
1968
+ const result = await conversation.forceClean();
1969
+ const responseText = formatCleanResult(result);
1970
+
1971
+ const assistantMsg = createAssistantMessage(responseText);
1972
+ const persistedAssistant = await addMessage(
1973
+ conversationId,
1974
+ "assistant",
1975
+ JSON.stringify(assistantMsg.content),
1976
+ channelMeta,
1977
+ );
1978
+ assistantMessagePersisted = true;
1979
+ conversation.getMessages().push(assistantMsg);
1980
+
1981
+ broadcastMessage({
1982
+ type: "assistant_text_delta",
1983
+ text: responseText,
1984
+ conversationId,
1985
+ });
1986
+ emitCannedMessageComplete(
1987
+ broadcastMessage,
1988
+ conversationId,
1989
+ persistedAssistant.id,
1990
+ );
1991
+ publishConversationMessagesChanged(conversationId, originClientId);
1992
+ } catch (err) {
1993
+ if (assistantMessagePersisted) {
1994
+ publishConversationMessagesChanged(conversationId, originClientId);
1995
+ }
1996
+ log.error({ err, conversationId }, "Clean command failed");
1997
+ broadcastMessage({
1998
+ type: "conversation_error",
1999
+ conversationId,
2000
+ code: "UNKNOWN",
2001
+ userMessage: `Clean failed: ${err instanceof Error ? err.message : String(err)}`,
2002
+ retryable: true,
2003
+ });
2199
2004
  }
2200
- log.error({ err, conversationId }, "Clean command failed");
2201
- broadcastMessage({
2202
- type: "conversation_error",
2005
+
2006
+ return {
2007
+ accepted: true,
2008
+ messageId: persisted.id,
2203
2009
  conversationId,
2204
- code: "UNKNOWN",
2205
- userMessage: `Clean failed: ${err instanceof Error ? err.message : String(err)}`,
2206
- retryable: true,
2207
- });
2010
+ };
2208
2011
  } finally {
2209
2012
  conversation.processing = false;
2210
2013
  silentlyWithLog(conversation.drainQueue(), "clean-command queue drain");
2211
2014
  }
2212
-
2213
- return {
2214
- accepted: true,
2215
- messageId: persisted.id,
2216
- conversationId,
2217
- };
2218
2015
  }
2219
2016
 
2220
2017
  const resolvedContent = slashResult.content;
@@ -2240,7 +2037,7 @@ export async function handleSendMessage(
2240
2037
  requestId,
2241
2038
  clientMessageId,
2242
2039
  });
2243
- publishConversationMessagesChanged(mapping.conversationId);
2040
+ publishConversationMessagesChanged(mapping.conversationId, originClientId);
2244
2041
 
2245
2042
  // Fire-and-forget the agent loop; events flow to the hub via broadcastMessage.
2246
2043
  conversation
@@ -2285,14 +2082,25 @@ async function generateLlmSuggestion(
2285
2082
  ? escapeXmlContent(priorUserText)
2286
2083
  : priorUserText;
2287
2084
 
2288
- const systemPrompt =
2289
- "You generate short, casual reply suggestions a user might type next in a chat. Match the tone and register of the preceding conversation. Output only the reply text inside the requested tags — no preamble, no commentary.";
2085
+ const systemPrompt = [
2086
+ "You generate short, casual reply suggestions a user might type next in a chat.",
2087
+ "Match the tone and register of the preceding conversation.",
2088
+ "",
2089
+ "CRITICAL — write from the USER'S perspective only, NEVER from the assistant's:",
2090
+ "- The suggestion is what the USER will type into the chat input",
2091
+ "- Use first-person \"I\" only if the user has used it in their prior messages",
2092
+ "- NEVER start with phrases like \"I can help\", \"Here's what\", \"Let me\", \"I'd suggest\" — those are assistant-voice",
2093
+ "- Think: if you were the user reading the assistant's reply, what question or follow-up would you ask next?",
2094
+ "",
2095
+ "Output only the reply text inside the requested tags — no preamble, no commentary.",
2096
+ ].join("\n");
2290
2097
 
2291
2098
  const userPrompt =
2292
2099
  `Here is the end of a conversation:\n\n` +
2293
2100
  `<user_message>${truncatedUser ?? "(no prior user message)"}</user_message>\n` +
2294
2101
  `<assistant_message>${truncatedAssistant}</assistant_message>\n\n` +
2295
- `Write the user's next reply, focusing on the LAST question or call-to-action in the assistant message. Keep it short (under 15 words), casual, and in the user's voice. Respond in this exact format:\n\n` +
2102
+ `Write the USER'S next reply what the user would type. Focus on the LAST question or call-to-action in the assistant message. Keep it short (under 15 words), casual, and in the user's voice. ` +
2103
+ `The reply must read as something typed BY the user, not something the assistant would say. Respond in this exact format:\n\n` +
2296
2104
  `<reply>YOUR_REPLY_HERE</reply>`;
2297
2105
 
2298
2106
  // Single user message only — no assistant-role prefill. Anthropic
@@ -2368,14 +2176,27 @@ export async function handleGetSuggestion(
2368
2176
  };
2369
2177
 
2370
2178
  const conversationKey = queryParams?.conversationKey;
2371
- if (!conversationKey) {
2372
- throw new BadRequestError("conversationKey query parameter is required");
2179
+ const conversationId = queryParams?.conversationId;
2180
+ if (!conversationKey && !conversationId) {
2181
+ throw new BadRequestError(
2182
+ "conversationKey or conversationId query parameter is required",
2183
+ );
2373
2184
  }
2374
2185
 
2375
- const mapping = getConversationByKey(conversationKey);
2376
- if (!mapping) return noSuggestion;
2186
+ let resolvedConversationId: string | undefined;
2187
+ if (conversationId) {
2188
+ resolvedConversationId = conversationId;
2189
+ } else if (conversationKey) {
2190
+ const mapping = getConversationByKey(conversationKey);
2191
+ if (mapping) {
2192
+ resolvedConversationId = mapping.conversationId;
2193
+ } else if (getConversation(conversationKey)) {
2194
+ resolvedConversationId = conversationKey;
2195
+ }
2196
+ }
2197
+ if (!resolvedConversationId) return noSuggestion;
2377
2198
 
2378
- const rawMessages = getMessages(mapping.conversationId);
2199
+ const rawMessages = getMessages(resolvedConversationId);
2379
2200
  if (rawMessages.length === 0) return noSuggestion;
2380
2201
 
2381
2202
  // Staleness check: compare requested messageId against the latest
@@ -2629,10 +2450,31 @@ export const ROUTES: RouteDefinition[] = [
2629
2450
  description:
2630
2451
  "Return an LLM-generated follow-up suggestion for the most recent assistant message.",
2631
2452
  tags: ["messages"],
2453
+ queryParams: [
2454
+ {
2455
+ name: "conversationId",
2456
+ type: "string",
2457
+ description:
2458
+ "Conversation ID to fetch a suggestion for. Either this or conversationKey is required.",
2459
+ },
2460
+ {
2461
+ name: "conversationKey",
2462
+ type: "string",
2463
+ description:
2464
+ "Legacy conversation key. Either this or conversationId is required.",
2465
+ },
2466
+ {
2467
+ name: "messageId",
2468
+ type: "string",
2469
+ description:
2470
+ "Optional. Latest assistant message ID the client has seen — used to detect staleness.",
2471
+ },
2472
+ ],
2632
2473
  responseBody: z.object({
2633
- suggestion: z.string(),
2634
- messageId: z.string(),
2474
+ suggestion: z.string().nullable(),
2475
+ messageId: z.string().nullable(),
2635
2476
  source: z.string(),
2477
+ stale: z.boolean().optional(),
2636
2478
  }),
2637
2479
  handler: async (args) =>
2638
2480
  handleGetSuggestion(args, {