@vellumai/assistant 0.8.3 → 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 (665) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docker-entrypoint.sh +0 -1
  3. package/docs/browser-use-architecture-phase2.md +1 -1
  4. package/knip.json +2 -1
  5. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  6. package/openapi.yaml +1492 -100
  7. package/package.json +1 -1
  8. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  9. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +302 -33
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
  14. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
  15. package/src/__tests__/audit-log-rotation.test.ts +70 -16
  16. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -3
  17. package/src/__tests__/btw-routes.test.ts +2 -3
  18. package/src/__tests__/call-controller.test.ts +0 -1
  19. package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
  20. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  21. package/src/__tests__/channel-guardian.test.ts +3 -3
  22. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  23. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  24. package/src/__tests__/checker.test.ts +6 -15
  25. package/src/__tests__/compaction-events.test.ts +2 -1
  26. package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
  27. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  28. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
  29. package/src/__tests__/computer-use-tools.test.ts +2 -4
  30. package/src/__tests__/config-watcher.test.ts +1 -1
  31. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  32. package/src/__tests__/context-token-estimator.test.ts +91 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  34. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
  35. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +55 -4
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +228 -8
  37. package/src/__tests__/conversation-agent-loop.test.ts +188 -129
  38. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
  39. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  40. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  41. package/src/__tests__/conversation-clear-safety.test.ts +25 -25
  42. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  43. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
  44. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  45. package/src/__tests__/conversation-error.test.ts +31 -0
  46. package/src/__tests__/conversation-fork-crud.test.ts +324 -0
  47. package/src/__tests__/conversation-lifecycle.test.ts +53 -12
  48. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  49. package/src/__tests__/conversation-load-history-stripped.test.ts +279 -0
  50. package/src/__tests__/conversation-pairing.test.ts +2 -2
  51. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  52. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -1
  53. package/src/__tests__/conversation-queue.test.ts +1 -1
  54. package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
  55. package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
  56. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  57. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  58. package/src/__tests__/conversation-skill-tools.test.ts +2 -5
  59. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  60. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  61. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  62. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  63. package/src/__tests__/conversation-store.test.ts +1 -1
  64. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  65. package/src/__tests__/conversation-sync-tags.test.ts +99 -32
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -1
  67. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  69. package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
  70. package/src/__tests__/credential-execution-tools.test.ts +6 -6
  71. package/src/__tests__/credential-security-invariants.test.ts +7 -0
  72. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  73. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  74. package/src/__tests__/dm-backfill.test.ts +64 -0
  75. package/src/__tests__/dm-persistence.test.ts +33 -0
  76. package/src/__tests__/document-find-replace.test.ts +501 -0
  77. package/src/__tests__/dynamic-page-surface.test.ts +2 -2
  78. package/src/__tests__/email-html-renderer.test.ts +12 -0
  79. package/src/__tests__/first-greeting.test.ts +23 -2
  80. package/src/__tests__/gateway-flag-listener.test.ts +237 -0
  81. package/src/__tests__/gemini-provider.test.ts +78 -0
  82. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  83. package/src/__tests__/guardian-outbound-http.test.ts +7 -5
  84. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  85. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  86. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
  87. package/src/__tests__/heartbeat-service.test.ts +4 -0
  88. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  89. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  90. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  91. package/src/__tests__/host-file-proxy.test.ts +8 -1
  92. package/src/__tests__/host-shell-tool.test.ts +1 -1
  93. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  94. package/src/__tests__/identity-routes.test.ts +57 -0
  95. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  96. package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
  97. package/src/__tests__/injector-chain.test.ts +2 -0
  98. package/src/__tests__/injector-document-comments.test.ts +378 -0
  99. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  100. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  101. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  102. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  103. package/src/__tests__/list-messages-tool-merge.test.ts +77 -17
  104. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  105. package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
  106. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
  107. package/src/__tests__/llm-resolver.test.ts +161 -9
  108. package/src/__tests__/llm-usage-store.test.ts +66 -0
  109. package/src/__tests__/log-export-routes.test.ts +99 -2
  110. package/src/__tests__/logger.test.ts +89 -0
  111. package/src/__tests__/mcp-abort-signal.test.ts +2 -2
  112. package/src/__tests__/media-generate-image.test.ts +31 -0
  113. package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
  114. package/src/__tests__/message-queue-steer.test.ts +114 -0
  115. package/src/__tests__/model-intents.test.ts +2 -4
  116. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  117. package/src/__tests__/onboarding-template-contract.test.ts +1 -1
  118. package/src/__tests__/openai-provider.test.ts +151 -0
  119. package/src/__tests__/openai-responses-provider.test.ts +118 -16
  120. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  121. package/src/__tests__/pending-interactions-resolved-event.test.ts +189 -0
  122. package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
  123. package/src/__tests__/platform.test.ts +2 -5
  124. package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
  125. package/src/__tests__/plugin-bootstrap.test.ts +2 -2
  126. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  127. package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
  128. package/src/__tests__/plugin-types.test.ts +3 -2
  129. package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
  130. package/src/__tests__/pricing.test.ts +12 -0
  131. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  132. package/src/__tests__/process-message-display-content.test.ts +21 -16
  133. package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
  134. package/src/__tests__/registry.test.ts +2 -8
  135. package/src/__tests__/require-fresh-approval.test.ts +2 -2
  136. package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
  137. package/src/__tests__/server-history-render.test.ts +83 -4
  138. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  139. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  140. package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
  141. package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
  142. package/src/__tests__/skill-tool-factory.test.ts +1 -1
  143. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  144. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  145. package/src/__tests__/suggestion-routes.test.ts +1 -0
  146. package/src/__tests__/sync-message-contract.test.ts +59 -0
  147. package/src/__tests__/system-prompt.test.ts +161 -124
  148. package/src/__tests__/terminal-tools.test.ts +12 -2
  149. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  150. package/src/__tests__/thread-backfill.test.ts +370 -22
  151. package/src/__tests__/tool-approval-handler.test.ts +1 -5
  152. package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
  153. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
  155. package/src/__tests__/tool-executor.test.ts +89 -53
  156. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
  157. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  158. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  159. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
  160. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  161. package/src/__tests__/twilio-routes.test.ts +1 -1
  162. package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
  163. package/src/__tests__/usage-routes.test.ts +3 -0
  164. package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
  165. package/src/__tests__/web-fetch.test.ts +2 -2
  166. package/src/__tests__/workspace-git-service.test.ts +94 -10
  167. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  168. package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
  169. package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
  170. package/src/acp/prepare-agent-env.ts +78 -0
  171. package/src/acp/session-manager.ts +1 -1
  172. package/src/agent/attachments.ts +1 -0
  173. package/src/agent/loop.ts +65 -20
  174. package/src/api/README.md +5 -0
  175. package/src/api/index.ts +4 -0
  176. package/src/api/package.json +10 -0
  177. package/src/background-wake/background-wake-routes.test.ts +233 -0
  178. package/src/background-wake/next-wake.test.ts +289 -0
  179. package/src/background-wake/next-wake.ts +172 -0
  180. package/src/background-wake/runtime-registry.ts +24 -0
  181. package/src/browser/operations.ts +15 -0
  182. package/src/cli/commands/__tests__/browser.test.ts +23 -5
  183. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  184. package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
  185. package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
  186. package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
  187. package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
  188. package/src/cli/commands/__tests__/memory-v2.test.ts +10 -12
  189. package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
  190. package/src/cli/commands/browser.ts +247 -0
  191. package/src/cli/commands/conversations.ts +128 -1
  192. package/src/cli/commands/domain.ts +91 -41
  193. package/src/cli/commands/inference-providers.ts +147 -1
  194. package/src/cli/commands/inference.ts +93 -40
  195. package/src/cli/commands/memory-v2-compare-render.ts +115 -0
  196. package/src/cli/commands/memory-v2.ts +483 -0
  197. package/src/cli/commands/memory-v3-render.ts +344 -0
  198. package/src/cli/commands/memory-v3.ts +316 -0
  199. package/src/cli/commands/notifications.ts +24 -2
  200. package/src/cli/program.ts +2 -0
  201. package/src/cli/utils/conversation-id.ts +17 -5
  202. package/src/config/assistant-feature-flags.ts +21 -9
  203. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  204. package/src/config/bundled-skills/document-editor/SKILL.md +124 -0
  205. package/src/config/bundled-skills/document-editor/TOOLS.json +258 -0
  206. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  207. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  208. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  209. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  210. package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
  211. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  212. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  213. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  214. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  215. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
  216. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
  217. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
  218. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
  219. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
  220. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  221. package/src/config/bundled-tool-registry.ts +24 -12
  222. package/src/config/call-site-defaults.ts +20 -0
  223. package/src/config/feature-flag-registry.json +115 -3
  224. package/src/config/llm-resolver.ts +16 -2
  225. package/src/config/schemas/__tests__/memory-v2.test.ts +217 -1
  226. package/src/config/schemas/call-site-catalog.ts +35 -0
  227. package/src/config/schemas/llm.ts +14 -0
  228. package/src/config/schemas/memory-v2.ts +294 -1
  229. package/src/config/schemas/memory.ts +2 -1
  230. package/src/context/compactor.ts +60 -1
  231. package/src/context/token-estimator.ts +47 -4
  232. package/src/context/window-manager.ts +25 -0
  233. package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
  234. package/src/conversations/message-consolidation.ts +404 -0
  235. package/src/credential-health/credential-health-service.ts +34 -19
  236. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
  237. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  238. package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
  239. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  240. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  241. package/src/daemon/conversation-agent-loop-handlers.ts +155 -36
  242. package/src/daemon/conversation-agent-loop.ts +307 -88
  243. package/src/daemon/conversation-error.ts +31 -1
  244. package/src/daemon/conversation-lifecycle.ts +149 -118
  245. package/src/daemon/conversation-messaging.ts +3 -0
  246. package/src/daemon/conversation-process.ts +273 -0
  247. package/src/daemon/conversation-queue-manager.ts +14 -0
  248. package/src/daemon/conversation-runtime-assembly.ts +145 -84
  249. package/src/daemon/conversation-slash.ts +37 -5
  250. package/src/daemon/conversation-surfaces.ts +45 -2
  251. package/src/daemon/conversation-tool-setup.ts +70 -3
  252. package/src/daemon/conversation-usage.ts +2 -0
  253. package/src/daemon/conversation.ts +54 -32
  254. package/src/daemon/disk-pressure-guard.ts +14 -2
  255. package/src/daemon/first-greeting.ts +10 -0
  256. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  257. package/src/daemon/handlers/config-a2a.ts +160 -0
  258. package/src/daemon/handlers/config-model.test.ts +2 -0
  259. package/src/daemon/handlers/conversations.ts +90 -3
  260. package/src/daemon/handlers/shared.ts +92 -29
  261. package/src/daemon/host-bash-proxy.ts +1 -1
  262. package/src/daemon/host-browser-proxy.ts +5 -5
  263. package/src/daemon/host-cu-proxy.ts +5 -5
  264. package/src/daemon/host-file-proxy.ts +5 -5
  265. package/src/daemon/host-proxy-base.ts +4 -4
  266. package/src/daemon/host-transfer-proxy.ts +11 -11
  267. package/src/daemon/lifecycle.ts +40 -23
  268. package/src/daemon/meet-manifest-loader.ts +1 -7
  269. package/src/daemon/message-protocol.ts +4 -0
  270. package/src/daemon/message-types/conversations.ts +14 -9
  271. package/src/daemon/message-types/document-comments.ts +50 -0
  272. package/src/daemon/message-types/home.ts +1 -13
  273. package/src/daemon/message-types/messages.ts +66 -7
  274. package/src/daemon/message-types/surfaces.ts +3 -1
  275. package/src/daemon/message-types/sync.ts +14 -0
  276. package/src/daemon/message-types/web-activity.ts +57 -0
  277. package/src/daemon/plugin-source-watcher.ts +135 -3
  278. package/src/daemon/process-message.ts +69 -12
  279. package/src/daemon/shutdown-handlers.ts +24 -5
  280. package/src/daemon/switch-inference-profile-tool.ts +52 -0
  281. package/src/daemon/tool-setup-types.ts +13 -0
  282. package/src/daemon/trust-context.ts +6 -0
  283. package/src/documents/document-comments-store.test.ts +338 -0
  284. package/src/documents/document-comments-store.ts +237 -0
  285. package/src/documents/document-store.ts +202 -0
  286. package/src/events/relationship-state-updated.ts +25 -0
  287. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -2
  288. package/src/heartbeat/heartbeat-service.ts +1 -0
  289. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  290. package/src/home/feed-types.ts +6 -1
  291. package/src/home/home-content-refresh.ts +52 -0
  292. package/src/home/home-greeting-cache.ts +69 -0
  293. package/src/home/home-greeting.ts +85 -0
  294. package/src/home/suggested-prompts.ts +168 -9
  295. package/src/ipc/gateway-flag-listener.ts +123 -0
  296. package/src/ipc/skill-routes/registries.ts +8 -12
  297. package/src/memory/__tests__/db-async-query.test.ts +165 -0
  298. package/src/memory/__tests__/db-maintenance.test.ts +115 -0
  299. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
  300. package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
  301. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  302. package/src/memory/__tests__/memory-retrospective-job.test.ts +327 -6
  303. package/src/memory/auto-analysis-enqueue.ts +5 -1
  304. package/src/memory/conversation-crud.ts +191 -100
  305. package/src/memory/conversation-starters-cadence.ts +3 -1
  306. package/src/memory/conversation-title-service.ts +19 -3
  307. package/src/memory/db-async-query.ts +214 -0
  308. package/src/memory/db-init.ts +26 -0
  309. package/src/memory/db-maintenance.ts +30 -21
  310. package/src/memory/delivery-crud.ts +41 -0
  311. package/src/memory/delivery-status.ts +141 -15
  312. package/src/memory/external-conversation-store.ts +32 -1
  313. package/src/memory/graph/bootstrap.ts +8 -1
  314. package/src/memory/graph/capability-seed.ts +7 -3
  315. package/src/memory/graph/conversation-graph-memory.ts +100 -17
  316. package/src/memory/graph/extraction.ts +1 -5
  317. package/src/memory/graph/graph-search.ts +7 -1
  318. package/src/memory/indexer.ts +28 -18
  319. package/src/memory/job-handlers/cleanup.ts +76 -18
  320. package/src/memory/job-handlers/conversation-starters.ts +1 -4
  321. package/src/memory/jobs/embed-pkb-file.ts +6 -1
  322. package/src/memory/jobs-store.ts +14 -0
  323. package/src/memory/jobs-worker.ts +68 -15
  324. package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
  325. package/src/memory/llm-request-log-source-local.ts +7 -0
  326. package/src/memory/llm-request-log-source.ts +9 -2
  327. package/src/memory/llm-request-log-store.ts +43 -1
  328. package/src/memory/llm-usage-store.ts +24 -0
  329. package/src/memory/memory-retrospective-constants.ts +28 -0
  330. package/src/memory/memory-retrospective-enqueue.ts +11 -3
  331. package/src/memory/memory-retrospective-job.ts +413 -18
  332. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  333. package/src/memory/memory-v2-activation-log-store.ts +41 -14
  334. package/src/memory/migrations/100-core-tables.ts +1 -0
  335. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  336. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  337. package/src/memory/migrations/253-document-comments.ts +47 -0
  338. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  339. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  340. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  341. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  342. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  343. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  344. package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
  345. package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
  346. package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
  347. package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
  348. package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
  349. package/src/memory/migrations/index.ts +34 -0
  350. package/src/memory/migrations/registry.ts +58 -0
  351. package/src/memory/onboarding-events-store.ts +7 -0
  352. package/src/memory/schema/calls.ts +1 -0
  353. package/src/memory/schema/conversations.ts +3 -0
  354. package/src/memory/schema/infrastructure.ts +22 -0
  355. package/src/memory/tool-usage-store.ts +36 -8
  356. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
  357. package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
  358. package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
  359. package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
  360. package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
  361. package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
  362. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  363. package/src/memory/v2/__tests__/injection.test.ts +158 -112
  364. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  365. package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
  366. package/src/memory/v2/__tests__/router.test.ts +660 -4
  367. package/src/memory/v2/consolidation-job.ts +14 -0
  368. package/src/memory/v2/harness/compare.ts +57 -0
  369. package/src/memory/v2/harness/metrics.ts +124 -0
  370. package/src/memory/v2/harness/oracle.ts +145 -0
  371. package/src/memory/v2/harness/replay-input.ts +224 -0
  372. package/src/memory/v2/harness/retriever.ts +74 -0
  373. package/src/memory/v2/harness/router-retriever.ts +43 -0
  374. package/src/memory/v2/harness/runner.ts +106 -0
  375. package/src/memory/v2/harness/trace.ts +58 -0
  376. package/src/memory/v2/injection-events.ts +101 -0
  377. package/src/memory/v2/injection.ts +42 -25
  378. package/src/memory/v2/page-index.ts +209 -7
  379. package/src/memory/v2/page-store.ts +18 -0
  380. package/src/memory/v2/prompts/router.ts +26 -1
  381. package/src/memory/v2/qdrant.ts +14 -2
  382. package/src/memory/v2/router.ts +369 -62
  383. package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
  384. package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
  385. package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
  386. package/src/memory/v3/__tests__/edges.test.ts +563 -0
  387. package/src/memory/v3/__tests__/filter.test.ts +512 -0
  388. package/src/memory/v3/__tests__/gate.test.ts +574 -0
  389. package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
  390. package/src/memory/v3/__tests__/loop.test.ts +530 -0
  391. package/src/memory/v3/__tests__/retriever.test.ts +226 -0
  392. package/src/memory/v3/__tests__/scouts.test.ts +440 -0
  393. package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
  394. package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
  395. package/src/memory/v3/__tests__/traversal.test.ts +469 -0
  396. package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
  397. package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
  398. package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
  399. package/src/memory/v3/__tests__/validate.test.ts +245 -0
  400. package/src/memory/v3/auto-edges.ts +223 -0
  401. package/src/memory/v3/coactivation-store.ts +124 -0
  402. package/src/memory/v3/consolidation-job.ts +323 -0
  403. package/src/memory/v3/edge-learning-job.ts +160 -0
  404. package/src/memory/v3/edges.ts +249 -0
  405. package/src/memory/v3/filter.ts +281 -0
  406. package/src/memory/v3/gate.ts +334 -0
  407. package/src/memory/v3/index-composition.ts +113 -0
  408. package/src/memory/v3/llm-capture.ts +46 -0
  409. package/src/memory/v3/loop.ts +382 -0
  410. package/src/memory/v3/maintenance.ts +144 -0
  411. package/src/memory/v3/prompt-context.ts +33 -0
  412. package/src/memory/v3/prompts/consolidation.ts +458 -0
  413. package/src/memory/v3/prompts/system-prompts.ts +196 -0
  414. package/src/memory/v3/retriever.ts +33 -0
  415. package/src/memory/v3/scouts.ts +420 -0
  416. package/src/memory/v3/shadow-middleware.ts +305 -0
  417. package/src/memory/v3/traversal.ts +206 -0
  418. package/src/memory/v3/tree-index.ts +237 -0
  419. package/src/memory/v3/tree-store.ts +394 -0
  420. package/src/memory/v3/tree-walk.ts +351 -0
  421. package/src/memory/v3/types.ts +65 -0
  422. package/src/memory/v3/validate.ts +300 -0
  423. package/src/messaging/providers/index.ts +7 -1
  424. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  425. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  426. package/src/messaging/providers/slack/adapter.ts +178 -25
  427. package/src/messaging/providers/slack/api.test.ts +54 -0
  428. package/src/messaging/providers/slack/api.ts +119 -3
  429. package/src/messaging/providers/slack/client.ts +12 -0
  430. package/src/messaging/providers/slack/deep-link.ts +20 -1
  431. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  432. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  433. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  434. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  435. package/src/messaging/providers/slack/send.test.ts +77 -0
  436. package/src/messaging/providers/slack/send.ts +8 -2
  437. package/src/messaging/providers/slack/types.ts +14 -0
  438. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  439. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  440. package/src/notifications/adapters/macos.ts +18 -1
  441. package/src/notifications/adapters/platform.ts +1 -1
  442. package/src/notifications/conversation-seed-composer.ts +14 -2
  443. package/src/notifications/decision-engine.ts +1 -4
  444. package/src/notifications/deferred-emit.ts +135 -0
  445. package/src/notifications/emit-signal.ts +38 -50
  446. package/src/notifications/home-feed-side-effect.ts +60 -30
  447. package/src/oauth/connect-orchestrator.ts +3 -0
  448. package/src/oauth/credential-token-resolver.ts +2 -0
  449. package/src/oauth/manual-token-connection.ts +19 -0
  450. package/src/oauth/oauth-store.ts +12 -0
  451. package/src/oauth/seed-providers.ts +22 -0
  452. package/src/permissions/prompter.ts +8 -5
  453. package/src/permissions/question-prompter.ts +5 -2
  454. package/src/permissions/secret-prompter.ts +6 -3
  455. package/src/plugin-api/index.ts +4 -0
  456. package/src/plugin-api/types.ts +7 -33
  457. package/src/plugins/defaults/index.ts +6 -0
  458. package/src/plugins/defaults/injectors.ts +100 -20
  459. package/src/plugins/external-plugin-loader.ts +5 -68
  460. package/src/plugins/types.ts +11 -16
  461. package/src/proactive-artifact/aux-message-injector.ts +17 -4
  462. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  463. package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
  464. package/src/prompts/normalize-onboarding.ts +40 -0
  465. package/src/prompts/persona-resolver.ts +36 -21
  466. package/src/prompts/sections.ts +69 -19
  467. package/src/prompts/system-prompt.ts +118 -216
  468. package/src/prompts/template-detection.ts +37 -0
  469. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  470. package/src/prompts/templates/BOOTSTRAP.md +10 -2
  471. package/src/prompts/templates/VOICE.md +3 -0
  472. package/src/prompts/templates/system-sections.ts +281 -9
  473. package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
  474. package/src/providers/__tests__/retry-callsite.test.ts +85 -5
  475. package/src/providers/anthropic/client.ts +159 -66
  476. package/src/providers/call-site-routing.ts +14 -2
  477. package/src/providers/connection-model-compat.ts +38 -0
  478. package/src/providers/connection-resolution.ts +16 -2
  479. package/src/providers/fireworks/client.ts +20 -2
  480. package/src/providers/gemini/client.ts +49 -6
  481. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  482. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  483. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  484. package/src/providers/inference/adapter-factory.ts +18 -1
  485. package/src/providers/inference/auth.ts +3 -3
  486. package/src/providers/inference/codex-token-refresh.ts +128 -0
  487. package/src/providers/inference/resolve-auth.ts +49 -6
  488. package/src/providers/minimax/client.ts +106 -0
  489. package/src/providers/model-catalog.ts +91 -1
  490. package/src/providers/model-intents.ts +1 -1
  491. package/src/providers/openai/chat-completions-provider.ts +63 -23
  492. package/src/providers/openai/codex-models.ts +18 -0
  493. package/src/providers/openai/responses-provider.ts +86 -23
  494. package/src/providers/openrouter/client.ts +5 -1
  495. package/src/providers/provider-send-message.ts +7 -1
  496. package/src/providers/retry.ts +34 -3
  497. package/src/providers/thinking-config.ts +26 -1
  498. package/src/providers/types.ts +25 -0
  499. package/src/providers/usage-tracking.ts +2 -0
  500. package/src/runtime/AGENTS.md +2 -2
  501. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  502. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  503. package/src/runtime/agent-wake.ts +152 -56
  504. package/src/runtime/assistant-event-hub.ts +76 -6
  505. package/src/runtime/auth/route-policy.ts +43 -3
  506. package/src/runtime/background-job-runner.ts +26 -0
  507. package/src/runtime/btw-sidechain.ts +0 -6
  508. package/src/runtime/channel-reply-delivery.ts +182 -47
  509. package/src/runtime/channel-retry-sweep.ts +141 -16
  510. package/src/runtime/http-types.ts +7 -6
  511. package/src/runtime/migrations/vbundle-builder.ts +10 -3
  512. package/src/runtime/pending-interactions.ts +50 -8
  513. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  514. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +161 -1
  515. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  516. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +290 -0
  517. package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
  518. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  519. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  520. package/src/runtime/routes/acp-routes.test.ts +255 -6
  521. package/src/runtime/routes/acp-routes.ts +8 -1
  522. package/src/runtime/routes/approval-routes.ts +4 -1
  523. package/src/runtime/routes/avatar-routes.ts +10 -10
  524. package/src/runtime/routes/background-wake-routes.ts +188 -0
  525. package/src/runtime/routes/browser-tabs-routes.ts +200 -0
  526. package/src/runtime/routes/btw-routes.ts +0 -6
  527. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  528. package/src/runtime/routes/content-source-routes.ts +78 -0
  529. package/src/runtime/routes/conversation-cli-routes.ts +147 -2
  530. package/src/runtime/routes/conversation-list-routes.ts +12 -4
  531. package/src/runtime/routes/conversation-management-routes.ts +77 -20
  532. package/src/runtime/routes/conversation-query-routes.ts +196 -31
  533. package/src/runtime/routes/conversation-routes.ts +472 -425
  534. package/src/runtime/routes/conversation-starter-routes.ts +6 -3
  535. package/src/runtime/routes/disk-pressure-routes.ts +1 -1
  536. package/src/runtime/routes/document-comments-routes.ts +287 -0
  537. package/src/runtime/routes/documents-routes.ts +33 -0
  538. package/src/runtime/routes/domain-routes.ts +60 -10
  539. package/src/runtime/routes/email-routes.ts +5 -2
  540. package/src/runtime/routes/events-routes.ts +54 -10
  541. package/src/runtime/routes/group-routes.ts +24 -8
  542. package/src/runtime/routes/home-feed-routes.ts +6 -3
  543. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  544. package/src/runtime/routes/host-browser-routes.ts +17 -2
  545. package/src/runtime/routes/host-cu-routes.ts +2 -2
  546. package/src/runtime/routes/identity-routes.ts +21 -0
  547. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  548. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
  549. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  550. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  551. package/src/runtime/routes/index.ts +20 -4
  552. package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
  553. package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
  554. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  555. package/src/runtime/routes/integrations/a2a.ts +60 -1
  556. package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
  557. package/src/runtime/routes/log-export-routes.ts +39 -0
  558. package/src/runtime/routes/memory-item-routes.ts +8 -3
  559. package/src/runtime/routes/memory-v2-routes.ts +427 -0
  560. package/src/runtime/routes/memory-v3-routes.ts +316 -0
  561. package/src/runtime/routes/migration-routes.ts +21 -24
  562. package/src/runtime/routes/notification-routes.ts +19 -2
  563. package/src/runtime/routes/plugins-routes.ts +337 -0
  564. package/src/runtime/routes/question-routes.ts +4 -1
  565. package/src/runtime/routes/rename-conversation-routes.ts +6 -2
  566. package/src/runtime/routes/sanity-routes.ts +159 -0
  567. package/src/runtime/routes/secret-routes.ts +25 -5
  568. package/src/runtime/routes/settings-routes.ts +12 -11
  569. package/src/runtime/routes/slack-channel-routes.ts +188 -0
  570. package/src/runtime/routes/workspace-routes.ts +25 -10
  571. package/src/runtime/services/conversation-serializer.ts +30 -4
  572. package/src/runtime/sync/resource-sync-events.ts +106 -38
  573. package/src/runtime/sync/sync-publisher.test.ts +49 -0
  574. package/src/runtime/sync/sync-publisher.ts +2 -1
  575. package/src/runtime/verification-outbound-actions.ts +73 -1
  576. package/src/schedule/integration-status.ts +3 -1
  577. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  578. package/src/security/oauth2-device-code.ts +307 -0
  579. package/src/security/oauth2.ts +26 -9
  580. package/src/security/secure-keys.ts +5 -0
  581. package/src/skills/catalog-install.ts +6 -2
  582. package/src/telemetry/types.ts +12 -0
  583. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  584. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  585. package/src/tools/acp/spawn.test.ts +119 -0
  586. package/src/tools/acp/spawn.ts +15 -2
  587. package/src/tools/apps/definitions.ts +2 -8
  588. package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
  589. package/src/tools/ask-question/ask-question-tool.ts +38 -45
  590. package/src/tools/browser/__tests__/pinned-tabs.test.ts +150 -0
  591. package/src/tools/browser/browser-execution.ts +106 -0
  592. package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
  593. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  594. package/src/tools/browser/cdp-client/__tests__/types.test.ts +4 -0
  595. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +22 -0
  596. package/src/tools/browser/cdp-client/extension-cdp-client.ts +42 -2
  597. package/src/tools/browser/cdp-client/factory.ts +171 -4
  598. package/src/tools/browser/cdp-client/local-cdp-client.ts +21 -0
  599. package/src/tools/browser/cdp-client/types.ts +101 -0
  600. package/src/tools/browser/pinned-tabs.ts +146 -0
  601. package/src/tools/computer-use/definitions.ts +22 -78
  602. package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
  603. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
  604. package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
  605. package/src/tools/credentials/vault.ts +3 -9
  606. package/src/tools/document/document-comment-tool.test.ts +379 -0
  607. package/src/tools/document/document-comment-tool.ts +156 -0
  608. package/src/tools/document/document-tool.ts +187 -2
  609. package/src/tools/execution-target.ts +21 -23
  610. package/src/tools/executor.ts +6 -1
  611. package/src/tools/filesystem/edit.ts +3 -9
  612. package/src/tools/filesystem/list.ts +3 -9
  613. package/src/tools/filesystem/read.ts +3 -9
  614. package/src/tools/filesystem/write.ts +3 -9
  615. package/src/tools/host-filesystem/edit.ts +3 -9
  616. package/src/tools/host-filesystem/read.ts +3 -9
  617. package/src/tools/host-filesystem/transfer.ts +3 -9
  618. package/src/tools/host-filesystem/write.ts +3 -9
  619. package/src/tools/host-terminal/host-shell.ts +3 -9
  620. package/src/tools/mcp/mcp-tool-factory.ts +1 -8
  621. package/src/tools/memory/register.test.ts +1 -1
  622. package/src/tools/memory/register.ts +4 -9
  623. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  624. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  625. package/src/tools/network/domain-normalize.ts +17 -0
  626. package/src/tools/network/web-fetch.ts +216 -73
  627. package/src/tools/network/web-search.ts +216 -98
  628. package/src/tools/registry.ts +7 -23
  629. package/src/tools/schema-transforms.ts +1 -1
  630. package/src/tools/skills/execute.ts +3 -9
  631. package/src/tools/skills/load.ts +3 -9
  632. package/src/tools/skills/skill-tool-factory.ts +1 -8
  633. package/src/tools/subagent/notify-parent.ts +3 -9
  634. package/src/tools/system/request-permission.ts +3 -9
  635. package/src/tools/terminal/safe-env.ts +3 -2
  636. package/src/tools/terminal/shell.ts +3 -9
  637. package/src/tools/tool-approval-handler.ts +19 -12
  638. package/src/tools/tool-defaults.ts +94 -0
  639. package/src/tools/types.ts +31 -98
  640. package/src/tools/ui-surface/definitions.ts +9 -23
  641. package/src/types/onboarding-context.ts +4 -0
  642. package/src/usage/pricing.ts +23 -0
  643. package/src/usage/types.ts +12 -0
  644. package/src/util/__tests__/favicon.test.ts +84 -0
  645. package/src/util/favicon.ts +40 -0
  646. package/src/util/logger.ts +16 -7
  647. package/src/util/platform.ts +7 -7
  648. package/src/util/sqlite3-runtime.ts +65 -0
  649. package/src/workspace/git-service.ts +75 -4
  650. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
  651. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  652. package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
  653. package/src/workspace/migrations/registry.ts +4 -0
  654. package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
  655. package/src/__tests__/message-complete-display-id.test.ts +0 -175
  656. package/src/config/bundled-skills/document/SKILL.md +0 -54
  657. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  658. package/src/daemon/seed-files.ts +0 -18
  659. package/src/prompts/cache-boundary.ts +0 -8
  660. package/src/runtime/routes/interface-routes.ts +0 -43
  661. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  662. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  663. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  664. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  665. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -1,14 +1,7 @@
