@vellumai/assistant 0.8.4 → 0.8.6

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 (802) hide show
  1. package/AGENTS.md +33 -1
  2. package/ARCHITECTURE.md +3 -3
  3. package/bunfig.toml +6 -1
  4. package/docs/browser-use-architecture-phase2.md +1 -1
  5. package/docs/credential-execution-service.md +6 -6
  6. package/docs/plugins.md +4 -3
  7. package/knip.json +2 -1
  8. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +12 -13
  9. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +4 -1
  10. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +16 -14
  11. package/openapi.yaml +2748 -216
  12. package/package.json +1 -1
  13. package/src/__tests__/actor-token-service.test.ts +3 -2
  14. package/src/__tests__/agent-loop-exit-reason.test.ts +102 -9
  15. package/src/__tests__/agent-loop-override-profile.test.ts +2 -1
  16. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
  17. package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
  18. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  19. package/src/__tests__/annotate-risk-options.test.ts +1 -0
  20. package/src/__tests__/anthropic-provider.test.ts +34 -37
  21. package/src/__tests__/approval-cascade.test.ts +1 -0
  22. package/src/__tests__/approval-routes-http.test.ts +9 -13
  23. package/src/__tests__/assert-not-live-db.ts +79 -0
  24. package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
  25. package/src/__tests__/assistant-feature-flags-integration.test.ts +12 -28
  26. package/src/__tests__/audit-log-rotation.test.ts +72 -18
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +6 -6
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +8 -11
  29. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  30. package/src/__tests__/btw-routes.test.ts +5 -5
  31. package/src/__tests__/call-controller.test.ts +3 -3
  32. package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
  33. package/src/__tests__/channel-approval-routes.test.ts +3 -2
  34. package/src/__tests__/channel-guardian.test.ts +6 -5
  35. package/src/__tests__/channel-readiness-slack-remote.test.ts +175 -0
  36. package/src/__tests__/channel-reply-delivery.test.ts +35 -0
  37. package/src/__tests__/channel-retry-sweep.test.ts +320 -3
  38. package/src/__tests__/checker.test.ts +18 -27
  39. package/src/__tests__/compaction-events.test.ts +2 -0
  40. package/src/__tests__/compaction-trail-store.test.ts +264 -0
  41. package/src/__tests__/compactor-call-site-logging.test.ts +215 -0
  42. package/src/__tests__/compactor-preserved-tail-count.test.ts +1 -0
  43. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -16
  44. package/src/__tests__/computer-use-tools.test.ts +14 -18
  45. package/src/__tests__/config-loader-backfill.test.ts +13 -28
  46. package/src/__tests__/config-loader-corrupt.test.ts +5 -5
  47. package/src/__tests__/config-loader-platform-defaults.test.ts +93 -26
  48. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +3 -3
  49. package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -4
  50. package/src/__tests__/config-schema.test.ts +10 -10
  51. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  52. package/src/__tests__/connection-model-compat.test.ts +83 -0
  53. package/src/__tests__/contacts-tools.test.ts +3 -2
  54. package/src/__tests__/context-token-estimator.test.ts +22 -0
  55. package/src/__tests__/conversation-abort-tool-results.test.ts +5 -0
  56. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -1
  57. package/src/__tests__/conversation-agent-loop-handlers-max-tokens.test.ts +55 -0
  58. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -1
  59. package/src/__tests__/conversation-agent-loop-overflow.test.ts +231 -2
  60. package/src/__tests__/conversation-agent-loop.test.ts +581 -54
  61. package/src/__tests__/conversation-analysis-routes.test.ts +1 -0
  62. package/src/__tests__/conversation-app-control-instantiation.test.ts +31 -24
  63. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -0
  64. package/src/__tests__/conversation-attention-store.test.ts +101 -0
  65. package/src/__tests__/conversation-attention-telegram.test.ts +3 -2
  66. package/src/__tests__/conversation-clear-safety.test.ts +25 -25
  67. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  68. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
  69. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  70. package/src/__tests__/conversation-error.test.ts +61 -0
  71. package/src/__tests__/conversation-fork-crud.test.ts +239 -15
  72. package/src/__tests__/conversation-fork-route.test.ts +3 -2
  73. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  74. package/src/__tests__/conversation-inference-profile-list.test.ts +3 -2
  75. package/src/__tests__/conversation-inference-profile-route.test.ts +3 -2
  76. package/src/__tests__/conversation-lifecycle.test.ts +53 -11
  77. package/src/__tests__/conversation-list-source.test.ts +3 -2
  78. package/src/__tests__/conversation-load-history-repair.test.ts +2 -1
  79. package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +14 -13
  80. package/src/__tests__/conversation-pairing.test.ts +53 -0
  81. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +26 -7
  82. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  83. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  84. package/src/__tests__/conversation-queue.test.ts +333 -291
  85. package/src/__tests__/conversation-routes-disk-view.test.ts +112 -18
  86. package/src/__tests__/conversation-routes-guardian-reply.test.ts +33 -8
  87. package/src/__tests__/conversation-routes-slash-commands.test.ts +68 -2
  88. package/src/__tests__/conversation-runtime-assembly.test.ts +78 -0
  89. package/src/__tests__/conversation-skill-tools.test.ts +40 -147
  90. package/src/__tests__/conversation-slash-queue.test.ts +84 -32
  91. package/src/__tests__/conversation-slash-unknown.test.ts +5 -0
  92. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  93. package/src/__tests__/conversation-store.test.ts +1 -1
  94. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +46 -0
  95. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -0
  96. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +6 -3
  97. package/src/__tests__/conversation-surfaces-standalone.test.ts +6 -3
  98. package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -3
  99. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -17
  100. package/src/__tests__/conversation-sync-tags.test.ts +218 -35
  101. package/src/__tests__/conversation-title-service.test.ts +1 -0
  102. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +30 -0
  103. package/src/__tests__/conversation-usage.test.ts +1 -0
  104. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  105. package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
  106. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -1
  107. package/src/__tests__/credential-broker-browser-fill.test.ts +3 -3
  108. package/src/__tests__/credential-broker-server-use.test.ts +5 -5
  109. package/src/__tests__/credential-execution-client.test.ts +72 -1
  110. package/src/__tests__/credential-execution-feature-gates.test.ts +19 -19
  111. package/src/__tests__/credential-execution-tools.test.ts +6 -6
  112. package/src/__tests__/credential-health-service.test.ts +252 -3
  113. package/src/__tests__/credential-security-invariants.test.ts +6 -5
  114. package/src/__tests__/credential-vault-unit.test.ts +21 -21
  115. package/src/__tests__/credential-vault.test.ts +5 -5
  116. package/src/__tests__/cross-provider-web-search.test.ts +56 -2
  117. package/src/__tests__/db-connection-isolation.test.ts +7 -6
  118. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +8 -10
  119. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +7 -10
  120. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +9 -15
  121. package/src/__tests__/db-test-helpers.ts +58 -0
  122. package/src/__tests__/disk-pressure-guard.test.ts +58 -41
  123. package/src/__tests__/disk-pressure-lifecycle.test.ts +13 -10
  124. package/src/__tests__/disk-pressure-routes.test.ts +0 -33
  125. package/src/__tests__/disk-pressure-tools.test.ts +0 -4
  126. package/src/__tests__/dm-persistence.test.ts +26 -40
  127. package/src/__tests__/document-create-dedupe.test.ts +189 -0
  128. package/src/__tests__/document-find-replace.test.ts +3 -2
  129. package/src/__tests__/document-tool-security.test.ts +81 -2
  130. package/src/__tests__/dynamic-page-surface.test.ts +2 -2
  131. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +5 -4
  132. package/src/__tests__/email-html-renderer.test.ts +12 -0
  133. package/src/__tests__/encrypted-store-test-helpers.ts +56 -0
  134. package/src/__tests__/encrypted-store.test.ts +11 -9
  135. package/src/__tests__/feature-flag-test-helpers.ts +53 -0
  136. package/src/__tests__/filing-service.test.ts +1 -0
  137. package/src/__tests__/first-greeting.test.ts +62 -12
  138. package/src/__tests__/gateway-flag-listener.test.ts +236 -0
  139. package/src/__tests__/gemini-provider.test.ts +104 -0
  140. package/src/__tests__/guardian-action-sweep.test.ts +3 -2
  141. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  142. package/src/__tests__/guardian-outbound-http.test.ts +10 -7
  143. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +48 -3
  144. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -1
  145. package/src/__tests__/heartbeat-disk-pressure.test.ts +5 -0
  146. package/src/__tests__/heartbeat-service.test.ts +5 -0
  147. package/src/__tests__/helpers/mock-logger.ts +26 -0
  148. package/src/__tests__/host-bash-routes.test.ts +1 -0
  149. package/src/__tests__/host-cu-routes-targeted.test.ts +1 -0
  150. package/src/__tests__/host-file-routes-targeted.test.ts +1 -0
  151. package/src/__tests__/host-shell-tool.test.ts +6 -5
  152. package/src/__tests__/host-transfer-routes-targeted.test.ts +1 -0
  153. package/src/__tests__/http-conversation-lineage.test.ts +3 -2
  154. package/src/__tests__/http-user-message-parity.test.ts +29 -7
  155. package/src/__tests__/identity-intro-cache.test.ts +133 -22
  156. package/src/__tests__/inbound-slack-persistence.test.ts +44 -72
  157. package/src/__tests__/inference-profile-reaper.test.ts +3 -2
  158. package/src/__tests__/inference-profile-session-ipc.test.ts +3 -2
  159. package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
  160. package/src/__tests__/injector-disk-pressure.test.ts +3 -17
  161. package/src/__tests__/inline-skill-load-permissions.test.ts +4 -4
  162. package/src/__tests__/list-messages-hidden-metadata.test.ts +80 -0
  163. package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
  164. package/src/__tests__/llm-context-normalization.test.ts +42 -0
  165. package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
  166. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
  167. package/src/__tests__/llm-resolver.test.ts +408 -9
  168. package/src/__tests__/llm-schema.test.ts +1 -1
  169. package/src/__tests__/llm-usage-store.test.ts +66 -0
  170. package/src/__tests__/logger.test.ts +89 -0
  171. package/src/__tests__/manual-token-reconciliation.test.ts +76 -1
  172. package/src/__tests__/mcp-abort-signal.test.ts +16 -2
  173. package/src/__tests__/mcp-client-auth.test.ts +14 -0
  174. package/src/__tests__/media-generate-image.test.ts +31 -0
  175. package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
  176. package/src/__tests__/messaging-send-tool.test.ts +1 -0
  177. package/src/__tests__/migration-import-from-url.test.ts +3 -3
  178. package/src/__tests__/mock-gateway-ipc.ts +18 -2
  179. package/src/__tests__/model-intents.test.ts +4 -6
  180. package/src/__tests__/native-web-search.test.ts +30 -2
  181. package/src/__tests__/notification-deep-link.test.ts +62 -0
  182. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  183. package/src/__tests__/oauth-commands-routes.test.ts +37 -0
  184. package/src/__tests__/oauth-provider-visibility.test.ts +8 -8
  185. package/src/__tests__/oauth-store.test.ts +3 -2
  186. package/src/__tests__/onboarding-template-contract.test.ts +4 -3
  187. package/src/__tests__/openai-provider.test.ts +54 -9
  188. package/src/__tests__/openai-responses-provider.test.ts +176 -14
  189. package/src/__tests__/openrouter-provider-only.test.ts +27 -5
  190. package/src/__tests__/outbound-slack-persistence.test.ts +46 -1
  191. package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
  192. package/src/__tests__/persistence-pipeline.test.ts +139 -1
  193. package/src/__tests__/persistence-secret-redaction.test.ts +83 -12
  194. package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
  195. package/src/__tests__/platform.test.ts +2 -2
  196. package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
  197. package/src/__tests__/plugin-bootstrap.test.ts +11 -13
  198. package/src/__tests__/plugin-tool-contribution.test.ts +50 -40
  199. package/src/__tests__/plugin-types.test.ts +3 -2
  200. package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
  201. package/src/__tests__/pricing.test.ts +12 -0
  202. package/src/__tests__/process-message-background-slack.test.ts +21 -16
  203. package/src/__tests__/process-message-display-content.test.ts +19 -22
  204. package/src/__tests__/provider-catalog-visibility.test.ts +9 -9
  205. package/src/__tests__/provider-platform-proxy-integration.test.ts +216 -4
  206. package/src/__tests__/provider-registry-ollama.test.ts +45 -22
  207. package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
  208. package/src/__tests__/recording-handler.test.ts +1 -0
  209. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  210. package/src/__tests__/registry.test.ts +84 -84
  211. package/src/__tests__/relay-server.test.ts +10 -10
  212. package/src/__tests__/require-fresh-approval.test.ts +2 -2
  213. package/src/__tests__/runtime-attachment-metadata.test.ts +3 -2
  214. package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
  215. package/src/__tests__/schedule-store.test.ts +16 -1
  216. package/src/__tests__/scheduler-reuse-conversation.test.ts +48 -3
  217. package/src/__tests__/secret-ingress-http.test.ts +5 -1
  218. package/src/__tests__/secure-keys.test.ts +3 -3
  219. package/src/__tests__/send-endpoint-busy.test.ts +81 -42
  220. package/src/__tests__/server-history-render.test.ts +4 -1
  221. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  222. package/src/__tests__/skill-feature-flags-integration.test.ts +8 -10
  223. package/src/__tests__/skill-feature-flags.test.ts +16 -18
  224. package/src/__tests__/skill-load-feature-flag.test.ts +5 -5
  225. package/src/__tests__/skill-projection-feature-flag.test.ts +48 -37
  226. package/src/__tests__/skill-projection.benchmark.test.ts +7 -13
  227. package/src/__tests__/skill-tool-factory.test.ts +97 -96
  228. package/src/__tests__/slack-channel-config.test.ts +3 -3
  229. package/src/__tests__/subagent-call-site-routing.test.ts +11 -3
  230. package/src/__tests__/subagent-disposal.test.ts +27 -8
  231. package/src/__tests__/subagent-fork-notifications.test.ts +24 -9
  232. package/src/__tests__/subagent-fork-spawn.test.ts +13 -4
  233. package/src/__tests__/subagent-manager-notify.test.ts +20 -8
  234. package/src/__tests__/subagent-notify-parent.test.ts +6 -5
  235. package/src/__tests__/subagent-spawn-tool-fork.test.ts +58 -0
  236. package/src/__tests__/subagent-tools.test.ts +2 -1
  237. package/src/__tests__/suggestion-routes.test.ts +2 -0
  238. package/src/__tests__/sync-message-contract.test.ts +59 -0
  239. package/src/__tests__/system-prompt.test.ts +183 -131
  240. package/src/__tests__/terminal-tools.test.ts +1 -1
  241. package/src/__tests__/test-preload-verifier.ts +68 -0
  242. package/src/__tests__/test-preload.ts +32 -39
  243. package/src/__tests__/tool-approval-handler.test.ts +1 -5
  244. package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
  245. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
  246. package/src/__tests__/tool-executor-lifecycle-events.test.ts +35 -12
  247. package/src/__tests__/tool-executor.test.ts +64 -72
  248. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
  249. package/src/__tests__/tool-preview-lifecycle.test.ts +1 -0
  250. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  251. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  252. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
  253. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  254. package/src/__tests__/twilio-routes.test.ts +3 -2
  255. package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
  256. package/src/__tests__/usage-routes.test.ts +3 -0
  257. package/src/__tests__/validate-input.test.ts +381 -0
  258. package/src/__tests__/verification-control-plane-policy.test.ts +3 -2
  259. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -1
  260. package/src/__tests__/voice-session-bridge.test.ts +37 -28
  261. package/src/__tests__/workspace-git-service.test.ts +6 -5
  262. package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
  263. package/src/__tests__/workspace-migration-090-memory-router-cost-optimized-profile.test.ts +326 -0
  264. package/src/__tests__/workspace-migration-091-retighten-migration-onboarding-thread.test.ts +166 -0
  265. package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
  266. package/src/acp/prepare-agent-env.ts +78 -0
  267. package/src/acp/session-manager.ts +6 -7
  268. package/src/agent/loop.ts +88 -0
  269. package/src/api/README.md +127 -0
  270. package/src/api/constants/call-sites.ts +27 -0
  271. package/src/api/events/assistant-outbound-attachment.ts +51 -0
  272. package/src/api/events/assistant-text-delta.ts +32 -0
  273. package/src/api/events/assistant-turn-start.ts +33 -0
  274. package/src/api/events/document-comment-created.ts +48 -0
  275. package/src/api/events/document-comment-deleted.ts +24 -0
  276. package/src/api/events/document-comment-reopened.ts +25 -0
  277. package/src/api/events/document-comment-resolved.ts +27 -0
  278. package/src/api/events/generation-cancelled.ts +24 -0
  279. package/src/api/events/generation-handoff.ts +41 -0
  280. package/src/api/events/message-complete.ts +42 -0
  281. package/src/api/events/open-url.ts +30 -0
  282. package/src/api/events/relationship-state-updated.ts +25 -0
  283. package/src/api/events/tool-use-start.ts +32 -0
  284. package/src/api/index.ts +129 -0
  285. package/src/api/package.json +10 -0
  286. package/src/api/responses/llm-context-response.ts +39 -0
  287. package/src/api/responses/llm-request-log-entry.ts +93 -0
  288. package/src/api/responses/memory-recall-log.ts +65 -0
  289. package/src/api/responses/memory-v2-activation-log.ts +78 -0
  290. package/src/background-wake/background-wake-routes.test.ts +868 -0
  291. package/src/background-wake/platform-client.test.ts +308 -0
  292. package/src/background-wake/platform-client.ts +167 -0
  293. package/src/background-wake/publisher.ts +91 -0
  294. package/src/background-wake/runtime-registry.ts +24 -0
  295. package/src/background-wake/wake-intent-hooks.test.ts +282 -0
  296. package/src/calls/guardian-dispatch.ts +1 -0
  297. package/src/calls/voice-session-bridge.ts +4 -4
  298. package/src/cli/commands/__tests__/browser.test.ts +23 -5
  299. package/src/cli/commands/__tests__/conversations-slack.test.ts +16 -0
  300. package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
  301. package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
  302. package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
  303. package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
  304. package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
  305. package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
  306. package/src/cli/commands/__tests__/notifications.test.ts +184 -40
  307. package/src/cli/commands/browser.ts +247 -0
  308. package/src/cli/commands/channels/__tests__/channels.test.ts +143 -0
  309. package/src/cli/commands/channels/index.ts +229 -0
  310. package/src/cli/commands/domain.ts +91 -41
  311. package/src/cli/commands/inference.ts +93 -40
  312. package/src/cli/commands/memory-v2-compare-render.ts +115 -0
  313. package/src/cli/commands/memory-v2.ts +176 -1
  314. package/src/cli/commands/memory-v3-render.ts +491 -0
  315. package/src/cli/commands/memory-v3.ts +567 -0
  316. package/src/cli/commands/notifications.ts +365 -55
  317. package/src/cli/lib/open-browser.ts +7 -2
  318. package/src/cli/program.ts +4 -0
  319. package/src/config/assistant-feature-flags.ts +39 -46
  320. package/src/config/bundled-skills/document-editor/SKILL.md +16 -3
  321. package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
  322. package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
  323. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  324. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  325. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
  326. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
  327. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
  328. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
  329. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
  330. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  331. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  332. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -0
  333. package/src/config/bundled-tool-registry.ts +2 -0
  334. package/src/config/call-site-defaults.ts +8 -7
  335. package/src/config/feature-flag-cache.ts +86 -0
  336. package/src/config/feature-flag-registry.json +33 -17
  337. package/src/config/llm-context-resolution.ts +10 -1
  338. package/src/config/llm-resolver.ts +121 -15
  339. package/src/config/loader.ts +4 -5
  340. package/src/config/schemas/__tests__/memory-v2.test.ts +228 -1
  341. package/src/config/schemas/call-site-catalog.ts +21 -7
  342. package/src/config/schemas/heartbeat.ts +1 -1
  343. package/src/config/schemas/llm.ts +102 -2
  344. package/src/config/schemas/memory-v2.ts +272 -0
  345. package/src/config/schemas/memory.ts +2 -1
  346. package/src/config/schemas/services.ts +6 -2
  347. package/src/config/seed-inference-profiles.ts +36 -16
  348. package/src/context/compactor.ts +52 -0
  349. package/src/context/token-estimator.ts +10 -5
  350. package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
  351. package/src/conversations/message-consolidation.ts +404 -0
  352. package/src/credential-execution/executable-discovery.ts +40 -0
  353. package/src/credential-execution/process-manager.ts +6 -2
  354. package/src/credential-health/credential-health-service.ts +125 -40
  355. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -6
  356. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +13 -15
  357. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +2 -3
  358. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -0
  359. package/src/daemon/__tests__/meet-manifest-loader.test.ts +25 -12
  360. package/src/daemon/__tests__/native-web-search-metadata.test.ts +1 -0
  361. package/src/daemon/__tests__/switch-inference-profile-tool.test.ts +107 -0
  362. package/src/daemon/__tests__/web-search-status-text.test.ts +1 -0
  363. package/src/daemon/conversation-agent-loop-handlers.ts +390 -80
  364. package/src/daemon/conversation-agent-loop.ts +244 -90
  365. package/src/daemon/conversation-error.ts +64 -6
  366. package/src/daemon/conversation-lifecycle.ts +27 -22
  367. package/src/daemon/conversation-messaging.ts +84 -43
  368. package/src/daemon/conversation-process.ts +74 -37
  369. package/src/daemon/conversation-runtime-assembly.ts +38 -17
  370. package/src/daemon/conversation-skill-tools.ts +14 -30
  371. package/src/daemon/conversation-surfaces.ts +69 -34
  372. package/src/daemon/conversation-tool-setup.ts +77 -32
  373. package/src/daemon/conversation-usage.ts +2 -0
  374. package/src/daemon/conversation.ts +40 -75
  375. package/src/daemon/daemon-control.ts +1 -1
  376. package/src/daemon/daemon-skill-host.ts +9 -2
  377. package/src/daemon/disk-pressure-guard.ts +39 -29
  378. package/src/daemon/first-greeting.ts +31 -13
  379. package/src/daemon/handlers/config-model.test.ts +1 -0
  380. package/src/daemon/handlers/conversations.ts +11 -3
  381. package/src/daemon/handlers/shared.ts +6 -1
  382. package/src/daemon/host-browser-proxy.ts +5 -5
  383. package/src/daemon/host-cu-proxy.ts +4 -4
  384. package/src/daemon/host-file-proxy.ts +4 -4
  385. package/src/daemon/host-proxy-base.ts +4 -4
  386. package/src/daemon/host-transfer-proxy.ts +10 -10
  387. package/src/daemon/lifecycle.ts +29 -26
  388. package/src/daemon/mcp-reload-service.ts +1 -1
  389. package/src/daemon/meet-manifest-loader.ts +11 -24
  390. package/src/daemon/message-types/conversations.ts +22 -27
  391. package/src/daemon/message-types/document-comments.ts +8 -44
  392. package/src/daemon/message-types/home.ts +2 -14
  393. package/src/daemon/message-types/integrations.ts +2 -7
  394. package/src/daemon/message-types/messages.ts +25 -48
  395. package/src/daemon/message-types/subagents.ts +6 -0
  396. package/src/daemon/message-types/sync.ts +14 -0
  397. package/src/daemon/process-message.ts +9 -9
  398. package/src/daemon/providers-setup.ts +1 -1
  399. package/src/daemon/server.ts +16 -0
  400. package/src/daemon/shutdown-handlers.ts +24 -5
  401. package/src/daemon/switch-inference-profile-tool.ts +62 -0
  402. package/src/daemon/tool-setup-types.ts +7 -0
  403. package/src/daemon/wake-target-adapter.ts +10 -0
  404. package/src/documents/document-store.ts +38 -0
  405. package/src/export/__tests__/transcript-formatter.test.ts +1 -0
  406. package/src/heartbeat/__tests__/heartbeat-service.test.ts +30 -1
  407. package/src/heartbeat/heartbeat-service.ts +63 -0
  408. package/src/home/__tests__/feed-writer.test.ts +161 -0
  409. package/src/home/__tests__/post-connect-feed.test.ts +1 -0
  410. package/src/home/__tests__/suggested-prompts.test.ts +55 -59
  411. package/src/home/feed-writer.ts +146 -7
  412. package/src/home/home-greeting.ts +0 -9
  413. package/src/home/suggested-prompts.ts +27 -154
  414. package/src/ipc/__tests__/cli-ipc.test.ts +1 -0
  415. package/src/ipc/gateway-client.test.ts +4 -1
  416. package/src/ipc/gateway-flag-listener.ts +123 -0
  417. package/src/ipc/skill-routes/__tests__/memory.test.ts +1 -0
  418. package/src/ipc/skill-routes/__tests__/registries.test.ts +36 -7
  419. package/src/ipc/skill-routes/memory.ts +4 -3
  420. package/src/ipc/skill-routes/registries.ts +35 -40
  421. package/src/memory/__tests__/db-async-query.test.ts +165 -0
  422. package/src/memory/__tests__/db-maintenance.test.ts +115 -0
  423. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +242 -0
  424. package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
  425. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +26 -5
  426. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +1 -0
  427. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  428. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +1 -0
  429. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +31 -0
  430. package/src/memory/auto-analysis-enqueue.ts +5 -1
  431. package/src/memory/conversation-attention-store.ts +17 -3
  432. package/src/memory/conversation-crud.ts +423 -182
  433. package/src/memory/conversation-starters-cadence.ts +3 -1
  434. package/src/memory/conversation-title-service.ts +19 -3
  435. package/src/memory/db-async-query.ts +214 -0
  436. package/src/memory/db-connection.ts +29 -19
  437. package/src/memory/db-init.ts +14 -0
  438. package/src/memory/db-maintenance.ts +30 -21
  439. package/src/memory/db-singleton.ts +77 -0
  440. package/src/memory/delivery-channels.ts +82 -0
  441. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +2 -4
  442. package/src/memory/graph/bootstrap.ts +8 -1
  443. package/src/memory/graph/capability-seed.ts +7 -3
  444. package/src/memory/graph/conversation-graph-memory.ts +100 -17
  445. package/src/memory/graph/extraction.ts +1 -5
  446. package/src/memory/graph/graph-search.ts +7 -1
  447. package/src/memory/graph/retriever.test.ts +3 -3
  448. package/src/memory/indexer.ts +28 -18
  449. package/src/memory/job-handlers/cleanup.ts +76 -18
  450. package/src/memory/job-handlers/conversation-starters.ts +1 -4
  451. package/src/memory/job-handlers/embedding.test.ts +3 -2
  452. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -2
  453. package/src/memory/jobs/embed-pkb-file.ts +6 -1
  454. package/src/memory/jobs-store.ts +14 -0
  455. package/src/memory/jobs-worker.ts +66 -22
  456. package/src/memory/llm-request-log-source-clickhouse.ts +122 -2
  457. package/src/memory/llm-request-log-source-local.ts +31 -0
  458. package/src/memory/llm-request-log-source.ts +40 -2
  459. package/src/memory/llm-request-log-store.ts +228 -1
  460. package/src/memory/llm-usage-store.ts +24 -0
  461. package/src/memory/memory-retrospective-enqueue.ts +8 -1
  462. package/src/memory/memory-retrospective-job.ts +5 -0
  463. package/src/memory/memory-v2-activation-log-store.ts +110 -7
  464. package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
  465. package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
  466. package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
  467. package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
  468. package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
  469. package/src/memory/migrations/265-drop-provider-connection-status.ts +26 -0
  470. package/src/memory/migrations/266-messages-client-message-id.ts +43 -0
  471. package/src/memory/migrations/index.ts +19 -0
  472. package/src/memory/migrations/registry.ts +33 -0
  473. package/src/memory/schema/conversations.ts +10 -2
  474. package/src/memory/schema/inference.ts +0 -1
  475. package/src/memory/schema/infrastructure.ts +21 -0
  476. package/src/memory/tool-usage-store.ts +36 -8
  477. package/src/memory/v2/__tests__/backfill-jobs.test.ts +5 -2
  478. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
  479. package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
  480. package/src/memory/v2/__tests__/harness-metrics.test.ts +83 -0
  481. package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
  482. package/src/memory/v2/__tests__/harness-replay-input.test.ts +230 -0
  483. package/src/memory/v2/__tests__/harness-runner.test.ts +135 -0
  484. package/src/memory/v2/__tests__/injection.test.ts +127 -98
  485. package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
  486. package/src/memory/v2/__tests__/router.test.ts +171 -3
  487. package/src/memory/v2/__tests__/sweep-job.test.ts +6 -3
  488. package/src/memory/v2/harness/compare.ts +57 -0
  489. package/src/memory/v2/harness/metrics.ts +128 -0
  490. package/src/memory/v2/harness/oracle.ts +145 -0
  491. package/src/memory/v2/harness/replay-input.ts +240 -0
  492. package/src/memory/v2/harness/retriever.ts +74 -0
  493. package/src/memory/v2/harness/router-retriever.ts +43 -0
  494. package/src/memory/v2/harness/runner.ts +112 -0
  495. package/src/memory/v2/harness/trace.ts +64 -0
  496. package/src/memory/v2/injection.ts +21 -15
  497. package/src/memory/v2/prompts/router.ts +26 -1
  498. package/src/memory/v2/qdrant.ts +14 -2
  499. package/src/memory/v2/router.ts +171 -18
  500. package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
  501. package/src/memory/v3/__tests__/consolidation-job.test.ts +466 -0
  502. package/src/memory/v3/__tests__/coretrieval-seed.test.ts +270 -0
  503. package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
  504. package/src/memory/v3/__tests__/edges.test.ts +706 -0
  505. package/src/memory/v3/__tests__/filter.test.ts +560 -0
  506. package/src/memory/v3/__tests__/gate.test.ts +637 -0
  507. package/src/memory/v3/__tests__/index-composition.test.ts +291 -0
  508. package/src/memory/v3/__tests__/loop.test.ts +775 -0
  509. package/src/memory/v3/__tests__/retriever.test.ts +226 -0
  510. package/src/memory/v3/__tests__/scouts.test.ts +489 -0
  511. package/src/memory/v3/__tests__/shadow-diff.test.ts +225 -0
  512. package/src/memory/v3/__tests__/shadow-middleware.test.ts +398 -0
  513. package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
  514. package/src/memory/v3/__tests__/traversal.test.ts +508 -0
  515. package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
  516. package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
  517. package/src/memory/v3/__tests__/tree-walk.test.ts +784 -0
  518. package/src/memory/v3/__tests__/validate.test.ts +277 -0
  519. package/src/memory/v3/auto-edges.ts +223 -0
  520. package/src/memory/v3/coactivation-store.ts +124 -0
  521. package/src/memory/v3/consolidation-job.ts +323 -0
  522. package/src/memory/v3/coretrieval-seed.ts +240 -0
  523. package/src/memory/v3/edge-learning-job.ts +160 -0
  524. package/src/memory/v3/edges.ts +286 -0
  525. package/src/memory/v3/filter.ts +286 -0
  526. package/src/memory/v3/gate.ts +349 -0
  527. package/src/memory/v3/index-composition.ts +126 -0
  528. package/src/memory/v3/llm-capture.ts +46 -0
  529. package/src/memory/v3/loop.ts +430 -0
  530. package/src/memory/v3/maintenance.ts +144 -0
  531. package/src/memory/v3/prompt-context.ts +33 -0
  532. package/src/memory/v3/prompts/consolidation.ts +458 -0
  533. package/src/memory/v3/prompts/system-prompts.ts +196 -0
  534. package/src/memory/v3/retriever.ts +33 -0
  535. package/src/memory/v3/scouts.ts +431 -0
  536. package/src/memory/v3/shadow-diff.ts +287 -0
  537. package/src/memory/v3/shadow-middleware.ts +347 -0
  538. package/src/memory/v3/traversal.ts +211 -0
  539. package/src/memory/v3/tree-index.ts +237 -0
  540. package/src/memory/v3/tree-store.ts +394 -0
  541. package/src/memory/v3/tree-walk.ts +356 -0
  542. package/src/memory/v3/types.ts +65 -0
  543. package/src/memory/v3/validate.ts +323 -0
  544. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  545. package/src/notifications/__tests__/home-feed-side-effect.test.ts +1 -0
  546. package/src/notifications/adapters/macos.ts +18 -1
  547. package/src/notifications/adapters/platform.ts +1 -1
  548. package/src/notifications/adapters/slack.ts +45 -11
  549. package/src/notifications/broadcaster.ts +114 -63
  550. package/src/notifications/conversation-pairing.ts +23 -3
  551. package/src/notifications/decision-engine.ts +1 -4
  552. package/src/notifications/decisions-store.ts +32 -1
  553. package/src/notifications/deliveries-store.ts +45 -0
  554. package/src/notifications/edit-notification.ts +201 -0
  555. package/src/notifications/emit-signal.ts +40 -50
  556. package/src/notifications/signal.ts +10 -0
  557. package/src/notifications/types.ts +37 -0
  558. package/src/oauth/byo-connection.test.ts +67 -3
  559. package/src/oauth/byo-connection.ts +32 -5
  560. package/src/oauth/connect-orchestrator.ts +9 -0
  561. package/src/oauth/connection-resolver.test.ts +76 -0
  562. package/src/oauth/connection-resolver.ts +49 -10
  563. package/src/oauth/manual-token-connection.ts +51 -3
  564. package/src/oauth/seed-providers.ts +3 -0
  565. package/src/permissions/approval-policy.test.ts +19 -5
  566. package/src/permissions/approval-policy.ts +14 -3
  567. package/src/permissions/checker.ts +21 -8
  568. package/src/permissions/prompter.ts +3 -3
  569. package/src/permissions/question-prompter.ts +5 -2
  570. package/src/permissions/secret-prompter.ts +2 -2
  571. package/src/platform/client.test.ts +24 -1
  572. package/src/platform/client.ts +8 -0
  573. package/src/platform/feature-gate.ts +15 -0
  574. package/src/plugin-api/index.ts +4 -0
  575. package/src/plugin-api/types.ts +7 -33
  576. package/src/plugins/defaults/index.ts +6 -0
  577. package/src/plugins/defaults/injectors.ts +20 -19
  578. package/src/plugins/defaults/persistence.ts +25 -6
  579. package/src/plugins/external-plugin-loader.ts +5 -68
  580. package/src/plugins/types.ts +68 -29
  581. package/src/proactive-artifact/aux-message-injector.ts +17 -4
  582. package/src/proactive-artifact/job.test.ts +1 -0
  583. package/src/prompts/__tests__/system-prompt.test.ts +4 -4
  584. package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
  585. package/src/prompts/persona-resolver.ts +36 -21
  586. package/src/prompts/sections.ts +39 -7
  587. package/src/prompts/system-prompt.ts +84 -221
  588. package/src/prompts/template-detection.ts +10 -4
  589. package/src/prompts/templates/BOOTSTRAP.md +9 -13
  590. package/src/prompts/templates/IDENTITY.md +0 -2
  591. package/src/prompts/templates/system-sections.ts +230 -8
  592. package/src/providers/__tests__/connection-model-compat.test.ts +233 -0
  593. package/src/providers/__tests__/registry-native-web-search.test.ts +122 -0
  594. package/src/providers/__tests__/retry-callsite.test.ts +85 -5
  595. package/src/providers/anthropic/client.ts +32 -66
  596. package/src/providers/call-site-routing.ts +42 -6
  597. package/src/providers/connection-model-compat.ts +61 -0
  598. package/src/providers/connection-resolution.ts +47 -14
  599. package/src/providers/fireworks/client.ts +1 -0
  600. package/src/providers/gemini/client.ts +70 -6
  601. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +0 -2
  602. package/src/providers/inference/__tests__/base-url-security.test.ts +2 -3
  603. package/src/providers/inference/__tests__/{connections-status-label.test.ts → connections-label.test.ts} +12 -111
  604. package/src/providers/inference/adapter-factory.ts +3 -0
  605. package/src/providers/inference/auth.ts +0 -8
  606. package/src/providers/inference/connections.ts +3 -66
  607. package/src/providers/inference/resolve-auth.ts +2 -3
  608. package/src/providers/minimax/client.ts +106 -0
  609. package/src/providers/model-catalog.ts +78 -1
  610. package/src/providers/model-intents.ts +4 -4
  611. package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
  612. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +157 -5
  613. package/src/providers/openai/chat-completions-provider.ts +116 -15
  614. package/src/providers/openai/codex-models.ts +20 -0
  615. package/src/providers/openai/responses-provider.ts +87 -30
  616. package/src/providers/openrouter/client.ts +13 -8
  617. package/src/providers/provider-send-message.ts +20 -5
  618. package/src/providers/registry.ts +48 -8
  619. package/src/providers/retry.ts +50 -7
  620. package/src/providers/search-provider-catalog.ts +17 -9
  621. package/src/providers/thinking-config.ts +26 -1
  622. package/src/providers/types.ts +9 -0
  623. package/src/providers/usage-tracking.ts +2 -0
  624. package/src/runtime/AGENTS.md +2 -2
  625. package/src/runtime/__tests__/agent-wake.test.ts +1 -0
  626. package/src/runtime/__tests__/background-job-runner.test.ts +1 -0
  627. package/src/runtime/access-request-helper.ts +1 -0
  628. package/src/runtime/agent-wake.ts +1 -0
  629. package/src/runtime/assistant-event-hub.ts +76 -6
  630. package/src/runtime/auth/route-policy.ts +46 -0
  631. package/src/runtime/btw-sidechain.ts +0 -6
  632. package/src/runtime/channel-readiness-service.ts +68 -0
  633. package/src/runtime/channel-reply-delivery.ts +23 -0
  634. package/src/runtime/channel-retry-sweep.ts +47 -14
  635. package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
  636. package/src/runtime/http-types.ts +0 -2
  637. package/src/runtime/migrations/vbundle-builder.ts +12 -4
  638. package/src/runtime/pending-interactions.ts +0 -1
  639. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +1 -0
  640. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +406 -0
  641. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +204 -0
  642. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  643. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +209 -1
  644. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +13 -50
  645. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +76 -9
  646. package/src/runtime/routes/__tests__/memory-v3-simulate-params.test.ts +35 -0
  647. package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
  648. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +3 -2
  649. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +294 -0
  650. package/src/runtime/routes/__tests__/task-routes.test.ts +48 -3
  651. package/src/runtime/routes/acp-routes-list.test.ts +3 -0
  652. package/src/runtime/routes/acp-routes.test.ts +255 -6
  653. package/src/runtime/routes/acp-routes.ts +8 -1
  654. package/src/runtime/routes/app-management-routes.ts +111 -4
  655. package/src/runtime/routes/avatar-routes.ts +10 -10
  656. package/src/runtime/routes/background-wake-routes.ts +356 -0
  657. package/src/runtime/routes/browser-tabs-routes.ts +200 -0
  658. package/src/runtime/routes/btw-routes.ts +4 -10
  659. package/src/runtime/routes/conversation-analysis-routes.ts +6 -0
  660. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  661. package/src/runtime/routes/conversation-compaction-routes.ts +263 -0
  662. package/src/runtime/routes/conversation-list-routes.ts +159 -4
  663. package/src/runtime/routes/conversation-management-routes.ts +108 -26
  664. package/src/runtime/routes/conversation-query-routes.ts +200 -44
  665. package/src/runtime/routes/conversation-routes.ts +409 -521
  666. package/src/runtime/routes/conversation-starter-routes.ts +6 -3
  667. package/src/runtime/routes/conversations-import-routes.ts +19 -6
  668. package/src/runtime/routes/disk-pressure-routes.ts +1 -1
  669. package/src/runtime/routes/documents-routes.ts +10 -1
  670. package/src/runtime/routes/domain-routes.ts +60 -10
  671. package/src/runtime/routes/email-routes.ts +5 -2
  672. package/src/runtime/routes/events-routes.ts +54 -10
  673. package/src/runtime/routes/group-routes.ts +35 -8
  674. package/src/runtime/routes/home-feed-routes.ts +129 -0
  675. package/src/runtime/routes/host-browser-routes.ts +10 -2
  676. package/src/runtime/routes/host-cu-routes.ts +2 -2
  677. package/src/runtime/routes/identity-intro-cache.ts +61 -16
  678. package/src/runtime/routes/identity-routes.ts +30 -9
  679. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
  680. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +530 -6
  681. package/src/runtime/routes/inbound-stages/background-dispatch.ts +57 -8
  682. package/src/runtime/routes/index.ts +10 -0
  683. package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
  684. package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
  685. package/src/runtime/routes/inference-provider-connection-routes.ts +5 -26
  686. package/src/runtime/routes/integrations/vercel.ts +15 -0
  687. package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
  688. package/src/runtime/routes/llm-context-normalization.ts +7 -2
  689. package/src/runtime/routes/memory-item-routes.ts +8 -3
  690. package/src/runtime/routes/memory-v2-routes.ts +215 -5
  691. package/src/runtime/routes/memory-v3-routes.ts +474 -0
  692. package/src/runtime/routes/migration-routes.ts +32 -28
  693. package/src/runtime/routes/notification-routes.ts +63 -1
  694. package/src/runtime/routes/oauth-commands-routes.ts +6 -1
  695. package/src/runtime/routes/plugins-routes.ts +337 -0
  696. package/src/runtime/routes/rename-conversation-routes.ts +6 -2
  697. package/src/runtime/routes/secret-routes.ts +25 -5
  698. package/src/runtime/routes/settings-routes.ts +12 -11
  699. package/src/runtime/routes/slack-channel-routes.ts +5 -4
  700. package/src/runtime/routes/surface-action-routes.ts +1 -38
  701. package/src/runtime/routes/surface-content-routes.ts +12 -5
  702. package/src/runtime/routes/surface-conversation-resolver.ts +65 -0
  703. package/src/runtime/routes/wipe-conversation-routes.ts +3 -0
  704. package/src/runtime/routes/workspace-routes.ts +25 -10
  705. package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -0
  706. package/src/runtime/slack-dm-text-delivery.ts +177 -0
  707. package/src/runtime/sync/resource-sync-events.ts +106 -38
  708. package/src/runtime/sync/sync-publisher.test.ts +49 -0
  709. package/src/runtime/sync/sync-publisher.ts +2 -1
  710. package/src/runtime/tool-grant-request-helper.ts +1 -0
  711. package/src/runtime/verification-outbound-actions.ts +73 -1
  712. package/src/schedule/schedule-store.ts +8 -1
  713. package/src/schedule/scheduler.ts +111 -15
  714. package/src/security/__tests__/provider-key-env-fallback.test.ts +3 -3
  715. package/src/security/encrypted-store.ts +7 -16
  716. package/src/security/store-path-override.ts +61 -0
  717. package/src/signals/user-message.ts +5 -8
  718. package/src/skills/validate-input.ts +177 -0
  719. package/src/subagent/manager.ts +13 -13
  720. package/src/subagent/types.ts +6 -0
  721. package/src/tasks/tool-sanitizer.ts +2 -2
  722. package/src/telemetry/types.ts +12 -0
  723. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  724. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  725. package/src/tools/acp/spawn.test.ts +119 -0
  726. package/src/tools/acp/spawn.ts +15 -2
  727. package/src/tools/apps/definitions.ts +36 -28
  728. package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
  729. package/src/tools/ask-question/ask-question-tool.ts +38 -45
  730. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +2 -8
  731. package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
  732. package/src/tools/browser/browser-execution.ts +16 -3
  733. package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
  734. package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
  735. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
  736. package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
  737. package/src/tools/browser/cdp-client/factory.ts +100 -17
  738. package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
  739. package/src/tools/browser/cdp-client/types.ts +65 -0
  740. package/src/tools/browser/pinned-tabs.ts +96 -40
  741. package/src/tools/computer-use/definitions.ts +282 -336
  742. package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
  743. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
  744. package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
  745. package/src/tools/credentials/vault.ts +3 -9
  746. package/src/tools/document/document-tool.ts +189 -7
  747. package/src/tools/execution-target.ts +18 -23
  748. package/src/tools/executor.ts +24 -56
  749. package/src/tools/filesystem/edit.ts +3 -9
  750. package/src/tools/filesystem/list.ts +3 -9
  751. package/src/tools/filesystem/read.ts +3 -9
  752. package/src/tools/filesystem/write.ts +3 -9
  753. package/src/tools/host-filesystem/edit.test.ts +1 -0
  754. package/src/tools/host-filesystem/edit.ts +3 -9
  755. package/src/tools/host-filesystem/read.test.ts +1 -0
  756. package/src/tools/host-filesystem/read.ts +3 -9
  757. package/src/tools/host-filesystem/transfer.test.ts +31 -6
  758. package/src/tools/host-filesystem/transfer.ts +3 -9
  759. package/src/tools/host-filesystem/write.test.ts +1 -0
  760. package/src/tools/host-filesystem/write.ts +3 -9
  761. package/src/tools/host-terminal/host-shell.ts +3 -9
  762. package/src/tools/mcp/mcp-tool-factory.ts +1 -10
  763. package/src/tools/memory/register.test.ts +1 -1
  764. package/src/tools/memory/register.ts +4 -9
  765. package/src/tools/network/__tests__/managed-search-proxy.test.ts +282 -0
  766. package/src/tools/network/__tests__/web-search.test.ts +211 -3
  767. package/src/tools/network/managed-search-proxy.ts +183 -0
  768. package/src/tools/network/web-fetch.ts +3 -9
  769. package/src/tools/network/web-search.ts +224 -76
  770. package/src/tools/policy-context.ts +3 -1
  771. package/src/tools/registry.ts +150 -123
  772. package/src/tools/schedule/create.ts +1 -1
  773. package/src/tools/schema-transforms.ts +1 -1
  774. package/src/tools/skills/execute.ts +3 -9
  775. package/src/tools/skills/load.ts +3 -9
  776. package/src/tools/skills/skill-tool-factory.ts +18 -44
  777. package/src/tools/subagent/notify-parent.ts +3 -9
  778. package/src/tools/subagent/spawn.ts +3 -0
  779. package/src/tools/system/request-permission.ts +3 -9
  780. package/src/tools/terminal/shell.ts +3 -9
  781. package/src/tools/tool-approval-handler.ts +10 -4
  782. package/src/tools/tool-defaults.ts +94 -0
  783. package/src/tools/tool-name-aliases.ts +72 -14
  784. package/src/tools/types.ts +32 -101
  785. package/src/tools/ui-surface/definitions.ts +104 -108
  786. package/src/types/onboarding-context.ts +6 -0
  787. package/src/usage/attribution.ts +32 -1
  788. package/src/usage/pricing.ts +23 -0
  789. package/src/usage/types.ts +12 -0
  790. package/src/util/browser.ts +7 -2
  791. package/src/util/logger.ts +16 -7
  792. package/src/util/platform.ts +7 -2
  793. package/src/util/sqlite3-runtime.ts +65 -0
  794. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
  795. package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
  796. package/src/workspace/migrations/090-memory-router-cost-optimized-profile.ts +109 -0
  797. package/src/workspace/migrations/091-retighten-migration-onboarding-thread.ts +41 -0
  798. package/src/workspace/migrations/registry.ts +6 -0
  799. package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
  800. package/src/__tests__/message-complete-display-id.test.ts +0 -175
  801. package/src/daemon/query-complexity-router.ts +0 -75
  802. package/src/prompts/cache-boundary.ts +0 -8
