@vellumai/assistant 0.6.2 → 0.6.4

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 (895) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +41 -49
  4. package/bunfig.toml +3 -0
  5. package/docs/architecture/memory.md +1 -1
  6. package/docs/backup-troubleshooting.md +52 -0
  7. package/docs/browser-use-architecture-phase2.md +174 -0
  8. package/docs/stt-provider-onboarding.md +120 -0
  9. package/knip.json +12 -2
  10. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  11. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  12. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  13. package/openapi.yaml +1111 -86
  14. package/package.json +40 -42
  15. package/scripts/generate-openapi.ts +0 -2
  16. package/scripts/test.sh +73 -18
  17. package/src/__tests__/acp-session.test.ts +43 -0
  18. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  19. package/src/__tests__/agent-loop.test.ts +123 -0
  20. package/src/__tests__/anthropic-provider.test.ts +263 -10
  21. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  22. package/src/__tests__/app-executors.test.ts +1 -0
  23. package/src/__tests__/app-source-watcher.test.ts +37 -11
  24. package/src/__tests__/approval-routes-http.test.ts +178 -1
  25. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  26. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +240 -94
  28. package/src/__tests__/browser-manager.test.ts +40 -27
  29. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  30. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  31. package/src/__tests__/btw-routes.test.ts +7 -0
  32. package/src/__tests__/call-controller.test.ts +581 -20
  33. package/src/__tests__/catalog-files.test.ts +1000 -0
  34. package/src/__tests__/channel-approvals.test.ts +53 -0
  35. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  36. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  37. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  38. package/src/__tests__/checker.test.ts +157 -10
  39. package/src/__tests__/clawhub-files.test.ts +347 -0
  40. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  41. package/src/__tests__/config-analysis.test.ts +100 -0
  42. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  43. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  44. package/src/__tests__/config-schema.test.ts +1248 -224
  45. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  46. package/src/__tests__/config-watcher.test.ts +43 -8
  47. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  48. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  49. package/src/__tests__/contacts-write.test.ts +197 -0
  50. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  51. package/src/__tests__/context-window-manager.test.ts +88 -0
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
  54. package/src/__tests__/conversation-agent-loop.test.ts +99 -3
  55. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  56. package/src/__tests__/conversation-attachments.test.ts +80 -4
  57. package/src/__tests__/conversation-confirmation-signals.test.ts +290 -0
  58. package/src/__tests__/conversation-error.test.ts +70 -0
  59. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  60. package/src/__tests__/conversation-history-web-search.test.ts +12 -4
  61. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  62. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  63. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  64. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  65. package/src/__tests__/conversation-list-source.test.ts +145 -0
  66. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  67. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  68. package/src/__tests__/conversation-queue.test.ts +946 -62
  69. package/src/__tests__/conversation-routes-disk-view.test.ts +275 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +324 -46
  73. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  74. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  75. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  76. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  77. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  78. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  79. package/src/__tests__/conversation-store.test.ts +195 -0
  80. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  81. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  82. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  83. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  84. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  85. package/src/__tests__/credential-health-service.test.ts +352 -0
  86. package/src/__tests__/credential-security-invariants.test.ts +6 -3
  87. package/src/__tests__/credential-vault-unit.test.ts +383 -7
  88. package/src/__tests__/credential-vault.test.ts +152 -13
  89. package/src/__tests__/credentials-cli.test.ts +42 -18
  90. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  91. package/src/__tests__/date-context.test.ts +4 -4
  92. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  93. package/src/__tests__/device-id.test.ts +112 -0
  94. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  95. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  96. package/src/__tests__/email-html-renderer.test.ts +71 -0
  97. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  98. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  99. package/src/__tests__/emit-event-signal.test.ts +71 -0
  100. package/src/__tests__/extension-id-sync-guard.test.ts +222 -0
  101. package/src/__tests__/fixtures/mock-chrome-extension.ts +386 -0
  102. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -0
  104. package/src/__tests__/gemini-provider.test.ts +66 -2
  105. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  106. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  107. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  108. package/src/__tests__/gmail-preferences.test.ts +117 -0
  109. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  110. package/src/__tests__/headless-browser-interactions.test.ts +738 -359
  111. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  112. package/src/__tests__/headless-browser-navigate.test.ts +528 -49
  113. package/src/__tests__/headless-browser-read-tools.test.ts +274 -100
  114. package/src/__tests__/headless-browser-snapshot.test.ts +250 -77
  115. package/src/__tests__/heartbeat-service.test.ts +70 -17
  116. package/src/__tests__/home-state-routes.test.ts +162 -0
  117. package/src/__tests__/host-bash-proxy.test.ts +145 -1
  118. package/src/__tests__/host-browser-e2e-cloud.test.ts +596 -0
  119. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  120. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  121. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  122. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  123. package/src/__tests__/host-browser-routes.test.ts +198 -0
  124. package/src/__tests__/host-browser-ws-events-e2e.test.ts +423 -0
  125. package/src/__tests__/host-cu-proxy.test.ts +166 -1
  126. package/src/__tests__/host-file-proxy.test.ts +185 -1
  127. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  128. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  129. package/src/__tests__/host-shell-tool.test.ts +1 -11
  130. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  131. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  132. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  133. package/src/__tests__/integration-status.test.ts +6 -7
  134. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  135. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  136. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  137. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  138. package/src/__tests__/llm-usage-store.test.ts +363 -0
  139. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  140. package/src/__tests__/mcp-health-check.test.ts +10 -3
  141. package/src/__tests__/media-stream-output.test.ts +555 -0
  142. package/src/__tests__/media-stream-parser.test.ts +374 -0
  143. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  144. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  145. package/src/__tests__/media-turn-detector.test.ts +440 -0
  146. package/src/__tests__/message-queue.test.ts +125 -0
  147. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  148. package/src/__tests__/migration-export-http.test.ts +67 -8
  149. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  150. package/src/__tests__/migration-import-commit-http.test.ts +109 -7
  151. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  152. package/src/__tests__/migration-validate-http.test.ts +3 -3
  153. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  154. package/src/__tests__/model-intents.test.ts +2 -2
  155. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  156. package/src/__tests__/oauth-apps-routes.test.ts +18 -12
  157. package/src/__tests__/oauth-cli.test.ts +709 -60
  158. package/src/__tests__/oauth-connect-orchestrator.test.ts +118 -24
  159. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  160. package/src/__tests__/oauth-provider-serializer.test.ts +147 -10
  161. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  162. package/src/__tests__/oauth-providers-routes.test.ts +52 -14
  163. package/src/__tests__/oauth-store.test.ts +1465 -176
  164. package/src/__tests__/oauth2-gateway-transport.test.ts +460 -26
  165. package/src/__tests__/onboarding-template-contract.test.ts +81 -70
  166. package/src/__tests__/openai-provider.test.ts +178 -2
  167. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  168. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  169. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  170. package/src/__tests__/outlook-categories.test.ts +1 -1
  171. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  172. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  173. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  174. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  175. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  176. package/src/__tests__/outlook-trash.test.ts +1 -1
  177. package/src/__tests__/outlook-unsubscribe.test.ts +32 -3
  178. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  179. package/src/__tests__/permission-mode.test.ts +28 -56
  180. package/src/__tests__/persona-resolver.test.ts +251 -0
  181. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  182. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  183. package/src/__tests__/platform.test.ts +92 -1
  184. package/src/__tests__/post-turn-tool-result-truncation.test.ts +343 -0
  185. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  186. package/src/__tests__/pricing.test.ts +174 -0
  187. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  188. package/src/__tests__/qdrant-manager.test.ts +29 -8
  189. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  190. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  191. package/src/__tests__/relay-server.test.ts +423 -5
  192. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  193. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  194. package/src/__tests__/schedule-routes.test.ts +162 -0
  195. package/src/__tests__/search-skills-unified.test.ts +118 -0
  196. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  197. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  198. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  199. package/src/__tests__/secure-keys.test.ts +107 -0
  200. package/src/__tests__/send-endpoint-busy.test.ts +8 -1
  201. package/src/__tests__/sequence-store.test.ts +1 -1
  202. package/src/__tests__/server-history-render.test.ts +49 -0
  203. package/src/__tests__/set-permission-mode.test.ts +13 -250
  204. package/src/__tests__/settings-routes.test.ts +201 -0
  205. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  206. package/src/__tests__/skills-file-content-endpoint.test.ts +801 -0
  207. package/src/__tests__/skills-files-catalog-fallback.test.ts +738 -0
  208. package/src/__tests__/skills.test.ts +5 -2
  209. package/src/__tests__/skillssh-files.test.ts +446 -0
  210. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  211. package/src/__tests__/slack-channel-config.test.ts +576 -16
  212. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  213. package/src/__tests__/stt-stream-session.test.ts +535 -0
  214. package/src/__tests__/subagent-detail.test.ts +44 -2
  215. package/src/__tests__/subagent-disposal.test.ts +1 -0
  216. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  217. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  218. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  219. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  220. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  221. package/src/__tests__/subagent-tools.test.ts +1 -0
  222. package/src/__tests__/subagent-types.test.ts +1 -0
  223. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  224. package/src/__tests__/system-prompt.test.ts +184 -27
  225. package/src/__tests__/task-scheduler.test.ts +32 -6
  226. package/src/__tests__/telegram-config.test.ts +10 -13
  227. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  228. package/src/__tests__/terminal-tools.test.ts +25 -5
  229. package/src/__tests__/test-preload.ts +18 -0
  230. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  231. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  232. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  233. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  234. package/src/__tests__/tool-executor.test.ts +33 -24
  235. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  236. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  237. package/src/__tests__/top-level-renderer.test.ts +73 -1
  238. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  239. package/src/__tests__/trust-store.test.ts +7 -1
  240. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  241. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  242. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  243. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  244. package/src/__tests__/twilio-routes.test.ts +376 -0
  245. package/src/__tests__/unicode.test.ts +293 -0
  246. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  247. package/src/__tests__/update-bulletin.test.ts +206 -5
  248. package/src/__tests__/usage-routes.test.ts +25 -4
  249. package/src/__tests__/user-reference.test.ts +46 -61
  250. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  251. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  252. package/src/__tests__/voice-config-update.test.ts +403 -0
  253. package/src/__tests__/voice-quality.test.ts +434 -19
  254. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  255. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  256. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  257. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  258. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  259. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  260. package/src/__tests__/workspace-policy.test.ts +2 -0
  261. package/src/acp/client-handler.ts +30 -4
  262. package/src/agent/image-optimize.ts +24 -12
  263. package/src/agent/loop.ts +55 -9
  264. package/src/approvals/guardian-request-resolvers.ts +21 -15
  265. package/src/backup/__tests__/backup-key.test.ts +152 -0
  266. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  267. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  268. package/src/backup/__tests__/local-writer.test.ts +218 -0
  269. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  270. package/src/backup/__tests__/paths.test.ts +300 -0
  271. package/src/backup/__tests__/restore.test.ts +498 -0
  272. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  273. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  274. package/src/backup/backup-key.ts +137 -0
  275. package/src/backup/backup-worker.ts +459 -0
  276. package/src/backup/list-snapshots.ts +147 -0
  277. package/src/backup/local-writer.ts +133 -0
  278. package/src/backup/offsite-writer.ts +222 -0
  279. package/src/backup/paths.ts +226 -0
  280. package/src/backup/restore.ts +322 -0
  281. package/src/backup/snapshot-lock.ts +431 -0
  282. package/src/backup/stream-crypt.ts +263 -0
  283. package/src/browser-session/__tests__/manager.test.ts +297 -0
  284. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  285. package/src/browser-session/backends/extension.ts +26 -0
  286. package/src/browser-session/backends/local.ts +24 -0
  287. package/src/browser-session/events.ts +164 -0
  288. package/src/browser-session/index.ts +27 -0
  289. package/src/browser-session/manager.ts +159 -0
  290. package/src/browser-session/types.ts +28 -0
  291. package/src/bundler/package-resolver.ts +4 -0
  292. package/src/calls/audio-store.ts +11 -5
  293. package/src/calls/call-controller.ts +226 -71
  294. package/src/calls/call-domain.ts +9 -0
  295. package/src/calls/call-speech-output.ts +190 -0
  296. package/src/calls/call-transport.ts +77 -0
  297. package/src/calls/media-stream-audio-transcode.ts +173 -0
  298. package/src/calls/media-stream-output.ts +660 -0
  299. package/src/calls/media-stream-parser.ts +300 -0
  300. package/src/calls/media-stream-protocol.ts +166 -0
  301. package/src/calls/media-stream-server.ts +592 -0
  302. package/src/calls/media-stream-stt-session.ts +460 -0
  303. package/src/calls/media-turn-detector.ts +230 -0
  304. package/src/calls/relay-server.ts +90 -75
  305. package/src/calls/resolve-call-tts-provider.ts +136 -0
  306. package/src/calls/telephony-stt-routing.ts +145 -0
  307. package/src/calls/tts-call-strategy.ts +161 -0
  308. package/src/calls/tts-text-sanitizer.ts +32 -16
  309. package/src/calls/twilio-routes.ts +281 -17
  310. package/src/calls/voice-quality.ts +78 -35
  311. package/src/calls/voice-session-bridge.ts +8 -1
  312. package/src/channels/__tests__/types.test.ts +134 -0
  313. package/src/channels/types.ts +69 -3
  314. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  315. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  316. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  317. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  318. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  319. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  320. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  321. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  322. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  323. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  324. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  325. package/src/cli/commands/backup.ts +993 -0
  326. package/src/cli/commands/conversations.ts +77 -0
  327. package/src/cli/commands/credentials.ts +3 -4
  328. package/src/cli/commands/domain.ts +210 -0
  329. package/src/cli/commands/email.ts +273 -16
  330. package/src/cli/commands/mcp.ts +16 -4
  331. package/src/cli/commands/oauth/__tests__/connect.test.ts +56 -44
  332. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  333. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  334. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  335. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +32 -33
  336. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +330 -0
  337. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +117 -12
  338. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  339. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  340. package/src/cli/commands/oauth/apps.ts +7 -4
  341. package/src/cli/commands/oauth/connect.ts +6 -3
  342. package/src/cli/commands/oauth/disconnect.ts +1 -1
  343. package/src/cli/commands/oauth/mode.ts +12 -3
  344. package/src/cli/commands/oauth/providers.ts +215 -36
  345. package/src/cli/commands/oauth/shared.ts +7 -6
  346. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +254 -0
  347. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  348. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  349. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  350. package/src/cli/commands/platform/index.ts +107 -10
  351. package/src/cli/commands/usage.ts +10 -9
  352. package/src/cli/lib/daemon-credential-client.ts +4 -0
  353. package/src/cli/program.ts +30 -4
  354. package/src/config/__tests__/backup-schema.test.ts +134 -0
  355. package/src/config/assistant-feature-flags.ts +61 -62
  356. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  357. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +141 -0
  358. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  359. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  360. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  361. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  362. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  363. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  364. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  365. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  366. package/src/config/bundled-skills/contacts/SKILL.md +5 -2
  367. package/src/config/bundled-skills/document/SKILL.md +4 -0
  368. package/src/config/bundled-skills/gmail/SKILL.md +54 -8
  369. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  370. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  371. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  372. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  373. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  374. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  375. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  376. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  377. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  378. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  379. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  380. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  381. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  382. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  383. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  384. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  385. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  386. package/src/config/bundled-skills/outlook/SKILL.md +9 -2
  387. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  388. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  389. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  390. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  391. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  392. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  393. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  394. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  395. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  396. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  397. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  398. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  399. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  400. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  401. package/src/config/bundled-tool-registry.ts +8 -0
  402. package/src/config/env-registry.ts +38 -0
  403. package/src/config/env.ts +49 -4
  404. package/src/config/feature-flag-registry.json +85 -14
  405. package/src/config/loader.ts +82 -13
  406. package/src/config/sanitize-for-transfer.ts +47 -0
  407. package/src/config/schema.ts +81 -15
  408. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  409. package/src/config/schemas/analysis.ts +51 -0
  410. package/src/config/schemas/backup.ts +72 -0
  411. package/src/config/schemas/calls.ts +1 -26
  412. package/src/config/schemas/elevenlabs.ts +0 -59
  413. package/src/config/schemas/filing.ts +47 -7
  414. package/src/config/schemas/heartbeat.ts +27 -5
  415. package/src/config/schemas/host-browser.ts +112 -0
  416. package/src/config/schemas/inference.ts +1 -1
  417. package/src/config/schemas/memory-lifecycle.ts +14 -2
  418. package/src/config/schemas/memory-retrieval.ts +103 -0
  419. package/src/config/schemas/security.ts +0 -6
  420. package/src/config/schemas/services.ts +52 -0
  421. package/src/config/schemas/stt.ts +59 -0
  422. package/src/config/schemas/tts.ts +230 -0
  423. package/src/config/schemas/updates.ts +14 -0
  424. package/src/config/skills.ts +4 -0
  425. package/src/config/types.ts +4 -1
  426. package/src/contacts/contact-store.ts +56 -11
  427. package/src/contacts/contacts-write.ts +38 -1
  428. package/src/context/post-turn-tool-result-truncation.ts +177 -0
  429. package/src/context/tool-result-truncation.ts +2 -1
  430. package/src/context/window-manager.ts +61 -10
  431. package/src/credential-execution/approval-bridge.ts +49 -15
  432. package/src/credential-execution/executable-discovery.ts +12 -2
  433. package/src/credential-execution/process-manager.ts +33 -2
  434. package/src/credential-health/credential-health-service.ts +366 -0
  435. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  436. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  437. package/src/daemon/__tests__/conversation-tool-setup.test.ts +195 -0
  438. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  439. package/src/daemon/app-source-watcher.ts +35 -0
  440. package/src/daemon/config-watcher.ts +99 -5
  441. package/src/daemon/context-overflow-approval.ts +5 -0
  442. package/src/daemon/conversation-agent-loop-handlers.ts +23 -2
  443. package/src/daemon/conversation-agent-loop.ts +153 -42
  444. package/src/daemon/conversation-attachments.ts +40 -0
  445. package/src/daemon/conversation-error.ts +11 -0
  446. package/src/daemon/conversation-history.ts +40 -6
  447. package/src/daemon/conversation-launch.ts +220 -0
  448. package/src/daemon/conversation-lifecycle.ts +59 -9
  449. package/src/daemon/conversation-messaging.ts +37 -3
  450. package/src/daemon/conversation-notifiers.ts +5 -0
  451. package/src/daemon/conversation-process.ts +622 -13
  452. package/src/daemon/conversation-queue-manager.ts +24 -0
  453. package/src/daemon/conversation-runtime-assembly.ts +128 -36
  454. package/src/daemon/conversation-slash.ts +36 -0
  455. package/src/daemon/conversation-surfaces.ts +131 -40
  456. package/src/daemon/conversation-tool-setup.ts +99 -8
  457. package/src/daemon/conversation-usage.ts +7 -4
  458. package/src/daemon/conversation-workspace.ts +12 -0
  459. package/src/daemon/conversation.ts +292 -16
  460. package/src/daemon/date-context.ts +10 -10
  461. package/src/daemon/first-greeting.ts +3 -2
  462. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  463. package/src/daemon/handlers/conversations.ts +13 -141
  464. package/src/daemon/handlers/shared.ts +80 -0
  465. package/src/daemon/handlers/skills.ts +483 -44
  466. package/src/daemon/host-bash-proxy.ts +48 -13
  467. package/src/daemon/host-browser-proxy.ts +192 -0
  468. package/src/daemon/host-cu-proxy.ts +36 -11
  469. package/src/daemon/host-file-proxy.ts +57 -9
  470. package/src/daemon/lifecycle.ts +179 -28
  471. package/src/daemon/message-protocol.ts +13 -0
  472. package/src/daemon/message-types/conversations.ts +89 -14
  473. package/src/daemon/message-types/home.ts +40 -0
  474. package/src/daemon/message-types/host-browser.ts +100 -0
  475. package/src/daemon/message-types/meet.ts +143 -0
  476. package/src/daemon/message-types/messages.ts +19 -5
  477. package/src/daemon/message-types/schedules.ts +34 -2
  478. package/src/daemon/message-types/skills.ts +26 -0
  479. package/src/daemon/message-types/subagents.ts +2 -0
  480. package/src/daemon/message-types/surfaces.ts +2 -0
  481. package/src/daemon/server.ts +439 -14
  482. package/src/daemon/shutdown-handlers.ts +32 -4
  483. package/src/daemon/shutdown-registry.ts +40 -0
  484. package/src/daemon/tool-side-effects.ts +15 -0
  485. package/src/daemon/transport-hints.ts +5 -24
  486. package/src/email/html-renderer.ts +76 -0
  487. package/src/heartbeat/heartbeat-service.ts +93 -7
  488. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  489. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  490. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  491. package/src/home/__tests__/feed-types.test.ts +275 -0
  492. package/src/home/__tests__/feed-writer.test.ts +688 -0
  493. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  494. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  495. package/src/home/__tests__/progress-formula.test.ts +213 -0
  496. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  497. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  498. package/src/home/assistant-feed-authoring.ts +124 -0
  499. package/src/home/emit-feed-event.ts +158 -0
  500. package/src/home/feed-scheduler.ts +247 -0
  501. package/src/home/feed-types.ts +181 -0
  502. package/src/home/feed-writer.ts +469 -0
  503. package/src/home/platform-gmail-digest.ts +163 -0
  504. package/src/home/progress-formula.ts +86 -0
  505. package/src/home/relationship-state-writer.ts +824 -0
  506. package/src/home/relationship-state.ts +143 -0
  507. package/src/home/rollup-producer.ts +384 -0
  508. package/src/hooks/runner.ts +7 -0
  509. package/src/inbound/platform-callback-registration.ts +30 -20
  510. package/src/inbound/public-ingress-urls.ts +12 -0
  511. package/src/instrument.ts +1 -1
  512. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  513. package/src/ipc/cli-client.ts +151 -0
  514. package/src/ipc/cli-server.ts +234 -0
  515. package/src/ipc/gateway-client.ts +180 -0
  516. package/src/ipc/routes/index.ts +5 -0
  517. package/src/ipc/routes/wake-conversation.ts +19 -0
  518. package/src/mcp/client.ts +59 -24
  519. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  520. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  521. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  522. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  523. package/src/memory/app-store.ts +31 -1
  524. package/src/memory/attachments-store.ts +70 -0
  525. package/src/memory/auto-analysis-enqueue.ts +127 -0
  526. package/src/memory/auto-analysis-guard.ts +27 -0
  527. package/src/memory/cleanup-schedule-state.ts +37 -0
  528. package/src/memory/conversation-analyze-job.ts +73 -0
  529. package/src/memory/conversation-crud.ts +122 -0
  530. package/src/memory/conversation-disk-view.ts +7 -0
  531. package/src/memory/conversation-group-migration.ts +34 -2
  532. package/src/memory/conversation-queries.ts +6 -5
  533. package/src/memory/conversation-starters-cadence.ts +76 -0
  534. package/src/memory/conversation-title-service.ts +5 -2
  535. package/src/memory/db-init.ts +18 -0
  536. package/src/memory/db-maintenance.ts +108 -0
  537. package/src/memory/db.ts +1 -0
  538. package/src/memory/embedding-backend.test.ts +75 -0
  539. package/src/memory/embedding-backend.ts +131 -5
  540. package/src/memory/embedding-gemini.test.ts +54 -0
  541. package/src/memory/embedding-gemini.ts +20 -9
  542. package/src/memory/embedding-local.ts +176 -17
  543. package/src/memory/graph/consolidation.ts +10 -23
  544. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  545. package/src/memory/graph/extraction-job.ts +15 -0
  546. package/src/memory/graph/extraction.test.ts +23 -0
  547. package/src/memory/graph/extraction.ts +8 -0
  548. package/src/memory/graph/retriever.ts +67 -40
  549. package/src/memory/graph/scoring.test.ts +186 -0
  550. package/src/memory/graph/scoring.ts +31 -1
  551. package/src/memory/graph/store.test.ts +7 -3
  552. package/src/memory/graph/store.ts +47 -12
  553. package/src/memory/graph/tools.ts +1 -1
  554. package/src/memory/group-crud.ts +6 -1
  555. package/src/memory/indexer.ts +95 -16
  556. package/src/memory/job-handlers/cleanup.ts +11 -8
  557. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  558. package/src/memory/jobs-store.ts +64 -4
  559. package/src/memory/jobs-worker.ts +22 -9
  560. package/src/memory/llm-usage-store.ts +137 -60
  561. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  562. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  563. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  564. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  565. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  566. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  567. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  568. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  569. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  570. package/src/memory/migrations/index.ts +12 -0
  571. package/src/memory/migrations/registry.ts +16 -0
  572. package/src/memory/qdrant-manager.ts +43 -16
  573. package/src/memory/schema/conversations.ts +3 -0
  574. package/src/memory/schema/oauth.ts +21 -13
  575. package/src/memory/usage-buckets.ts +396 -0
  576. package/src/messaging/providers/gmail/client.ts +57 -6
  577. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  578. package/src/messaging/providers/slack/adapter.ts +143 -38
  579. package/src/messaging/providers/slack/client.ts +16 -0
  580. package/src/messaging/providers/slack/types.ts +4 -0
  581. package/src/notifications/decision-engine.ts +3 -3
  582. package/src/notifications/signal.ts +5 -0
  583. package/src/oauth/AGENTS.md +76 -0
  584. package/src/oauth/__tests__/identity-verifier.test.ts +25 -19
  585. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  586. package/src/oauth/byo-connection.test.ts +26 -9
  587. package/src/oauth/byo-connection.ts +10 -8
  588. package/src/oauth/connect-orchestrator.ts +25 -21
  589. package/src/oauth/connect-types.ts +3 -3
  590. package/src/oauth/connection-resolver.test.ts +17 -4
  591. package/src/oauth/connection-resolver.ts +22 -18
  592. package/src/oauth/connection.ts +3 -1
  593. package/src/oauth/manual-token-connection.ts +13 -13
  594. package/src/oauth/oauth-store.ts +223 -100
  595. package/src/oauth/platform-connection.test.ts +101 -3
  596. package/src/oauth/platform-connection.ts +56 -35
  597. package/src/oauth/provider-serializer.ts +31 -5
  598. package/src/oauth/revoke.ts +76 -0
  599. package/src/oauth/seed-providers.ts +133 -87
  600. package/src/oauth/token-persistence.ts +1 -1
  601. package/src/permissions/checker.ts +16 -6
  602. package/src/permissions/defaults.ts +49 -1
  603. package/src/permissions/permission-mode.ts +4 -11
  604. package/src/permissions/prompter.ts +13 -1
  605. package/src/permissions/trust-store.ts +3 -3
  606. package/src/permissions/v2-consent-policy.ts +87 -0
  607. package/src/permissions/workspace-policy.ts +3 -0
  608. package/src/platform/client.test.ts +10 -0
  609. package/src/platform/sync-identity.ts +129 -0
  610. package/src/prompts/persona-resolver.ts +126 -2
  611. package/src/prompts/system-prompt.ts +76 -38
  612. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  613. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  614. package/src/prompts/templates/SOUL.md +3 -1
  615. package/src/prompts/templates/UPDATES.md +12 -0
  616. package/src/prompts/templates/channels/slack.md +20 -0
  617. package/src/prompts/update-bulletin-format.ts +26 -9
  618. package/src/prompts/update-bulletin.ts +34 -23
  619. package/src/prompts/user-reference.ts +20 -17
  620. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  621. package/src/providers/anthropic/client.ts +157 -60
  622. package/src/providers/fireworks/client.ts +2 -2
  623. package/src/providers/gemini/client.ts +9 -1
  624. package/src/providers/model-catalog.ts +6 -0
  625. package/src/providers/model-intents.ts +4 -4
  626. package/src/providers/ollama/client.ts +2 -2
  627. package/src/providers/openai/chat-completions-provider.ts +474 -0
  628. package/src/providers/openai/client.ts +25 -440
  629. package/src/providers/openai/responses-provider.ts +502 -0
  630. package/src/providers/openrouter/client.ts +101 -4
  631. package/src/providers/provider-secret-catalog.ts +139 -0
  632. package/src/providers/registry.ts +2 -2
  633. package/src/providers/retry.ts +14 -3
  634. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  635. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  636. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  637. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  638. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  639. package/src/providers/speech-to-text/deepgram.ts +115 -0
  640. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  641. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  642. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  643. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  644. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  645. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  646. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  647. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  648. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  649. package/src/providers/speech-to-text/resolve.ts +386 -6
  650. package/src/providers/types.ts +10 -1
  651. package/src/runtime/AGENTS.md +65 -0
  652. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  653. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  654. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  655. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  656. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  657. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  658. package/src/runtime/agent-wake.ts +512 -0
  659. package/src/runtime/assistant-event-hub.ts +2 -2
  660. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  661. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  662. package/src/runtime/auth/__tests__/route-policy.test.ts +48 -0
  663. package/src/runtime/auth/middleware.ts +98 -0
  664. package/src/runtime/auth/route-policy.ts +33 -9
  665. package/src/runtime/auth/token-service.ts +56 -1
  666. package/src/runtime/btw-sidechain.ts +2 -0
  667. package/src/runtime/capability-tokens.ts +414 -0
  668. package/src/runtime/channel-approvals.ts +18 -5
  669. package/src/runtime/channel-invite-transport.ts +1 -1
  670. package/src/runtime/channel-invite-transports/email.ts +14 -6
  671. package/src/runtime/channel-readiness-service.ts +12 -22
  672. package/src/runtime/chrome-extension-registry.ts +368 -0
  673. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  674. package/src/runtime/guardian-decision-types.ts +7 -0
  675. package/src/runtime/http-server.ts +815 -75
  676. package/src/runtime/http-types.ts +6 -2
  677. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  678. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  679. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +198 -0
  680. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  681. package/src/runtime/migrations/migration-transport.ts +7 -0
  682. package/src/runtime/migrations/migration-wizard.ts +23 -2
  683. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  684. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  685. package/src/runtime/migrations/vbundle-import-analyzer.ts +96 -1
  686. package/src/runtime/migrations/vbundle-importer.ts +89 -5
  687. package/src/runtime/pending-interactions.ts +18 -13
  688. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  689. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  690. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  691. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  692. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  693. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  694. package/src/runtime/routes/app-management-routes.ts +12 -18
  695. package/src/runtime/routes/approval-routes.ts +90 -16
  696. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  697. package/src/runtime/routes/attachment-routes.ts +216 -17
  698. package/src/runtime/routes/backup-routes.ts +519 -0
  699. package/src/runtime/routes/browser-extension-pair-routes.ts +556 -0
  700. package/src/runtime/routes/btw-routes.ts +8 -6
  701. package/src/runtime/routes/contact-routes.test.ts +298 -0
  702. package/src/runtime/routes/contact-routes.ts +132 -5
  703. package/src/runtime/routes/conversation-analysis-routes.ts +22 -141
  704. package/src/runtime/routes/conversation-management-routes.ts +223 -0
  705. package/src/runtime/routes/conversation-routes.ts +598 -103
  706. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  707. package/src/runtime/routes/filing-routes.ts +93 -0
  708. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  709. package/src/runtime/routes/home-feed-routes.ts +334 -0
  710. package/src/runtime/routes/home-state-routes.ts +138 -0
  711. package/src/runtime/routes/host-browser-routes.ts +268 -0
  712. package/src/runtime/routes/host-file-routes.ts +9 -1
  713. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  714. package/src/runtime/routes/identity-routes.ts +262 -33
  715. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  716. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  717. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  718. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  719. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  720. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  721. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  722. package/src/runtime/routes/log-export-routes.ts +42 -22
  723. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  724. package/src/runtime/routes/memory-item-routes.ts +1 -7
  725. package/src/runtime/routes/migration-routes.ts +122 -2
  726. package/src/runtime/routes/oauth-apps.ts +15 -17
  727. package/src/runtime/routes/oauth-providers.ts +4 -0
  728. package/src/runtime/routes/schedule-routes.ts +24 -11
  729. package/src/runtime/routes/settings-routes.ts +31 -102
  730. package/src/runtime/routes/skills-routes.ts +128 -9
  731. package/src/runtime/routes/stt-routes.ts +233 -0
  732. package/src/runtime/routes/subagents-routes.ts +14 -10
  733. package/src/runtime/routes/surface-action-routes.ts +41 -2
  734. package/src/runtime/routes/tts-routes.ts +108 -24
  735. package/src/runtime/routes/usage-routes.ts +38 -9
  736. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  737. package/src/runtime/routes/user-routes.ts +13 -1
  738. package/src/runtime/routes/work-items-routes.ts +8 -1
  739. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  740. package/src/runtime/routes/workspace-routes.ts +8 -1
  741. package/src/runtime/routes/workspace-utils.ts +2 -0
  742. package/src/runtime/runtime-mode.ts +33 -0
  743. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  744. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  745. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  746. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  747. package/src/runtime/services/analyze-conversation.ts +344 -0
  748. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  749. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  750. package/src/runtime/skill-route-registry.ts +49 -0
  751. package/src/runtime/slack-block-formatting.ts +437 -10
  752. package/src/schedule/scheduler.ts +57 -5
  753. package/src/security/ces-credential-client.ts +20 -0
  754. package/src/security/ces-rpc-credential-backend.ts +17 -0
  755. package/src/security/credential-backend.ts +5 -0
  756. package/src/security/oauth2.ts +68 -29
  757. package/src/security/secure-keys.ts +143 -27
  758. package/src/security/token-manager.ts +31 -10
  759. package/src/sequence/engine.ts +23 -0
  760. package/src/sequence/types.ts +1 -1
  761. package/src/skills/catalog-files.ts +554 -0
  762. package/src/skills/category-inference.ts +122 -0
  763. package/src/skills/clawhub-files.ts +213 -0
  764. package/src/skills/clawhub.ts +84 -23
  765. package/src/skills/skill-file-provider.ts +40 -0
  766. package/src/skills/skillssh-files.ts +395 -0
  767. package/src/skills/skillssh-registry.ts +4 -4
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  769. package/src/stt/__tests__/types.test.ts +89 -0
  770. package/src/stt/daemon-batch-transcriber.ts +195 -0
  771. package/src/stt/stt-stream-session.ts +499 -0
  772. package/src/stt/types.ts +330 -0
  773. package/src/stt/wav-encoder.test.ts +373 -0
  774. package/src/stt/wav-encoder.ts +175 -0
  775. package/src/subagent/manager.ts +169 -40
  776. package/src/subagent/types.ts +19 -0
  777. package/src/tools/apps/executors.ts +11 -2
  778. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  779. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  780. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  781. package/src/tools/browser/auth-detector.ts +43 -12
  782. package/src/tools/browser/browser-execution.ts +1787 -342
  783. package/src/tools/browser/browser-manager.ts +81 -12
  784. package/src/tools/browser/browser-mode-constants.ts +12 -0
  785. package/src/tools/browser/browser-mode.ts +92 -0
  786. package/src/tools/browser/browser-status-constants.ts +33 -0
  787. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  788. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  789. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +1263 -0
  790. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +359 -0
  791. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1993 -0
  792. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  793. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  794. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  795. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  796. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  797. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  798. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +1007 -0
  799. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  800. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +744 -0
  801. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  802. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +868 -0
  803. package/src/tools/browser/cdp-client/errors.ts +49 -0
  804. package/src/tools/browser/cdp-client/extension-cdp-client.ts +148 -0
  805. package/src/tools/browser/cdp-client/factory.ts +914 -0
  806. package/src/tools/browser/cdp-client/index.ts +28 -0
  807. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  808. package/src/tools/browser/cdp-client/types.ts +120 -0
  809. package/src/tools/credentials/vault.ts +35 -6
  810. package/src/tools/filesystem/edit.ts +1 -1
  811. package/src/tools/filesystem/list.ts +1 -1
  812. package/src/tools/filesystem/read.ts +1 -1
  813. package/src/tools/filesystem/write.ts +2 -1
  814. package/src/tools/host-filesystem/edit.ts +1 -1
  815. package/src/tools/host-filesystem/read.ts +12 -15
  816. package/src/tools/host-filesystem/write.ts +1 -1
  817. package/src/tools/host-terminal/host-shell.ts +21 -16
  818. package/src/tools/network/web-fetch.ts +5 -2
  819. package/src/tools/network/web-search.ts +5 -2
  820. package/src/tools/permission-checker.ts +77 -82
  821. package/src/tools/registry.ts +0 -2
  822. package/src/tools/secret-detection-handler.ts +34 -0
  823. package/src/tools/shared/filesystem/image-read.ts +61 -40
  824. package/src/tools/shared/shell-output.ts +3 -1
  825. package/src/tools/side-effects.ts +2 -0
  826. package/src/tools/skills/sandbox-runner.ts +3 -2
  827. package/src/tools/subagent/spawn.ts +47 -3
  828. package/src/tools/subagent/status.ts +2 -0
  829. package/src/tools/system/register.ts +2 -16
  830. package/src/tools/terminal/safe-env.ts +15 -0
  831. package/src/tools/terminal/shell.ts +36 -20
  832. package/src/tools/tool-approval-handler.ts +48 -2
  833. package/src/tools/tool-manifest.ts +21 -0
  834. package/src/tools/types.ts +19 -0
  835. package/src/tools/ui-surface/definitions.ts +6 -1
  836. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  837. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  838. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  839. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  840. package/src/tts/provider-catalog.ts +201 -0
  841. package/src/tts/provider-registry.ts +73 -0
  842. package/src/tts/providers/deepgram-provider.ts +219 -0
  843. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  844. package/src/tts/providers/fish-audio-provider.ts +183 -0
  845. package/src/tts/providers/index.ts +42 -0
  846. package/src/tts/providers/register-builtins.ts +130 -0
  847. package/src/tts/synthesize-text.ts +110 -0
  848. package/src/tts/tts-config-resolver.ts +78 -0
  849. package/src/tts/types.ts +153 -0
  850. package/src/types/onboarding-context.ts +7 -0
  851. package/src/util/abort-reasons.ts +58 -0
  852. package/src/util/device-id.ts +32 -16
  853. package/src/util/errors.ts +9 -1
  854. package/src/util/platform.ts +63 -24
  855. package/src/util/pricing.ts +66 -3
  856. package/src/util/spawn.ts +1 -1
  857. package/src/util/truncate.ts +4 -2
  858. package/src/util/unicode.ts +201 -0
  859. package/src/version.ts +19 -24
  860. package/src/watcher/engine.ts +23 -0
  861. package/src/watcher/watcher-store.ts +31 -0
  862. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  863. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  864. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  865. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  866. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  867. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  868. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  869. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  870. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  871. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  872. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  873. package/src/workspace/migrations/registry.ts +16 -0
  874. package/src/workspace/top-level-renderer.ts +31 -1
  875. package/src/workspace/turn-commit.ts +31 -0
  876. package/src/__tests__/chrome-cdp.test.ts +0 -419
  877. package/src/__tests__/email-cli.test.ts +0 -297
  878. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  879. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  880. package/src/__tests__/permission-mode-store.test.ts +0 -277
  881. package/src/browser-extension-relay/protocol.ts +0 -63
  882. package/src/browser-extension-relay/server.ts +0 -203
  883. package/src/cli/commands/browser-relay.ts +0 -536
  884. package/src/config/schemas/sandbox.ts +0 -14
  885. package/src/email/guardrails.ts +0 -221
  886. package/src/email/provider.ts +0 -117
  887. package/src/email/providers/agentmail.ts +0 -361
  888. package/src/email/providers/index.ts +0 -65
  889. package/src/email/service.ts +0 -384
  890. package/src/email/types.ts +0 -126
  891. package/src/permissions/permission-mode-store.ts +0 -180
  892. package/src/prompts/templates/USER.md +0 -13
  893. package/src/providers/speech-to-text/types.ts +0 -17
  894. package/src/tools/browser/chrome-cdp.ts +0 -239
  895. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Route handlers for conversation messages and suggestions.