1
1
  /**
2
2
  * Route handlers for conversation messages and suggestions.
3
3
  */
4
- import {
5
- existsSync,
6
- readdirSync,
7
- readFileSync,
8
- statSync,
9
- writeFileSync,
10
- } from "node:fs";
11
- import { join, relative } from "node:path";
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
5
 
13
6
  import { z } from "zod";
14
7
 
@@ -27,10 +20,15 @@ import {
27
20
  } from "../../channels/types.js";
28
21
  import { isHttpAuthDisabled } from "../../config/env.js";
29
22
  import { getConfig } from "../../config/loader.js";
23
+ import {
24
+ mergeConsecutiveAssistantMessages,
25
+ mergeToolResultsIntoAssistantMessages,
26
+ } from "../../conversations/message-consolidation.js";
30
27
  import { createApprovalConversationGenerator } from "../../daemon/approval-generators.js";
31
28
  import type { Conversation } from "../../daemon/conversation.js";
32
29
  import {
33
30
  buildModelInfoEvent,
31
+ formatCleanResult,
34
32
  formatCompactResult,
35
33
  isModelSlashCommand,
36
34
  } from "../../daemon/conversation-process.js";
@@ -41,6 +39,7 @@ import {
41
39
  import { getOrCreateConversation as getOrCreateConversationInstance } from "../../daemon/conversation-store.js";
42
40
  import { canonicalizeTimeZone } from "../../daemon/date-context.js";
43
41
  import {
42
+ buildScanFirstMessage,
44
43
  getCannedFirstGreeting,
45
44
  isWakeUpGreeting,
46
45
  } from "../../daemon/first-greeting.js";
@@ -51,6 +50,7 @@ import {
51
50
  preactivateHostProxySkills,
52
51
  shouldAttachHostProxyForCapability,
53
52
  } from "../../daemon/host-proxy-preactivation.js";
53
+ import { getAssistantName } from "../../daemon/identity-helpers.js";
54
54
  import type { ServerMessage } from "../../daemon/message-protocol.js";
55
55
  import type {
56
56
  HostProxyTransportMetadata,
@@ -75,7 +75,7 @@ import {
75
75
  } from "../../memory/canonical-guardian-store.js";
76
76
  import {
77
77
  addMessage,
78
- getLastAssistantTimestampBefore,
78
+ getConversation,
79
79
  getMessages,
80
80
  getMessagesPaginated,
81
81
  hasMessages,
@@ -103,10 +103,7 @@ import type { Provider } from "../../providers/types.js";
103
103
  import { checkIngressForSecrets } from "../../security/secret-ingress.js";
104
104
  import { getSubagentManager } from "../../subagent/index.js";
105
105
  import { getLogger } from "../../util/logger.js";
106
- import {
107
- getInterfacesDir,
108
- getWorkspacePromptPath,
109
- } from "../../util/platform.js";
106
+ import { getWorkspacePromptPath } from "../../util/platform.js";
110
107
  import { silentlyWithLog } from "../../util/silently.js";
111
108
  import { assistantEventHub, broadcastMessage } from "../assistant-event-hub.js";
112
109
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
@@ -128,7 +125,12 @@ import {
128
125
  resolveTrustContext,
129
126
  withSourceChannel,
130
127
  } from "../trust-context-resolver.js";
131
- import { BadRequestError, InternalError, RouteError } from "./errors.js";
128
+ import {
129
+ BadRequestError,
130
+ InternalError,
131
+ NotFoundError,
132
+ RouteError,
133
+ } from "./errors.js";
132
134
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
133
135
  import { RouteResponse } from "./types.js";
134
136
 
@@ -136,6 +138,7 @@ const log = getLogger("conversation-routes");
136
138
 
137
139
  /** Matches the `<no_response/>` sentinel used by channel delivery suppression. */
138
140
  const NO_RESPONSE_INLINE_RE = /<no_response\s*\/?>/g;
141
+ const ATTACHMENT_ENTRY_RE = /^attachment:(\d+)$/;
139
142
 
140
143
  const SUGGESTION_CACHE_MAX = 100;
141
144
  const VALID_RISK_THRESHOLDS = ["none", "low", "medium", "high"] as const;
@@ -148,31 +151,95 @@ function isValidRiskThreshold(value: unknown): value is RiskThreshold {
148
151
  );
149
152
  }
150
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
+
182
+ /**
183
+ * True when a message's persisted metadata explicitly flags it as hidden.
184
+ * Used to suppress internal scaffolding messages from UI history while
185
+ * leaving them in the LLM-side context.
186
+ */
187
+ function isHiddenMessage(metadata: string | null): boolean {
188
+ if (!metadata) return false;
189
+ try {
190
+ const meta = JSON.parse(metadata) as { hidden?: unknown };
191
+ return meta?.hidden === true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+
151
197
  function buildSlackHistoryMessage(
152
198
  slackMeta: SlackMessageMetadata | null,
199
+ opts?: { role?: string; assistantDisplayName?: string },
153
200
  ): RuntimeMessagePayload["slackMessage"] | undefined {
154
201
  if (!slackMeta) return undefined;
155
202
 
156
203
  const slackConfig = getConfig().slack;
204
+ const replyThreadTs =
205
+ slackMeta.threadTs && slackMeta.threadTs !== slackMeta.channelTs
206
+ ? slackMeta.threadTs
207
+ : undefined;
157
208
  const messageLink = buildSlackMessageDeepLinks({
158
209
  teamId: slackConfig?.teamId,
159
210
  teamUrl: slackConfig?.teamUrl,
160
211
  channelId: slackMeta.channelId,
161
212
  messageTs: slackMeta.channelTs,
213
+ ...(replyThreadTs ? { threadTs: replyThreadTs } : {}),
162
214
  });
163
- const threadLink = slackMeta.threadTs
215
+ const threadLink = replyThreadTs
164
216
  ? buildSlackMessageDeepLinks({
165
217
  teamId: slackConfig?.teamId,
166
218
  teamUrl: slackConfig?.teamUrl,
167
219
  channelId: slackMeta.channelId,
168
- messageTs: slackMeta.threadTs,
220
+ messageTs: replyThreadTs,
169
221
  })
170
222
  : undefined;
223
+ const assistantDisplayName =
224
+ opts?.role === "assistant" ? opts.assistantDisplayName : undefined;
225
+ const senderDisplayName =
226
+ slackMeta.displayName?.trim() || assistantDisplayName;
171
227
 
172
228
  return {
173
229
  channelId: slackMeta.channelId,
230
+ ...(slackMeta.channelName ? { channelName: slackMeta.channelName } : {}),
174
231
  channelTs: slackMeta.channelTs,
175
232
  ...(slackMeta.threadTs ? { threadTs: slackMeta.threadTs } : {}),
233
+ ...(senderDisplayName || slackMeta.actorExternalUserId
234
+ ? {
235
+ sender: {
236
+ ...(senderDisplayName ? { displayName: senderDisplayName } : {}),
237
+ ...(slackMeta.actorExternalUserId
238
+ ? { externalUserId: slackMeta.actorExternalUserId }
239
+ : {}),
240
+ },
241
+ }
242
+ : {}),
176
243
  ...(messageLink ? { messageLink } : {}),
177
244
  ...(threadLink ? { threadLink } : {}),
178
245
  };
@@ -253,6 +320,8 @@ async function tryConsumeCanonicalGuardianReply(params: {
253
320
  verifiedActorExternalUserId?: string;
254
321
  /** Verified actor principal ID for principal-based authorization. */
255
322
  verifiedActorPrincipalId?: string;
323
+ /** Originating client identifier for sync_changed self-echo suppression. */
324
+ originClientId?: string;
256
325
  }): Promise<{ consumed: boolean; messageId?: string }> {
257
326
  const {
258
327
  conversationId,
@@ -265,6 +334,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
265
334
  approvalConversationGenerator,
266
335
  verifiedActorExternalUserId,
267
336
  verifiedActorPrincipalId,
337
+ originClientId,
268
338
  } = params;
269
339
  const trimmedContent = content.trim();
270
340
 
@@ -362,7 +432,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
362
432
  ? "Decision applied."
363
433
  : "Request already resolved.");
364
434
  const assistantMessage = createAssistantMessage(replyText);
365
- await addMessage(
435
+ const persistedAssistant = await addMessage(
366
436
  conversationId,
367
437
  "assistant",
368
438
  JSON.stringify(assistantMessage.content),
@@ -377,9 +447,9 @@ async function tryConsumeCanonicalGuardianReply(params: {
377
447
  text: replyText,
378
448
  conversationId: conversationId,
379
449
  });
380
- onEvent({ type: "message_complete", conversationId: conversationId });
450
+ emitCannedMessageComplete(onEvent, conversationId, persistedAssistant.id);
381
451
  }
382
- publishConversationMessagesChanged(conversationId);
452
+ publishConversationMessagesChanged(conversationId, originClientId);
383
453
  } catch (err) {
384
454
  log.warn(
385
455
  { err, conversationId },
@@ -390,32 +460,9 @@ async function tryConsumeCanonicalGuardianReply(params: {
390
460
  return { consumed: true, messageId };
391
461
  }
392
462
 
393
- function getInterfaceFilesWithMtimes(
394
- interfacesDir: string | null,
395
- ): Array<{ path: string; mtimeMs: number }> {
396
- if (!interfacesDir || !existsSync(interfacesDir)) return [];
397
- const results: Array<{ path: string; mtimeMs: number }> = [];
398
- const scan = (dir: string): void => {
399
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
400
- const fullPath = join(dir, entry.name);
401
- if (entry.isDirectory()) {
402
- scan(fullPath);
403
- } else {
404
- results.push({
405
- path: relative(interfacesDir, fullPath),
406
- mtimeMs: statSync(fullPath).mtimeMs,
407
- });
408
- }
409
- }
410
- };
411
- scan(interfacesDir);
412
- return results;
413
- }
414
-
415
- export function handleListMessages(
416
- { queryParams }: RouteHandlerArgs,
417
- interfacesDir: string | null,
418
- ): Record<string, unknown> {
463
+ export function handleListMessages({
464
+ queryParams,
465
+ }: RouteHandlerArgs): Record<string, unknown> {
419
466
  const conversationId = queryParams?.conversationId;
420
467
  const conversationKey = queryParams?.conversationKey;
421
468
 
@@ -423,8 +470,20 @@ export function handleListMessages(
423
470
  if (conversationId) {
424
471
  resolvedConversationId = conversationId;
425
472
  } else if (conversationKey) {
473
+ // Dual lookup, key-first: prefer the `conversation_keys` table — the
474
+ // canonical channel/external → internal-id mapping — so legacy or
475
+ // externally-sourced keys keep their explicit mapping precedence and
476
+ // never collide with an unrelated `conversations.id`. Fall back to a
477
+ // direct id lookup only when no mapping exists, which covers
478
+ // background/scheduled conversations bootstrapped without a
479
+ // `conversation_keys` row (web clients use the conversation list's
480
+ // `id` as `conversationKey` for those).
426
481
  const mapping = getConversationByKey(conversationKey);
427
- resolvedConversationId = mapping?.conversationId;
482
+ if (mapping) {
483
+ resolvedConversationId = mapping.conversationId;
484
+ } else if (getConversation(conversationKey)) {
485
+ resolvedConversationId = conversationKey;
486
+ }
428
487
  } else {
429
488
  throw new BadRequestError(
430
489
  "conversationKey or conversationId query parameter is required",
@@ -480,16 +539,30 @@ export function handleListMessages(
480
539
  let rawMessages: MessageRow[];
481
540
  let hasMore = false;
482
541
 
542
+ // Drop messages flagged as hidden in metadata (e.g. internal scaffolding
543
+ // like retrospective instructions). The LLM-side history loader
544
+ // (`getMessages` in memory/conversation-crud.ts) intentionally does not
545
+ // filter — hidden messages remain in agent context but are suppressed from
546
+ // the UI list. Filtering is pushed into the paginated query so `hasMore`
547
+ // and the cursor reflect visible rows; otherwise a fully-hidden page would
548
+ // return `hasMore: true` with no cursor and stall the web client.
549
+ // Hidden tool_use/tool_result pairs must be hidden together — if a hidden
550
+ // assistant message has tool_use blocks but its matching user tool_result
551
+ // is left visible, the result will render as a standalone orphan because
552
+ // `mergeToolResultsIntoAssistantMessages` has nothing to merge it into.
553
+ const visibleFilter = (m: MessageRow) => !isHiddenMessage(m.metadata);
554
+
483
555
  if (isPaginated) {
484
556
  const result = getMessagesPaginated(
485
557
  resolvedConversationId,
486
558
  limit,
487
559
  beforeTimestamp,
560
+ visibleFilter,
488
561
  );
489
562
  rawMessages = result.messages;
490
563
  hasMore = result.hasMore;
491
564
  } else {
492
- rawMessages = getMessages(resolvedConversationId);
565
+ rawMessages = getMessages(resolvedConversationId).filter(visibleFilter);
493
566
  }
494
567
 
495
568
  // During streaming, tool_use (assistant) and tool_result (user) events are
@@ -508,6 +581,7 @@ export function handleListMessages(
508
581
  // (consecutive tool refs grouped together).
509
582
  const { messages: consolidatedMessages, mergedIdMap } =
510
583
  mergeConsecutiveAssistantMessages(mergedMessages);
584
+ const assistantSlackDisplayName = getAssistantName()?.trim() || undefined;
511
585
 
512
586
  // Parse content blocks and extract text + tool calls
513
587
  const parsed = consolidatedMessages.map((msg) => {
@@ -559,6 +633,10 @@ export function handleListMessages(
559
633
  }
560
634
  const slackMessage = buildSlackHistoryMessage(
561
635
  readSlackMetadataFromMessageMetadata(msg.metadata),
636
+ {
637
+ role: msg.role,
638
+ assistantDisplayName: assistantSlackDisplayName,
639
+ },
562
640
  );
563
641
 
564
642
  // Strip <no_response/> markers from assistant messages so web/API
@@ -599,6 +677,7 @@ export function handleListMessages(
599
677
  textSegments: filteredSegments,
600
678
  contentOrder: filteredContentOrder,
601
679
  surfaces: rendered.surfaces,
680
+ attachmentRefs: rendered.attachments,
602
681
  slackMessage,
603
682
  ...(rendered.thinkingSegments.length > 0
604
683
  ? { thinkingSegments: rendered.thinkingSegments }
@@ -618,6 +697,7 @@ export function handleListMessages(
618
697
  textSegments: rendered.textSegments,
619
698
  contentOrder: rendered.contentOrder,
620
699
  surfaces: rendered.surfaces,
700
+ attachmentRefs: rendered.attachments,
621
701
  slackMessage,
622
702
  ...(rendered.thinkingSegments.length > 0
623
703
  ? { thinkingSegments: rendered.thinkingSegments }
@@ -627,15 +707,6 @@ export function handleListMessages(
627
707
  };
628
708
  });
629
709
 
630
- const interfaceFiles = getInterfaceFilesWithMtimes(interfacesDir);
631
-
632
- let prevAssistantTimestamp = 0;
633
- if (isPaginated && rawMessages.length > 0) {
634
- prevAssistantTimestamp = getLastAssistantTimestampBefore(
635
- resolvedConversationId!,
636
- rawMessages[0].createdAt,
637
- );
638
- }
639
710
  const messages: RuntimeMessagePayload[] = parsed.map((m) => {
640
711
  let msgAttachments: RuntimeAttachmentMetadata[] = [];
641
712
  if (m.id) {
@@ -682,19 +753,76 @@ export function handleListMessages(
682
753
  }
683
754
  }
684
755
 
685
- let interfaces: string[] | undefined;
686
- if (m.role === "assistant") {
687
- const msgTimestamp = new Date(m.timestamp).getTime();
688
- const dirtied = interfaceFiles
689
- .filter(
690
- (f) =>
691
- f.mtimeMs > prevAssistantTimestamp && f.mtimeMs <= msgTimestamp,
692
- )
693
- .map((f) => f.path);
694
- if (dirtied.length > 0) {
695
- interfaces = dirtied;
756
+ // Align msgAttachments order with the file-block order captured by
757
+ // renderHistoryContent. When a file block was persisted with
758
+ // `_attachmentId`, we can join on that id to position the chip inline
759
+ // (the `attachment:N` entries in contentOrder index into msgAttachments).
760
+ // DB rows without a matching ref go to the tail as orphan chips;
761
+ // unmatched refs drop their contentOrder entry and trigger a remap.
762
+ let alignedContentOrder = m.contentOrder;
763
+ if (
764
+ m.attachmentRefs.length > 0 &&
765
+ msgAttachments.length > 0 &&
766
+ m.contentOrder.length > 0
767
+ ) {
768
+ const byId = new Map<string, number>();
769
+ msgAttachments.forEach((att, idx) => {
770
+ if (att.id) byId.set(att.id, idx);
771
+ });
772
+ const consumed = new Set<number>();
773
+ const orderedRowIdx: Array<number | null> = m.attachmentRefs.map(
774
+ (ref) => {
775
+ if (!ref.attachmentId) return null;
776
+ const idx = byId.get(ref.attachmentId);
777
+ if (idx === undefined || consumed.has(idx)) return null;
778
+ consumed.add(idx);
779
+ return idx;
780
+ },
781
+ );
782
+ const matchedRows = orderedRowIdx.filter(
783
+ (idx): idx is number => idx !== null,
784
+ );
785
+ if (matchedRows.length > 0) {
786
+ const orphanRows: number[] = [];
787
+ for (let i = 0; i < msgAttachments.length; i++) {
788
+ if (!consumed.has(i)) orphanRows.push(i);
789
+ }
790
+ msgAttachments = [
791
+ ...matchedRows.map((i) => msgAttachments[i]),
792
+ ...orphanRows.map((i) => msgAttachments[i]),
793
+ ];
794
+ const refToNewIdx = new Map<number, number>();
795
+ let nextIdx = 0;
796
+ orderedRowIdx.forEach((rowIdx, refIdx) => {
797
+ if (rowIdx !== null) {
798
+ refToNewIdx.set(refIdx, nextIdx);
799
+ nextIdx++;
800
+ }
801
+ });
802
+ alignedContentOrder = m.contentOrder
803
+ .map((entry) => {
804
+ const match = entry.match(ATTACHMENT_ENTRY_RE);
805
+ if (!match) return entry;
806
+ const remapped = refToNewIdx.get(Number(match[1]));
807
+ return remapped !== undefined
808
+ ? `attachment:${remapped}`
809
+ : undefined;
810
+ })
811
+ .filter((e): e is string => e !== undefined);
812
+ } else {
813
+ // No refs carried an attachmentId we could match — strip any
814
+ // attachment:N entries so the client doesn't try to position
815
+ // attachments inline against a misaligned array.
816
+ alignedContentOrder = m.contentOrder.filter(
817
+ (entry) => !ATTACHMENT_ENTRY_RE.test(entry),
818
+ );
696
819
  }
697
- prevAssistantTimestamp = msgTimestamp;
820
+ } else if (m.attachmentRefs.length > 0 && msgAttachments.length === 0) {
821
+ // Refs were captured but no DB rows came back — drop the
822
+ // contentOrder entries to avoid out-of-bounds renders.
823
+ alignedContentOrder = m.contentOrder.filter(
824
+ (entry) => !ATTACHMENT_ENTRY_RE.test(entry),
825
+ );
698
826
  }
699
827
 
700
828
  // Use sentAt (actual event time) for the display timestamp when
@@ -704,26 +832,21 @@ export function handleListMessages(
704
832
  // on createdAt. The mismatch is benign — it may return slightly extra
705
833
  // data on a page boundary but never loses messages.
706
834
  const displayTimestamp = m.sentAt ?? m.timestamp;
707
- const mergedMessageIds = mergedIdMap.get(m.id) ?? [];
708
- const daemonMessageId =
709
- m.role === "assistant"
710
- ? (mergedMessageIds[mergedMessageIds.length - 1] ?? m.id)
711
- : undefined;
712
835
  return {
713
836
  id: m.id ?? "",
714
- ...(daemonMessageId ? { daemonMessageId } : {}),
715
837
  role: m.role,
716
838
  content: m.text,
717
839
  timestamp: new Date(displayTimestamp).toISOString(),
718
840
  attachments: msgAttachments,
719
841
  ...(m.toolCalls.length > 0 ? { toolCalls: m.toolCalls } : {}),
720
- ...(interfaces ? { interfaces } : {}),
721
842
  ...(m.surfaces.length > 0 ? { surfaces: m.surfaces } : {}),
722
843
  ...(m.textSegments.length > 0 ? { textSegments: m.textSegments } : {}),
723
844
  ...(m.thinkingSegments?.length
724
845
  ? { thinkingSegments: m.thinkingSegments }
725
846
  : {}),
726
- ...(m.contentOrder.length > 0 ? { contentOrder: m.contentOrder } : {}),
847
+ ...(alignedContentOrder.length > 0
848
+ ? { contentOrder: alignedContentOrder }
849
+ : {}),
727
850
  ...(m.subagentNotification
728
851
  ? { subagentNotification: m.subagentNotification }
729
852
  : {}),
@@ -760,305 +883,6 @@ export function handleListMessages(
760
883
  return { messages };
761
884
  }
762
885
 
763
- // ── Tool-result merging ─────────────────────────────────────────────
764
-
765
- function isToolResultType(type: string): boolean {
766
- return type === "tool_result" || type === "web_search_tool_result";
767
- }
768
-
769
- function isSystemNoticeText(block: Record<string, unknown>): boolean {
770
- if (block.type !== "text") return false;
771
- const text = typeof block.text === "string" ? block.text : "";
772
- return (
773
- text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
774
- );
775
- }
776
-
777
- /**
778
- * Merge tool_result blocks from user messages into the preceding assistant
779
- * message's content array. This lets renderHistoryContent's pendingToolUses
780
- * map pair tool_use and tool_result blocks, preventing "unknown" tool names.
781
- *
782
- * User messages that consist entirely of tool_result blocks (and optional
783
- * system_notice text) are removed from the output. Mixed messages (tool_result
784
- * + real user text) keep only the non-tool-result blocks.
785
- */
786
- function mergeToolResultsIntoAssistantMessages(
787
- messages: MessageRow[],
788
- ): MessageRow[] {
789
- // Index of the most recent assistant message in the output array.
790
- let lastAssistantIdx = -1;
791
- // Parsed content caches — lazily populated per assistant message.
792
- const parsedAssistantContent = new Map<number, unknown[]>();
793
-
794
- const result: MessageRow[] = [];
795
-
796
- for (const msg of messages) {
797
- if (msg.role === "assistant") {
798
- lastAssistantIdx = result.length;
799
- result.push(msg);
800
- continue;
801
- }
802
-
803
- // Only process user messages — other roles pass through.
804
- if (msg.role !== "user") {
805
- result.push(msg);
806
- continue;
807
- }
808
-
809
- let blocks: unknown[];
810
- try {
811
- const parsed = JSON.parse(msg.content);
812
- if (!Array.isArray(parsed)) {
813
- result.push(msg);
814
- continue;
815
- }
816
- blocks = parsed;
817
- } catch {
818
- result.push(msg);
819
- continue;
820
- }
821
-
822
- // Separate tool-result blocks from real user content.
823
- const toolResultBlocks: unknown[] = [];
824
- const otherBlocks: unknown[] = [];
825
- for (const block of blocks) {
826
- if (
827
- typeof block === "object" &&
828
- block !== null &&
829
- typeof (block as Record<string, unknown>).type === "string"
830
- ) {
831
- const rec = block as Record<string, unknown>;
832
- if (isToolResultType(rec.type as string)) {
833
- toolResultBlocks.push(block);
834
- } else if (isSystemNoticeText(rec)) {
835
- // System notices don't count as user content — drop them when
836
- // the message is otherwise tool-result-only.
837
- otherBlocks.push(block);
838
- } else {
839
- otherBlocks.push(block);
840
- }
841
- } else {
842
- otherBlocks.push(block);
843
- }
844
- }
845
-
846
- // No tool results → pass through unchanged. System notices are only
847
- // injected alongside tool results in the agent loop, so a pure user
848
- // message (no tool_result blocks) should never be filtered — even if
849
- // the user's text happens to look like a system_notice tag.
850
- if (toolResultBlocks.length === 0) {
851
- result.push(msg);
852
- continue;
853
- }
854
-
855
- // Append tool_result blocks to the preceding assistant message's content.
856
- if (lastAssistantIdx >= 0) {
857
- const assistant = result[lastAssistantIdx];
858
- let assistantContent = parsedAssistantContent.get(lastAssistantIdx);
859
- if (!assistantContent) {
860
- try {
861
- const parsed = JSON.parse(assistant.content);
862
- assistantContent = Array.isArray(parsed) ? parsed : [parsed];
863
- } catch {
864
- assistantContent = [];
865
- }
866
- parsedAssistantContent.set(lastAssistantIdx, assistantContent);
867
- }
868
- assistantContent.push(...toolResultBlocks);
869
- } else {
870
- // No preceding assistant message (pagination boundary) — keep the
871
- // original message as-is to avoid permanent data loss. The preceding
872
- // assistant tool_use lives in the previous page; dropping the result
873
- // here would be unrecoverable.
874
- // Still strip system notices so internal prompt text isn't exposed.
875
- const filteredBlocks = blocks.filter(
876
- (b) =>
877
- !(
878
- typeof b === "object" &&
879
- b !== null &&
880
- isSystemNoticeText(b as Record<string, unknown>)
881
- ),
882
- );
883
- result.push({
884
- ...msg,
885
- content:
886
- filteredBlocks.length === blocks.length
887
- ? msg.content
888
- : JSON.stringify(filteredBlocks),
889
- });
890
- continue;
891
- }
892
-
893
- // If the user message had only tool_result (+ system_notice) blocks,
894
- // suppress it entirely. Otherwise keep the non-tool-result content.
895
- const realUserContent = otherBlocks.filter(
896
- (b) =>
897
- !(
898
- typeof b === "object" &&
899
- b !== null &&
900
- isSystemNoticeText(b as Record<string, unknown>)
901
- ),
902
- );
903
- if (realUserContent.length > 0) {
904
- result.push({ ...msg, content: JSON.stringify(otherBlocks) });
905
- }
906
- // else: tool-result-only → suppressed (results already merged above)
907
- }
908
-
909
- // Write back any modified assistant message content.
910
- for (const [idx, content] of parsedAssistantContent) {
911
- result[idx] = { ...result[idx], content: JSON.stringify(content) };
912
- }
913
-
914
- return result;
915
- }
916
-
917
- // ── Consecutive assistant message merging ────────────────────────────
918
-
919
- /** Parse a message's JSON content into an array of content blocks. */
920
- function parseContentBlocks(content: string): unknown[] {
921
- try {
922
- const parsed = JSON.parse(content);
923
- return Array.isArray(parsed) ? parsed : [parsed];
924
- } catch (err) {
925
- log.warn(
926
- { err },
927
- "Failed to parse content blocks during assistant message merge",
928
- );
929
- return [];
930
- }
931
- }
932
-
933
- /**
934
- * Append content blocks from a donor message onto a target block array.
935
- * Parses the donor's JSON content and pushes each block into `target`.
936
- */
937
- function appendContentBlocks(target: unknown[], donorContent: string): void {
938
- try {
939
- const parsed = JSON.parse(donorContent);
940
- if (Array.isArray(parsed)) {
941
- target.push(...parsed);
942
- } else {
943
- target.push(parsed);
944
- }
945
- } catch (err) {
946
- log.warn(
947
- { err },
948
- "Failed to parse donor content blocks during assistant message merge",
949
- );
950
- }
951
- }
952
-
953
- /**
954
- * Promote metadata fields from a donor message to the surviving message
955
- * when the survivor lacks them. Currently promotes `subagentNotification`.
956
- * Returns a new MessageRow if promotion occurred, otherwise the original.
957
- */
958
- function promoteMetadata(survivor: MessageRow, donor: MessageRow): MessageRow {
959
- if (donor.metadata && survivor.metadata) {
960
- try {
961
- const survivorMeta = JSON.parse(survivor.metadata);
962
- const donorMeta = JSON.parse(donor.metadata);
963
- if (
964
- !survivorMeta.subagentNotification &&
965
- donorMeta.subagentNotification
966
- ) {
967
- survivorMeta.subagentNotification = donorMeta.subagentNotification;
968
- return { ...survivor, metadata: JSON.stringify(survivorMeta) };
969
- }
970
- } catch (err) {
971
- log.warn(
972
- { err },
973
- "Failed to parse metadata during assistant message merge",
974
- );
975
- }
976
- } else if (donor.metadata && !survivor.metadata) {
977
- return { ...survivor, metadata: donor.metadata };
978
- }
979
- return survivor;
980
- }
981
-
982
- /**
983
- * Merge consecutive assistant messages into a single message at query time.
984
- *
985
- * During streaming, all assistant turns within one agent loop accumulate on
986
- * a single client-side ChatMessage. In the DB, each API turn is stored as a
987
- * separate assistant row (consolidation is deferred to compaction for
988
- * prefix-cache stability). This produces N separate assistant messages that
989
- * the client renders as N individual bubbles — each showing "Completed 1
990
- * step" instead of one grouped "Completed N steps" accordion.
991
- *
992
- * This function concatenates the content block arrays of consecutive
993
- * assistant messages (no intervening user messages after tool-result
994
- * merging) into the first message of each run. The merged messages are
995
- * removed from the output. This is query-time only — the DB is not
996
- * modified.
997
- *
998
- * The first message in each run keeps its id, createdAt, and metadata so
999
- * that attachment lookups, display timestamps, and subagent notifications
1000
- * continue to work. Metadata from later messages in the run (e.g.
1001
- * subagentNotification) is preserved by promoting it to the surviving
1002
- * message when the surviving message has no metadata of its own for that
1003
- * field.
1004
- */
1005
- function mergeConsecutiveAssistantMessages(messages: MessageRow[]): {
1006
- messages: MessageRow[];
1007
- /** Maps each surviving message ID → all original message IDs merged into it. */
1008
- mergedIdMap: Map<string, string[]>;
1009
- } {
1010
- const result: MessageRow[] = [];
1011
- // Key = index in `result`, value = accumulated content blocks.
1012
- const pendingMerges = new Map<number, unknown[]>();
1013
- // Key = index in `result`, value = IDs of messages merged into the target.
1014
- const mergedIds = new Map<number, string[]>();
1015
-
1016
- for (const msg of messages) {
1017
- const lastIdx = result.length - 1;
1018
- const isConsecutiveAssistant =
1019
- msg.role === "assistant" &&
1020
- lastIdx >= 0 &&
1021
- result[lastIdx].role === "assistant";
1022
-
1023
- if (!isConsecutiveAssistant) {
1024
- result.push(msg);
1025
- continue;
1026
- }
1027
-
1028
- // Track the donor message ID.
1029
- let ids = mergedIds.get(lastIdx);
1030
- if (!ids) {
1031
- ids = [];
1032
- mergedIds.set(lastIdx, ids);
1033
- }
1034
- ids.push(msg.id);
1035
-
1036
- // Lazily parse the target's content on first merge.
1037
- let targetContent = pendingMerges.get(lastIdx);
1038
- if (!targetContent) {
1039
- targetContent = parseContentBlocks(result[lastIdx].content);
1040
- pendingMerges.set(lastIdx, targetContent);
1041
- }
1042
-
1043
- appendContentBlocks(targetContent, msg.content);
1044
- result[lastIdx] = promoteMetadata(result[lastIdx], msg);
1045
- }
1046
-
1047
- // Write back merged content for any messages that were targets.
1048
- for (const [idx, content] of pendingMerges) {
1049
- result[idx] = { ...result[idx], content: JSON.stringify(content) };
1050
- }
1051
-
1052
- // Build the merged ID map keyed by surviving message ID.
1053
- const mergedIdMap = new Map<string, string[]>();
1054
- for (const [idx, ids] of mergedIds) {
1055
- mergedIdMap.set(result[idx].id, ids);
1056
- }
1057
-
1058
- return { messages: result, mergedIdMap };
1059
- }
1060
-
1061
- /**
1062
886
  /**
1063
887
  * Persist the pre-chat onboarding payload to disk.
1064
888
  *
@@ -1092,6 +916,10 @@ export function persistOnboardingArtifacts(onboarding: {
1092
916
  tone: string;
1093
917
  userName?: string;
1094
918
  assistantName?: string;
919
+ priorAssistants?: string[];
920
+ cohort?: string;
921
+ websiteUrl?: string;
922
+ contentSourceUrl?: string;
1095
923
  }): void {
1096
924
  writeOnboardingSidecar(onboarding);
1097
925
 
@@ -1147,6 +975,7 @@ export async function handleSendMessage(
1147
975
  ): Promise<unknown> {
1148
976
  const body = (rawBody ?? {}) as {
1149
977
  conversationKey?: string;
978
+ conversationId?: string;
1150
979
  content?: string;
1151
980
  attachmentIds?: string[];
1152
981
  sourceChannel?: string;
@@ -1169,13 +998,23 @@ export async function handleSendMessage(
1169
998
  assistantName?: string;
1170
999
  googleConnected?: boolean;
1171
1000
  googleScopes?: string[];
1001
+ priorAssistants?: string[];
1002
+ cohort?: string;
1003
+ websiteUrl?: string;
1004
+ contentSourceUrl?: string;
1172
1005
  };
1173
1006
  };
1174
1007
 
1175
1008
  const actorPrincipalId = headers?.["x-vellum-actor-principal-id"];
1176
1009
  const principalType = headers?.["x-vellum-principal-type"];
1010
+ const originClientId =
1011
+ headers?.["x-vellum-client-id"]?.trim() || undefined;
1177
1012
 
1178
1013
  const { conversationKey, content, attachmentIds } = body;
1014
+ const inboundConversationId =
1015
+ typeof body.conversationId === "string" && body.conversationId.length > 0
1016
+ ? body.conversationId
1017
+ : undefined;
1179
1018
  const clientMessageId =
1180
1019
  typeof body.clientMessageId === "string" ? body.clientMessageId : undefined;
1181
1020
  const requestedInferenceProfile =
@@ -1243,12 +1082,6 @@ export async function handleSendMessage(
1243
1082
  ? (canonicalizeTimeZone(body.clientTimezone) ?? undefined)
1244
1083
  : undefined;
1245
1084
 
1246
- // When conversationKey is omitted, derive a stable default from
1247
- // sourceChannel + sourceInterface so that repeated calls from the same
1248
- // channel/interface pair share a single conversation thread.
1249
- const resolvedConversationKey =
1250
- conversationKey ?? `default:${sourceChannel}:${sourceInterface}`;
1251
-
1252
1085
  // Reject non-string content values (numbers, objects, etc.)
1253
1086
  if (content != null && typeof content !== "string") {
1254
1087
  throw new BadRequestError("content must be a string");
@@ -1312,9 +1145,40 @@ export async function handleSendMessage(
1312
1145
  // timer so the next heartbeat is a full interval after this interaction.
1313
1146
  HeartbeatService.getInstance()?.resetTimer();
1314
1147
 
1315
- const mapping = getOrCreateConversation(resolvedConversationKey, {
1316
- conversationType: "standard",
1317
- });
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
+ }
1318
1182
 
1319
1183
  if (requestedRiskThreshold !== undefined) {
1320
1184
  const result = await ipcCall("set_conversation_threshold", {
@@ -1348,6 +1212,7 @@ export async function handleSendMessage(
1348
1212
  publishConversationListAndMetadataChanged(
1349
1213
  "created",
1350
1214
  mapping.conversationId,
1215
+ originClientId,
1351
1216
  );
1352
1217
  }
1353
1218
  }
@@ -1500,12 +1365,34 @@ export async function handleSendMessage(
1500
1365
  );
1501
1366
  }
1502
1367
 
1503
- // ── Canned first-greeting fast path ──
1504
- // On a completely fresh workspace, skip LLM inference for the macOS
1505
- // wake-up greeting and return a pre-written response. When onboarding
1506
- // context is present the greeting is personalized using the selections;
1507
- // otherwise a generic greeting is served. Both paths are instant.
1508
- if (isWakeUpGreeting(trimmedContent, conversation.getMessages().length)) {
1368
+ // ── URL scan path: rewrite first message for scan onboarding ──
1369
+ // When onboarding provides a websiteUrl or contentSourceUrl and the
1370
+ // first message is the macOS wake-up greeting, bypass the canned
1371
+ // greeting and rewrite the user message to a scan instruction so real
1372
+ // LLM inference runs against the URL.
1373
+ const sanitizeUrl = (u?: string) =>
1374
+ u?.trim().replace(/[\r\n\t]/g, "") || undefined;
1375
+ const websiteUrl = sanitizeUrl(body.onboarding?.websiteUrl);
1376
+ const contentSourceUrl = sanitizeUrl(body.onboarding?.contentSourceUrl);
1377
+ const scanUrl = websiteUrl || contentSourceUrl;
1378
+ const isWakeUp = isWakeUpGreeting(
1379
+ trimmedContent,
1380
+ conversation.getMessages().length,
1381
+ );
1382
+ const isScanPath = !!scanUrl && isWakeUp;
1383
+
1384
+ let effectiveContent: string | undefined;
1385
+ if (isScanPath) {
1386
+ const scanVariant = websiteUrl
1387
+ ? ("website" as const)
1388
+ : ("content-source" as const);
1389
+ effectiveContent = buildScanFirstMessage(scanUrl, scanVariant);
1390
+ // Fall through to normal inference path below
1391
+ } else if (isWakeUp && body.onboarding?.cohort === "content-automation") {
1392
+ effectiveContent = "I want to write articles that rank better in GEO";
1393
+ // Fall through to normal inference path — the bootstrap template
1394
+ // and geo-writing skill handle this message.
1395
+ } else if (isWakeUp) {
1509
1396
  const cannedGreeting = getCannedFirstGreeting(body.onboarding ?? undefined);
1510
1397
 
1511
1398
  conversation.processing = true;
@@ -1545,7 +1432,7 @@ export async function handleSendMessage(
1545
1432
  const conversationId = mapping.conversationId;
1546
1433
 
1547
1434
  const assistantMsg = createAssistantMessage(cannedGreeting);
1548
- await addMessage(
1435
+ const persistedAssistant = await addMessage(
1549
1436
  mapping.conversationId,
1550
1437
  "assistant",
1551
1438
  JSON.stringify(assistantMsg.content),
@@ -1569,6 +1456,7 @@ export async function handleSendMessage(
1569
1456
  tone: body.onboarding!.tone,
1570
1457
  googleConnected: body.onboarding!.googleConnected,
1571
1458
  googleScopes: body.onboarding!.googleScopes,
1459
+ priorAssistants: body.onboarding!.priorAssistants,
1572
1460
  });
1573
1461
  } catch (err) {
1574
1462
  log.warn({ err }, "Failed to record onboarding telemetry event");
@@ -1588,8 +1476,12 @@ export async function handleSendMessage(
1588
1476
  text: cannedGreeting,
1589
1477
  conversationId,
1590
1478
  });
1591
- broadcastMessage({ type: "message_complete", conversationId });
1592
- publishConversationMessagesChanged(conversationId);
1479
+ emitCannedMessageComplete(
1480
+ broadcastMessage,
1481
+ conversationId,
1482
+ persistedAssistant.id,
1483
+ );
1484
+ publishConversationMessagesChanged(conversationId, originClientId);
1593
1485
  conversation.processing = false;
1594
1486
  silentlyWithLog(
1595
1487
  conversation.drainQueue(),
@@ -1623,12 +1515,18 @@ export async function handleSendMessage(
1623
1515
  tone: body.onboarding!.tone,
1624
1516
  googleConnected: body.onboarding!.googleConnected,
1625
1517
  googleScopes: body.onboarding!.googleScopes,
1518
+ priorAssistants: body.onboarding!.priorAssistants,
1626
1519
  });
1627
1520
  } catch (err) {
1628
1521
  log.warn({ err }, "Failed to record onboarding telemetry event");
1629
1522
  }
1630
1523
  }
1631
1524
 
1525
+ // When the scan path rewrote the first message, prefer the rewritten
1526
+ // content for all downstream consumers (guardian reply, enqueue, agent
1527
+ // loop) so they see the scan instruction rather than the wake-up greeting.
1528
+ const contentAfterScan = effectiveContent ?? content ?? "";
1529
+
1632
1530
  const attachments = hasAttachments
1633
1531
  ? smDeps.resolveAttachments(attachmentIds)
1634
1532
  : [];
@@ -1648,7 +1546,7 @@ export async function handleSendMessage(
1648
1546
  conversationId: mapping.conversationId,
1649
1547
  sourceChannel,
1650
1548
  sourceInterface,
1651
- content: content ?? "",
1549
+ content: contentAfterScan,
1652
1550
  attachments,
1653
1551
  conversation,
1654
1552
  onEvent: broadcastMessage,
@@ -1661,6 +1559,7 @@ export async function handleSendMessage(
1661
1559
  : deps.approvalConversationGenerator,
1662
1560
  verifiedActorExternalUserId,
1663
1561
  verifiedActorPrincipalId,
1562
+ originClientId,
1664
1563
  });
1665
1564
  if (inlineReplyResult.consumed) {
1666
1565
  return {
@@ -1682,7 +1581,7 @@ export async function handleSendMessage(
1682
1581
  // Queue the message so it's processed when the current turn completes
1683
1582
  const requestId = crypto.randomUUID();
1684
1583
  const enqueueResult = conversation.enqueueMessage(
1685
- content ?? "",
1584
+ contentAfterScan,
1686
1585
  attachments,
1687
1586
  broadcastMessage,
1688
1587
  requestId,
@@ -1754,6 +1653,7 @@ export async function handleSendMessage(
1754
1653
  accepted: true,
1755
1654
  queued: true,
1756
1655
  conversationId: mapping.conversationId,
1656
+ requestId,
1757
1657
  };
1758
1658
  }
1759
1659
 
@@ -1801,7 +1701,9 @@ export async function handleSendMessage(
1801
1701
  await conversation.ensureActorScopedHistory();
1802
1702
 
1803
1703
  // Resolve slash commands before persisting or running the agent loop.
1804
- const rawContent = content ?? "";
1704
+ // `contentAfterScan` already carries the scan-rewritten content when
1705
+ // applicable; reuse it here for consistency.
1706
+ const rawContent = contentAfterScan;
1805
1707
  const slashContext = buildSlashContextForContent(rawContent, {
1806
1708
  conversationId: mapping.conversationId,
1807
1709
  messageCount: conversation.getMessages().length,
@@ -1846,7 +1748,7 @@ export async function handleSendMessage(
1846
1748
  conversation.getMessages().push(llmMsg);
1847
1749
 
1848
1750
  const assistantMsg = createAssistantMessage(slashResult.message);
1849
- await addMessage(
1751
+ const persistedAssistant = await addMessage(
1850
1752
  mapping.conversationId,
1851
1753
  "assistant",
1852
1754
  JSON.stringify(assistantMsg.content),
@@ -1901,11 +1803,12 @@ export async function handleSendMessage(
1901
1803
  text: message,
1902
1804
  conversationId,
1903
1805
  });
1904
- broadcastMessage({
1905
- type: "message_complete",
1906
- conversationId: conversationId,
1907
- });
1908
- publishConversationMessagesChanged(conversationId);
1806
+ emitCannedMessageComplete(
1807
+ broadcastMessage,
1808
+ conversationId,
1809
+ persistedAssistant.id,
1810
+ );
1811
+ publishConversationMessagesChanged(conversationId, originClientId);
1909
1812
  conversation.processing = false;
1910
1813
  silentlyWithLog(conversation.drainQueue(), "slash-command queue drain");
1911
1814
  }, 0);
@@ -1933,12 +1836,22 @@ export async function handleSendMessage(
1933
1836
  assistantMessageInterface: sourceInterface,
1934
1837
  };
1935
1838
  const cleanMsg = createUserMessage(rawContent, attachments);
1936
- const persisted = await addMessage(
1937
- mapping.conversationId,
1938
- "user",
1939
- JSON.stringify(cleanMsg.content),
1940
- channelMeta,
1941
- );
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
+ }
1942
1855
  conversation.getMessages().push(cleanMsg);
1943
1856
 
1944
1857
  const conversationId = mapping.conversationId;
@@ -1956,7 +1869,7 @@ export async function handleSendMessage(
1956
1869
  messageId: persisted.id,
1957
1870
  clientMessageId,
1958
1871
  });
1959
- publishConversationMessagesChanged(conversationId);
1872
+ publishConversationMessagesChanged(conversationId, originClientId);
1960
1873
  conversation.emitActivityState(
1961
1874
  "thinking",
1962
1875
  "context_compacting",
@@ -1968,7 +1881,7 @@ export async function handleSendMessage(
1968
1881
  const responseText = formatCompactResult(result);
1969
1882
 
1970
1883
  const assistantMsg = createAssistantMessage(responseText);
1971
- await addMessage(
1884
+ const persistedAssistant = await addMessage(
1972
1885
  conversationId,
1973
1886
  "assistant",
1974
1887
  JSON.stringify(assistantMsg.content),
@@ -1982,11 +1895,15 @@ export async function handleSendMessage(
1982
1895
  text: responseText,
1983
1896
  conversationId,
1984
1897
  });
1985
- broadcastMessage({ type: "message_complete", conversationId });
1986
- publishConversationMessagesChanged(conversationId);
1898
+ emitCannedMessageComplete(
1899
+ broadcastMessage,
1900
+ conversationId,
1901
+ persistedAssistant.id,
1902
+ );
1903
+ publishConversationMessagesChanged(conversationId, originClientId);
1987
1904
  } catch (err) {
1988
1905
  if (assistantMessagePersisted) {
1989
- publishConversationMessagesChanged(conversationId);
1906
+ publishConversationMessagesChanged(conversationId, originClientId);
1990
1907
  }
1991
1908
  log.error({ err, conversationId }, "Compact command failed");
1992
1909
  broadcastMessage({
@@ -2012,6 +1929,91 @@ export async function handleSendMessage(
2012
1929
  };
2013
1930
  }
2014
1931
 
1932
+ if (slashResult.kind === "clean") {
1933
+ conversation.processing = true;
1934
+ const conversationId = mapping.conversationId;
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.
1939
+ try {
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),
1953
+ channelMeta,
1954
+ );
1955
+ conversation.getMessages().push(cleanMsg);
1956
+
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
+ });
2004
+ }
2005
+
2006
+ return {
2007
+ accepted: true,
2008
+ messageId: persisted.id,
2009
+ conversationId,
2010
+ };
2011
+ } finally {
2012
+ conversation.processing = false;
2013
+ silentlyWithLog(conversation.drainQueue(), "clean-command queue drain");
2014
+ }
2015
+ }
2016
+
2015
2017
  const resolvedContent = slashResult.content;
2016
2018
 
2017
2019
  const requestId = crypto.randomUUID();
@@ -2035,7 +2037,7 @@ export async function handleSendMessage(
2035
2037
  requestId,
2036
2038
  clientMessageId,
2037
2039
  });
2038
- publishConversationMessagesChanged(mapping.conversationId);
2040
+ publishConversationMessagesChanged(mapping.conversationId, originClientId);
2039
2041
 
2040
2042
  // Fire-and-forget the agent loop; events flow to the hub via broadcastMessage.
2041
2043
  conversation
@@ -2080,14 +2082,25 @@ async function generateLlmSuggestion(
2080
2082
  ? escapeXmlContent(priorUserText)
2081
2083
  : priorUserText;
2082
2084
 
2083
- const systemPrompt =
2084
- "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");
2085
2097
 
2086
2098
  const userPrompt =
2087
2099
  `Here is the end of a conversation:\n\n` +
2088
2100
  `<user_message>${truncatedUser ?? "(no prior user message)"}</user_message>\n` +
2089
2101
  `<assistant_message>${truncatedAssistant}</assistant_message>\n\n` +
2090
- `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` +
2091
2104
  `<reply>YOUR_REPLY_HERE</reply>`;
2092
2105
 
2093
2106
  // Single user message only — no assistant-role prefill. Anthropic
@@ -2163,14 +2176,27 @@ export async function handleGetSuggestion(
2163
2176
  };
2164
2177
 
2165
2178
  const conversationKey = queryParams?.conversationKey;
2166
- if (!conversationKey) {
2167
- 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
+ );
2168
2184
  }
2169
2185
 
2170
- const mapping = getConversationByKey(conversationKey);
2171
- 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;
2172
2198
 
2173
- const rawMessages = getMessages(mapping.conversationId);
2199
+ const rawMessages = getMessages(resolvedConversationId);
2174
2200
  if (rawMessages.length === 0) return noSuggestion;
2175
2201
 
2176
2202
  // Staleness check: compare requested messageId against the latest
@@ -2369,7 +2395,7 @@ export const ROUTES: RouteDefinition[] = [
2369
2395
  .optional()
2370
2396
  .describe("ID of the oldest message in this page"),
2371
2397
  }),
2372
- handler: (args) => handleListMessages(args, getInterfacesDir()),
2398
+ handler: (args) => handleListMessages(args),
2373
2399
  },
2374
2400
  {
2375
2401
  operationId: "messages_post",
@@ -2424,10 +2450,31 @@ export const ROUTES: RouteDefinition[] = [
2424
2450
  description:
2425
2451
  "Return an LLM-generated follow-up suggestion for the most recent assistant message.",
2426
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
+ ],
2427
2473
  responseBody: z.object({
2428
- suggestion: z.string(),
2429
- messageId: z.string(),
2474
+ suggestion: z.string().nullable(),
2475
+ messageId: z.string().nullable(),
2430
2476
  source: z.string(),
2477
+ stale: z.boolean().optional(),
2431
2478
  }),
2432
2479
  handler: async (args) =>
2433
2480
  handleGetSuggestion(args, {