@vellumai/assistant 0.6.3 → 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 (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -11,14 +11,13 @@ import {
11
11
  createAssistantMessage,
12
12
  createUserMessage,
13
13
  } from "../agent/message-types.js";
14
- import type {
15
- TurnChannelContext,
16
- TurnInterfaceContext,
17
- } from "../channels/types.js";
18
14
  import {
15
+ canServiceRegistryBrowser,
19
16
  parseChannelId,
20
17
  parseInterfaceId,
21
18
  supportsHostProxy,
19
+ type TurnChannelContext,
20
+ type TurnInterfaceContext,
22
21
  } from "../channels/types.js";
23
22
  import { getConfig } from "../config/loader.js";
24
23
  import type { ContextWindowResult } from "../context/window-manager.js";
@@ -34,13 +33,21 @@ import { createPreference } from "../notifications/preferences-store.js";
34
33
  import type { Message } from "../providers/types.js";
35
34
  import { routeGuardianReply } from "../runtime/guardian-reply-router.js";
36
35
  import { getLogger } from "../util/logger.js";
37
- import type { MessageQueue } from "./conversation-queue-manager.js";
38
- import type { QueueDrainReason } from "./conversation-queue-manager.js";
36
+ import { persistQueuedMessageBody } from "./conversation-messaging.js";
37
+ import type {
38
+ MessageQueue,
39
+ QueuedMessage,
40
+ QueueDrainReason,
41
+ } from "./conversation-queue-manager.js";
39
42
  import type {
40
43
  ChannelCapabilities,
41
44
  TrustContext,
42
45
  } from "./conversation-runtime-assembly.js";
43
- import { resolveSlash, type SlashContext } from "./conversation-slash.js";
46
+ import {
47
+ classifySlash,
48
+ resolveSlash,
49
+ type SlashContext,
50
+ } from "./conversation-slash.js";
44
51
  import { getModelInfo } from "./handlers/config-model.js";
45
52
  import type {
46
53
  ServerMessage,
@@ -94,6 +101,12 @@ export interface ProcessConversationContext {
94
101
  currentRequestId?: string;
95
102
  readonly queue: MessageQueue;
96
103
  readonly traceEmitter: TraceEmitter;
104
+ /**
105
+ * Set of requestIds created by surface-action responses. Used to
106
+ * distinguish surface-action turns from regular user turns (e.g. for
107
+ * stale-surface auto-dismiss guards and batched-drain exclusion).
108
+ */
109
+ readonly surfaceActionRequestIds: Set<string>;
97
110
  currentActiveSurfaceId?: string;
98
111
  currentPage?: string;
99
112
  /** Cumulative token usage stats for the conversation. */
@@ -135,10 +148,22 @@ export interface ProcessConversationContext {
135
148
  setTurnInterfaceContext(ctx: TurnInterfaceContext): void;
136
149
  /** Mark host proxies as unavailable so tool execution uses local fallback. */
137
150
  clearProxyAvailability(): void;
138
- /** Restore host proxy availability based on whether a real client is connected. */
139
- restoreProxyAvailability(): void;
140
- /** Restore only the host browser proxy (used by chrome-extension drains). */
151
+ /**
152
+ * Restore host proxy availability based on whether a real client is connected.
153
+ * When `skipBrowser` is true, the browser proxy is left untouched use this
154
+ * when `restoreBrowserProxyAvailability()` will handle the browser proxy
155
+ * separately with the correct registry-routed sender.
156
+ */
157
+ restoreProxyAvailability(options?: { skipBrowser?: boolean }): void;
158
+ /** Restore only the host browser proxy (used by chrome-extension and macOS+extension drains). */
141
159
  restoreBrowserProxyAvailability(): void;
160
+ /**
161
+ * Registry-routed sender override for the host browser proxy. When set,
162
+ * `restoreBrowserProxyAvailability()` uses this function instead of
163
+ * `sendToClient`. Set by the POST /messages handler when the guardian
164
+ * has an active extension connection (regardless of interface).
165
+ */
166
+ hostBrowserSenderOverride?: (msg: ServerMessage) => void;
142
167
  /** Replace or clear the conversation's host browser proxy. */
143
168
  setHostBrowserProxy(
144
169
  proxy: import("./host-browser-proxy.js").HostBrowserProxy | undefined,
@@ -242,6 +267,82 @@ function buildSlashContext(
242
267
  };
243
268
  }
244
269
 
270
+ /**
271
+ * Walk the head of the queue and return the longest contiguous run of
272
+ * passthrough messages (non-slash, non-verification-intent) that share the
273
+ * same `userMessageInterface`. Returns `[]` when the head is itself a slash
274
+ * command or verification-intent direct-setup — in that case `drainQueue`
275
+ * pops the head via `queue.shift()` and the single-message path handles it.
276
+ *
277
+ * The builder uses `peek` for lookahead and only calls `shiftN(matched)` once
278
+ * a contiguous passthrough run is identified. This keeps byte-budget
279
+ * accounting centralized in `MessageQueue` rather than mutating mid-walk.
280
+ */
281
+ async function buildPassthroughBatch(
282
+ conversation: ProcessConversationContext,
283
+ ): Promise<QueuedMessage[]> {
284
+ const head = conversation.queue.peek(0);
285
+ if (head === undefined) return [];
286
+
287
+ const headInterface = resolveQueuedTurnInterfaceContext(
288
+ head,
289
+ conversation.getTurnInterfaceContext(),
290
+ );
291
+ // Pure classifier — no side effects. `resolveSlash` runs /pair's side
292
+ // effects (pairing registration, QR PNG write); if we called it here the
293
+ // real drain would invoke those again and the second call would fail with
294
+ // "active pairing already in progress".
295
+ if (classifySlash(head.content) !== "passthrough") return [];
296
+ if (resolveVerificationSessionIntent(head.content).kind === "direct_setup") {
297
+ // Verification intents stay on the single-message path so their per-turn
298
+ // skill preactivation isn't leaked into batched tail messages.
299
+ return [];
300
+ }
301
+ // Surface-action messages rely on per-message `activeSurfaceId` and
302
+ // `surfaceActionRequestIds` semantics that last-wins batching would
303
+ // corrupt (e.g. erasing the head's surface context when the last tail is
304
+ // a regular text message). Keep them on the single-message path.
305
+ if (
306
+ head.activeSurfaceId !== undefined ||
307
+ conversation.surfaceActionRequestIds.has(head.requestId)
308
+ ) {
309
+ return [];
310
+ }
311
+
312
+ let i = 1;
313
+ for (;;) {
314
+ const candidate = conversation.queue.peek(i);
315
+ if (candidate === undefined) break;
316
+ const candIf = resolveQueuedTurnInterfaceContext(
317
+ candidate,
318
+ conversation.getTurnInterfaceContext(),
319
+ );
320
+ // Treat an undefined interface as distinct from a defined one so we don't
321
+ // silently batch cross-interface messages whose env/transport would
322
+ // otherwise diverge.
323
+ if (candIf?.userMessageInterface !== headInterface?.userMessageInterface)
324
+ break;
325
+ if (classifySlash(candidate.content) !== "passthrough") break;
326
+ if (
327
+ resolveVerificationSessionIntent(candidate.content).kind ===
328
+ "direct_setup"
329
+ )
330
+ break;
331
+ // Stop at the first surface-action tail; it will drain via the single-
332
+ // message path so its per-message surface context is preserved.
333
+ if (
334
+ candidate.activeSurfaceId !== undefined ||
335
+ conversation.surfaceActionRequestIds.has(candidate.requestId)
336
+ ) {
337
+ break;
338
+ }
339
+ i++;
340
+ }
341
+
342
+ const matched = i;
343
+ return conversation.queue.shiftN(matched);
344
+ }
345
+
245
346
  // ── drainQueue ───────────────────────────────────────────────────────
246
347
 
247
348
  /**
@@ -258,9 +359,26 @@ export async function drainQueue(
258
359
  conversation: ProcessConversationContext,
259
360
  reason: QueueDrainReason = "loop_complete",
260
361
  ): Promise<void> {
261
- const next = conversation.queue.shift();
262
- if (!next) return;
362
+ const batch = await buildPassthroughBatch(conversation);
363
+ if (batch.length === 0) {
364
+ // Head is a slash / verification intent / empty queue. If the queue has
365
+ // an item the builder rejected, pop it and hand it to the single-message
366
+ // path — which owns slash / compact / verification-intent behavior.
367
+ const next = conversation.queue.shift();
368
+ if (!next) return;
369
+ return drainSingleMessage(conversation, next, reason);
370
+ }
371
+ if (batch.length === 1) {
372
+ return drainSingleMessage(conversation, batch[0], reason);
373
+ }
374
+ return drainBatch(conversation, batch, reason);
375
+ }
263
376
 
377
+ async function drainSingleMessage(
378
+ conversation: ProcessConversationContext,
379
+ next: QueuedMessage,
380
+ reason: QueueDrainReason,
381
+ ): Promise<void> {
264
382
  // Reset per-turn preactivation so a prior iteration (e.g. an unknown-slash
265
383
  // from a desktop source that skips runAgentLoop) can't leak CU preactivation
266
384
  // into the next queued message.
@@ -354,21 +472,49 @@ export async function drainQueue(
354
472
  queuedInterfaceCtx ?? conversation.getTurnInterfaceContext();
355
473
  const sourceInterface = interfaceCtx?.userMessageInterface;
356
474
  if (sourceInterface && supportsHostProxy(sourceInterface)) {
357
- conversation.restoreProxyAvailability();
475
+ // When hostBrowserSenderOverride is set, skip the browser proxy here
476
+ // — restoreBrowserProxyAvailability() below will handle it with the
477
+ // correct registry-routed sender instead of the SSE hub emitter.
478
+ conversation.restoreProxyAvailability(
479
+ conversation.hostBrowserSenderOverride
480
+ ? { skipBrowser: true }
481
+ : undefined,
482
+ );
358
483
  conversation.addPreactivatedSkillId("computer-use");
359
484
  }
360
485
  // Tear down a stale hostBrowserProxy inherited from a prior turn on a
361
- // different interface (e.g. chrome-extension installed one, then a
362
- // macos turn drains). Without this, restoreProxyAvailability() above
363
- // would re-enable the proxy and getCdpClient() would route browser
364
- // tools through host_browser_request and hang waiting for a client
365
- // that this turn's interface can't service.
486
+ // different interface (e.g. chrome-extension installed one, then a CLI
487
+ // turn drains). Without this, restoreProxyAvailability() above would
488
+ // re-enable the proxy and getCdpClient() would route browser tools
489
+ // through host_browser_request and hang waiting for a client that this
490
+ // turn's interface can't service.
491
+ //
492
+ // Skip teardown only when BOTH conditions hold:
493
+ // 1. `hostBrowserSenderOverride` is set (live registry-routed sender)
494
+ // 2. The current turn's interface can service host_browser frames
495
+ // (chrome-extension or macOS).
496
+ // Without the interface check, queued turns from CLI/iOS/Vellum would
497
+ // inherit a stale override left by a prior extension-connected turn
498
+ // and keep the proxy alive, causing cross-interface misrouting.
499
+ const currentTurnCanServiceBrowser =
500
+ !!sourceInterface && canServiceRegistryBrowser(sourceInterface);
366
501
  if (
367
502
  sourceInterface &&
368
- !supportsHostProxy(sourceInterface, "host_browser")
503
+ !supportsHostProxy(sourceInterface, "host_browser") &&
504
+ !(conversation.hostBrowserSenderOverride && currentTurnCanServiceBrowser)
369
505
  ) {
370
506
  conversation.setHostBrowserProxy(undefined);
371
507
  }
508
+ // When a macOS turn has a registry-routed sender override (active
509
+ // extension connection), restore the browser proxy so host_browser
510
+ // tools route through the extension rather than cdp-inspect/local.
511
+ if (
512
+ sourceInterface &&
513
+ supportsHostProxy(sourceInterface) &&
514
+ conversation.hostBrowserSenderOverride
515
+ ) {
516
+ conversation.restoreBrowserProxyAvailability();
517
+ }
372
518
  }
373
519
 
374
520
  // Snapshot persona context at turn start so later tool turns can't pick up
@@ -644,6 +790,16 @@ export async function drainQueue(
644
790
  return;
645
791
  }
646
792
 
793
+ // Broadcast the user message to all hub subscribers so passive devices
794
+ // see the user turn before the assistant reply starts streaming.
795
+ next.onEvent({
796
+ type: "user_message_echo",
797
+ text: resolvedContent,
798
+ conversationId: conversation.conversationId,
799
+ messageId: userMessageId,
800
+ requestId: next.requestId,
801
+ });
802
+
647
803
  // Set the active surface for the dequeued message so runAgentLoop can inject context
648
804
  conversation.currentActiveSurfaceId = next.activeSurfaceId;
649
805
  conversation.currentPage = next.currentPage;
@@ -715,6 +871,412 @@ export async function drainQueue(
715
871
  });
716
872
  }
717
873
 
874
+ // Drives a batched turn where multiple queued passthrough messages share one
875
+ // runAgentLoop run. Per-message dequeue events and DB persistence are
876
+ // preserved; the agent reply fans out to every batched client.
877
+ async function drainBatch(
878
+ conversation: ProcessConversationContext,
879
+ batch: QueuedMessage[],
880
+ reason: QueueDrainReason,
881
+ ): Promise<void> {
882
+ // Head-wins: the batch-builder guarantees identical userMessageInterface
883
+ // across the batch; channel/transport divergence is accepted with the head's
884
+ // environment.
885
+ const head = batch[0];
886
+
887
+ // Reset per-turn preactivation so a prior iteration can't leak CU
888
+ // preactivation into this batched turn.
889
+ conversation.preactivatedSkillIds = undefined;
890
+
891
+ log.info(
892
+ {
893
+ conversationId: conversation.conversationId,
894
+ requestId: head.requestId,
895
+ reason,
896
+ batchSize: batch.length,
897
+ },
898
+ "Dequeuing batched messages",
899
+ );
900
+
901
+ const queuedTurnCtx = resolveQueuedTurnContext(
902
+ head,
903
+ conversation.getTurnChannelContext(),
904
+ );
905
+ if (queuedTurnCtx) {
906
+ conversation.setTurnChannelContext(queuedTurnCtx);
907
+ }
908
+
909
+ const queuedInterfaceCtx = resolveQueuedTurnInterfaceContext(
910
+ head,
911
+ conversation.getTurnInterfaceContext(),
912
+ );
913
+ if (queuedInterfaceCtx) {
914
+ conversation.setTurnInterfaceContext(queuedInterfaceCtx);
915
+ }
916
+
917
+ // Apply transport hints from the head message so this batched turn uses
918
+ // the head's transport metadata. Tail transport divergence is accepted
919
+ // per the head-wins contract.
920
+ if (head.transport) {
921
+ conversation.setTransportHints(buildTransportHints(head.transport));
922
+ conversation.applyHostEnvFromTransport(head.transport);
923
+ }
924
+
925
+ // Non-interactive queued messages (channel requests) must not execute tools
926
+ // via the desktop host proxy. Clear proxy availability so isAvailable()
927
+ // returns false and tool execution falls back to local. Mirrors the
928
+ // single-message path exactly — sourced from `head`.
929
+ if (head.isInteractive === false) {
930
+ conversation.clearProxyAvailability();
931
+ const drainInterfaceCtx =
932
+ queuedInterfaceCtx ?? conversation.getTurnInterfaceContext();
933
+ const drainInterface = drainInterfaceCtx?.userMessageInterface;
934
+ if (
935
+ drainInterface &&
936
+ !supportsHostProxy(drainInterface) &&
937
+ supportsHostProxy(drainInterface, "host_browser")
938
+ ) {
939
+ conversation.restoreBrowserProxyAvailability();
940
+ }
941
+ } else {
942
+ const interfaceCtx =
943
+ queuedInterfaceCtx ?? conversation.getTurnInterfaceContext();
944
+ const sourceInterface = interfaceCtx?.userMessageInterface;
945
+ if (sourceInterface && supportsHostProxy(sourceInterface)) {
946
+ conversation.restoreProxyAvailability(
947
+ conversation.hostBrowserSenderOverride
948
+ ? { skipBrowser: true }
949
+ : undefined,
950
+ );
951
+ conversation.addPreactivatedSkillId("computer-use");
952
+ }
953
+ const currentTurnCanServiceBrowser =
954
+ !!sourceInterface && canServiceRegistryBrowser(sourceInterface);
955
+ if (
956
+ sourceInterface &&
957
+ !supportsHostProxy(sourceInterface, "host_browser") &&
958
+ !(conversation.hostBrowserSenderOverride && currentTurnCanServiceBrowser)
959
+ ) {
960
+ conversation.setHostBrowserProxy(undefined);
961
+ }
962
+ if (
963
+ sourceInterface &&
964
+ supportsHostProxy(sourceInterface) &&
965
+ conversation.hostBrowserSenderOverride
966
+ ) {
967
+ conversation.restoreBrowserProxyAvailability();
968
+ }
969
+ }
970
+
971
+ // Snapshot persona context at turn start so later tool turns can't pick up
972
+ // a different actor's context if a concurrent request mutates the live fields.
973
+ conversation.currentTurnTrustContext = conversation.trustContext;
974
+ conversation.currentTurnChannelCapabilities =
975
+ conversation.channelCapabilities;
976
+
977
+ // Single activity-state transition for the batched turn. Per-message
978
+ // emissions would publish N "thinking" phase transitions to every
979
+ // connected SSE client (via activityVersion increments), whipsawing the
980
+ // client-side thinking indicator. The single-message path emits exactly
981
+ // one such event per turn; match it here.
982
+ conversation.emitActivityState(
983
+ "thinking",
984
+ "message_dequeued",
985
+ "assistant_turn",
986
+ head.requestId,
987
+ );
988
+
989
+ // Per-message dequeue events and persistence loop. Track the last
990
+ // SUCCESSFUL persist separately from the batch tail — a failed tail
991
+ // must not corrupt the requestId/surface context that `runAgentLoop`
992
+ // will tag `message_complete` / `generation_cancelled` with.
993
+ let lastSuccessfulRequestId: string | undefined;
994
+ let lastSuccessfulActiveSurfaceId: string | undefined;
995
+ let lastSuccessfulCurrentPage: string | undefined;
996
+ let lastSuccessfulContent: string | undefined;
997
+ let lastUserMessageId: string | undefined;
998
+ // Members whose persist succeeded. `fanOutOnEvent` below must only
999
+ // broadcast agent-loop events to these — clients whose persist failed
1000
+ // already received an error event and must not also receive the
1001
+ // assistant's streaming response for a turn that isn't theirs.
1002
+ const successfulBatch: QueuedMessage[] = [];
1003
+ for (let i = 0; i < batch.length; i++) {
1004
+ const qm = batch[i];
1005
+ qm.onEvent({
1006
+ type: "message_dequeued",
1007
+ conversationId: conversation.conversationId,
1008
+ requestId: qm.requestId,
1009
+ });
1010
+ conversation.traceEmitter.emit(
1011
+ "request_dequeued",
1012
+ "Message dequeued (batched)",
1013
+ {
1014
+ requestId: qm.requestId,
1015
+ status: "info",
1016
+ attributes: { reason, batchIndex: i, batchSize: batch.length },
1017
+ },
1018
+ );
1019
+
1020
+ const qmSlash = await resolveSlash(
1021
+ qm.content,
1022
+ buildSlashContext(conversation),
1023
+ );
1024
+ if (qmSlash.kind !== "passthrough") {
1025
+ // Defensive recovery. `buildPassthroughBatch` should make this
1026
+ // unreachable, but if it ever fires we must avoid stranding
1027
+ // per-turn state and dropping the batch tails that have already
1028
+ // been shifted out of the queue. Log, emit an error to the
1029
+ // affected client, and either recover-and-drain (head case) or
1030
+ // skip the tail (tail case) so the rest of the batch still runs.
1031
+ const invariantMessage =
1032
+ "Internal error: batch drain invariant violated (non-passthrough message in batch)";
1033
+ log.error(
1034
+ {
1035
+ conversationId: conversation.conversationId,
1036
+ requestId: qm.requestId,
1037
+ batchIndex: i,
1038
+ batchSize: batch.length,
1039
+ slashKind: qmSlash.kind,
1040
+ },
1041
+ "drainBatch invariant violated — non-passthrough message found in batch",
1042
+ );
1043
+ conversation.traceEmitter.emit("request_error", invariantMessage, {
1044
+ requestId: qm.requestId,
1045
+ status: "error",
1046
+ attributes: { reason: "batch_invariant_violation" },
1047
+ });
1048
+ qm.onEvent({ type: "error", message: invariantMessage });
1049
+ if (i === 0) {
1050
+ // Head invariant fired — no in-flight turn yet (the check runs
1051
+ // before persistUserMessage, so the head was never persisted).
1052
+ // Clear per-turn state and recursively drain the remaining tails,
1053
+ // which were already shifted out of the queue by
1054
+ // buildPassthroughBatch and would otherwise be stranded. Mirrors
1055
+ // the head persist-failure recovery below.
1056
+ conversation.processing = false;
1057
+ conversation.abortController = null;
1058
+ conversation.currentRequestId = undefined;
1059
+ conversation.preactivatedSkillIds = undefined;
1060
+ const remaining = batch.slice(1);
1061
+ if (remaining.length >= 2) {
1062
+ await drainBatch(conversation, remaining, reason);
1063
+ } else if (remaining.length === 1) {
1064
+ await drainSingleMessage(conversation, remaining[0], reason);
1065
+ } else {
1066
+ await drainQueue(conversation);
1067
+ }
1068
+ return;
1069
+ }
1070
+ // Tail case — processing is live, just skip this message. Loop
1071
+ // continues to drain any remaining tails.
1072
+ continue;
1073
+ }
1074
+ const qmContent = qmSlash.content;
1075
+
1076
+ try {
1077
+ if (i === 0) {
1078
+ lastUserMessageId = await conversation.persistUserMessage(
1079
+ qmContent,
1080
+ qm.attachments,
1081
+ qm.requestId,
1082
+ { ...qm.metadata, sentAt: qm.sentAt },
1083
+ qm.displayContent,
1084
+ );
1085
+ } else {
1086
+ lastUserMessageId = await persistQueuedMessageBody(
1087
+ conversation,
1088
+ qmContent,
1089
+ qm.attachments,
1090
+ qm.requestId,
1091
+ { ...qm.metadata, sentAt: qm.sentAt },
1092
+ qm.displayContent,
1093
+ );
1094
+ }
1095
+ } catch (err) {
1096
+ const message = err instanceof Error ? err.message : String(err);
1097
+ log.error(
1098
+ {
1099
+ err,
1100
+ conversationId: conversation.conversationId,
1101
+ requestId: qm.requestId,
1102
+ batchIndex: i,
1103
+ },
1104
+ "Failed to persist batched queued message",
1105
+ );
1106
+ conversation.traceEmitter.emit(
1107
+ "request_error",
1108
+ `Queued message persist failed: ${message}`,
1109
+ {
1110
+ requestId: qm.requestId,
1111
+ status: "error",
1112
+ attributes: { reason: "persist_failure" },
1113
+ },
1114
+ );
1115
+ qm.onEvent({ type: "error", message });
1116
+
1117
+ if (i === 0) {
1118
+ // Head persist failed — processing is not set yet, no in-flight turn
1119
+ // to fan tails into. We've already shifted the tails out of the queue
1120
+ // as part of this batch, so if we simply called drainQueue the tails
1121
+ // would be stranded. Reset per-turn state and recursively drain the
1122
+ // remaining tails (they're still valid by the batch invariant).
1123
+ conversation.preactivatedSkillIds = undefined;
1124
+ const remaining = batch.slice(1);
1125
+ if (remaining.length >= 2) {
1126
+ await drainBatch(conversation, remaining, reason);
1127
+ } else if (remaining.length === 1) {
1128
+ await drainSingleMessage(conversation, remaining[0], reason);
1129
+ } else {
1130
+ await drainQueue(conversation);
1131
+ }
1132
+ return;
1133
+ }
1134
+ // Tail persist failed — we cannot abandon the batch without stranding
1135
+ // the head's in-flight turn. Processing state is already set; skip
1136
+ // this message and continue accumulating siblings. The emitted error
1137
+ // event lets the tail client see the failure. Crucially we do NOT
1138
+ // update lastSuccessful* here, so runAgentLoop tags completion with
1139
+ // the most recent successfully-persisted message's requestId.
1140
+ continue;
1141
+ }
1142
+
1143
+ // Broadcast the user message to all hub subscribers so passive devices
1144
+ // see each batched user turn before the assistant reply starts streaming.
1145
+ qm.onEvent({
1146
+ type: "user_message_echo",
1147
+ text: qmContent,
1148
+ conversationId: conversation.conversationId,
1149
+ messageId: lastUserMessageId,
1150
+ requestId: qm.requestId,
1151
+ });
1152
+
1153
+ // Persist succeeded. Update last-successful markers so a later tail
1154
+ // failure won't overwrite them.
1155
+ lastSuccessfulRequestId = qm.requestId;
1156
+ lastSuccessfulActiveSurfaceId = qm.activeSurfaceId;
1157
+ lastSuccessfulCurrentPage = qm.currentPage;
1158
+ lastSuccessfulContent = qmContent;
1159
+ successfulBatch.push(qm);
1160
+
1161
+ // Fire-and-forget: detect notification preferences in each batched user
1162
+ // message and persist any that are found, mirroring drainSingleMessage.
1163
+ if (conversation.assistantId) {
1164
+ extractPreferences(qmContent)
1165
+ .then((result) => {
1166
+ if (!result.detected) return;
1167
+ for (const pref of result.preferences) {
1168
+ createPreference({
1169
+ preferenceText: pref.preferenceText,
1170
+ appliesWhen: pref.appliesWhen,
1171
+ priority: pref.priority,
1172
+ });
1173
+ }
1174
+ log.info(
1175
+ {
1176
+ count: result.preferences.length,
1177
+ conversationId: conversation.conversationId,
1178
+ },
1179
+ "Persisted extracted notification preferences (batched)",
1180
+ );
1181
+ })
1182
+ .catch((err) => {
1183
+ const errMsg = err instanceof Error ? err.message : String(err);
1184
+ log.warn(
1185
+ { err: errMsg, conversationId: conversation.conversationId },
1186
+ "Background preference extraction failed (batched)",
1187
+ );
1188
+ });
1189
+ }
1190
+
1191
+ // If the user hit abort mid-batch, stop persisting remaining tails.
1192
+ // runAgentLoop's existing abort handling will emit generation_cancelled
1193
+ // and clear processing state for whatever did persist.
1194
+ if (conversation.abortController?.signal.aborted) {
1195
+ log.info(
1196
+ {
1197
+ conversationId: conversation.conversationId,
1198
+ requestId: qm.requestId,
1199
+ batchIndex: i,
1200
+ batchSize: batch.length,
1201
+ },
1202
+ "drainBatch: abort signaled mid-batch; stopping tail persist",
1203
+ );
1204
+ break;
1205
+ }
1206
+ }
1207
+
1208
+ if (lastUserMessageId === undefined || lastSuccessfulContent === undefined) {
1209
+ // Nothing persisted — either the head's invariant-violation recovery
1210
+ // already drained and returned, or every message failed. Head failure
1211
+ // has its own recovery path above; if we get here it's because a
1212
+ // defensive code path left us with nothing to run. Log and bail.
1213
+ log.error(
1214
+ {
1215
+ conversationId: conversation.conversationId,
1216
+ batchSize: batch.length,
1217
+ },
1218
+ "drainBatch: no messages persisted successfully; skipping runAgentLoop",
1219
+ );
1220
+ conversation.preactivatedSkillIds = undefined;
1221
+ return;
1222
+ }
1223
+
1224
+ // Tag turn-completion state with the last SUCCESSFUL persist so client-
1225
+ // side correlation (message_complete / generation_cancelled /
1226
+ // generation_handoff) surfaces a requestId that actually has a DB row.
1227
+ conversation.currentRequestId = lastSuccessfulRequestId;
1228
+ conversation.currentActiveSurfaceId = lastSuccessfulActiveSurfaceId;
1229
+ conversation.currentPage = lastSuccessfulCurrentPage;
1230
+
1231
+ // Broadcast agent-loop events only to members whose persist succeeded.
1232
+ // Members whose persist failed already received an error event in the
1233
+ // catch block above; sending them the assistant's streaming response
1234
+ // would surface a reply for a user message that isn't in their DB.
1235
+ const fanOutOnEvent = (msg: ServerMessage) => {
1236
+ for (const qm of successfulBatch) qm.onEvent(msg);
1237
+ };
1238
+
1239
+ const drainLoopOptions: {
1240
+ isInteractive?: boolean;
1241
+ isUserMessage?: boolean;
1242
+ titleText?: string;
1243
+ } = { isUserMessage: true };
1244
+ // Source interactive flag from the last successfully-persisted sibling so
1245
+ // a trailing failed tail doesn't flip the agent loop's interactivity.
1246
+ const lastSuccessfulBatchEntry =
1247
+ successfulBatch.length > 0
1248
+ ? successfulBatch[successfulBatch.length - 1]
1249
+ : undefined;
1250
+ if (lastSuccessfulBatchEntry?.isInteractive !== undefined)
1251
+ drainLoopOptions.isInteractive = lastSuccessfulBatchEntry.isInteractive;
1252
+
1253
+ // Fire-and-forget: runAgentLoop's finally block recursively calls drainQueue
1254
+ // when this run completes. Mirrors drainSingleMessage.
1255
+ conversation
1256
+ .runAgentLoop(
1257
+ lastSuccessfulContent,
1258
+ lastUserMessageId,
1259
+ fanOutOnEvent,
1260
+ drainLoopOptions,
1261
+ )
1262
+ .catch((err) => {
1263
+ const message = err instanceof Error ? err.message : String(err);
1264
+ log.error(
1265
+ {
1266
+ err,
1267
+ conversationId: conversation.conversationId,
1268
+ requestId: lastSuccessfulRequestId,
1269
+ batchSize: batch.length,
1270
+ },
1271
+ "Error processing batched queued messages",
1272
+ );
1273
+ fanOutOnEvent({
1274
+ type: "error",
1275
+ message: `Failed to process queued messages: ${message}`,
1276
+ });
1277
+ });
1278
+ }
1279
+
718
1280
  // ── processMessage ───────────────────────────────────────────────────
719
1281
 
720
1282
  /**