3
3
  */
4
- import { existsSync, readdirSync, statSync } from "node:fs";
4
+ import { existsSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
5
  import { join, relative } from "node:path";
6
6
 
7
7
  import { z } from "zod";
@@ -12,8 +12,10 @@ import {
12
12
  createUserMessage,
13
13
  } from "../../agent/message-types.js";
14
14
  import {
15
+ canServiceRegistryBrowser,
15
16
  CHANNEL_IDS,
16
17
  INTERFACE_IDS,
18
+ type InterfaceId,
17
19
  isInteractiveInterface,
18
20
  parseChannelId,
19
21
  parseInterfaceId,
@@ -21,6 +23,7 @@ import {
21
23
  } from "../../channels/types.js";
22
24
  import { isHttpAuthDisabled } from "../../config/env.js";
23
25
  import { getConfig } from "../../config/loader.js";
26
+ import type { Conversation } from "../../daemon/conversation.js";
24
27
  import {
25
28
  buildModelInfoEvent,
26
29
  formatCompactResult,
@@ -36,14 +39,19 @@ import {
36
39
  } from "../../daemon/first-greeting.js";
37
40
  import { renderHistoryContent } from "../../daemon/handlers/shared.js";
38
41
  import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
42
+ import { HostBrowserProxy } from "../../daemon/host-browser-proxy.js";
39
43
  import { HostCuProxy } from "../../daemon/host-cu-proxy.js";
40
44
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
41
45
  import type { ServerMessage } from "../../daemon/message-protocol.js";
42
46
  import type {
43
- MacosTransportMetadata,
44
- NonMacosTransportMetadata,
47
+ HostProxyTransportMetadata,
48
+ NonHostProxyTransportMetadata,
45
49
  } from "../../daemon/message-types/conversations.js";
46
50
  import type { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
51
+ import {
52
+ writeOnboardingSidecar,
53
+ writeRelationshipState,
54
+ } from "../../home/relationship-state-writer.js";
47
55
  import * as attachmentsStore from "../../memory/attachments-store.js";
48
56
  import {
49
57
  createCanonicalGuardianRequest,
@@ -73,10 +81,12 @@ import { checkIngressForSecrets } from "../../security/secret-ingress.js";
73
81
  import { redactSecrets } from "../../security/secret-scanner.js";
74
82
  import { summarizeToolInput } from "../../tools/tool-input-summary.js";
75
83
  import { getLogger } from "../../util/logger.js";
84
+ import { getWorkspacePromptPath } from "../../util/platform.js";
76
85
  import { silentlyWithLog } from "../../util/silently.js";
77
86
  import { buildAssistantEvent } from "../assistant-event.js";
78
87
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
79
88
  import type { AuthContext } from "../auth/types.js";
89
+ import { getChromeExtensionRegistry } from "../chrome-extension-registry.js";
80
90
  import { bridgeConfirmationRequestToGuardian } from "../confirmation-request-guardian-bridge.js";
81
91
  import { routeGuardianReply } from "../guardian-reply-router.js";
82
92
  import { healGuardianBindingDrift } from "../guardian-vellum-migration.js";
@@ -105,7 +115,7 @@ const SUGGESTION_CACHE_MAX = 100;
105
115
  function collectCanonicalGuardianRequestHintIds(
106
116
  conversationId: string,
107
117
  sourceChannel: string,
108
- conversation: import("../../daemon/conversation.js").Conversation,
118
+ conversation: Conversation,
109
119
  ): string[] {
110
120
  const requests = listPendingRequestsByConversationScope(
111
121
  conversationId,
@@ -170,7 +180,7 @@ async function tryConsumeCanonicalGuardianReply(params: {
170
180
  data: string;
171
181
  filePath?: string;
172
182
  }>;
173
- conversation: import("../../daemon/conversation.js").Conversation;
183
+ conversation: Conversation;
174
184
  onEvent: (msg: ServerMessage) => void;
175
185
  approvalConversationGenerator?: ApprovalConversationGenerator;
176
186
  /** Verified actor identity from actor-token middleware. */
@@ -422,8 +432,18 @@ export function handleListMessages(
422
432
  // pendingToolUses map — otherwise they render as "Unknown" tool calls.
423
433
  const mergedMessages = mergeToolResultsIntoAssistantMessages(rawMessages);
424
434
 
435
+ // During streaming, all assistant turns within one agent loop accumulate
436
+ // on a single client-side ChatMessage (via currentAssistantMessageId).
437
+ // In the DB, each API turn is a separate assistant row because
438
+ // consolidation is deferred to compaction for prefix-cache stability.
439
+ // Merge consecutive assistant messages here at query time so
440
+ // renderHistoryContent produces the same contentOrder shape as streaming
441
+ // (consecutive tool refs grouped together).
442
+ const { messages: consolidatedMessages, mergedIdMap } =
443
+ mergeConsecutiveAssistantMessages(mergedMessages);
444
+
425
445
  // Parse content blocks and extract text + tool calls
426
- const parsed = mergedMessages.map((msg) => {
446
+ const parsed = consolidatedMessages.map((msg) => {
427
447
  let content: unknown;
428
448
  try {
429
449
  content = JSON.parse(msg.content);
@@ -548,7 +568,13 @@ export function handleListMessages(
548
568
  // blobs for non-image attachments (documents, audio). Then
549
569
  // selectively fetch full data only for images so the client can
550
570
  // generate thumbnails for inline display on history restore.
551
- const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id);
571
+ // Also query attachments for any messages that were merged into
572
+ // this one (consecutive assistant merge), so their attachments
573
+ // aren't lost before DB compaction relinks them.
574
+ const idsToQuery = [m.id, ...(mergedIdMap.get(m.id) ?? [])];
575
+ const linked = idsToQuery.flatMap((id) =>
576
+ attachmentsStore.getAttachmentMetadataForMessage(id),
577
+ );
552
578
  if (linked.length > 0) {
553
579
  msgAttachments = linked.map((a) => {
554
580
  if (a.mimeType.startsWith("image/")) {
@@ -793,19 +819,163 @@ function mergeToolResultsIntoAssistantMessages(
793
819
  return result;
794
820
  }
795
821
 
822
+ // ── Consecutive assistant message merging ────────────────────────────
823
+
824
+ /** Parse a message's JSON content into an array of content blocks. */
825
+ function parseContentBlocks(content: string): unknown[] {
826
+ try {
827
+ const parsed = JSON.parse(content);
828
+ return Array.isArray(parsed) ? parsed : [parsed];
829
+ } catch (err) {
830
+ log.warn(
831
+ { err },
832
+ "Failed to parse content blocks during assistant message merge",
833
+ );
834
+ return [];
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Append content blocks from a donor message onto a target block array.
840
+ * Parses the donor's JSON content and pushes each block into `target`.
841
+ */
842
+ function appendContentBlocks(target: unknown[], donorContent: string): void {
843
+ try {
844
+ const parsed = JSON.parse(donorContent);
845
+ if (Array.isArray(parsed)) {
846
+ target.push(...parsed);
847
+ } else {
848
+ target.push(parsed);
849
+ }
850
+ } catch (err) {
851
+ log.warn(
852
+ { err },
853
+ "Failed to parse donor content blocks during assistant message merge",
854
+ );
855
+ }
856
+ }
857
+
858
+ /**
859
+ * Promote metadata fields from a donor message to the surviving message
860
+ * when the survivor lacks them. Currently promotes `subagentNotification`.
861
+ * Returns a new MessageRow if promotion occurred, otherwise the original.
862
+ */
863
+ function promoteMetadata(survivor: MessageRow, donor: MessageRow): MessageRow {
864
+ if (donor.metadata && survivor.metadata) {
865
+ try {
866
+ const survivorMeta = JSON.parse(survivor.metadata);
867
+ const donorMeta = JSON.parse(donor.metadata);
868
+ if (
869
+ !survivorMeta.subagentNotification &&
870
+ donorMeta.subagentNotification
871
+ ) {
872
+ survivorMeta.subagentNotification = donorMeta.subagentNotification;
873
+ return { ...survivor, metadata: JSON.stringify(survivorMeta) };
874
+ }
875
+ } catch (err) {
876
+ log.warn(
877
+ { err },
878
+ "Failed to parse metadata during assistant message merge",
879
+ );
880
+ }
881
+ } else if (donor.metadata && !survivor.metadata) {
882
+ return { ...survivor, metadata: donor.metadata };
883
+ }
884
+ return survivor;
885
+ }
886
+
887
+ /**
888
+ * Merge consecutive assistant messages into a single message at query time.
889
+ *
890
+ * During streaming, all assistant turns within one agent loop accumulate on
891
+ * a single client-side ChatMessage. In the DB, each API turn is stored as a
892
+ * separate assistant row (consolidation is deferred to compaction for
893
+ * prefix-cache stability). This produces N separate assistant messages that
894
+ * the client renders as N individual bubbles — each showing "Completed 1
895
+ * step" instead of one grouped "Completed N steps" accordion.
896
+ *
897
+ * This function concatenates the content block arrays of consecutive
898
+ * assistant messages (no intervening user messages after tool-result
899
+ * merging) into the first message of each run. The merged messages are
900
+ * removed from the output. This is query-time only — the DB is not
901
+ * modified.
902
+ *
903
+ * The first message in each run keeps its id, createdAt, and metadata so
904
+ * that attachment lookups, display timestamps, and subagent notifications
905
+ * continue to work. Metadata from later messages in the run (e.g.
906
+ * subagentNotification) is preserved by promoting it to the surviving
907
+ * message when the surviving message has no metadata of its own for that
908
+ * field.
909
+ */
910
+ function mergeConsecutiveAssistantMessages(messages: MessageRow[]): {
911
+ messages: MessageRow[];
912
+ /** Maps each surviving message ID → all original message IDs merged into it. */
913
+ mergedIdMap: Map<string, string[]>;
914
+ } {
915
+ const result: MessageRow[] = [];
916
+ // Key = index in `result`, value = accumulated content blocks.
917
+ const pendingMerges = new Map<number, unknown[]>();
918
+ // Key = index in `result`, value = IDs of messages merged into the target.
919
+ const mergedIds = new Map<number, string[]>();
920
+
921
+ for (const msg of messages) {
922
+ const lastIdx = result.length - 1;
923
+ const isConsecutiveAssistant =
924
+ msg.role === "assistant" &&
925
+ lastIdx >= 0 &&
926
+ result[lastIdx].role === "assistant";
927
+
928
+ if (!isConsecutiveAssistant) {
929
+ result.push(msg);
930
+ continue;
931
+ }
932
+
933
+ // Track the donor message ID.
934
+ let ids = mergedIds.get(lastIdx);
935
+ if (!ids) {
936
+ ids = [];
937
+ mergedIds.set(lastIdx, ids);
938
+ }
939
+ ids.push(msg.id);
940
+
941
+ // Lazily parse the target's content on first merge.
942
+ let targetContent = pendingMerges.get(lastIdx);
943
+ if (!targetContent) {
944
+ targetContent = parseContentBlocks(result[lastIdx].content);
945
+ pendingMerges.set(lastIdx, targetContent);
946
+ }
947
+
948
+ appendContentBlocks(targetContent, msg.content);
949
+ result[lastIdx] = promoteMetadata(result[lastIdx], msg);
950
+ }
951
+
952
+ // Write back merged content for any messages that were targets.
953
+ for (const [idx, content] of pendingMerges) {
954
+ result[idx] = { ...result[idx], content: JSON.stringify(content) };
955
+ }
956
+
957
+ // Build the merged ID map keyed by surviving message ID.
958
+ const mergedIdMap = new Map<string, string[]>();
959
+ for (const [idx, ids] of mergedIds) {
960
+ mergedIdMap.set(result[idx].id, ids);
961
+ }
962
+
963
+ return { messages: result, mergedIdMap };
964
+ }
965
+
796
966
  /**
797
967
  * Build an `onEvent` callback that publishes every outbound event to the
798
968
  * assistant event hub, maintaining ordered delivery through a serial chain.
799
969
  *
800
970
  * Also registers pending interactions when confirmation_request,
801
- * secret_request, host_bash_request, or host_file_request events flow
802
- * through, so standalone approval/result endpoints can look up the conversation
803
- * by requestId.
971
+ * secret_request, host_bash_request, host_browser_request, host_file_request,
972
+ * or host_cu_request events flow through, so standalone approval/result
973
+ * endpoints can look up the conversation by requestId.
804
974
  */
805
975
  function makeHubPublisher(
806
976
  deps: SendMessageDeps,
807
977
  conversationId: string,
808
- conversation: import("../../daemon/conversation.js").Conversation,
978
+ conversation: Conversation,
809
979
  ): (msg: ServerMessage) => void {
810
980
  let hubChain: Promise<void> = Promise.resolve();
811
981
  return (msg: ServerMessage) => {
@@ -886,24 +1056,8 @@ function makeHubPublisher(
886
1056
  conversationId,
887
1057
  kind: "secret",
888
1058
  });
889
- } else if (msg.type === "host_bash_request") {
890
- pendingInteractions.register(msg.requestId, {
891
- conversation,
892
- conversationId,
893
- kind: "host_bash",
894
- });
895
- } else if (msg.type === "host_file_request") {
896
- pendingInteractions.register(msg.requestId, {
897
- conversation,
898
- conversationId,
899
- kind: "host_file",
900
- });
901
- } else if (msg.type === "host_cu_request") {
902
- pendingInteractions.register(msg.requestId, {
903
- conversation,
904
- conversationId,
905
- kind: "host_cu",
906
- });
1059
+ } else {
1060
+ registerHostProxyPendingInteraction(msg, conversation, conversationId);
907
1061
  }
908
1062
 
909
1063
  // ServerMessage is a large union; conversationId exists on most but not all variants.
@@ -932,6 +1086,202 @@ function makeHubPublisher(
932
1086
  };
933
1087
  }
934
1088
 
1089
+ /**
1090
+ * Register pending interactions for host proxy request envelopes so
1091
+ * standalone result endpoints can resolve by requestId.
1092
+ *
1093
+ * Returns the registered requestId when a host proxy request was registered.
1094
+ * Callers that route through non-hub transports (e.g. registry-routed
1095
+ * host_browser sends) can use this to clean up the registration if send fails.
1096
+ */
1097
+ function registerHostProxyPendingInteraction(
1098
+ msg: ServerMessage,
1099
+ conversation: Conversation,
1100
+ conversationId: string,
1101
+ ): string | undefined {
1102
+ if (msg.type === "host_bash_request") {
1103
+ pendingInteractions.register(msg.requestId, {
1104
+ conversation,
1105
+ conversationId,
1106
+ kind: "host_bash",
1107
+ });
1108
+ return msg.requestId;
1109
+ }
1110
+ if (msg.type === "host_browser_request") {
1111
+ pendingInteractions.register(msg.requestId, {
1112
+ conversation,
1113
+ conversationId,
1114
+ kind: "host_browser",
1115
+ });
1116
+ return msg.requestId;
1117
+ }
1118
+ if (msg.type === "host_file_request") {
1119
+ pendingInteractions.register(msg.requestId, {
1120
+ conversation,
1121
+ conversationId,
1122
+ kind: "host_file",
1123
+ });
1124
+ return msg.requestId;
1125
+ }
1126
+ if (msg.type === "host_cu_request") {
1127
+ pendingInteractions.register(msg.requestId, {
1128
+ conversation,
1129
+ conversationId,
1130
+ kind: "host_cu",
1131
+ });
1132
+ return msg.requestId;
1133
+ }
1134
+ return undefined;
1135
+ }
1136
+
1137
+ /**
1138
+ * Resolve the host_browser sender function for a conversation turn.
1139
+ *
1140
+ * When the guardian has an active extension connection in the
1141
+ * ChromeExtensionRegistry, returns a registry-routed sender that forwards
1142
+ * `host_browser_request` / `host_browser_cancel` frames through the
1143
+ * WebSocket to the connected extension. Otherwise returns the SSE hub
1144
+ * emitter (`onEvent`).
1145
+ *
1146
+ * For `chrome-extension` turns the registry sender is **always** returned
1147
+ * regardless of the POST-time connection check. The chrome-extension
1148
+ * interface has no SSE consumer for `host_browser_request` frames, so
1149
+ * falling back to `onEvent` would cause CDP calls to stall until the proxy
1150
+ * timeout (30 s) instead of failing immediately at send time when the
1151
+ * registry throws on a missing connection.
1152
+ *
1153
+ * This helper is interface-agnostic: both chrome-extension and macOS turns
1154
+ * can obtain a registry-routed sender when extension connectivity exists.
1155
+ * The `isRegistryRouted` flag lets the caller decide whether to set
1156
+ * `hostBrowserSenderOverride` and whether to provision a `HostBrowserProxy`
1157
+ * for interfaces that don't statically support host_browser (e.g. macOS).
1158
+ */
1159
+ function resolveHostBrowserSender(
1160
+ conversation: Conversation,
1161
+ conversationId: string,
1162
+ authContext: AuthContext,
1163
+ onEvent: (msg: ServerMessage) => void,
1164
+ sourceInterface: InterfaceId,
1165
+ ): { sender: (msg: ServerMessage) => void; isRegistryRouted: boolean } {
1166
+ // Check whether the guardian has any active extension connection.
1167
+ const guardianId =
1168
+ conversation.trustContext?.guardianPrincipalId ??
1169
+ authContext.actorPrincipalId;
1170
+ const hasExtensionConnection =
1171
+ !!guardianId && !!getChromeExtensionRegistry().get(guardianId);
1172
+
1173
+ // For chrome-extension, always use the registry sender so that send-time
1174
+ // failures produce immediate errors rather than 30-second proxy timeouts.
1175
+ // The SSE hub has no extension consumer, so falling back to onEvent is
1176
+ // never correct for this interface.
1177
+ if (!hasExtensionConnection && sourceInterface !== "chrome-extension") {
1178
+ return { sender: onEvent, isRegistryRouted: false };
1179
+ }
1180
+
1181
+ // Build a registry-routed sender. The guardian principal ID is resolved
1182
+ // at send time rather than captured here so that queue-drain restores
1183
+ // (which re-fire this closure outside the original POST context) follow
1184
+ // the conversation's bound guardian identity rather than a stale
1185
+ // authContext.actorPrincipalId.
1186
+ const registrySender = (msg: ServerMessage): void => {
1187
+ const requestId = registerHostProxyPendingInteraction(
1188
+ msg,
1189
+ conversation,
1190
+ conversationId,
1191
+ );
1192
+ const gid =
1193
+ conversation.trustContext?.guardianPrincipalId ??
1194
+ authContext.actorPrincipalId;
1195
+ if (!gid) {
1196
+ if (requestId) pendingInteractions.resolve(requestId);
1197
+ throw new Error(
1198
+ "host_browser send skipped: no guardianId on AuthContext",
1199
+ );
1200
+ }
1201
+ const ok = getChromeExtensionRegistry().send(gid, msg);
1202
+ if (!ok) {
1203
+ if (requestId) pendingInteractions.resolve(requestId);
1204
+ throw new Error(
1205
+ `host_browser send failed: no active connection for guardian ${gid}`,
1206
+ );
1207
+ }
1208
+ };
1209
+
1210
+ return { sender: registrySender, isRegistryRouted: true };
1211
+ }
1212
+
1213
+ /**
1214
+ * Persist the pre-chat onboarding payload to disk.
1215
+ *
1216
+ * Runs only on the very first message of a fresh conversation. Three
1217
+ * artifacts are produced:
1218
+ *
1219
+ * 1. `data/onboarding-context.json` — sidecar read by the
1220
+ * relationship-state writer so onboarding-sourced facts survive
1221
+ * the pure-recomputation write cycle (every turn boundary rebuilds
1222
+ * facts from markdown; the sidecar is the durable source for the
1223
+ * tool/task/tone chips).
1224
+ * 2. `IDENTITY.md` / `USER.md` — persona seed files, only written
1225
+ * when missing so we never clobber existing content. These feed
1226
+ * the system prompt and the relationship-state writer's
1227
+ * `parseIdentity` / `parseUserName` helpers after a daemon
1228
+ * restart when the in-memory onboarding context is gone.
1229
+ * 3. `data/relationship-state.json` — kicked off fire-and-forget so
1230
+ * the Home page can populate immediately on first visit instead
1231
+ * of waiting for the first agent-turn boundary.
1232
+ *
1233
+ * Never throws: every write is guarded and logged as a warning on
1234
+ * failure. The route handler path must never reject because of a
1235
+ * best-effort persistence step.
1236
+ */
1237
+ function persistOnboardingArtifacts(onboarding: {
1238
+ tools: string[];
1239
+ tasks: string[];
1240
+ tone: string;
1241
+ userName?: string;
1242
+ assistantName?: string;
1243
+ }): void {
1244
+ writeOnboardingSidecar(onboarding);
1245
+
1246
+ const assistantName = onboarding.assistantName?.trim();
1247
+ if (assistantName) {
1248
+ const identityPath = getWorkspacePromptPath("IDENTITY.md");
1249
+ if (!existsSync(identityPath)) {
1250
+ try {
1251
+ writeFileSync(
1252
+ identityPath,
1253
+ `# Identity\n\n- Name: ${assistantName}\n`,
1254
+ "utf-8",
1255
+ );
1256
+ } catch (err) {
1257
+ log.warn(
1258
+ { err, identityPath },
1259
+ "Failed to seed IDENTITY.md from onboarding",
1260
+ );
1261
+ }
1262
+ }
1263
+ }
1264
+
1265
+ const userName = onboarding.userName?.trim();
1266
+ if (userName) {
1267
+ const userPath = getWorkspacePromptPath("USER.md");
1268
+ if (!existsSync(userPath)) {
1269
+ try {
1270
+ writeFileSync(userPath, `# User\n\n- Name: ${userName}\n`, "utf-8");
1271
+ } catch (err) {
1272
+ log.warn({ err, userPath }, "Failed to seed USER.md from onboarding");
1273
+ }
1274
+ }
1275
+ }
1276
+
1277
+ void writeRelationshipState().catch((err) => {
1278
+ log.warn(
1279
+ { err },
1280
+ "Failed to kick off relationship-state write after onboarding",
1281
+ );
1282
+ });
1283
+ }
1284
+
935
1285
  export async function handleSendMessage(
936
1286
  req: Request,
937
1287
  deps: {
@@ -952,6 +1302,13 @@ export async function handleSendMessage(
952
1302
  bypassSecretCheck?: boolean;
953
1303
  hostHomeDir?: string;
954
1304
  hostUsername?: string;
1305
+ onboarding?: {
1306
+ tools: string[];
1307
+ tasks: string[];
1308
+ tone: string;
1309
+ userName?: string;
1310
+ assistantName?: string;
1311
+ };
955
1312
  };
956
1313
 
957
1314
  const { conversationKey, content, attachmentIds } = body;
@@ -1054,28 +1411,58 @@ export async function handleSendMessage(
1054
1411
  const mapping = getOrCreateConversation(resolvedConversationKey, {
1055
1412
  conversationType,
1056
1413
  });
1414
+
1057
1415
  const smDeps = deps.sendMessageDeps;
1058
1416
 
1417
+ // Notify all connected clients that the conversation list changed when a
1418
+ // new standard conversation is created so sidebars can refresh.
1419
+ if (mapping.created && mapping.conversationType === "standard") {
1420
+ smDeps.assistantEventHub
1421
+ .publish(
1422
+ buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, {
1423
+ type: "conversation_list_invalidated",
1424
+ reason: "created",
1425
+ }),
1426
+ )
1427
+ .catch((err) => {
1428
+ log.warn({ err }, "Failed to publish conversation_list_invalidated");
1429
+ });
1430
+ }
1431
+
1059
1432
  // Build transport metadata from the request so the daemon can inject
1060
1433
  // host environment hints (home directory, username) into the LLM context.
1061
- const transport =
1062
- sourceInterface === "macos"
1063
- ? ({
1064
- channelId: sourceChannel,
1065
- interfaceId: "macos" as const,
1066
- hostHomeDir: body.hostHomeDir,
1067
- hostUsername: body.hostUsername,
1068
- } satisfies MacosTransportMetadata)
1069
- : ({
1070
- channelId: sourceChannel,
1071
- interfaceId: sourceInterface,
1072
- } satisfies NonMacosTransportMetadata);
1434
+ // The `supportsHostProxy` type predicate narrows `sourceInterface` to
1435
+ // `HostProxyInterfaceId` in the truthy branch, which is exactly the
1436
+ // discriminant the `HostProxyTransportMetadata` variant expects — so the
1437
+ // construction site stays in lock-step with the runtime capability gate.
1438
+ const transport = supportsHostProxy(sourceInterface)
1439
+ ? ({
1440
+ channelId: sourceChannel,
1441
+ interfaceId: sourceInterface,
1442
+ hostHomeDir: body.hostHomeDir,
1443
+ hostUsername: body.hostUsername,
1444
+ } satisfies HostProxyTransportMetadata)
1445
+ : ({
1446
+ channelId: sourceChannel,
1447
+ interfaceId: sourceInterface,
1448
+ } satisfies NonHostProxyTransportMetadata);
1073
1449
 
1074
1450
  const conversation = await smDeps.getOrCreateConversation(
1075
1451
  mapping.conversationId,
1076
1452
  { transport },
1077
1453
  );
1078
1454
 
1455
+ // Store pre-chat onboarding context on the conversation when this is the
1456
+ // very first message (no prior messages loaded). Also persist the
1457
+ // onboarding selections so the Home page shows onboarding-sourced
1458
+ // chips immediately, and seed IDENTITY.md / USER.md so subsequent
1459
+ // turn-boundary recomputes of relationship-state have a stable
1460
+ // persona source beyond the in-memory conversation object.
1461
+ if (body.onboarding && conversation.messages.length === 0) {
1462
+ conversation.setOnboardingContext(body.onboarding);
1463
+ persistOnboardingArtifacts(body.onboarding);
1464
+ }
1465
+
1079
1466
  // Resolve guardian context from the AuthContext's actorPrincipalId.
1080
1467
  // The JWT-verified principal is used as the sender identity through
1081
1468
  // the same trust resolution pipeline that channel ingress uses.
@@ -1139,12 +1526,13 @@ export async function handleSendMessage(
1139
1526
  conversation,
1140
1527
  );
1141
1528
  const isInteractive = isInteractiveInterface(sourceInterface);
1142
- // Only create the host bash proxy for desktop client interfaces that can
1143
- // execute commands on the user's machine. Non-desktop conversations (CLI,
1144
- // channels, headless) fall back to local execution.
1529
+ // Only create each host proxy for interfaces that support the matching
1530
+ // capability. macOS supports all four; the chrome-extension interface only
1531
+ // supports host_browser. Non-desktop conversations (CLI, channels, headless)
1532
+ // fall back to local execution.
1145
1533
  // Set the proxy BEFORE updateClient so updateClient's call to
1146
1534
  // hostBashProxy.updateSender targets the correct (new) proxy.
1147
- if (supportsHostProxy(sourceInterface)) {
1535
+ if (supportsHostProxy(sourceInterface, "host_bash")) {
1148
1536
  // Reuse the existing proxy if the conversation is actively processing a
1149
1537
  // host bash request to avoid orphaning in-flight requests.
1150
1538
  if (!conversation.isProcessing() || !conversation.hostBashProxy) {
@@ -1153,12 +1541,70 @@ export async function handleSendMessage(
1153
1541
  });
1154
1542
  conversation.setHostBashProxy(proxy);
1155
1543
  }
1544
+ } else if (!conversation.isProcessing()) {
1545
+ conversation.setHostBashProxy(undefined);
1546
+ }
1547
+ // Resolve the host_browser sender — registry-routed when the guardian has
1548
+ // an active extension connection, SSE hub otherwise. This applies to both
1549
+ // chrome-extension and macOS interfaces so that macOS turns can route
1550
+ // browser automation through the user's real Chrome session when available.
1551
+ const { sender: browserProxySendToClient, isRegistryRouted } =
1552
+ resolveHostBrowserSender(
1553
+ conversation,
1554
+ mapping.conversationId,
1555
+ authContext,
1556
+ onEvent,
1557
+ sourceInterface,
1558
+ );
1559
+
1560
+ // Stash the registry-routed sender on the conversation so queue-drain
1561
+ // restores (which run outside of conversation-routes.ts and only have
1562
+ // access to `sendToClient`) can preserve it when calling
1563
+ // `restoreBrowserProxyAvailability()`. The override is set when the
1564
+ // sender is registry-routed (regardless of interface) and cleared when
1565
+ // the SSE hub sender is used, so the drain path always restores the
1566
+ // correct transport.
1567
+ if (isRegistryRouted) {
1568
+ conversation.hostBrowserSenderOverride = browserProxySendToClient;
1569
+ } else {
1570
+ conversation.hostBrowserSenderOverride = undefined;
1571
+ }
1572
+
1573
+ // Provision the host browser proxy. For interfaces that natively support
1574
+ // host_browser (chrome-extension), always provision it. For macOS, the
1575
+ // static capability check returns false (supportsHostProxy("macos",
1576
+ // "host_browser") === false) because the extension isn't guaranteed to be
1577
+ // attached — but when the registry confirms an active extension
1578
+ // connection, we provision the proxy anyway so macOS turns can drive the
1579
+ // user's real Chrome session. When no extension is connected, macOS skips
1580
+ // provisioning and browser tools fall through to cdp-inspect/local.
1581
+ const shouldProvisionBrowserProxy =
1582
+ supportsHostProxy(sourceInterface, "host_browser") ||
1583
+ (canServiceRegistryBrowser(sourceInterface) && isRegistryRouted);
1584
+ if (shouldProvisionBrowserProxy) {
1585
+ if (!conversation.isProcessing() || !conversation.hostBrowserProxy) {
1586
+ const browserProxy = new HostBrowserProxy(
1587
+ browserProxySendToClient,
1588
+ (requestId) => {
1589
+ pendingInteractions.resolve(requestId);
1590
+ },
1591
+ );
1592
+ conversation.setHostBrowserProxy(browserProxy);
1593
+ }
1594
+ } else if (!conversation.isProcessing()) {
1595
+ conversation.setHostBrowserProxy(undefined);
1596
+ }
1597
+ if (supportsHostProxy(sourceInterface, "host_file")) {
1156
1598
  if (!conversation.isProcessing() || !conversation.hostFileProxy) {
1157
1599
  const fileProxy = new HostFileProxy(onEvent, (requestId) => {
1158
1600
  pendingInteractions.resolve(requestId);
1159
1601
  });
1160
1602
  conversation.setHostFileProxy(fileProxy);
1161
1603
  }
1604
+ } else if (!conversation.isProcessing()) {
1605
+ conversation.setHostFileProxy(undefined);
1606
+ }
1607
+ if (supportsHostProxy(sourceInterface, "host_cu")) {
1162
1608
  if (!conversation.isProcessing() || !conversation.hostCuProxy) {
1163
1609
  const cuProxy = new HostCuProxy(onEvent, (requestId) => {
1164
1610
  pendingInteractions.resolve(requestId);
@@ -1172,19 +1618,40 @@ export async function handleSendMessage(
1172
1618
  conversation.addPreactivatedSkillId("computer-use");
1173
1619
  }
1174
1620
  } else if (!conversation.isProcessing()) {
1175
- conversation.setHostBashProxy(undefined);
1176
- conversation.setHostFileProxy(undefined);
1177
1621
  conversation.setHostCuProxy(undefined);
1178
1622
  }
1179
1623
  // Wire sendToClient to the SSE hub so all subsystems can reach the HTTP client.
1180
1624
  // Called after setHostBashProxy so updateSender targets the current proxy.
1181
1625
  // When proxies are preserved during an active turn (non-desktop request while
1182
- // processing), skip updating proxy senders to avoid degrading them.
1626
+ // processing), skip updating proxy senders to avoid degrading them. The gate
1627
+ // matches the host_bash capability because the legacy "reject send during
1628
+ // host bash" flow is what this is really protecting.
1183
1629
  const preservingProxies =
1184
- conversation.isProcessing() && !supportsHostProxy(sourceInterface);
1630
+ conversation.isProcessing() &&
1631
+ !supportsHostProxy(sourceInterface, "host_bash");
1632
+ // hasNoClient must remain `!isInteractive` so downstream tool gating
1633
+ // (`isToolActiveForContext` for HOST_TOOL_NAMES, `createToolExecutor`'s
1634
+ // `isInteractive: !ctx.hasNoClient`) keeps host_bash/host_file/host_cu
1635
+ // tools gated for non-desktop interfaces. The chrome-extension interface
1636
+ // is non-interactive (no SSE prompter UI) but still has a connected client
1637
+ // that can service host_browser_request events; we restore that single
1638
+ // proxy explicitly below without relaxing `hasNoClient`.
1185
1639
  conversation.updateClient(onEvent, !isInteractive, {
1186
1640
  skipProxySenderUpdate: preservingProxies,
1187
1641
  });
1642
+ // Re-enable the browser proxy for turns that provisioned one. This covers:
1643
+ // - chrome-extension: natively supports host_browser (non-interactive but
1644
+ // has a connected client for host_browser_request events)
1645
+ // - macOS with extension: provisioned above when isRegistryRouted is true
1646
+ //
1647
+ // The helper bypasses the `hasNoClient` gate so chrome-extension turns can
1648
+ // drive the browser via CDP without leaking host_bash/host_file tool
1649
+ // availability. It reads `hostBrowserSenderOverride` (set above when
1650
+ // registry-routed) and applies the correct sender — including after
1651
+ // queue-drain restores run from conversation-process.ts.
1652
+ if (shouldProvisionBrowserProxy) {
1653
+ conversation.restoreBrowserProxyAvailability?.();
1654
+ }
1188
1655
 
1189
1656
  // ── Canned first-greeting fast path ──
1190
1657
  // On a completely fresh workspace, skip LLM inference for the macOS
@@ -1248,6 +1715,12 @@ export async function handleSendMessage(
1248
1715
  // fast path) so the HTTP response reaches the client before SSE
1249
1716
  // events arrive.
1250
1717
  setTimeout(() => {
1718
+ onEvent({
1719
+ type: "user_message_echo",
1720
+ text: rawContent,
1721
+ conversationId,
1722
+ messageId: persisted.id,
1723
+ });
1251
1724
  onEvent({ type: "assistant_text_delta", text: cannedGreeting });
1252
1725
  onEvent({ type: "message_complete", conversationId });
1253
1726
  conversation.processing = false;
@@ -1541,6 +2014,12 @@ export async function handleSendMessage(
1541
2014
  const conversationId = mapping.conversationId;
1542
2015
  const message = slashResult.message;
1543
2016
  setTimeout(() => {
2017
+ onEvent({
2018
+ type: "user_message_echo",
2019
+ text: rawContent,
2020
+ conversationId,
2021
+ messageId: persisted.id,
2022
+ });
1544
2023
  if (modelInfoEvent) {
1545
2024
  onEvent(modelInfoEvent);
1546
2025
  }
@@ -1567,80 +2046,88 @@ export async function handleSendMessage(
1567
2046
 
1568
2047
  if (slashResult.kind === "compact") {
1569
2048
  conversation.processing = true;
1570
- let cleanupDeferred = false;
1571
- try {
1572
- const provenance = provenanceFromTrustContext(conversation.trustContext);
1573
- const channelMeta = {
1574
- ...provenance,
1575
- userMessageChannel: sourceChannel,
1576
- assistantMessageChannel: sourceChannel,
1577
- userMessageInterface: sourceInterface,
1578
- assistantMessageInterface: sourceInterface,
1579
- };
1580
- const cleanMsg = createUserMessage(rawContent, attachments);
1581
- const persisted = await addMessage(
1582
- mapping.conversationId,
1583
- "user",
1584
- JSON.stringify(cleanMsg.content),
1585
- channelMeta,
1586
- );
1587
- conversation.getMessages().push(cleanMsg);
2049
+ const provenance = provenanceFromTrustContext(conversation.trustContext);
2050
+ const channelMeta = {
2051
+ ...provenance,
2052
+ userMessageChannel: sourceChannel,
2053
+ assistantMessageChannel: sourceChannel,
2054
+ userMessageInterface: sourceInterface,
2055
+ assistantMessageInterface: sourceInterface,
2056
+ };
2057
+ const cleanMsg = createUserMessage(rawContent, attachments);
2058
+ const persisted = await addMessage(
2059
+ mapping.conversationId,
2060
+ "user",
2061
+ JSON.stringify(cleanMsg.content),
2062
+ channelMeta,
2063
+ );
2064
+ conversation.getMessages().push(cleanMsg);
1588
2065
 
1589
- conversation.emitActivityState(
1590
- "thinking",
1591
- "context_compacting",
1592
- "assistant_turn",
1593
- );
1594
- const result = await conversation.forceCompact();
1595
- const responseText = formatCompactResult(result);
2066
+ const conversationId = mapping.conversationId;
1596
2067
 
1597
- const assistantMsg = createAssistantMessage(responseText);
1598
- await addMessage(
1599
- mapping.conversationId,
1600
- "assistant",
1601
- JSON.stringify(assistantMsg.content),
1602
- channelMeta,
1603
- );
1604
- conversation.getMessages().push(assistantMsg);
1605
-
1606
- const response = Response.json(
1607
- {
1608
- accepted: true,
2068
+ // Fire-and-forget: return 202 immediately, run compaction async.
2069
+ // forceCompact() makes an LLM call that can exceed the client's
2070
+ // HTTP timeout on large contexts, causing a false "Failed to send".
2071
+ (async () => {
2072
+ try {
2073
+ onEvent({
2074
+ type: "user_message_echo",
2075
+ text: rawContent,
2076
+ conversationId,
1609
2077
  messageId: persisted.id,
1610
- conversationId: mapping.conversationId,
1611
- },
1612
- { status: 202 },
1613
- );
2078
+ });
2079
+ conversation.emitActivityState(
2080
+ "thinking",
2081
+ "context_compacting",
2082
+ "assistant_turn",
2083
+ );
2084
+ const result = await conversation.forceCompact();
2085
+ const responseText = formatCompactResult(result);
2086
+
2087
+ const assistantMsg = createAssistantMessage(responseText);
2088
+ await addMessage(
2089
+ conversationId,
2090
+ "assistant",
2091
+ JSON.stringify(assistantMsg.content),
2092
+ channelMeta,
2093
+ );
2094
+ conversation.getMessages().push(assistantMsg);
1614
2095
 
1615
- const conversationId = mapping.conversationId;
1616
- setTimeout(() => {
1617
2096
  onEvent({ type: "assistant_text_delta", text: responseText });
2097
+ onEvent({ type: "message_complete", conversationId });
2098
+ } catch (err) {
2099
+ log.error({ err, conversationId }, "Compact command failed");
1618
2100
  onEvent({
1619
- type: "message_complete",
2101
+ type: "conversation_error",
1620
2102
  conversationId,
2103
+ code: "UNKNOWN",
2104
+ userMessage: `Compaction failed: ${err instanceof Error ? err.message : String(err)}`,
2105
+ retryable: true,
1621
2106
  });
2107
+ } finally {
1622
2108
  conversation.processing = false;
1623
2109
  silentlyWithLog(
1624
2110
  conversation.drainQueue(),
1625
2111
  "compact-command queue drain",
1626
2112
  );
1627
- }, 0);
1628
-
1629
- cleanupDeferred = true;
1630
- return response;
1631
- } finally {
1632
- if (!cleanupDeferred && conversation.processing) {
1633
- conversation.processing = false;
1634
- silentlyWithLog(conversation.drainQueue(), "error-path queue drain");
1635
2113
  }
1636
- }
2114
+ })();
2115
+
2116
+ return Response.json(
2117
+ {
2118
+ accepted: true,
2119
+ messageId: persisted.id,
2120
+ conversationId,
2121
+ },
2122
+ { status: 202 },
2123
+ );
1637
2124
  }
1638
2125
 
1639
2126
  const resolvedContent = slashResult.content;
1640
2127
 
2128
+ const requestId = crypto.randomUUID();
1641
2129
  let messageId: string;
1642
2130
  try {
1643
- const requestId = crypto.randomUUID();
1644
2131
  messageId = await conversation.persistUserMessage(
1645
2132
  resolvedContent,
1646
2133
  attachments,
@@ -1651,6 +2138,14 @@ export async function handleSendMessage(
1651
2138
  throw err;
1652
2139
  }
1653
2140
 
2141
+ onEvent({
2142
+ type: "user_message_echo",
2143
+ text: resolvedContent,
2144
+ conversationId: mapping.conversationId,
2145
+ messageId,
2146
+ requestId,
2147
+ });
2148
+
1654
2149
  // Fire-and-forget the agent loop; events flow to the hub via onEvent.
1655
2150
  conversation
1656
2151
  .runAgentLoop(resolvedContent, messageId, onEvent, {