@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,5 +1,13 @@
1
1
  import { rmSync, writeFileSync } from "node:fs";
2
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import {
3
+ afterAll,
4
+ afterEach,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ mock,
9
+ test,
10
+ } from "bun:test";
3
11
 
4
12
  import type {
5
13
  AgentEvent,
@@ -95,6 +103,22 @@ mock.module("../config/loader.js", () => ({
95
103
  invalidateConfigCache: () => {},
96
104
  }));
97
105
 
106
+ const mockedConversationHostAccess = new Map<string, boolean>();
107
+
108
+ const capturedAddMessages: Array<{
109
+ id: string;
110
+ role: string;
111
+ content: string;
112
+ metadata?: Record<string, unknown>;
113
+ }> = [];
114
+
115
+ /**
116
+ * Content substrings that should cause `addMessage` to throw — used to
117
+ * simulate a mid-batch persist failure (e.g. a DB error on a specific
118
+ * tail message while its siblings succeed).
119
+ */
120
+ const addMessageShouldThrowForContent = new Set<string>();
121
+
98
122
  mock.module("../prompts/system-prompt.js", () => ({
99
123
  buildSystemPrompt: () => "system prompt",
100
124
  }));
@@ -113,6 +137,7 @@ mock.module("../permissions/trust-store.js", () => ({
113
137
  addRule: () => {},
114
138
  findHighestPriorityRule: () => null,
115
139
  clearCache: () => {},
140
+ patternMatchesCandidate: () => false,
116
141
  }));
117
142
 
118
143
  mock.module("../security/secret-allowlist.js", () => ({
@@ -122,7 +147,16 @@ mock.module("../security/secret-allowlist.js", () => ({
122
147
  mock.module("../memory/conversation-crud.js", () => ({
123
148
  getConversationType: () => "default",
124
149
  setConversationOriginChannelIfUnset: () => {},
150
+ setConversationOriginInterfaceIfUnset: () => {},
125
151
  updateConversationContextWindow: () => {},
152
+ getConversationHostAccess: (conversationId: string) =>
153
+ mockedConversationHostAccess.get(conversationId) ?? false,
154
+ updateConversationHostAccess: (
155
+ conversationId: string,
156
+ hostAccess: boolean,
157
+ ) => {
158
+ mockedConversationHostAccess.set(conversationId, hostAccess);
159
+ },
126
160
  deleteMessageById: () => {},
127
161
  provenanceFromTrustContext: () => ({
128
162
  source: "user",
@@ -140,11 +174,28 @@ mock.module("../memory/conversation-crud.js", () => ({
140
174
  totalEstimatedCost: 0,
141
175
  }),
142
176
  createConversation: () => ({ id: "conv-1" }),
143
- addMessage: (_convId: string, _role: string, _content: string) => {
144
- return { id: `msg-${Date.now()}` };
177
+ addMessage: (
178
+ _convId: string,
179
+ role: string,
180
+ content: string,
181
+ metadata?: Record<string, unknown>,
182
+ ) => {
183
+ // Simulate a persist failure for tests that need to exercise the
184
+ // tail-persist-failed path in drainBatch. Triggered by matching any
185
+ // registered substring against the serialized content payload.
186
+ for (const needle of addMessageShouldThrowForContent) {
187
+ if (content.includes(needle)) {
188
+ throw new Error(`Simulated addMessage failure for content: ${needle}`);
189
+ }
190
+ }
191
+ const id = `msg-${Date.now()}-${capturedAddMessages.length}`;
192
+ capturedAddMessages.push({ id, role, content, metadata });
193
+ return { id };
145
194
  },
146
195
  updateConversationUsage: () => {},
147
196
  updateConversationTitle: () => {},
197
+ getMessageById: () => null,
198
+ getLastUserTimestampBefore: () => 0,
148
199
  }));
149
200
 
150
201
  mock.module("../memory/conversation-queries.js", () => ({
@@ -437,6 +488,7 @@ beforeEach(() => {
437
488
  turnCommitCalls.length = 0;
438
489
  turnCommitHangForever = false;
439
490
  linkAttachmentShouldThrow = false;
491
+ addMessageShouldThrowForContent.clear();
440
492
  });
441
493
 
442
494
  afterAll(() => {
@@ -502,44 +554,73 @@ describe("Conversation message queue", () => {
502
554
  await new Promise((r) => setTimeout(r, 10));
503
555
  });
504
556
 
505
- test("[experimental] queued messages are processed in FIFO order", async () => {
557
+ test("[experimental] queued passthrough siblings drain as a single batched run", async () => {
506
558
  const conversation = makeConversation();
507
559
  await conversation.loadFromDb();
508
560
 
509
- const processedOrder: string[] = [];
510
-
511
- const makeHandler = (label: string) => (e: ServerMessage) => {
512
- if (e.type === "message_complete") processedOrder.push(label);
513
- };
561
+ const events1: ServerMessage[] = [];
562
+ const events2: ServerMessage[] = [];
563
+ const events3: ServerMessage[] = [];
514
564
 
515
565
  // Start first message
516
566
  const p1 = conversation.processMessage(
517
567
  "msg-1",
518
568
  [],
519
- makeHandler("msg-1"),
569
+ (e) => events1.push(e),
520
570
  "req-1",
521
571
  );
522
572
  await waitForPendingRun(1);
523
573
 
524
- // Enqueue two more
525
- conversation.enqueueMessage("msg-2", [], makeHandler("msg-2"), "req-2");
526
- conversation.enqueueMessage("msg-3", [], makeHandler("msg-3"), "req-3");
574
+ // Enqueue two more sibling passthrough messages
575
+ conversation.enqueueMessage("msg-2", [], (e) => events2.push(e), "req-2");
576
+ conversation.enqueueMessage("msg-3", [], (e) => events3.push(e), "req-3");
527
577
  expect(conversation.getQueueDepth()).toBe(2);
528
578
 
529
- // Complete firsttriggers second
579
+ // Complete run 0 drain pulls msg-2 and msg-3 into ONE batched run.
530
580
  resolveRun(0);
531
581
  await p1;
532
582
  await waitForPendingRun(2);
533
583
 
534
- // Complete second triggers third
535
- resolveRun(1);
536
- await waitForPendingRun(3);
584
+ // Exactly two runs total (not three): run 0 = msg-1, run 1 = batched [msg-2, msg-3]
585
+ expect(pendingRuns.length).toBe(2);
537
586
 
538
- // Complete third
539
- resolveRun(2);
587
+ // Each batched client saw its own message_dequeued tagged with its own requestId.
588
+ const dequeued2 = events2.filter((e) => e.type === "message_dequeued");
589
+ expect(dequeued2).toHaveLength(1);
590
+ expect(dequeued2[0]).toEqual({
591
+ type: "message_dequeued",
592
+ conversationId: "conv-1",
593
+ requestId: "req-2",
594
+ });
595
+ const dequeued3 = events3.filter((e) => e.type === "message_dequeued");
596
+ expect(dequeued3).toHaveLength(1);
597
+ expect(dequeued3[0]).toEqual({
598
+ type: "message_dequeued",
599
+ conversationId: "conv-1",
600
+ requestId: "req-3",
601
+ });
602
+
603
+ // The batched run's captured history carries both siblings. Either as
604
+ // separate user entries (raw history) or merged into one user entry
605
+ // (after history-repair's alternation enforcement — required by the
606
+ // Anthropic API). Either way, both msg-2 and msg-3 text must appear.
607
+ const batchedHistory = pendingRuns[1].messages;
608
+ const userMessages = batchedHistory.filter((m) => m.role === "user");
609
+ const textOf = (m: Message) =>
610
+ (Array.isArray(m.content) ? m.content : [])
611
+ .filter((b) => b.type === "text")
612
+ .map((b) => (b as { text: string }).text)
613
+ .join("\n");
614
+ const combinedUserText = userMessages.map(textOf).join("\n");
615
+ expect(combinedUserText).toContain("msg-2");
616
+ expect(combinedUserText).toContain("msg-3");
617
+
618
+ // Resolve the batched run; message_complete must fan out to both clients.
619
+ resolveRun(1);
540
620
  await new Promise((r) => setTimeout(r, 10));
541
621
 
542
- expect(processedOrder).toEqual(["msg-1", "msg-2", "msg-3"]);
622
+ expect(events2.some((e) => e.type === "message_complete")).toBe(true);
623
+ expect(events3.some((e) => e.type === "message_complete")).toBe(true);
543
624
  });
544
625
 
545
626
  test("message_queued and message_dequeued events are emitted", async () => {
@@ -680,27 +761,17 @@ describe("Conversation message queue", () => {
680
761
  conversation.enqueueMessage("msg-4", [], () => {}, "req-4");
681
762
  expect(conversation.getQueueDepth()).toBe(3);
682
763
 
683
- // Complete first → drains one from queue
764
+ // Complete first → drain pulls all three same-interface passthroughs
765
+ // into a single batched run (depth → 0, runs → 2 total).
684
766
  resolveRun(0);
685
767
  await p1;
686
768
  await waitForPendingRun(2);
687
769
 
688
- expect(conversation.getQueueDepth()).toBe(2);
689
-
690
- // Complete second → drains another
691
- resolveRun(1);
692
- await waitForPendingRun(3);
693
-
694
- expect(conversation.getQueueDepth()).toBe(1);
695
-
696
- // Complete third → drains last
697
- resolveRun(2);
698
- await waitForPendingRun(4);
699
-
700
770
  expect(conversation.getQueueDepth()).toBe(0);
771
+ expect(pendingRuns.length).toBe(2);
701
772
 
702
- // Complete fourth (final queued message)
703
- resolveRun(3);
773
+ // Complete the batched run; conversation finishes cleanly.
774
+ resolveRun(1);
704
775
  await new Promise((r) => setTimeout(r, 10));
705
776
  });
706
777
 
@@ -754,6 +825,763 @@ describe("Conversation message queue", () => {
754
825
  });
755
826
  });
756
827
 
828
+ // ---------------------------------------------------------------------------
829
+ // Batched drain — mixed-interface, slash-in-middle, attachments, byte budget
830
+ // ---------------------------------------------------------------------------
831
+
832
+ describe("Batched drain", () => {
833
+ beforeEach(() => {
834
+ pendingRuns = [];
835
+ });
836
+
837
+ test("mixed-interface queue splits into multiple batches at each interface boundary", async () => {
838
+ const conversation = makeConversation();
839
+ await conversation.loadFromDb();
840
+
841
+ const events2: ServerMessage[] = [];
842
+ const events3: ServerMessage[] = [];
843
+ const events4: ServerMessage[] = [];
844
+ const events5: ServerMessage[] = [];
845
+
846
+ // Start in-flight message (msg-1)
847
+ const p1 = conversation.processMessage("msg-1", [], () => {}, "req-1");
848
+ await waitForPendingRun(1);
849
+
850
+ // Enqueue 4 messages with interfaces [macos, macos, cli, macos].
851
+ // Expected drain: [macos batch of 2] → [cli single] → [macos single].
852
+ const meta = (iface: string) => ({
853
+ userMessageInterface: iface,
854
+ assistantMessageInterface: iface,
855
+ });
856
+ conversation.enqueueMessage(
857
+ "msg-2",
858
+ [],
859
+ (e) => events2.push(e),
860
+ "req-2",
861
+ undefined,
862
+ undefined,
863
+ meta("macos"),
864
+ );
865
+ conversation.enqueueMessage(
866
+ "msg-3",
867
+ [],
868
+ (e) => events3.push(e),
869
+ "req-3",
870
+ undefined,
871
+ undefined,
872
+ meta("macos"),
873
+ );
874
+ conversation.enqueueMessage(
875
+ "msg-4",
876
+ [],
877
+ (e) => events4.push(e),
878
+ "req-4",
879
+ undefined,
880
+ undefined,
881
+ meta("cli"),
882
+ );
883
+ conversation.enqueueMessage(
884
+ "msg-5",
885
+ [],
886
+ (e) => events5.push(e),
887
+ "req-5",
888
+ undefined,
889
+ undefined,
890
+ meta("macos"),
891
+ );
892
+ expect(conversation.getQueueDepth()).toBe(4);
893
+
894
+ // Resolve msg-1 → batched run pulls macos msg-2 + msg-3.
895
+ resolveRun(0);
896
+ await p1;
897
+ await waitForPendingRun(2);
898
+
899
+ // Batched run's history must contain both macos messages (either as
900
+ // separate user entries or merged into one after history-repair).
901
+ const macosBatchedHistory = pendingRuns[1].messages;
902
+ const macosUserMessages = macosBatchedHistory.filter(
903
+ (m) => m.role === "user",
904
+ );
905
+ const textOf = (m: Message) =>
906
+ (Array.isArray(m.content) ? m.content : [])
907
+ .filter((b) => b.type === "text")
908
+ .map((b) => (b as { text: string }).text)
909
+ .join("\n");
910
+ const combinedMacosText = macosUserMessages.map(textOf).join("\n");
911
+ expect(combinedMacosText).toContain("msg-2");
912
+ expect(combinedMacosText).toContain("msg-3");
913
+
914
+ // Both msg-2 and msg-3 received their own dequeue event.
915
+ expect(events2.filter((e) => e.type === "message_dequeued")).toHaveLength(
916
+ 1,
917
+ );
918
+ expect(events3.filter((e) => e.type === "message_dequeued")).toHaveLength(
919
+ 1,
920
+ );
921
+
922
+ // Resolve the batched run → drain pulls the cli single-message run.
923
+ resolveRun(1);
924
+ await waitForPendingRun(3);
925
+
926
+ // cli run contains msg-4 as a single-message run.
927
+ const cliHistory = pendingRuns[2].messages;
928
+ const cliUserText = cliHistory
929
+ .filter((m) => m.role === "user")
930
+ .map(textOf)
931
+ .join("\n");
932
+ expect(cliUserText).toContain("msg-4");
933
+ expect(events4.filter((e) => e.type === "message_dequeued")).toHaveLength(
934
+ 1,
935
+ );
936
+
937
+ // Resolve the cli run → drain pulls the final macos single-message run.
938
+ resolveRun(2);
939
+ await waitForPendingRun(4);
940
+ const finalHistory = pendingRuns[3].messages;
941
+ const finalUserText = finalHistory
942
+ .filter((m) => m.role === "user")
943
+ .map(textOf)
944
+ .join("\n");
945
+ expect(finalUserText).toContain("msg-5");
946
+ expect(events5.filter((e) => e.type === "message_dequeued")).toHaveLength(
947
+ 1,
948
+ );
949
+
950
+ // Four total runs: msg-1, batched [msg-2, msg-3], msg-4, msg-5.
951
+ expect(pendingRuns.length).toBe(4);
952
+
953
+ resolveRun(3);
954
+ await new Promise((r) => setTimeout(r, 10));
955
+ });
956
+
957
+ test("slash-in-middle splits the queue at the slash boundary", async () => {
958
+ const conversation = makeConversation();
959
+ await conversation.loadFromDb();
960
+
961
+ const eventsHello: ServerMessage[] = [];
962
+ const eventsSlash: ServerMessage[] = [];
963
+ const eventsWorld: ServerMessage[] = [];
964
+
965
+ // Start in-flight message
966
+ const p1 = conversation.processMessage("msg-1", [], () => {}, "req-1");
967
+ await waitForPendingRun(1);
968
+
969
+ // Enqueue ["hello", "/compact", "world"]. /compact resolves to a non-
970
+ // passthrough slash, so the batch builder stops at "hello" (length 1),
971
+ // then /compact takes the single-message /compact short-circuit path
972
+ // (no new runAgentLoop invocation), then "world" drains as its own run.
973
+ conversation.enqueueMessage(
974
+ "hello",
975
+ [],
976
+ (e) => eventsHello.push(e),
977
+ "req-hello",
978
+ );
979
+ conversation.enqueueMessage(
980
+ "/compact",
981
+ [],
982
+ (e) => eventsSlash.push(e),
983
+ "req-slash",
984
+ );
985
+ conversation.enqueueMessage(
986
+ "world",
987
+ [],
988
+ (e) => eventsWorld.push(e),
989
+ "req-world",
990
+ );
991
+ expect(conversation.getQueueDepth()).toBe(3);
992
+
993
+ // Resolve msg-1 → drain pulls "hello" as its own run (batch stops at
994
+ // /compact boundary).
995
+ resolveRun(0);
996
+ await p1;
997
+ await waitForPendingRun(2);
998
+
999
+ expect(pendingRuns.length).toBe(2);
1000
+ expect(eventsHello.some((e) => e.type === "message_dequeued")).toBe(true);
1001
+ // /compact and "world" are still queued.
1002
+ expect(conversation.getQueueDepth()).toBe(2);
1003
+
1004
+ // Resolve "hello" → drain pops /compact via the builder-rejected path,
1005
+ // runs its short-circuit (no new runAgentLoop), then drains "world".
1006
+ resolveRun(1);
1007
+ await waitForPendingRun(3);
1008
+
1009
+ // /compact should have emitted its own message_complete via the short-
1010
+ // circuit path (not via a runAgentLoop run).
1011
+ expect(eventsSlash.some((e) => e.type === "message_complete")).toBe(true);
1012
+ expect(eventsWorld.some((e) => e.type === "message_dequeued")).toBe(true);
1013
+ expect(pendingRuns.length).toBe(3);
1014
+
1015
+ resolveRun(2);
1016
+ await new Promise((r) => setTimeout(r, 10));
1017
+ });
1018
+
1019
+ test("unknown-slash in middle splits the queue at the unknown-slash boundary", async () => {
1020
+ // Covers the `kind: "unknown"` short-circuit branch in drainSingleMessage
1021
+ // specifically. The sibling /compact-in-middle test covers the `kind:
1022
+ // "compact"` short-circuit (via a different code path), so this test
1023
+ // exists to guarantee the batch builder also stops at unknown-kind
1024
+ // boundaries and that the unknown-slash drain path does NOT invoke a new
1025
+ // runAgentLoop run.
1026
+ //
1027
+ // We use `/status`, which the real `resolveSlash` returns as
1028
+ // `{ kind: "unknown", message: <status report> }` when a SlashContext is
1029
+ // present (always true for queued drains via buildSlashContext).
1030
+ const conversation = makeConversation();
1031
+ await conversation.loadFromDb();
1032
+
1033
+ const eventsPlainA: ServerMessage[] = [];
1034
+ const eventsSlash: ServerMessage[] = [];
1035
+ const eventsPlainB: ServerMessage[] = [];
1036
+
1037
+ // Start in-flight message
1038
+ const p1 = conversation.processMessage("msg-1", [], () => {}, "req-1");
1039
+ await waitForPendingRun(1);
1040
+
1041
+ // Enqueue ["plain-a", "/status", "plain-b"]. /status resolves to a non-
1042
+ // passthrough slash (kind: "unknown"), so the batch builder stops at
1043
+ // "plain-a" (length-1 batch → drainSingleMessage), then /status takes the
1044
+ // unknown-slash short-circuit path (no new runAgentLoop invocation — it
1045
+ // emits assistant_text_delta + message_complete inline), then "plain-b"
1046
+ // drains as its own run.
1047
+ conversation.enqueueMessage(
1048
+ "plain-a",
1049
+ [],
1050
+ (e) => eventsPlainA.push(e),
1051
+ "req-plain-a",
1052
+ );
1053
+ conversation.enqueueMessage(
1054
+ "/status",
1055
+ [],
1056
+ (e) => eventsSlash.push(e),
1057
+ "req-slash",
1058
+ );
1059
+ conversation.enqueueMessage(
1060
+ "plain-b",
1061
+ [],
1062
+ (e) => eventsPlainB.push(e),
1063
+ "req-plain-b",
1064
+ );
1065
+ expect(conversation.getQueueDepth()).toBe(3);
1066
+
1067
+ // Resolve msg-1 → drain pulls "plain-a" as its own run (batch stops at
1068
+ // the /status boundary).
1069
+ resolveRun(0);
1070
+ await p1;
1071
+ await waitForPendingRun(2);
1072
+
1073
+ expect(pendingRuns.length).toBe(2);
1074
+ expect(eventsPlainA.some((e) => e.type === "message_dequeued")).toBe(true);
1075
+ // /status and "plain-b" are still queued.
1076
+ expect(conversation.getQueueDepth()).toBe(2);
1077
+
1078
+ // Resolve "plain-a" → drain pops /status via the builder-rejected path,
1079
+ // runs its unknown-slash short-circuit (no new runAgentLoop, emits
1080
+ // assistant_text_delta + message_complete inline), then drains "plain-b"
1081
+ // as its own run.
1082
+ resolveRun(1);
1083
+ await waitForPendingRun(3);
1084
+
1085
+ // /status should have emitted its own assistant_text_delta + message_complete
1086
+ // via the unknown-slash short-circuit path (not via a runAgentLoop run).
1087
+ expect(eventsSlash.some((e) => e.type === "assistant_text_delta")).toBe(
1088
+ true,
1089
+ );
1090
+ expect(eventsSlash.some((e) => e.type === "message_complete")).toBe(true);
1091
+ expect(eventsPlainB.some((e) => e.type === "message_dequeued")).toBe(true);
1092
+ // Only three runs total: msg-1, "plain-a", "plain-b". /status short-circuits
1093
+ // without a runAgentLoop invocation.
1094
+ expect(pendingRuns.length).toBe(3);
1095
+
1096
+ resolveRun(2);
1097
+ await new Promise((r) => setTimeout(r, 10));
1098
+ });
1099
+
1100
+ test("attachments are preserved across a batched drain", async () => {
1101
+ capturedAddMessages.length = 0;
1102
+ const conversation = makeConversation();
1103
+ await conversation.loadFromDb();
1104
+
1105
+ // Start in-flight message
1106
+ const p1 = conversation.processMessage("msg-1", [], () => {}, "req-1");
1107
+ await waitForPendingRun(1);
1108
+
1109
+ // Two sibling messages, each with a distinct image attachment.
1110
+ const attachA = [
1111
+ {
1112
+ id: "att-a",
1113
+ filename: "a.png",
1114
+ mimeType: "image/png",
1115
+ data: Buffer.from("imageA").toString("base64"),
1116
+ filePath: "/tmp/a.png",
1117
+ },
1118
+ ];
1119
+ const attachB = [
1120
+ {
1121
+ id: "att-b",
1122
+ filename: "b.png",
1123
+ mimeType: "image/png",
1124
+ data: Buffer.from("imageB").toString("base64"),
1125
+ filePath: "/tmp/b.png",
1126
+ },
1127
+ ];
1128
+ conversation.enqueueMessage("with-A", attachA, () => {}, "req-A");
1129
+ conversation.enqueueMessage("with-B", attachB, () => {}, "req-B");
1130
+ expect(conversation.getQueueDepth()).toBe(2);
1131
+
1132
+ resolveRun(0);
1133
+ await p1;
1134
+ await waitForPendingRun(2);
1135
+
1136
+ // Two persisted user rows in the DB (one per batched message), each with
1137
+ // its own imageSourcePaths metadata keyed by the right filename.
1138
+ const userRows = capturedAddMessages.filter(
1139
+ (m) => m.role === "user" && m.content.includes('"image"'),
1140
+ );
1141
+ expect(userRows).toHaveLength(2);
1142
+ const pathsA = (userRows[0].metadata as Record<string, unknown>)
1143
+ ?.imageSourcePaths as Record<string, string> | undefined;
1144
+ expect(pathsA).toBeDefined();
1145
+ expect(pathsA!["0:a.png"]).toBe("/tmp/a.png");
1146
+ const pathsB = (userRows[1].metadata as Record<string, unknown>)
1147
+ ?.imageSourcePaths as Record<string, string> | undefined;
1148
+ expect(pathsB).toBeDefined();
1149
+ expect(pathsB!["0:b.png"]).toBe("/tmp/b.png");
1150
+
1151
+ // The batched run's in-memory history also reflects both image sources
1152
+ // (enrichMessageWithSourcePaths injects file:// references for images).
1153
+ const batchedHistory = pendingRuns[1].messages;
1154
+ const userMessages = batchedHistory.filter((m) => m.role === "user");
1155
+ const allText = userMessages
1156
+ .map((m) =>
1157
+ (Array.isArray(m.content) ? m.content : [])
1158
+ .filter((b) => b.type === "text")
1159
+ .map((b) => (b as { text: string }).text)
1160
+ .join("\n"),
1161
+ )
1162
+ .join("\n");
1163
+ expect(allText).toContain("a.png");
1164
+ expect(allText).toContain("b.png");
1165
+
1166
+ resolveRun(1);
1167
+ await new Promise((r) => setTimeout(r, 10));
1168
+ });
1169
+
1170
+ test("byte-budget accounting is unchanged by shiftN-based batching", async () => {
1171
+ // Uses a small budget so we can observe reclamation after drain.
1172
+ // Each ~500-char message ≈ 1512 bytes.
1173
+ const conversation = makeConversation();
1174
+ await conversation.loadFromDb();
1175
+
1176
+ const budget = 4000;
1177
+ (conversation as unknown as { queue: MessageQueue }).queue =
1178
+ new MessageQueue(budget);
1179
+
1180
+ // Start in-flight so subsequent enqueues are queued (not processed).
1181
+ const p1 = conversation.processMessage("msg-1", [], () => {}, "req-1");
1182
+ await waitForPendingRun(1);
1183
+
1184
+ // Fill to just-under budget: two ~500-char messages (1512+1512 = 3024 bytes).
1185
+ const accepted1 = conversation.enqueueMessage(
1186
+ "x".repeat(500),
1187
+ [],
1188
+ () => {},
1189
+ "req-big-1",
1190
+ );
1191
+ const accepted2 = conversation.enqueueMessage(
1192
+ "y".repeat(500),
1193
+ [],
1194
+ () => {},
1195
+ "req-big-2",
1196
+ );
1197
+ expect(accepted1.queued).toBe(true);
1198
+ expect(accepted2.queued).toBe(true);
1199
+ // A third would push the queue over budget → rejected. Capture its
1200
+ // onEvent callback so we can verify the queue_full error event reaches
1201
+ // the rejected caller (not just the synchronous return value).
1202
+ const rejectedEvents: ServerMessage[] = [];
1203
+ const rejected = conversation.enqueueMessage(
1204
+ "z".repeat(500),
1205
+ [],
1206
+ (e) => rejectedEvents.push(e),
1207
+ "req-over",
1208
+ );
1209
+ expect(rejected.queued).toBe(false);
1210
+ expect(rejected.rejected).toBe(true);
1211
+ expect(conversation.getQueueDepth()).toBe(2);
1212
+
1213
+ // The rejected caller must have received a `queue_full` error event on
1214
+ // its own onEvent callback — event emission is part of the public
1215
+ // contract, not just the return value.
1216
+ const queueFullErr = rejectedEvents.find(
1217
+ (e) => e.type === "error" && e.category === "queue_full",
1218
+ );
1219
+ expect(queueFullErr).toBeDefined();
1220
+ if (queueFullErr && queueFullErr.type === "error") {
1221
+ expect(queueFullErr.category).toBe("queue_full");
1222
+ expect(typeof queueFullErr.message).toBe("string");
1223
+ expect(queueFullErr.message.length).toBeGreaterThan(0);
1224
+ }
1225
+
1226
+ // Complete in-flight → drain pulls both queued passthroughs as ONE batched run.
1227
+ resolveRun(0);
1228
+ await p1;
1229
+ await waitForPendingRun(2);
1230
+ expect(conversation.getQueueDepth()).toBe(0);
1231
+
1232
+ // Resolve the batched run.
1233
+ resolveRun(1);
1234
+ await new Promise((r) => setTimeout(r, 10));
1235
+
1236
+ // After the full drain, the byte budget must be fully reclaimed — a fresh
1237
+ // round of enqueues up to the budget should succeed again. Spin up another
1238
+ // in-flight message to reach the queueing state.
1239
+ const p2 = conversation.processMessage("msg-2", [], () => {}, "req-2");
1240
+ await waitForPendingRun(3);
1241
+ expect(
1242
+ conversation.enqueueMessage("a".repeat(500), [], () => {}, "req-a")
1243
+ .queued,
1244
+ ).toBe(true);
1245
+ expect(
1246
+ conversation.enqueueMessage("b".repeat(500), [], () => {}, "req-b")
1247
+ .queued,
1248
+ ).toBe(true);
1249
+
1250
+ resolveRun(2);
1251
+ await p2;
1252
+ await waitForPendingRun(4);
1253
+ resolveRun(3);
1254
+ await new Promise((r) => setTimeout(r, 10));
1255
+ });
1256
+ });
1257
+
1258
+ // ---------------------------------------------------------------------------
1259
+ // Batched drain — correctness fixes (surface exclusion, abort, last-successful
1260
+ // tracking, single activity-state emission)
1261
+ // ---------------------------------------------------------------------------
1262
+
1263
+ describe("Batched drain correctness fixes", () => {
1264
+ beforeEach(() => {
1265
+ pendingRuns = [];
1266
+ capturedAddMessages.length = 0;
1267
+ });
1268
+
1269
+ test("surface-action messages are not batched with regular passthroughs", async () => {
1270
+ const conversation = makeConversation();
1271
+ await conversation.loadFromDb();
1272
+
1273
+ const eventsSurface: ServerMessage[] = [];
1274
+ const eventsRegular: ServerMessage[] = [];
1275
+
1276
+ // Start in-flight message
1277
+ const p1 = conversation.processMessage("msg-1", [], () => {}, "req-1");
1278
+ await waitForPendingRun(1);
1279
+
1280
+ // Enqueue a surface-action message (activeSurfaceId set + tracked in
1281
+ // surfaceActionRequestIds) followed by a regular passthrough from the
1282
+ // same interface. The batch builder must reject the surface-action head
1283
+ // so each drains as its own run.
1284
+ conversation.surfaceActionRequestIds.add("req-surface");
1285
+ conversation.enqueueMessage(
1286
+ "surface action response",
1287
+ [],
1288
+ (e) => eventsSurface.push(e),
1289
+ "req-surface",
1290
+ "surface-1", // activeSurfaceId
1291
+ );
1292
+ conversation.enqueueMessage(
1293
+ "regular follow-up",
1294
+ [],
1295
+ (e) => eventsRegular.push(e),
1296
+ "req-regular",
1297
+ );
1298
+ expect(conversation.getQueueDepth()).toBe(2);
1299
+
1300
+ // Complete run 0 → drain must NOT batch the surface-action with the
1301
+ // regular passthrough. Expect the surface-action to drain as a single
1302
+ // run first.
1303
+ resolveRun(0);
1304
+ await p1;
1305
+ await waitForPendingRun(2);
1306
+
1307
+ // The second run is the surface-action single-message run.
1308
+ const surfaceUserRowsAfterRun2 = capturedAddMessages.filter(
1309
+ (m) => m.role === "user" && m.content.includes("surface action response"),
1310
+ );
1311
+ expect(surfaceUserRowsAfterRun2).toHaveLength(1);
1312
+ expect(eventsSurface.filter((e) => e.type === "message_dequeued")).toHaveLength(
1313
+ 1,
1314
+ );
1315
+
1316
+ // Complete the surface-action run; drain pulls the regular passthrough
1317
+ // as its own separate run.
1318
+ resolveRun(1);
1319
+ await waitForPendingRun(3);
1320
+ expect(pendingRuns.length).toBe(3);
1321
+ expect(eventsRegular.filter((e) => e.type === "message_dequeued")).toHaveLength(
1322
+ 1,
1323
+ );
1324
+
1325
+ // Total runs = 3: msg-1, surface-action, regular — NOT 2 (would mean
1326
+ // they were batched).
1327
+ resolveRun(2);
1328
+ await new Promise((r) => setTimeout(r, 10));
1329
+ });
1330
+
1331
+ test("abort mid-batch stops tail persists", async () => {
1332
+ const conversation = makeConversation();
1333
+ await conversation.loadFromDb();
1334
+
1335
+ const events1: ServerMessage[] = [];
1336
+ const events2: ServerMessage[] = [];
1337
+ const events3: ServerMessage[] = [];
1338
+ const events4: ServerMessage[] = [];
1339
+
1340
+ // Start in-flight message
1341
+ const p1 = conversation.processMessage(
1342
+ "msg-1",
1343
+ [],
1344
+ (e) => events1.push(e),
1345
+ "req-1",
1346
+ );
1347
+ await waitForPendingRun(1);
1348
+
1349
+ // Enqueue three sibling passthroughs (msg-2 = head, msg-3 = mid,
1350
+ // msg-4 = tail). We trigger abort from msg-3's dequeue callback —
1351
+ // by the time that fires, msg-2 has already been persisted (which
1352
+ // REPLACED the abortController, since persistUserMessage creates a
1353
+ // fresh one). Calling abort() now aborts that fresh controller, and
1354
+ // the drainBatch loop's abort check after msg-3's persist will break,
1355
+ // so msg-4 never persists.
1356
+ conversation.enqueueMessage("msg-2", [], (e) => events2.push(e), "req-2");
1357
+
1358
+ // Install a one-shot abort trigger on msg-3's dequeue event. We do
1359
+ // this before enqueueing so the wrapped callback is what drainBatch
1360
+ // invokes.
1361
+ let aborted = false;
1362
+ const onMsg3Event = (e: ServerMessage) => {
1363
+ events3.push(e);
1364
+ if (!aborted && e.type === "message_dequeued") {
1365
+ aborted = true;
1366
+ conversation.abort();
1367
+ }
1368
+ };
1369
+ conversation.enqueueMessage("msg-3", [], onMsg3Event, "req-3");
1370
+ conversation.enqueueMessage("msg-4", [], (e) => events4.push(e), "req-4");
1371
+ expect(conversation.getQueueDepth()).toBe(3);
1372
+
1373
+ const persistedUserRowCountBefore = capturedAddMessages.filter(
1374
+ (m) => m.role === "user",
1375
+ ).length;
1376
+
1377
+ // Complete run 0 → drain pulls the sibling batch.
1378
+ resolveRun(0);
1379
+ await p1;
1380
+
1381
+ // Give the drain loop a chance to iterate. Abort happens on msg-3's
1382
+ // dequeue (between msg-2's persist and msg-3's persist), so msg-3 may
1383
+ // still persist before the abort check at the end of its iteration.
1384
+ // Either way, msg-4 must NOT persist.
1385
+ await new Promise((r) => setTimeout(r, 30));
1386
+
1387
+ const userRowsAfter = capturedAddMessages
1388
+ .slice(persistedUserRowCountBefore)
1389
+ .filter((m) => m.role === "user");
1390
+ const contents = userRowsAfter.map((r) => r.content).join("||");
1391
+ expect(contents).toContain("msg-2");
1392
+ expect(contents).not.toContain("msg-4");
1393
+ expect(
1394
+ events4.filter((e) => e.type === "message_dequeued"),
1395
+ ).toHaveLength(0);
1396
+ });
1397
+
1398
+ test("failed tail persist uses last-successful requestId", async () => {
1399
+ const conversation = makeConversation();
1400
+ await conversation.loadFromDb();
1401
+
1402
+ const events1: ServerMessage[] = [];
1403
+ const events2: ServerMessage[] = [];
1404
+ const events3: ServerMessage[] = [];
1405
+ const events4: ServerMessage[] = [];
1406
+
1407
+ // Start in-flight message
1408
+ const p1 = conversation.processMessage(
1409
+ "msg-1",
1410
+ [],
1411
+ (e) => events1.push(e),
1412
+ "req-1",
1413
+ );
1414
+ await waitForPendingRun(1);
1415
+
1416
+ // Enqueue three siblings. Configure addMessage to throw for the second
1417
+ // tail (msg-mid) but succeed for msg-head and msg-tail. This simulates
1418
+ // a middle tail persist failure — currentRequestId should end up as
1419
+ // msg-tail's requestId (the LAST successful persist), not msg-mid's.
1420
+ addMessageShouldThrowForContent.add("msg-mid-unique-marker");
1421
+
1422
+ conversation.enqueueMessage(
1423
+ "msg-head",
1424
+ [],
1425
+ (e) => events2.push(e),
1426
+ "req-head",
1427
+ );
1428
+ conversation.enqueueMessage(
1429
+ "msg-mid-unique-marker",
1430
+ [],
1431
+ (e) => events3.push(e),
1432
+ "req-mid",
1433
+ );
1434
+ conversation.enqueueMessage(
1435
+ "msg-tail",
1436
+ [],
1437
+ (e) => events4.push(e),
1438
+ "req-tail",
1439
+ );
1440
+ expect(conversation.getQueueDepth()).toBe(3);
1441
+
1442
+ // Complete run 0 → batched drain.
1443
+ resolveRun(0);
1444
+ await p1;
1445
+ await waitForPendingRun(2);
1446
+
1447
+ // mid should have emitted an error event via persist failure.
1448
+ const errMid = events3.find((e) => e.type === "error");
1449
+ expect(errMid).toBeDefined();
1450
+
1451
+ // The agent loop should have been invoked with the tail's userMessageId
1452
+ // (last SUCCESSFUL persist), not the mid's. We check via currentRequestId
1453
+ // on the conversation which drainBatch assigns after the loop.
1454
+ expect(
1455
+ (conversation as unknown as { currentRequestId?: string }).currentRequestId,
1456
+ ).toBe("req-tail");
1457
+
1458
+ // Cleanup: resolve the batched run.
1459
+ resolveRun(1);
1460
+ await new Promise((r) => setTimeout(r, 20));
1461
+ });
1462
+
1463
+ test("failed tail persist is excluded from fanOutOnEvent agent events", async () => {
1464
+ const conversation = makeConversation();
1465
+ await conversation.loadFromDb();
1466
+
1467
+ const events1: ServerMessage[] = [];
1468
+ const events2: ServerMessage[] = [];
1469
+ const events3: ServerMessage[] = [];
1470
+ const events4: ServerMessage[] = [];
1471
+
1472
+ const p1 = conversation.processMessage(
1473
+ "msg-1",
1474
+ [],
1475
+ (e) => events1.push(e),
1476
+ "req-1",
1477
+ );
1478
+ await waitForPendingRun(1);
1479
+
1480
+ // Mid tail will fail to persist. After the batched run resolves,
1481
+ // message_complete (broadcast via fanOutOnEvent) must NOT land on the
1482
+ // failed mid tail — it already received an error event and persisting
1483
+ // the assistant reply for a user message that has no DB row would
1484
+ // desync the client.
1485
+ addMessageShouldThrowForContent.add("fanout-mid-marker");
1486
+
1487
+ conversation.enqueueMessage(
1488
+ "fanout-head",
1489
+ [],
1490
+ (e) => events2.push(e),
1491
+ "req-fanout-head",
1492
+ );
1493
+ conversation.enqueueMessage(
1494
+ "fanout-mid-marker",
1495
+ [],
1496
+ (e) => events3.push(e),
1497
+ "req-fanout-mid",
1498
+ );
1499
+ conversation.enqueueMessage(
1500
+ "fanout-tail",
1501
+ [],
1502
+ (e) => events4.push(e),
1503
+ "req-fanout-tail",
1504
+ );
1505
+
1506
+ resolveRun(0);
1507
+ await p1;
1508
+ await waitForPendingRun(2);
1509
+
1510
+ // Drive the batched run to emit message_complete via fanOutOnEvent.
1511
+ resolveRun(1);
1512
+ await new Promise((r) => setTimeout(r, 20));
1513
+
1514
+ expect(events3.find((e) => e.type === "error")).toBeDefined();
1515
+ expect(events3.find((e) => e.type === "message_complete")).toBeUndefined();
1516
+
1517
+ expect(events2.find((e) => e.type === "message_complete")).toBeDefined();
1518
+ expect(events4.find((e) => e.type === "message_complete")).toBeDefined();
1519
+ });
1520
+
1521
+ test("drainBatch emits exactly one activity-state event for the whole batch", async () => {
1522
+ const activityStates: ServerMessage[] = [];
1523
+ const conversation = makeConversation((msg) => {
1524
+ if ("type" in msg && msg.type === "assistant_activity_state") {
1525
+ activityStates.push(msg);
1526
+ }
1527
+ });
1528
+ await conversation.loadFromDb();
1529
+
1530
+ // Start in-flight message
1531
+ const p1 = conversation.processMessage("msg-1", [], () => {}, "req-1");
1532
+ await waitForPendingRun(1);
1533
+
1534
+ // Snapshot the count before drain so we only compare batch-emitted
1535
+ // transitions (msg-1's processMessage already fired one).
1536
+ const baseline = activityStates.length;
1537
+
1538
+ // Enqueue three sibling passthroughs.
1539
+ conversation.enqueueMessage("msg-2", [], () => {}, "req-2");
1540
+ conversation.enqueueMessage("msg-3", [], () => {}, "req-3");
1541
+ conversation.enqueueMessage("msg-4", [], () => {}, "req-4");
1542
+
1543
+ // Complete run 0 → drain pulls the batched siblings as ONE run.
1544
+ resolveRun(0);
1545
+ await p1;
1546
+ await waitForPendingRun(2);
1547
+
1548
+ // Filter for "message_dequeued" reasons emitted by the batched drain.
1549
+ const batchEmissions = activityStates
1550
+ .slice(baseline)
1551
+ .filter(
1552
+ (m) =>
1553
+ "type" in m &&
1554
+ m.type === "assistant_activity_state" &&
1555
+ (m as { reason?: string }).reason === "message_dequeued",
1556
+ );
1557
+ expect(batchEmissions).toHaveLength(1);
1558
+ expect(batchEmissions[0]).toMatchObject({
1559
+ type: "assistant_activity_state",
1560
+ reason: "message_dequeued",
1561
+ requestId: "req-2", // head's requestId, per the fix
1562
+ });
1563
+
1564
+ resolveRun(1);
1565
+ await new Promise((r) => setTimeout(r, 10));
1566
+ });
1567
+
1568
+ // Defensive recovery path: buildPassthroughBatch is designed to make
1569
+ // the invariant throw unreachable in practice, so neither the head
1570
+ // branch (re-dispatch batch.slice(1) to drainBatch/drainSingleMessage/
1571
+ // drainQueue) nor the tail branch (skip + continue) can fire in normal
1572
+ // operation. Left as a todo so the harness contract is documented
1573
+ // without wedging mainline CI. Covering this would require either
1574
+ // (a) reflecting into drainBatch to short-circuit resolveSlash for a
1575
+ // specific batch entry, or (b) exposing a seam on SlashContext — both
1576
+ // are more invasive than the safety-net value justifies.
1577
+ test.todo(
1578
+ "invariant violation in persist loop triggers error event + recovery, not stranded state",
1579
+ async () => {
1580
+ // no-op: see comment above.
1581
+ },
1582
+ );
1583
+ });
1584
+
757
1585
  // ---------------------------------------------------------------------------
758
1586
  // Queue policy primitives
759
1587
  // ---------------------------------------------------------------------------
@@ -943,32 +1771,31 @@ describe("Conversation checkpoint handoff", () => {
943
1771
  await p1;
944
1772
  });
945
1773
 
946
- test("[experimental] FIFO ordering is preserved through checkpoint handoff", async () => {
1774
+ test("[experimental] checkpoint handoff pulls a batched run for all queued siblings", async () => {
947
1775
  const conversation = makeConversation();
948
1776
  await conversation.loadFromDb();
949
1777
 
950
- const processedOrder: string[] = [];
951
-
952
- const makeHandler = (label: string) => (e: ServerMessage) => {
953
- if (e.type === "message_complete" || e.type === "generation_handoff")
954
- processedOrder.push(label);
955
- };
1778
+ const events1: ServerMessage[] = [];
1779
+ const events2: ServerMessage[] = [];
1780
+ const events3: ServerMessage[] = [];
1781
+ const events4: ServerMessage[] = [];
956
1782
 
957
- // Start first message
1783
+ // Start first message (mid-tool-use — will yield at the next checkpoint)
958
1784
  const p1 = conversation.processMessage(
959
1785
  "msg-1",
960
1786
  [],
961
- makeHandler("msg-1"),
1787
+ (e) => events1.push(e),
962
1788
  "req-1",
963
1789
  );
964
1790
  await waitForPendingRun(1);
965
1791
 
966
- // Enqueue two messages
967
- conversation.enqueueMessage("msg-2", [], makeHandler("msg-2"), "req-2");
968
- conversation.enqueueMessage("msg-3", [], makeHandler("msg-3"), "req-3");
969
- expect(conversation.getQueueDepth()).toBe(2);
1792
+ // Enqueue three sibling passthroughs while msg-1 is mid-turn
1793
+ conversation.enqueueMessage("msg-2", [], (e) => events2.push(e), "req-2");
1794
+ conversation.enqueueMessage("msg-3", [], (e) => events3.push(e), "req-3");
1795
+ conversation.enqueueMessage("msg-4", [], (e) => events4.push(e), "req-4");
1796
+ expect(conversation.getQueueDepth()).toBe(3);
970
1797
 
971
- // Simulate the agent loop yielding at the checkpoint (first run)
1798
+ // Simulate the agent loop yielding at the checkpoint (first run is mid-tool-use)
972
1799
  const run0 = pendingRuns[0];
973
1800
  expect(run0.onCheckpoint).toBeDefined();
974
1801
  const decision = run0.onCheckpoint!({
@@ -983,19 +1810,23 @@ describe("Conversation checkpoint handoff", () => {
983
1810
  resolveRun(0);
984
1811
  await p1;
985
1812
 
986
- // msg-2 should be draining next
1813
+ // The yielded drain pulls ALL THREE queued siblings as ONE batched run —
1814
+ // not three separate runs.
987
1815
  await waitForPendingRun(2);
1816
+ expect(pendingRuns.length).toBe(2);
988
1817
 
989
- // Complete second run (msg-2)
990
- resolveRun(1);
991
- await waitForPendingRun(3);
1818
+ // Each client saw its own message_dequeued tagged with its own requestId.
1819
+ expect(events2.some((e) => e.type === "message_dequeued")).toBe(true);
1820
+ expect(events3.some((e) => e.type === "message_dequeued")).toBe(true);
1821
+ expect(events4.some((e) => e.type === "message_dequeued")).toBe(true);
992
1822
 
993
- // Complete third run (msg-3)
994
- resolveRun(2);
1823
+ // Resolve the batched run — message_complete fans out to all three clients.
1824
+ resolveRun(1);
995
1825
  await new Promise((r) => setTimeout(r, 10));
996
1826
 
997
- // FIFO order: msg-1 completes first, then msg-2, then msg-3
998
- expect(processedOrder).toEqual(["msg-1", "msg-2", "msg-3"]);
1827
+ expect(events2.some((e) => e.type === "message_complete")).toBe(true);
1828
+ expect(events3.some((e) => e.type === "message_complete")).toBe(true);
1829
+ expect(events4.some((e) => e.type === "message_complete")).toBe(true);
999
1830
  });
1000
1831
 
1001
1832
  test("[experimental] active run with repeated tool turns + queued message triggers checkpoint handoff", async () => {
@@ -1081,10 +1912,39 @@ describe("Conversation checkpoint handoff", () => {
1081
1912
  );
1082
1913
  await waitForPendingRun(1);
1083
1914
 
1084
- // Enqueue messages B, C, D
1085
- conversation.enqueueMessage("msg-B", [], makeHandler("B"), "req-B");
1086
- conversation.enqueueMessage("msg-C", [], makeHandler("C"), "req-C");
1087
- conversation.enqueueMessage("msg-D", [], makeHandler("D"), "req-D");
1915
+ // Enqueue messages B, C, D — each on a distinct userMessageInterface so the
1916
+ // batch builder stops at each boundary and we see one run per message.
1917
+ const meta = (iface: string) => ({
1918
+ userMessageInterface: iface,
1919
+ assistantMessageInterface: iface,
1920
+ });
1921
+ conversation.enqueueMessage(
1922
+ "msg-B",
1923
+ [],
1924
+ makeHandler("B"),
1925
+ "req-B",
1926
+ undefined,
1927
+ undefined,
1928
+ meta("macos"),
1929
+ );
1930
+ conversation.enqueueMessage(
1931
+ "msg-C",
1932
+ [],
1933
+ makeHandler("C"),
1934
+ "req-C",
1935
+ undefined,
1936
+ undefined,
1937
+ meta("cli"),
1938
+ );
1939
+ conversation.enqueueMessage(
1940
+ "msg-D",
1941
+ [],
1942
+ makeHandler("D"),
1943
+ "req-D",
1944
+ undefined,
1945
+ undefined,
1946
+ meta("vellum"),
1947
+ );
1088
1948
  expect(conversation.getQueueDepth()).toBe(3);
1089
1949
 
1090
1950
  // Handoff from A -> B
@@ -1315,8 +2175,18 @@ describe("Terminal trace events on rejection/failure", () => {
1315
2175
  // ---------------------------------------------------------------------------
1316
2176
 
1317
2177
  describe("Conversation host attachment directives", () => {
1318
- beforeEach(() => {
2178
+ beforeEach(async () => {
1319
2179
  pendingRuns = [];
2180
+ mockedConversationHostAccess.clear();
2181
+ const { _setOverridesForTesting } =
2182
+ await import("../config/assistant-feature-flags.js");
2183
+ _setOverridesForTesting({ "permission-controls-v2": true });
2184
+ });
2185
+
2186
+ afterEach(async () => {
2187
+ const { _setOverridesForTesting } =
2188
+ await import("../config/assistant-feature-flags.js");
2189
+ _setOverridesForTesting({});
1320
2190
  });
1321
2191
 
1322
2192
  test("host attachment prompts and resolves when user allows", async () => {
@@ -1364,6 +2234,19 @@ describe("Conversation host attachment directives", () => {
1364
2234
  (e) => e.type === "confirmation_request",
1365
2235
  );
1366
2236
  expect(confirmation).toBeDefined();
2237
+ expect(
2238
+ (confirmation as { persistentDecisionsAllowed?: boolean })
2239
+ .persistentDecisionsAllowed,
2240
+ ).toBe(false);
2241
+ expect(
2242
+ (
2243
+ confirmation as {
2244
+ temporaryOptionsAvailable?: Array<
2245
+ "allow_10m" | "allow_conversation"
2246
+ >;
2247
+ }
2248
+ ).temporaryOptionsAvailable ?? [],
2249
+ ).toEqual([]);
1367
2250
  conversation.handleConfirmationResponse(
1368
2251
  (confirmation as { requestId: string }).requestId,
1369
2252
  "allow",
@@ -1371,6 +2254,7 @@ describe("Conversation host attachment directives", () => {
1371
2254
 
1372
2255
  await p1;
1373
2256
 
2257
+ expect(mockedConversationHostAccess.get("conv-1")).toBe(true);
1374
2258
  expect(conversation.lastAssistantAttachments).toHaveLength(1);
1375
2259
  expect(conversation.lastAssistantAttachments[0].sourceType).toBe(
1376
2260
  "host_file",