@@ -20,8 +20,13 @@ import {
20
20
  } from "../../channels/types.js";
21
21
  import { isHttpAuthDisabled } from "../../config/env.js";
22
22
  import { getConfig } from "../../config/loader.js";
23
+ import {
24
+ mergeConsecutiveAssistantMessages,
25
+ mergeToolResultsIntoAssistantMessages,
26
+ } from "../../conversations/message-consolidation.js";
23
27
  import { createApprovalConversationGenerator } from "../../daemon/approval-generators.js";
24
28
  import type { Conversation } from "../../daemon/conversation.js";
29
+ import { persistQueuedMessageBody } from "../../daemon/conversation-messaging.js";
25
30
  import {
26
31
  buildModelInfoEvent,
27
32
  formatCleanResult,
@@ -71,6 +76,7 @@ import {
71
76
  } from "../../memory/canonical-guardian-store.js";
72
77
  import {
73
78
  addMessage,
79
+ extractImageSourcePaths,
74
80
  getConversation,
75
81
  getMessages,
76
82
  getMessagesPaginated,
@@ -78,8 +84,6 @@ import {
78
84
  type MessageRow,
79
85
  provenanceFromTrustContext,
80
86
  setConversationInferenceProfile,
81
- setConversationOriginChannelIfUnset,
82
- setConversationOriginInterfaceIfUnset,
83
87
  } from "../../memory/conversation-crud.js";
84
88
  import {
85
89
  getConversationByKey,
@@ -121,7 +125,12 @@ import {
121
125
  resolveTrustContext,
122
126
  withSourceChannel,
123
127
  } from "../trust-context-resolver.js";
124
- import { BadRequestError, InternalError, RouteError } from "./errors.js";
128
+ import {
129
+ BadRequestError,
130
+ InternalError,
131
+ NotFoundError,
132
+ RouteError,
133
+ } from "./errors.js";
125
134
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
126
135
  import { RouteResponse } from "./types.js";
127
136
 
@@ -142,6 +151,34 @@ function isValidRiskThreshold(value: unknown): value is RiskThreshold {
142
151
  );
143
152
  }
144
153
 
154
+ // ---------------------------------------------------------------------------
155
+ // Temporary fix — remove when #31994 lands
156
+ // ---------------------------------------------------------------------------
157
+ //
158
+ // The canned-response paths in this file (canned greeting, inline approval
159
+ // reply, slash command, /compact, /clean) bypass the agent loop and so don't
160
+ // pick up the per-turn anchor id allocated in conversation-agent-loop.ts.
161
+ // Their `message_complete` events therefore went out without `messageId`,
162
+ // and the macOS client filter at ChatActionHandler.swift:507 dropped those
163
+ // events when they raced past the 50 ms streaming-buffer flush — leaving
164
+ // `isSending` stuck for the full 60 s watchdog window.
165
+ //
166
+ // Centralized so the patch surface is one helper + N one-line callers rather
167
+ // than N duplicated literals. When #31994 lands and stamps these sites with
168
+ // `state.assistantTurnId` directly, grep for `emitCannedMessageComplete` to
169
+ // find every call site and inline-then-delete.
170
+ function emitCannedMessageComplete(
171
+ send: (msg: ServerMessage) => void,
172
+ conversationId: string,
173
+ persistedAssistantId: string,
174
+ ): void {
175
+ send({
176
+ type: "message_complete",
177
+ conversationId,
178
+ messageId: persistedAssistantId,
179
+ });
180
+ }
181
+
145
182
  /**
146
183
  * True when a message's persisted metadata explicitly flags it as hidden.
147
184
  * Used to suppress internal scaffolding messages from UI history while
@@ -283,6 +320,8 @@ async function tryConsumeCanonicalGuardianReply(params: {
283
320
  verifiedActorExternalUserId?: string;
284
321
  /** Verified actor principal ID for principal-based authorization. */
285
322
  verifiedActorPrincipalId?: string;
323
+ /** Originating client identifier for sync_changed self-echo suppression. */
324
+ originClientId?: string;
286
325
  }): Promise<{ consumed: boolean; messageId?: string }> {
287
326
  const {
288
327
  conversationId,
@@ -295,6 +334,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
295
334
  approvalConversationGenerator,
296
335
  verifiedActorExternalUserId,
297
336
  verifiedActorPrincipalId,
337
+ originClientId,
298
338
  } = params;
299
339
  const trimmedContent = content.trim();
300
340
 
@@ -355,23 +395,10 @@ async function tryConsumeCanonicalGuardianReply(params: {
355
395
  // is not re-processed as a new user turn.
356
396
  let messageId: string | undefined;
357
397
  try {
358
- const guardianImageSourcePaths: Record<string, string> = {};
359
- for (let i = 0; i < attachments.length; i++) {
360
- const a = attachments[i];
361
- if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
362
- guardianImageSourcePaths[`${i}:${a.filename}`] = a.filePath;
363
- }
364
- }
365
- const channelMeta = {
366
- userMessageChannel: sourceChannel,
367
- assistantMessageChannel: sourceChannel,
368
- userMessageInterface: sourceInterface,
369
- assistantMessageInterface: sourceInterface,
370
- provenanceTrustClass: "guardian" as const,
371
- ...(Object.keys(guardianImageSourcePaths).length > 0
372
- ? { imageSourcePaths: guardianImageSourcePaths }
373
- : {}),
374
- };
398
+ const channelMeta = buildChannelMetadata(sourceChannel, sourceInterface, {
399
+ provenanceOverride: { provenanceTrustClass: "guardian" },
400
+ attachments,
401
+ });
375
402
 
376
403
  const cleanUserMessage = createUserMessage(content, attachments);
377
404
  const llmUserMessage = enrichMessageWithSourcePaths(
@@ -392,7 +419,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
392
419
  ? "Decision applied."
393
420
  : "Request already resolved.");
394
421
  const assistantMessage = createAssistantMessage(replyText);
395
- await addMessage(
422
+ const persistedAssistant = await addMessage(
396
423
  conversationId,
397
424
  "assistant",
398
425
  JSON.stringify(assistantMessage.content),
@@ -407,9 +434,9 @@ async function tryConsumeCanonicalGuardianReply(params: {
407
434
  text: replyText,
408
435
  conversationId: conversationId,
409
436
  });
410
- onEvent({ type: "message_complete", conversationId: conversationId });
437
+ emitCannedMessageComplete(onEvent, conversationId, persistedAssistant.id);
411
438
  }
412
- publishConversationMessagesChanged(conversationId);
439
+ publishConversationMessagesChanged(conversationId, originClientId);
413
440
  } catch (err) {
414
441
  log.warn(
415
442
  { err, conversationId },
@@ -498,6 +525,10 @@ export function handleListMessages({
498
525
 
499
526
  let rawMessages: MessageRow[];
500
527
  let hasMore = false;
528
+ // Resume cursor surfaced when the paginated scan stops on its row cap with a
529
+ // (possibly empty) page — lets us still emit an oldest cursor so the client
530
+ // can request the next window instead of stalling.
531
+ let scanResumeCursor: { createdAt: number; id: string } | undefined;
501
532
 
502
533
  // Drop messages flagged as hidden in metadata (e.g. internal scaffolding
503
534
  // like retrospective instructions). The LLM-side history loader
@@ -521,6 +552,7 @@ export function handleListMessages({
521
552
  );
522
553
  rawMessages = result.messages;
523
554
  hasMore = result.hasMore;
555
+ scanResumeCursor = result.nextCursor;
524
556
  } else {
525
557
  rawMessages = getMessages(resolvedConversationId).filter(visibleFilter);
526
558
  }
@@ -715,10 +747,13 @@ export function handleListMessages({
715
747
 
716
748
  // Align msgAttachments order with the file-block order captured by
717
749
  // renderHistoryContent. When a file block was persisted with
718
- // `_attachmentId`, we can join on that id to position the chip inline
719
- // (the `attachment:N` entries in contentOrder index into msgAttachments).
720
- // DB rows without a matching ref go to the tail as orphan chips;
721
- // unmatched refs drop their contentOrder entry and trigger a remap.
750
+ // `_attachmentId` (user-message uploads), we join on that id to position
751
+ // the chip inline (the `attachment:N` entries in contentOrder index into
752
+ // msgAttachments). DB rows without a matching ref go to the tail as orphan
753
+ // chips; unmatched refs drop their contentOrder entry and trigger a remap.
754
+ // Assistant-authored file blocks carry no `_attachmentId`, so when no ids
755
+ // match we fall back to positional alignment if the ref and row counts
756
+ // agree; otherwise we strip the markers and let chips fall to the tail.
722
757
  let alignedContentOrder = m.contentOrder;
723
758
  if (
724
759
  m.attachmentRefs.length > 0 &&
@@ -769,14 +804,18 @@ export function handleListMessages({
769
804
  : undefined;
770
805
  })
771
806
  .filter((e): e is string => e !== undefined);
772
- } else {
773
- // No refs carried an attachmentId we could match strip any
774
- // attachment:N entries so the client doesn't try to position
775
- // attachments inline against a misaligned array.
807
+ } else if (m.attachmentRefs.length !== msgAttachments.length) {
808
+ // No ref carried an attachmentId we could match and the counts
809
+ // disagree, so positional mapping can't be trusted — strip any
810
+ // attachment:N entries so the client doesn't position attachments
811
+ // inline against a misaligned array (they fall to the tail instead).
776
812
  alignedContentOrder = m.contentOrder.filter(
777
813
  (entry) => !ATTACHMENT_ENTRY_RE.test(entry),
778
814
  );
779
815
  }
816
+ // Otherwise no ref matched an id but the counts agree (the
817
+ // assistant-authored case): the Nth marker maps to the Nth row
818
+ // positionally, so the original contentOrder is left untouched.
780
819
  } else if (m.attachmentRefs.length > 0 && msgAttachments.length === 0) {
781
820
  // Refs were captured but no DB rows came back — drop the
782
821
  // contentOrder entries to avoid out-of-bounds renders.
@@ -792,14 +831,8 @@ export function handleListMessages({
792
831
  // on createdAt. The mismatch is benign — it may return slightly extra
793
832
  // data on a page boundary but never loses messages.
794
833
  const displayTimestamp = m.sentAt ?? m.timestamp;
795
- const mergedMessageIds = mergedIdMap.get(m.id) ?? [];
796
- const daemonMessageId =
797
- m.role === "assistant"
798
- ? (mergedMessageIds[mergedMessageIds.length - 1] ?? m.id)
799
- : undefined;
800
834
  return {
801
835
  id: m.id ?? "",
802
- ...(daemonMessageId ? { daemonMessageId } : {}),
803
836
  role: m.role,
804
837
  content: m.text,
805
838
  timestamp: new Date(displayTimestamp).toISOString(),
@@ -821,10 +854,16 @@ export function handleListMessages({
821
854
  });
822
855
 
823
856
  if (isPaginated) {
857
+ // Prefer the page's oldest visible row (the documented cursor semantic).
858
+ // When a scan-cap-truncated page comes back empty there's no visible row
859
+ // to anchor on, so fall back to the resume cursor so the client still gets
860
+ // a `(timestamp, id)` to continue paginating from instead of stalling.
824
861
  const oldestTimestamp =
825
- rawMessages.length > 0 ? rawMessages[0].createdAt : undefined;
862
+ rawMessages.length > 0
863
+ ? rawMessages[0].createdAt
864
+ : scanResumeCursor?.createdAt;
826
865
  const oldestMessageId =
827
- rawMessages.length > 0 ? rawMessages[0].id : undefined;
866
+ rawMessages.length > 0 ? rawMessages[0].id : scanResumeCursor?.id;
828
867
  // `page=latest` always emits both metadata fields so the web client has
829
868
  // a stable contract; emit `null` when the conversation is empty.
830
869
  // The existing `beforeTimestamp` branch keeps its conditional shape to
@@ -849,305 +888,6 @@ export function handleListMessages({
849
888
  return { messages };
850
889
  }
851
890
 
852
- // ── Tool-result merging ─────────────────────────────────────────────
853
-
854
- function isToolResultType(type: string): boolean {
855
- return type === "tool_result" || type === "web_search_tool_result";
856
- }
857
-
858
- function isSystemNoticeText(block: Record<string, unknown>): boolean {
859
- if (block.type !== "text") return false;
860
- const text = typeof block.text === "string" ? block.text : "";
861
- return (
862
- text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
863
- );
864
- }
865
-
866
- /**
867
- * Merge tool_result blocks from user messages into the preceding assistant
868
- * message's content array. This lets renderHistoryContent's pendingToolUses
869
- * map pair tool_use and tool_result blocks, preventing "unknown" tool names.
870
- *
871
- * User messages that consist entirely of tool_result blocks (and optional
872
- * system_notice text) are removed from the output. Mixed messages (tool_result
873
- * + real user text) keep only the non-tool-result blocks.
874
- */
875
- function mergeToolResultsIntoAssistantMessages(
876
- messages: MessageRow[],
877
- ): MessageRow[] {
878
- // Index of the most recent assistant message in the output array.
879
- let lastAssistantIdx = -1;
880
- // Parsed content caches — lazily populated per assistant message.
881
- const parsedAssistantContent = new Map<number, unknown[]>();
882
-
883
- const result: MessageRow[] = [];
884
-
885
- for (const msg of messages) {
886
- if (msg.role === "assistant") {
887
- lastAssistantIdx = result.length;
888
- result.push(msg);
889
- continue;
890
- }
891
-
892
- // Only process user messages — other roles pass through.
893
- if (msg.role !== "user") {
894
- result.push(msg);
895
- continue;
896
- }
897
-
898
- let blocks: unknown[];
899
- try {
900
- const parsed = JSON.parse(msg.content);
901
- if (!Array.isArray(parsed)) {
902
- result.push(msg);
903
- continue;
904
- }
905
- blocks = parsed;
906
- } catch {
907
- result.push(msg);
908
- continue;
909
- }
910
-
911
- // Separate tool-result blocks from real user content.
912
- const toolResultBlocks: unknown[] = [];
913
- const otherBlocks: unknown[] = [];
914
- for (const block of blocks) {
915
- if (
916
- typeof block === "object" &&
917
- block !== null &&
918
- typeof (block as Record<string, unknown>).type === "string"
919
- ) {
920
- const rec = block as Record<string, unknown>;
921
- if (isToolResultType(rec.type as string)) {
922
- toolResultBlocks.push(block);
923
- } else if (isSystemNoticeText(rec)) {
924
- // System notices don't count as user content — drop them when
925
- // the message is otherwise tool-result-only.
926
- otherBlocks.push(block);
927
- } else {
928
- otherBlocks.push(block);
929
- }
930
- } else {
931
- otherBlocks.push(block);
932
- }
933
- }
934
-
935
- // No tool results → pass through unchanged. System notices are only
936
- // injected alongside tool results in the agent loop, so a pure user
937
- // message (no tool_result blocks) should never be filtered — even if
938
- // the user's text happens to look like a system_notice tag.
939
- if (toolResultBlocks.length === 0) {
940
- result.push(msg);
941
- continue;
942
- }
943
-
944
- // Append tool_result blocks to the preceding assistant message's content.
945
- if (lastAssistantIdx >= 0) {
946
- const assistant = result[lastAssistantIdx];
947
- let assistantContent = parsedAssistantContent.get(lastAssistantIdx);
948
- if (!assistantContent) {
949
- try {
950
- const parsed = JSON.parse(assistant.content);
951
- assistantContent = Array.isArray(parsed) ? parsed : [parsed];
952
- } catch {
953
- assistantContent = [];
954
- }
955
- parsedAssistantContent.set(lastAssistantIdx, assistantContent);
956
- }
957
- assistantContent.push(...toolResultBlocks);
958
- } else {
959
- // No preceding assistant message (pagination boundary) — keep the
960
- // original message as-is to avoid permanent data loss. The preceding
961
- // assistant tool_use lives in the previous page; dropping the result
962
- // here would be unrecoverable.
963
- // Still strip system notices so internal prompt text isn't exposed.
964
- const filteredBlocks = blocks.filter(
965
- (b) =>
966
- !(
967
- typeof b === "object" &&
968
- b !== null &&
969
- isSystemNoticeText(b as Record<string, unknown>)
970
- ),
971
- );
972
- result.push({
973
- ...msg,
974
- content:
975
- filteredBlocks.length === blocks.length
976
- ? msg.content
977
- : JSON.stringify(filteredBlocks),
978
- });
979
- continue;
980
- }
981
-
982
- // If the user message had only tool_result (+ system_notice) blocks,
983
- // suppress it entirely. Otherwise keep the non-tool-result content.
984
- const realUserContent = otherBlocks.filter(
985
- (b) =>
986
- !(
987
- typeof b === "object" &&
988
- b !== null &&
989
- isSystemNoticeText(b as Record<string, unknown>)
990
- ),
991
- );
992
- if (realUserContent.length > 0) {
993
- result.push({ ...msg, content: JSON.stringify(otherBlocks) });
994
- }
995
- // else: tool-result-only → suppressed (results already merged above)
996
- }
997
-
998
- // Write back any modified assistant message content.
999
- for (const [idx, content] of parsedAssistantContent) {
1000
- result[idx] = { ...result[idx], content: JSON.stringify(content) };
1001
- }
1002
-
1003
- return result;
1004
- }
1005
-
1006
- // ── Consecutive assistant message merging ────────────────────────────
1007
-
1008
- /** Parse a message's JSON content into an array of content blocks. */
1009
- function parseContentBlocks(content: string): unknown[] {
1010
- try {
1011
- const parsed = JSON.parse(content);
1012
- return Array.isArray(parsed) ? parsed : [parsed];
1013
- } catch (err) {
1014
- log.warn(
1015
- { err },
1016
- "Failed to parse content blocks during assistant message merge",
1017
- );
1018
- return [];
1019
- }
1020
- }
1021
-
1022
- /**
1023
- * Append content blocks from a donor message onto a target block array.
1024
- * Parses the donor's JSON content and pushes each block into `target`.
1025
- */
1026
- function appendContentBlocks(target: unknown[], donorContent: string): void {
1027
- try {
1028
- const parsed = JSON.parse(donorContent);
1029
- if (Array.isArray(parsed)) {
1030
- target.push(...parsed);
1031
- } else {
1032
- target.push(parsed);
1033
- }
1034
- } catch (err) {
1035
- log.warn(
1036
- { err },
1037
- "Failed to parse donor content blocks during assistant message merge",
1038
- );
1039
- }
1040
- }
1041
-
1042
- /**
1043
- * Promote metadata fields from a donor message to the surviving message
1044
- * when the survivor lacks them. Currently promotes `subagentNotification`.
1045
- * Returns a new MessageRow if promotion occurred, otherwise the original.
1046
- */
1047
- function promoteMetadata(survivor: MessageRow, donor: MessageRow): MessageRow {
1048
- if (donor.metadata && survivor.metadata) {
1049
- try {
1050
- const survivorMeta = JSON.parse(survivor.metadata);
1051
- const donorMeta = JSON.parse(donor.metadata);
1052
- if (
1053
- !survivorMeta.subagentNotification &&
1054
- donorMeta.subagentNotification
1055
- ) {
1056
- survivorMeta.subagentNotification = donorMeta.subagentNotification;
1057
- return { ...survivor, metadata: JSON.stringify(survivorMeta) };
1058
- }
1059
- } catch (err) {
1060
- log.warn(
1061
- { err },
1062
- "Failed to parse metadata during assistant message merge",
1063
- );
1064
- }
1065
- } else if (donor.metadata && !survivor.metadata) {
1066
- return { ...survivor, metadata: donor.metadata };
1067
- }
1068
- return survivor;
1069
- }
1070
-
1071
- /**
1072
- * Merge consecutive assistant messages into a single message at query time.
1073
- *
1074
- * During streaming, all assistant turns within one agent loop accumulate on
1075
- * a single client-side ChatMessage. In the DB, each API turn is stored as a
1076
- * separate assistant row (consolidation is deferred to compaction for
1077
- * prefix-cache stability). This produces N separate assistant messages that
1078
- * the client renders as N individual bubbles — each showing "Completed 1
1079
- * step" instead of one grouped "Completed N steps" accordion.
1080
- *
1081
- * This function concatenates the content block arrays of consecutive
1082
- * assistant messages (no intervening user messages after tool-result
1083
- * merging) into the first message of each run. The merged messages are
1084
- * removed from the output. This is query-time only — the DB is not
1085
- * modified.
1086
- *
1087
- * The first message in each run keeps its id, createdAt, and metadata so
1088
- * that attachment lookups, display timestamps, and subagent notifications
1089
- * continue to work. Metadata from later messages in the run (e.g.
1090
- * subagentNotification) is preserved by promoting it to the surviving
1091
- * message when the surviving message has no metadata of its own for that
1092
- * field.
1093
- */
1094
- function mergeConsecutiveAssistantMessages(messages: MessageRow[]): {
1095
- messages: MessageRow[];
1096
- /** Maps each surviving message ID → all original message IDs merged into it. */
1097
- mergedIdMap: Map<string, string[]>;
1098
- } {
1099
- const result: MessageRow[] = [];
1100
- // Key = index in `result`, value = accumulated content blocks.
1101
- const pendingMerges = new Map<number, unknown[]>();
1102
- // Key = index in `result`, value = IDs of messages merged into the target.
1103
- const mergedIds = new Map<number, string[]>();
1104
-
1105
- for (const msg of messages) {
1106
- const lastIdx = result.length - 1;
1107
- const isConsecutiveAssistant =
1108
- msg.role === "assistant" &&
1109
- lastIdx >= 0 &&
1110
- result[lastIdx].role === "assistant";
1111
-
1112
- if (!isConsecutiveAssistant) {
1113
- result.push(msg);
1114
- continue;
1115
- }
1116
-
1117
- // Track the donor message ID.
1118
- let ids = mergedIds.get(lastIdx);
1119
- if (!ids) {
1120
- ids = [];
1121
- mergedIds.set(lastIdx, ids);
1122
- }
1123
- ids.push(msg.id);
1124
-
1125
- // Lazily parse the target's content on first merge.
1126
- let targetContent = pendingMerges.get(lastIdx);
1127
- if (!targetContent) {
1128
- targetContent = parseContentBlocks(result[lastIdx].content);
1129
- pendingMerges.set(lastIdx, targetContent);
1130
- }
1131
-
1132
- appendContentBlocks(targetContent, msg.content);
1133
- result[lastIdx] = promoteMetadata(result[lastIdx], msg);
1134
- }
1135
-
1136
- // Write back merged content for any messages that were targets.
1137
- for (const [idx, content] of pendingMerges) {
1138
- result[idx] = { ...result[idx], content: JSON.stringify(content) };
1139
- }
1140
-
1141
- // Build the merged ID map keyed by surviving message ID.
1142
- const mergedIdMap = new Map<string, string[]>();
1143
- for (const [idx, ids] of mergedIds) {
1144
- mergedIdMap.set(result[idx].id, ids);
1145
- }
1146
-
1147
- return { messages: result, mergedIdMap };
1148
- }
1149
-
1150
- /**
1151
891
  /**
1152
892
  * Persist the pre-chat onboarding payload to disk.
1153
893
  *
@@ -1240,6 +980,7 @@ export async function handleSendMessage(
1240
980
  ): Promise<unknown> {
1241
981
  const body = (rawBody ?? {}) as {
1242
982
  conversationKey?: string;
983
+ conversationId?: string;
1243
984
  content?: string;
1244
985
  attachmentIds?: string[];
1245
986
  sourceChannel?: string;
@@ -1266,13 +1007,21 @@ export async function handleSendMessage(
1266
1007
  cohort?: string;
1267
1008
  websiteUrl?: string;
1268
1009
  contentSourceUrl?: string;
1010
+ bootstrapTemplate?: string;
1011
+ initialMessage?: string;
1012
+ skills?: string[];
1269
1013
  };
1270
1014
  };
1271
1015
 
1272
1016
  const actorPrincipalId = headers?.["x-vellum-actor-principal-id"];
1273
1017
  const principalType = headers?.["x-vellum-principal-type"];
1018
+ const originClientId = headers?.["x-vellum-client-id"]?.trim() || undefined;
1274
1019
 
1275
1020
  const { conversationKey, content, attachmentIds } = body;
1021
+ const inboundConversationId =
1022
+ typeof body.conversationId === "string" && body.conversationId.length > 0
1023
+ ? body.conversationId
1024
+ : undefined;
1276
1025
  const clientMessageId =
1277
1026
  typeof body.clientMessageId === "string" ? body.clientMessageId : undefined;
1278
1027
  const requestedInferenceProfile =
@@ -1340,12 +1089,6 @@ export async function handleSendMessage(
1340
1089
  ? (canonicalizeTimeZone(body.clientTimezone) ?? undefined)
1341
1090
  : undefined;
1342
1091
 
1343
- // When conversationKey is omitted, derive a stable default from
1344
- // sourceChannel + sourceInterface so that repeated calls from the same
1345
- // channel/interface pair share a single conversation thread.
1346
- const resolvedConversationKey =
1347
- conversationKey ?? `default:${sourceChannel}:${sourceInterface}`;
1348
-
1349
1092
  // Reject non-string content values (numbers, objects, etc.)
1350
1093
  if (content != null && typeof content !== "string") {
1351
1094
  throw new BadRequestError("content must be a string");
@@ -1409,9 +1152,45 @@ export async function handleSendMessage(
1409
1152
  // timer so the next heartbeat is a full interval after this interaction.
1410
1153
  HeartbeatService.getInstance()?.resetTimer();
1411
1154
 
1412
- const mapping = getOrCreateConversation(resolvedConversationKey, {
1413
- conversationType: "standard",
1414
- });
1155
+ // Resolve the target conversation. Fetch by `conversationId` (the
1156
+ // assistant-minted internal id) when the client supplies it — clients
1157
+ // must obtain this id from a prior daemon response, so a missing row
1158
+ // is a 404. Otherwise fall through to the external-key path: the
1159
+ // client-supplied `conversationKey` (external-key lookup; materializes
1160
+ // on first use) or, when neither is provided, a channel-dependent
1161
+ // default. The vellum channel mints a fresh conversation on every
1162
+ // empty-handed send so first-message-of-a-new-chat surfaces with a
1163
+ // server-minted id; other channels (phone, slack, …) share a stable
1164
+ // `default:<channel>:<interface>` thread so repeated calls from the
1165
+ // same channel/interface stay co-located.
1166
+ let mapping: {
1167
+ conversationId: string;
1168
+ conversationType: string;
1169
+ created: boolean;
1170
+ };
1171
+ if (inboundConversationId !== undefined) {
1172
+ const existing = getConversation(inboundConversationId);
1173
+ if (!existing) {
1174
+ throw new NotFoundError(
1175
+ `Conversation ${inboundConversationId} not found`,
1176
+ );
1177
+ }
1178
+ mapping = {
1179
+ conversationId: existing.id,
1180
+ conversationType: existing.conversationType,
1181
+ created: false,
1182
+ };
1183
+ } else {
1184
+ const resolvedConversationKey =
1185
+ conversationKey && conversationKey.length > 0
1186
+ ? conversationKey
1187
+ : sourceChannel === "vellum"
1188
+ ? crypto.randomUUID()
1189
+ : `default:${sourceChannel}:${sourceInterface}`;
1190
+ mapping = getOrCreateConversation(resolvedConversationKey, {
1191
+ conversationType: "standard",
1192
+ });
1193
+ }
1415
1194
 
1416
1195
  if (requestedRiskThreshold !== undefined) {
1417
1196
  const result = await ipcCall("set_conversation_threshold", {
@@ -1445,6 +1224,7 @@ export async function handleSendMessage(
1445
1224
  publishConversationListAndMetadataChanged(
1446
1225
  "created",
1447
1226
  mapping.conversationId,
1227
+ originClientId,
1448
1228
  );
1449
1229
  }
1450
1230
  }
@@ -1620,51 +1400,38 @@ export async function handleSendMessage(
1620
1400
  : ("content-source" as const);
1621
1401
  effectiveContent = buildScanFirstMessage(scanUrl, scanVariant);
1622
1402
  // Fall through to normal inference path below
1623
- } else if (isWakeUp && body.onboarding?.cohort === "content-automation") {
1624
- effectiveContent = "I want to write articles that rank better in GEO";
1625
- // Fall through to normal inference path — the bootstrap template
1626
- // and geo-writing skill handle this message.
1403
+ } else if (isWakeUp && body.onboarding?.initialMessage) {
1404
+ effectiveContent = body.onboarding.initialMessage;
1627
1405
  } else if (isWakeUp) {
1628
1406
  const cannedGreeting = getCannedFirstGreeting(body.onboarding ?? undefined);
1629
1407
 
1630
1408
  conversation.processing = true;
1631
1409
  let cleanupDeferred = false;
1632
1410
  try {
1633
- const provenance = provenanceFromTrustContext(conversation.trustContext);
1634
- const channelMeta = {
1635
- ...provenance,
1411
+ const rawContent = content ?? "";
1412
+ const attachments = hasAttachments
1413
+ ? smDeps.resolveAttachments(attachmentIds)
1414
+ : [];
1415
+ const greetingMeta = {
1636
1416
  userMessageChannel: sourceChannel,
1637
1417
  assistantMessageChannel: sourceChannel,
1638
1418
  userMessageInterface: sourceInterface,
1639
1419
  assistantMessageInterface: sourceInterface,
1640
1420
  };
1641
-
1642
- const rawContent = content ?? "";
1643
- const attachments = hasAttachments
1644
- ? smDeps.resolveAttachments(attachmentIds)
1645
- : [];
1646
- const userMsg = createUserMessage(rawContent, attachments);
1647
- const persisted = await addMessage(
1648
- mapping.conversationId,
1649
- "user",
1650
- JSON.stringify(userMsg.content),
1651
- channelMeta,
1652
- );
1653
- conversation.getMessages().push(userMsg);
1654
-
1655
- setConversationOriginChannelIfUnset(
1656
- mapping.conversationId,
1657
- sourceChannel,
1658
- );
1659
- setConversationOriginInterfaceIfUnset(
1660
- mapping.conversationId,
1661
- sourceInterface,
1662
- );
1421
+ const persisted = await persistQueuedMessageBody(conversation, {
1422
+ content: rawContent,
1423
+ attachments,
1424
+ requestId: crypto.randomUUID(),
1425
+ metadata: greetingMeta,
1426
+ });
1663
1427
 
1664
1428
  const conversationId = mapping.conversationId;
1429
+ const channelMeta = buildChannelMetadata(sourceChannel, sourceInterface, {
1430
+ trustContext: conversation.trustContext,
1431
+ });
1665
1432
 
1666
1433
  const assistantMsg = createAssistantMessage(cannedGreeting);
1667
- await addMessage(
1434
+ const persistedAssistant = await addMessage(
1668
1435
  mapping.conversationId,
1669
1436
  "assistant",
1670
1437
  JSON.stringify(assistantMsg.content),
@@ -1708,8 +1475,12 @@ export async function handleSendMessage(
1708
1475
  text: cannedGreeting,
1709
1476
  conversationId,
1710
1477
  });
1711
- broadcastMessage({ type: "message_complete", conversationId });
1712
- publishConversationMessagesChanged(conversationId);
1478
+ emitCannedMessageComplete(
1479
+ broadcastMessage,
1480
+ conversationId,
1481
+ persistedAssistant.id,
1482
+ );
1483
+ publishConversationMessagesChanged(conversationId, originClientId);
1713
1484
  conversation.processing = false;
1714
1485
  silentlyWithLog(
1715
1486
  conversation.drainQueue(),
@@ -1787,6 +1558,7 @@ export async function handleSendMessage(
1787
1558
  : deps.approvalConversationGenerator,
1788
1559
  verifiedActorExternalUserId,
1789
1560
  verifiedActorPrincipalId,
1561
+ originClientId,
1790
1562
  });
1791
1563
  if (inlineReplyResult.consumed) {
1792
1564
  return {
@@ -1807,25 +1579,22 @@ export async function handleSendMessage(
1807
1579
  if (conversation.isProcessing()) {
1808
1580
  // Queue the message so it's processed when the current turn completes
1809
1581
  const requestId = crypto.randomUUID();
1810
- const enqueueResult = conversation.enqueueMessage(
1811
- contentAfterScan,
1582
+ const enqueueResult = conversation.enqueueMessage({
1583
+ content: contentAfterScan,
1812
1584
  attachments,
1813
- broadcastMessage,
1585
+ onEvent: broadcastMessage,
1814
1586
  requestId,
1815
- undefined, // activeSurfaceId
1816
- undefined, // currentPage
1817
- {
1587
+ metadata: {
1818
1588
  userMessageChannel: sourceChannel,
1819
1589
  assistantMessageChannel: sourceChannel,
1820
1590
  userMessageInterface: sourceInterface,
1821
1591
  assistantMessageInterface: sourceInterface,
1822
1592
  ...(body.automated === true ? { automated: true } : {}),
1823
1593
  },
1824
- { isInteractive },
1825
- undefined, // displayContent
1594
+ isInteractive,
1826
1595
  transport,
1827
1596
  clientMessageId,
1828
- );
1597
+ });
1829
1598
  if (enqueueResult.rejected) {
1830
1599
  return new RouteResponse(
1831
1600
  JSON.stringify({ accepted: false, error: "queue_full" }),
@@ -1945,37 +1714,33 @@ export async function handleSendMessage(
1945
1714
  conversation.processing = true;
1946
1715
  let cleanupDeferred = false;
1947
1716
  try {
1948
- const provenance = provenanceFromTrustContext(conversation.trustContext);
1949
- const imageSourcePaths: Record<string, string> = {};
1950
- for (let i = 0; i < attachments.length; i++) {
1951
- const a = attachments[i];
1952
- if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
1953
- imageSourcePaths[`${i}:${a.filename}`] = a.filePath;
1954
- }
1955
- }
1956
- const channelMeta = {
1957
- ...provenance,
1717
+ const slashMeta = {
1958
1718
  userMessageChannel: sourceChannel,
1959
1719
  assistantMessageChannel: sourceChannel,
1960
1720
  userMessageInterface: sourceInterface,
1961
1721
  assistantMessageInterface: sourceInterface,
1962
1722
  ...(body.automated === true ? { automated: true } : {}),
1963
- ...(Object.keys(imageSourcePaths).length > 0
1964
- ? { imageSourcePaths }
1965
- : {}),
1966
1723
  };
1967
- const cleanMsg = createUserMessage(rawContent, attachments);
1968
- const llmMsg = enrichMessageWithSourcePaths(cleanMsg, attachments);
1969
- const persisted = await addMessage(
1970
- mapping.conversationId,
1971
- "user",
1972
- JSON.stringify(cleanMsg.content),
1973
- channelMeta,
1974
- );
1975
- conversation.getMessages().push(llmMsg);
1724
+ const persisted = await persistQueuedMessageBody(conversation, {
1725
+ content: rawContent,
1726
+ attachments,
1727
+ requestId: crypto.randomUUID(),
1728
+ metadata: slashMeta,
1729
+ clientMessageId,
1730
+ });
1731
+ if (persisted.deduplicated) {
1732
+ return {
1733
+ accepted: true,
1734
+ messageId: persisted.id,
1735
+ conversationId: mapping.conversationId,
1736
+ };
1737
+ }
1976
1738
 
1739
+ const channelMeta = buildChannelMetadata(sourceChannel, sourceInterface, {
1740
+ trustContext: conversation.trustContext,
1741
+ });
1977
1742
  const assistantMsg = createAssistantMessage(slashResult.message);
1978
- await addMessage(
1743
+ const persistedAssistant = await addMessage(
1979
1744
  mapping.conversationId,
1980
1745
  "assistant",
1981
1746
  JSON.stringify(assistantMsg.content),
@@ -1983,15 +1748,6 @@ export async function handleSendMessage(
1983
1748
  );
1984
1749
  conversation.getMessages().push(assistantMsg);
1985
1750
 
1986
- setConversationOriginChannelIfUnset(
1987
- mapping.conversationId,
1988
- sourceChannel,
1989
- );
1990
- setConversationOriginInterfaceIfUnset(
1991
- mapping.conversationId,
1992
- sourceInterface,
1993
- );
1994
-
1995
1751
  // Snapshot model info now so the deferred callback cannot observe
1996
1752
  // a config change from a concurrent request.
1997
1753
  const modelInfoEvent = isModelSlashCommand(rawContent)
@@ -2030,11 +1786,12 @@ export async function handleSendMessage(
2030
1786
  text: message,
2031
1787
  conversationId,
2032
1788
  });
2033
- broadcastMessage({
2034
- type: "message_complete",
2035
- conversationId: conversationId,
2036
- });
2037
- publishConversationMessagesChanged(conversationId);
1789
+ emitCannedMessageComplete(
1790
+ broadcastMessage,
1791
+ conversationId,
1792
+ persistedAssistant.id,
1793
+ );
1794
+ publishConversationMessagesChanged(conversationId, originClientId);
2038
1795
  conversation.processing = false;
2039
1796
  silentlyWithLog(conversation.drainQueue(), "slash-command queue drain");
2040
1797
  }, 0);
@@ -2053,24 +1810,43 @@ export async function handleSendMessage(
2053
1810
 
2054
1811
  if (slashResult.kind === "compact") {
2055
1812
  conversation.processing = true;
2056
- const provenance = provenanceFromTrustContext(conversation.trustContext);
2057
- const channelMeta = {
2058
- ...provenance,
1813
+ const slashMeta = {
2059
1814
  userMessageChannel: sourceChannel,
2060
1815
  assistantMessageChannel: sourceChannel,
2061
1816
  userMessageInterface: sourceInterface,
2062
1817
  assistantMessageInterface: sourceInterface,
2063
1818
  };
2064
- const cleanMsg = createUserMessage(rawContent, attachments);
2065
- const persisted = await addMessage(
2066
- mapping.conversationId,
2067
- "user",
2068
- JSON.stringify(cleanMsg.content),
2069
- channelMeta,
2070
- );
2071
- conversation.getMessages().push(cleanMsg);
1819
+ let persisted: Awaited<ReturnType<typeof persistQueuedMessageBody>>;
1820
+ try {
1821
+ persisted = await persistQueuedMessageBody(conversation, {
1822
+ content: rawContent,
1823
+ attachments,
1824
+ requestId: crypto.randomUUID(),
1825
+ metadata: slashMeta,
1826
+ clientMessageId,
1827
+ });
1828
+ } catch (err) {
1829
+ // The fire-and-forget compaction below owns clearing `processing`, but a
1830
+ // throw from this initial persist never reaches it — reset here so the
1831
+ // conversation isn't stranded in queued mode.
1832
+ conversation.processing = false;
1833
+ silentlyWithLog(conversation.drainQueue(), "compact-command queue drain");
1834
+ throw err;
1835
+ }
1836
+ if (persisted.deduplicated) {
1837
+ conversation.processing = false;
1838
+ silentlyWithLog(conversation.drainQueue(), "compact-dedup queue drain");
1839
+ return {
1840
+ accepted: true,
1841
+ messageId: persisted.id,
1842
+ conversationId: mapping.conversationId,
1843
+ };
1844
+ }
2072
1845
 
2073
1846
  const conversationId = mapping.conversationId;
1847
+ const channelMeta = buildChannelMetadata(sourceChannel, sourceInterface, {
1848
+ trustContext: conversation.trustContext,
1849
+ });
2074
1850
 
2075
1851
  // Fire-and-forget: return 202 immediately, run compaction async.
2076
1852
  // forceCompact() makes an LLM call that can exceed the client's
@@ -2085,7 +1861,7 @@ export async function handleSendMessage(
2085
1861
  messageId: persisted.id,
2086
1862
  clientMessageId,
2087
1863
  });
2088
- publishConversationMessagesChanged(conversationId);
1864
+ publishConversationMessagesChanged(conversationId, originClientId);
2089
1865
  conversation.emitActivityState(
2090
1866
  "thinking",
2091
1867
  "context_compacting",
@@ -2097,7 +1873,7 @@ export async function handleSendMessage(
2097
1873
  const responseText = formatCompactResult(result);
2098
1874
 
2099
1875
  const assistantMsg = createAssistantMessage(responseText);
2100
- await addMessage(
1876
+ const persistedAssistant = await addMessage(
2101
1877
  conversationId,
2102
1878
  "assistant",
2103
1879
  JSON.stringify(assistantMsg.content),
@@ -2111,11 +1887,15 @@ export async function handleSendMessage(
2111
1887
  text: responseText,
2112
1888
  conversationId,
2113
1889
  });
2114
- broadcastMessage({ type: "message_complete", conversationId });
2115
- publishConversationMessagesChanged(conversationId);
1890
+ emitCannedMessageComplete(
1891
+ broadcastMessage,
1892
+ conversationId,
1893
+ persistedAssistant.id,
1894
+ );
1895
+ publishConversationMessagesChanged(conversationId, originClientId);
2116
1896
  } catch (err) {
2117
1897
  if (assistantMessagePersisted) {
2118
- publishConversationMessagesChanged(conversationId);
1898
+ publishConversationMessagesChanged(conversationId, originClientId);
2119
1899
  }
2120
1900
  log.error({ err, conversationId }, "Compact command failed");
2121
1901
  broadcastMessage({
@@ -2143,93 +1923,115 @@ export async function handleSendMessage(
2143
1923
 
2144
1924
  if (slashResult.kind === "clean") {
2145
1925
  conversation.processing = true;
2146
- const provenance = provenanceFromTrustContext(conversation.trustContext);
2147
- const channelMeta = {
2148
- ...provenance,
2149
- userMessageChannel: sourceChannel,
2150
- assistantMessageChannel: sourceChannel,
2151
- userMessageInterface: sourceInterface,
2152
- assistantMessageInterface: sourceInterface,
2153
- };
2154
- const cleanMsg = createUserMessage(rawContent, attachments);
2155
- const persisted = await addMessage(
2156
- mapping.conversationId,
2157
- "user",
2158
- JSON.stringify(cleanMsg.content),
2159
- channelMeta,
2160
- );
2161
- conversation.getMessages().push(cleanMsg);
2162
-
2163
1926
  const conversationId = mapping.conversationId;
2164
-
2165
- let assistantMessagePersisted = false;
1927
+ // Outer try/finally guarantees the processing flag is cleared (and the
1928
+ // queue drained) on every failure path — including a throw from the
1929
+ // initial user-message persist below, which would otherwise leave the
1930
+ // conversation stuck in queued mode indefinitely.
2166
1931
  try {
2167
- broadcastMessage({
2168
- type: "user_message_echo",
2169
- text: rawContent,
2170
- conversationId,
2171
- messageId: persisted.id,
1932
+ const slashMeta = {
1933
+ userMessageChannel: sourceChannel,
1934
+ assistantMessageChannel: sourceChannel,
1935
+ userMessageInterface: sourceInterface,
1936
+ assistantMessageInterface: sourceInterface,
1937
+ };
1938
+ const persisted = await persistQueuedMessageBody(conversation, {
1939
+ content: rawContent,
1940
+ attachments,
1941
+ requestId: crypto.randomUUID(),
1942
+ metadata: slashMeta,
2172
1943
  clientMessageId,
2173
1944
  });
2174
- publishConversationMessagesChanged(conversationId);
1945
+ if (persisted.deduplicated) {
1946
+ return {
1947
+ accepted: true,
1948
+ messageId: persisted.id,
1949
+ conversationId,
1950
+ };
1951
+ }
2175
1952
 
2176
- const result = await conversation.forceClean();
2177
- const responseText = formatCleanResult(result);
1953
+ const channelMeta = buildChannelMetadata(sourceChannel, sourceInterface, {
1954
+ trustContext: conversation.trustContext,
1955
+ });
1956
+ let assistantMessagePersisted = false;
1957
+ try {
1958
+ broadcastMessage({
1959
+ type: "user_message_echo",
1960
+ text: rawContent,
1961
+ conversationId,
1962
+ messageId: persisted.id,
1963
+ clientMessageId,
1964
+ });
1965
+ publishConversationMessagesChanged(conversationId, originClientId);
2178
1966
 
2179
- const assistantMsg = createAssistantMessage(responseText);
2180
- await addMessage(
2181
- conversationId,
2182
- "assistant",
2183
- JSON.stringify(assistantMsg.content),
2184
- channelMeta,
2185
- );
2186
- assistantMessagePersisted = true;
2187
- conversation.getMessages().push(assistantMsg);
1967
+ const result = await conversation.forceClean();
1968
+ const responseText = formatCleanResult(result);
2188
1969
 
2189
- broadcastMessage({
2190
- type: "assistant_text_delta",
2191
- text: responseText,
2192
- conversationId,
2193
- });
2194
- broadcastMessage({ type: "message_complete", conversationId });
2195
- publishConversationMessagesChanged(conversationId);
2196
- } catch (err) {
2197
- if (assistantMessagePersisted) {
2198
- publishConversationMessagesChanged(conversationId);
1970
+ const assistantMsg = createAssistantMessage(responseText);
1971
+ const persistedAssistant = await addMessage(
1972
+ conversationId,
1973
+ "assistant",
1974
+ JSON.stringify(assistantMsg.content),
1975
+ channelMeta,
1976
+ );
1977
+ assistantMessagePersisted = true;
1978
+ conversation.getMessages().push(assistantMsg);
1979
+
1980
+ broadcastMessage({
1981
+ type: "assistant_text_delta",
1982
+ text: responseText,
1983
+ conversationId,
1984
+ });
1985
+ emitCannedMessageComplete(
1986
+ broadcastMessage,
1987
+ conversationId,
1988
+ persistedAssistant.id,
1989
+ );
1990
+ publishConversationMessagesChanged(conversationId, originClientId);
1991
+ } catch (err) {
1992
+ if (assistantMessagePersisted) {
1993
+ publishConversationMessagesChanged(conversationId, originClientId);
1994
+ }
1995
+ log.error({ err, conversationId }, "Clean command failed");
1996
+ broadcastMessage({
1997
+ type: "conversation_error",
1998
+ conversationId,
1999
+ code: "UNKNOWN",
2000
+ userMessage: `Clean failed: ${err instanceof Error ? err.message : String(err)}`,
2001
+ retryable: true,
2002
+ });
2199
2003
  }
2200
- log.error({ err, conversationId }, "Clean command failed");
2201
- broadcastMessage({
2202
- type: "conversation_error",
2004
+
2005
+ return {
2006
+ accepted: true,
2007
+ messageId: persisted.id,
2203
2008
  conversationId,
2204
- code: "UNKNOWN",
2205
- userMessage: `Clean failed: ${err instanceof Error ? err.message : String(err)}`,
2206
- retryable: true,
2207
- });
2009
+ };
2208
2010
  } finally {
2209
2011
  conversation.processing = false;
2210
2012
  silentlyWithLog(conversation.drainQueue(), "clean-command queue drain");
2211
2013
  }
2212
-
2213
- return {
2214
- accepted: true,
2215
- messageId: persisted.id,
2216
- conversationId,
2217
- };
2218
2014
  }
2219
2015
 
2220
2016
  const resolvedContent = slashResult.content;
2221
2017
 
2222
2018
  const requestId = crypto.randomUUID();
2223
- let messageId: string;
2224
- try {
2225
- messageId = await conversation.persistUserMessage(
2226
- resolvedContent,
2227
- attachments,
2228
- requestId,
2229
- body.automated === true ? { automated: true } : undefined,
2230
- );
2231
- } catch (err) {
2232
- throw err;
2019
+ const persistResult = await conversation.persistUserMessage({
2020
+ content: resolvedContent,
2021
+ attachments,
2022
+ requestId,
2023
+ metadata: body.automated === true ? { automated: true } : undefined,
2024
+ clientMessageId,
2025
+ });
2026
+
2027
+ const messageId = persistResult.id;
2028
+
2029
+ if (persistResult.deduplicated) {
2030
+ return {
2031
+ accepted: true,
2032
+ messageId,
2033
+ conversationId: mapping.conversationId,
2034
+ };
2233
2035
  }
2234
2036
 
2235
2037
  broadcastMessage({
@@ -2240,7 +2042,7 @@ export async function handleSendMessage(
2240
2042
  requestId,
2241
2043
  clientMessageId,
2242
2044
  });
2243
- publishConversationMessagesChanged(mapping.conversationId);
2045
+ publishConversationMessagesChanged(mapping.conversationId, originClientId);
2244
2046
 
2245
2047
  // Fire-and-forget the agent loop; events flow to the hub via broadcastMessage.
2246
2048
  conversation
@@ -2285,14 +2087,25 @@ async function generateLlmSuggestion(
2285
2087
  ? escapeXmlContent(priorUserText)
2286
2088
  : priorUserText;
2287
2089
 
2288
- const systemPrompt =
2289
- "You generate short, casual reply suggestions a user might type next in a chat. Match the tone and register of the preceding conversation. Output only the reply text inside the requested tags — no preamble, no commentary.";
2090
+ const systemPrompt = [
2091
+ "You generate short, casual reply suggestions a user might type next in a chat.",
2092
+ "Match the tone and register of the preceding conversation.",
2093
+ "",
2094
+ "CRITICAL — write from the USER'S perspective only, NEVER from the assistant's:",
2095
+ "- The suggestion is what the USER will type into the chat input",
2096
+ '- Use first-person "I" only if the user has used it in their prior messages',
2097
+ '- NEVER start with phrases like "I can help", "Here\'s what", "Let me", "I\'d suggest" — those are assistant-voice',
2098
+ "- Think: if you were the user reading the assistant's reply, what question or follow-up would you ask next?",
2099
+ "",
2100
+ "Output only the reply text inside the requested tags — no preamble, no commentary.",
2101
+ ].join("\n");
2290
2102
 
2291
2103
  const userPrompt =
2292
2104
  `Here is the end of a conversation:\n\n` +
2293
2105
  `<user_message>${truncatedUser ?? "(no prior user message)"}</user_message>\n` +
2294
2106
  `<assistant_message>${truncatedAssistant}</assistant_message>\n\n` +
2295
- `Write the user's next reply, focusing on the LAST question or call-to-action in the assistant message. Keep it short (under 15 words), casual, and in the user's voice. Respond in this exact format:\n\n` +
2107
+ `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. ` +
2108
+ `The reply must read as something typed BY the user, not something the assistant would say. Respond in this exact format:\n\n` +
2296
2109
  `<reply>YOUR_REPLY_HERE</reply>`;
2297
2110
 
2298
2111
  // Single user message only — no assistant-role prefill. Anthropic
@@ -2368,14 +2181,27 @@ export async function handleGetSuggestion(
2368
2181
  };
2369
2182
 
2370
2183
  const conversationKey = queryParams?.conversationKey;
2371
- if (!conversationKey) {
2372
- throw new BadRequestError("conversationKey query parameter is required");
2184
+ const conversationId = queryParams?.conversationId;
2185
+ if (!conversationKey && !conversationId) {
2186
+ throw new BadRequestError(
2187
+ "conversationKey or conversationId query parameter is required",
2188
+ );
2373
2189
  }
2374
2190
 
2375
- const mapping = getConversationByKey(conversationKey);
2376
- if (!mapping) return noSuggestion;
2191
+ let resolvedConversationId: string | undefined;
2192
+ if (conversationId) {
2193
+ resolvedConversationId = conversationId;
2194
+ } else if (conversationKey) {
2195
+ const mapping = getConversationByKey(conversationKey);
2196
+ if (mapping) {
2197
+ resolvedConversationId = mapping.conversationId;
2198
+ } else if (getConversation(conversationKey)) {
2199
+ resolvedConversationId = conversationKey;
2200
+ }
2201
+ }
2202
+ if (!resolvedConversationId) return noSuggestion;
2377
2203
 
2378
- const rawMessages = getMessages(mapping.conversationId);
2204
+ const rawMessages = getMessages(resolvedConversationId);
2379
2205
  if (rawMessages.length === 0) return noSuggestion;
2380
2206
 
2381
2207
  // Staleness check: compare requested messageId against the latest
@@ -2521,6 +2347,47 @@ function handleSearchConversations({
2521
2347
  return { query, results };
2522
2348
  }
2523
2349
 
2350
+ // ---------------------------------------------------------------------------
2351
+ // Metadata helpers
2352
+ // ---------------------------------------------------------------------------
2353
+
2354
+ /**
2355
+ * Assemble the standard channel metadata object for message persistence.
2356
+ *
2357
+ * Combines provenance (trust context), channel/interface routing, and
2358
+ * optional per-message fields (automated flag, image source paths) into the
2359
+ * Record that `addMessage` stores in the `metadata` column.
2360
+ */
2361
+ function buildChannelMetadata(
2362
+ sourceChannel: string,
2363
+ sourceInterface: string,
2364
+ opts?: {
2365
+ trustContext?: Parameters<typeof provenanceFromTrustContext>[0];
2366
+ provenanceOverride?: Record<string, unknown>;
2367
+ automated?: boolean;
2368
+ attachments?: ReadonlyArray<{
2369
+ filename: string;
2370
+ mimeType: string;
2371
+ filePath?: string;
2372
+ }>;
2373
+ },
2374
+ ): Record<string, unknown> {
2375
+ const provenance =
2376
+ opts?.provenanceOverride ?? provenanceFromTrustContext(opts?.trustContext);
2377
+ const imageSourcePaths = opts?.attachments
2378
+ ? extractImageSourcePaths(opts.attachments)
2379
+ : undefined;
2380
+ return {
2381
+ ...provenance,
2382
+ userMessageChannel: sourceChannel,
2383
+ assistantMessageChannel: sourceChannel,
2384
+ userMessageInterface: sourceInterface,
2385
+ assistantMessageInterface: sourceInterface,
2386
+ ...(opts?.automated ? { automated: true } : {}),
2387
+ ...(imageSourcePaths ? { imageSourcePaths } : {}),
2388
+ };
2389
+ }
2390
+
2524
2391
  // ---------------------------------------------------------------------------
2525
2392
  // Module-level state
2526
2393
  // ---------------------------------------------------------------------------
@@ -2629,10 +2496,31 @@ export const ROUTES: RouteDefinition[] = [
2629
2496
  description:
2630
2497
  "Return an LLM-generated follow-up suggestion for the most recent assistant message.",
2631
2498
  tags: ["messages"],
2499
+ queryParams: [
2500
+ {
2501
+ name: "conversationId",
2502
+ type: "string",
2503
+ description:
2504
+ "Conversation ID to fetch a suggestion for. Either this or conversationKey is required.",
2505
+ },
2506
+ {
2507
+ name: "conversationKey",
2508
+ type: "string",
2509
+ description:
2510
+ "Legacy conversation key. Either this or conversationId is required.",
2511
+ },
2512
+ {
2513
+ name: "messageId",
2514
+ type: "string",
2515
+ description:
2516
+ "Optional. Latest assistant message ID the client has seen — used to detect staleness.",
2517
+ },
2518
+ ],
2632
2519
  responseBody: z.object({
2633
- suggestion: z.string(),
2634
- messageId: z.string(),
2520
+ suggestion: z.string().nullable(),
2521
+ messageId: z.string().nullable(),
2635
2522
  source: z.string(),
2523
+ stale: z.boolean().optional(),
2636
2524
  }),
2637
2525
  handler: async (args) =>
2638
2526
  handleGetSuggestion(args, {