@vellumai/assistant 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -0,0 +1,831 @@
1
+ /**
2
+ * Tests for `wakeAgentForOpportunity()` — the generic internal agent-wake
3
+ * mechanism.
4
+ *
5
+ * Exercise strategy: the wake helper takes a `resolveTarget` dependency so
6
+ * these tests stub out the heavyweight `Conversation` class with a minimal
7
+ * `WakeTarget` that just tracks agent-event forwards, buffered messages,
8
+ * persisted tail messages, drain invocations, and a scripted
9
+ * `agentLoop.run()` response.
10
+ *
11
+ * Persistence is now delegated to `WakeTarget.persistTailMessage` (the
12
+ * daemon adapter is responsible for building channel/interface metadata
13
+ * and disk-view sync — out of scope for runtime tests), so we assert on
14
+ * the calls received by the mock instead of stubbing
15
+ * `memory/conversation-crud.js`.
16
+ */
17
+
18
+ import { beforeEach, describe, expect, test } from "bun:test";
19
+
20
+ import type { AgentEvent } from "../../agent/loop.js";
21
+ import type { Message } from "../../providers/types.js";
22
+ import {
23
+ __resetWakeChainForTests,
24
+ wakeAgentForOpportunity,
25
+ type WakeTarget,
26
+ } from "../agent-wake.js";
27
+
28
+ // ── Test helpers ─────────────────────────────────────────────────────
29
+
30
+ interface MockTarget extends WakeTarget {
31
+ emittedEvents: AgentEvent[];
32
+ pushedMessages: Message[];
33
+ runCalls: Array<{ input: Message[]; requestId?: string }>;
34
+ processingToggles: boolean[];
35
+ /** Tail messages handed to `persistTailMessage`, in call order. */
36
+ persistedTailCalls: Message[];
37
+ /** Number of times `drainQueue` was invoked. */
38
+ drainQueueCalls: number;
39
+ /**
40
+ * Cross-hook call sequence tag. Each push/persist/drain (and the
41
+ * processing toggles that bracket them) appends an entry so tests can
42
+ * assert end-to-end ordering, not just per-hook counts.
43
+ */
44
+ callSequence: string[];
45
+ /**
46
+ * Snapshot of `processing` at the moment `drainQueue` was invoked.
47
+ * Lets tests prove drain ran AFTER markProcessing(false), rather than
48
+ * just inferring it from the order of recorded toggles.
49
+ */
50
+ processingDuringDrain: boolean[];
51
+ }
52
+
53
+ function makeTarget(options: {
54
+ conversationId?: string;
55
+ baseline?: Message[];
56
+ scriptedAssistant?: Message | null;
57
+ /** Extra tail messages appended *after* `scriptedAssistant` (e.g. tool_result, follow-up assistant). */
58
+ scriptedTail?: Message[];
59
+ scriptedEvents?: AgentEvent[];
60
+ isProcessing?: boolean;
61
+ /** When true, omit `drainQueue` so we can verify the wake handles its absence. */
62
+ omitDrainQueue?: boolean;
63
+ }): MockTarget {
64
+ const emittedEvents: AgentEvent[] = [];
65
+ const pushedMessages: Message[] = [];
66
+ const runCalls: Array<{ input: Message[]; requestId?: string }> = [];
67
+ const processingToggles: boolean[] = [];
68
+ const persistedTailCalls: Message[] = [];
69
+ const callSequence: string[] = [];
70
+ const processingDuringDrain: boolean[] = [];
71
+ const history: Message[] = [...(options.baseline ?? [])];
72
+ let processing = options.isProcessing ?? false;
73
+ let drainQueueCalls = 0;
74
+
75
+ const target: MockTarget = {
76
+ conversationId: options.conversationId ?? "conv-test",
77
+ emittedEvents,
78
+ pushedMessages,
79
+ runCalls,
80
+ processingToggles,
81
+ persistedTailCalls,
82
+ callSequence,
83
+ processingDuringDrain,
84
+ get drainQueueCalls() {
85
+ return drainQueueCalls;
86
+ },
87
+ agentLoop: {
88
+ run: async (
89
+ input: Message[],
90
+ onEvent: (event: AgentEvent) => void | Promise<void>,
91
+ _signal?: AbortSignal,
92
+ requestId?: string,
93
+ ) => {
94
+ runCalls.push({ input: [...input], requestId });
95
+ // Emit any scripted events the test wanted us to produce.
96
+ for (const ev of options.scriptedEvents ?? []) {
97
+ await onEvent(ev);
98
+ }
99
+ // Final history = input + optional assistant message + optional tail.
100
+ const next = [...input];
101
+ if (options.scriptedAssistant) {
102
+ next.push(options.scriptedAssistant);
103
+ await onEvent({
104
+ type: "message_complete",
105
+ message: options.scriptedAssistant,
106
+ });
107
+ }
108
+ if (options.scriptedTail) {
109
+ for (const tailMsg of options.scriptedTail) {
110
+ next.push(tailMsg);
111
+ }
112
+ }
113
+ return next;
114
+ },
115
+ },
116
+ getMessages: () => history,
117
+ pushMessage: (msg: Message) => {
118
+ pushedMessages.push(msg);
119
+ history.push(msg);
120
+ callSequence.push("push");
121
+ },
122
+ emitAgentEvent: (event) => {
123
+ emittedEvents.push(event);
124
+ },
125
+ isProcessing: () => processing,
126
+ markProcessing: (on: boolean) => {
127
+ processing = on;
128
+ processingToggles.push(on);
129
+ callSequence.push(on ? "processing:true" : "processing:false");
130
+ },
131
+ persistTailMessage: async (msg: Message) => {
132
+ persistedTailCalls.push(msg);
133
+ callSequence.push("persist");
134
+ },
135
+ ...(options.omitDrainQueue
136
+ ? {}
137
+ : {
138
+ drainQueue: async () => {
139
+ drainQueueCalls++;
140
+ // Snapshot the live processing flag *inside* drain, not via
141
+ // the toggle log, so we directly observe the state visible
142
+ // to the dequeued message's enqueueMessage() gate.
143
+ processingDuringDrain.push(processing);
144
+ callSequence.push("drain");
145
+ },
146
+ }),
147
+ };
148
+
149
+ // Expose processing setter via test-only side-channel for tests that
150
+ // simulate an external (non-wake) processing state.
151
+ (target as unknown as { setProcessing: (v: boolean) => void }).setProcessing =
152
+ (v: boolean) => {
153
+ processing = v;
154
+ };
155
+
156
+ return target;
157
+ }
158
+
159
+ beforeEach(() => {
160
+ __resetWakeChainForTests();
161
+ });
162
+
163
+ // ── Tests ────────────────────────────────────────────────────────────
164
+
165
+ describe("wakeAgentForOpportunity", () => {
166
+ test("silent no-op when agent produces no tool calls and no text", async () => {
167
+ const target = makeTarget({
168
+ baseline: [
169
+ { role: "user", content: [{ type: "text", text: "hi" }] },
170
+ { role: "assistant", content: [{ type: "text", text: "hello" }] },
171
+ ],
172
+ // Assistant replies with empty text — counts as no output.
173
+ scriptedAssistant: {
174
+ role: "assistant",
175
+ content: [{ type: "text", text: "" }],
176
+ },
177
+ });
178
+
179
+ const result = await wakeAgentForOpportunity(
180
+ {
181
+ conversationId: target.conversationId,
182
+ hint: "someone asked a question",
183
+ source: "unit-test",
184
+ },
185
+ { resolveTarget: async () => target },
186
+ );
187
+
188
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
189
+ // Nothing emitted to client.
190
+ expect(target.emittedEvents).toHaveLength(0);
191
+ // Nothing persisted.
192
+ expect(target.persistedTailCalls).toHaveLength(0);
193
+ // Nothing pushed into live history.
194
+ expect(target.pushedMessages).toHaveLength(0);
195
+ // Hint was included in the run input, but baseline is unchanged.
196
+ expect(target.runCalls).toHaveLength(1);
197
+ const input = target.runCalls[0]!.input;
198
+ expect(input).toHaveLength(3); // 2 baseline + 1 hint
199
+ expect(input[2]).toEqual({
200
+ role: "user",
201
+ content: [
202
+ { type: "text", text: "[opportunity:unit-test] someone asked a question" },
203
+ ],
204
+ });
205
+ });
206
+
207
+ test("produces tool calls when LLM emits a tool_use block", async () => {
208
+ const assistantMessage: Message = {
209
+ role: "assistant",
210
+ content: [
211
+ {
212
+ type: "tool_use",
213
+ id: "tu-1",
214
+ name: "meet_send_chat",
215
+ input: { text: "Sure, here's the link" },
216
+ },
217
+ ],
218
+ };
219
+ const target = makeTarget({
220
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
221
+ scriptedAssistant: assistantMessage,
222
+ });
223
+
224
+ const result = await wakeAgentForOpportunity(
225
+ {
226
+ conversationId: target.conversationId,
227
+ hint: "question directed at assistant",
228
+ source: "meet-chat-opportunity",
229
+ },
230
+ { resolveTarget: async () => target },
231
+ );
232
+
233
+ expect(result).toEqual({ invoked: true, producedToolCalls: true });
234
+ // Assistant message persisted via the target hook.
235
+ expect(target.persistedTailCalls).toHaveLength(1);
236
+ expect(target.persistedTailCalls[0]).toEqual(assistantMessage);
237
+ // Assistant message pushed into live history.
238
+ expect(target.pushedMessages).toContainEqual(assistantMessage);
239
+ // message_complete event flushed to the client via the translator
240
+ // surface (raw AgentEvent — adapter is responsible for wire shape).
241
+ const flushed = target.emittedEvents.find(
242
+ (e) => e.type === "message_complete",
243
+ );
244
+ expect(flushed).toBeDefined();
245
+ });
246
+
247
+ test("persists full multi-turn tail (assistant → tool_result → follow-up assistant)", async () => {
248
+ // Simulate a wake that produces a tool_use, an executed tool_result
249
+ // user message, and a follow-up assistant summary. All three must be
250
+ // persisted; otherwise the next rehydration loses the tool_result
251
+ // and the provider rejects the orphaned tool_use.
252
+ const firstAssistant: Message = {
253
+ role: "assistant",
254
+ content: [
255
+ {
256
+ type: "tool_use",
257
+ id: "tu-1",
258
+ name: "meet_send_chat",
259
+ input: { text: "Sure" },
260
+ },
261
+ ],
262
+ };
263
+ const toolResultUserMsg: Message = {
264
+ role: "user",
265
+ content: [
266
+ {
267
+ type: "tool_result",
268
+ tool_use_id: "tu-1",
269
+ content: "sent",
270
+ },
271
+ ],
272
+ };
273
+ const followupAssistant: Message = {
274
+ role: "assistant",
275
+ content: [{ type: "text", text: "Done." }],
276
+ };
277
+
278
+ const target = makeTarget({
279
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
280
+ scriptedAssistant: firstAssistant,
281
+ scriptedTail: [toolResultUserMsg, followupAssistant],
282
+ });
283
+
284
+ const result = await wakeAgentForOpportunity(
285
+ {
286
+ conversationId: target.conversationId,
287
+ hint: "question directed at assistant",
288
+ source: "meet-chat-opportunity",
289
+ },
290
+ { resolveTarget: async () => target },
291
+ );
292
+
293
+ expect(result).toEqual({ invoked: true, producedToolCalls: true });
294
+
295
+ // All three tail messages persisted in order via the target hook.
296
+ expect(target.persistedTailCalls).toHaveLength(3);
297
+ expect(target.persistedTailCalls[0]).toEqual(firstAssistant);
298
+ expect(target.persistedTailCalls[1]).toEqual(toolResultUserMsg);
299
+ expect(target.persistedTailCalls[2]).toEqual(followupAssistant);
300
+
301
+ // All three also pushed into live history so next turn sees them.
302
+ expect(target.pushedMessages).toHaveLength(3);
303
+ expect(target.pushedMessages[0]).toEqual(firstAssistant);
304
+ expect(target.pushedMessages[1]).toEqual(toolResultUserMsg);
305
+ expect(target.pushedMessages[2]).toEqual(followupAssistant);
306
+ });
307
+
308
+ test("marks processing true during the run and false afterwards", async () => {
309
+ const target = makeTarget({
310
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
311
+ scriptedAssistant: {
312
+ role: "assistant",
313
+ content: [{ type: "text", text: "reply" }],
314
+ },
315
+ });
316
+
317
+ // Snapshot isProcessing() inside the run to prove we actually
318
+ // hold the processing flag while agentLoop.run executes.
319
+ const observedDuringRun: boolean[] = [];
320
+ const originalRun = target.agentLoop.run;
321
+ target.agentLoop.run = async (input, onEvent, signal, requestId) => {
322
+ observedDuringRun.push(target.isProcessing());
323
+ return originalRun(input, onEvent, signal, requestId);
324
+ };
325
+
326
+ await wakeAgentForOpportunity(
327
+ {
328
+ conversationId: target.conversationId,
329
+ hint: "x",
330
+ source: "unit-test",
331
+ },
332
+ { resolveTarget: async () => target },
333
+ );
334
+
335
+ // markProcessing toggled on then off exactly once.
336
+ expect(target.processingToggles).toEqual([true, false]);
337
+ // And the flag was observed as true inside the run body.
338
+ expect(observedDuringRun).toEqual([true]);
339
+ // Back to idle by the time the wake returns.
340
+ expect(target.isProcessing()).toBe(false);
341
+ });
342
+
343
+ test("marks processing false even when the agent loop throws", async () => {
344
+ const history: Message[] = [];
345
+ const toggles: boolean[] = [];
346
+ let processing = false;
347
+ const target: WakeTarget = {
348
+ conversationId: "conv-err-guard",
349
+ agentLoop: {
350
+ run: async () => {
351
+ throw new Error("LLM exploded");
352
+ },
353
+ },
354
+ getMessages: () => history,
355
+ pushMessage: () => {},
356
+ emitAgentEvent: () => {},
357
+ isProcessing: () => processing,
358
+ markProcessing: (on) => {
359
+ processing = on;
360
+ toggles.push(on);
361
+ },
362
+ persistTailMessage: async () => {},
363
+ };
364
+
365
+ const result = await wakeAgentForOpportunity(
366
+ { conversationId: "conv-err-guard", hint: "boom", source: "t" },
367
+ { resolveTarget: async () => target },
368
+ );
369
+
370
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
371
+ // Critical: the finally block must have released the flag despite
372
+ // the thrown error, otherwise the next user turn would hang.
373
+ expect(toggles).toEqual([true, false]);
374
+ expect(processing).toBe(false);
375
+ });
376
+
377
+ test("two concurrent wakes on the same conversation are serialized", async () => {
378
+ // Build a target whose agentLoop.run resolves only when we signal.
379
+ const gate1 = Promise.withResolvers<void>();
380
+ const gate2 = Promise.withResolvers<void>();
381
+ const runStartOrder: number[] = [];
382
+ const runCompleteOrder: number[] = [];
383
+
384
+ let callIndex = 0;
385
+ const history: Message[] = [];
386
+ let processing = false;
387
+ const target: WakeTarget = {
388
+ conversationId: "conv-serialize",
389
+ agentLoop: {
390
+ run: async (input) => {
391
+ const myIndex = ++callIndex;
392
+ runStartOrder.push(myIndex);
393
+ if (myIndex === 1) {
394
+ await gate1.promise;
395
+ } else {
396
+ await gate2.promise;
397
+ }
398
+ runCompleteOrder.push(myIndex);
399
+ return input; // no assistant message → silent no-op
400
+ },
401
+ },
402
+ getMessages: () => history,
403
+ pushMessage: (msg) => {
404
+ history.push(msg);
405
+ },
406
+ emitAgentEvent: () => {},
407
+ isProcessing: () => processing,
408
+ markProcessing: (on) => {
409
+ processing = on;
410
+ },
411
+ persistTailMessage: async () => {},
412
+ };
413
+
414
+ const deps = { resolveTarget: async () => target };
415
+
416
+ const p1 = wakeAgentForOpportunity(
417
+ { conversationId: "conv-serialize", hint: "first", source: "t1" },
418
+ deps,
419
+ );
420
+ const p2 = wakeAgentForOpportunity(
421
+ { conversationId: "conv-serialize", hint: "second", source: "t2" },
422
+ deps,
423
+ );
424
+
425
+ // Let the microtask queue flush so p1 can start.
426
+ await new Promise((resolve) => setTimeout(resolve, 10));
427
+ expect(runStartOrder).toEqual([1]);
428
+
429
+ // Releasing gate2 should NOT let p2 start — it's queued behind p1.
430
+ gate2.resolve();
431
+ await new Promise((resolve) => setTimeout(resolve, 10));
432
+ expect(runStartOrder).toEqual([1]);
433
+
434
+ // Now release gate1 — p1 completes, then p2 starts and completes.
435
+ gate1.resolve();
436
+ await Promise.all([p1, p2]);
437
+ expect(runStartOrder).toEqual([1, 2]);
438
+ expect(runCompleteOrder).toEqual([1, 2]);
439
+ });
440
+
441
+ test("waits while a concurrent user turn is in flight", async () => {
442
+ const history: Message[] = [];
443
+ let processing = true;
444
+ const target: WakeTarget & { setProcessing: (v: boolean) => void } = {
445
+ conversationId: "conv-user-turn",
446
+ agentLoop: {
447
+ run: async (input) => input,
448
+ },
449
+ getMessages: () => history,
450
+ pushMessage: (msg) => {
451
+ history.push(msg);
452
+ },
453
+ emitAgentEvent: () => {},
454
+ isProcessing: () => processing,
455
+ // The wake's own markProcessing updates track the flag too — the
456
+ // outer "user turn" holds it at true until setProcessing(false)
457
+ // is called below.
458
+ markProcessing: (on) => {
459
+ processing = on;
460
+ },
461
+ persistTailMessage: async () => {},
462
+ setProcessing: (v) => {
463
+ processing = v;
464
+ },
465
+ };
466
+
467
+ const wakePromise = wakeAgentForOpportunity(
468
+ {
469
+ conversationId: "conv-user-turn",
470
+ hint: "opportunity while user typing",
471
+ source: "unit-test",
472
+ },
473
+ { resolveTarget: async () => target },
474
+ );
475
+
476
+ // Wake should be waiting (isProcessing returns true).
477
+ await new Promise((resolve) => setTimeout(resolve, 100));
478
+ // Hasn't resolved yet.
479
+ let settled = false;
480
+ void wakePromise.then(() => {
481
+ settled = true;
482
+ });
483
+ await new Promise((resolve) => setTimeout(resolve, 20));
484
+ expect(settled).toBe(false);
485
+
486
+ // "User turn" completes — wake now proceeds.
487
+ target.setProcessing(false);
488
+ const result = await wakePromise;
489
+ expect(result.invoked).toBe(true);
490
+ expect(result.producedToolCalls).toBe(false);
491
+ });
492
+
493
+ test("returns invoked: false when the conversation cannot be resolved", async () => {
494
+ const result = await wakeAgentForOpportunity(
495
+ { conversationId: "missing", hint: "x", source: "y" },
496
+ { resolveTarget: async () => null },
497
+ );
498
+ expect(result).toEqual({ invoked: false, producedToolCalls: false });
499
+ });
500
+
501
+ test("agent loop error is treated as a no-op", async () => {
502
+ const history: Message[] = [];
503
+ let processing = false;
504
+ const persisted: Message[] = [];
505
+ const target: WakeTarget = {
506
+ conversationId: "conv-err",
507
+ agentLoop: {
508
+ run: async () => {
509
+ throw new Error("LLM exploded");
510
+ },
511
+ },
512
+ getMessages: () => history,
513
+ pushMessage: () => {},
514
+ emitAgentEvent: () => {},
515
+ isProcessing: () => processing,
516
+ markProcessing: (on) => {
517
+ processing = on;
518
+ },
519
+ persistTailMessage: async (m) => {
520
+ persisted.push(m);
521
+ },
522
+ };
523
+
524
+ const result = await wakeAgentForOpportunity(
525
+ { conversationId: "conv-err", hint: "boom", source: "t" },
526
+ { resolveTarget: async () => target },
527
+ );
528
+
529
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
530
+ expect(persisted).toHaveLength(0);
531
+ });
532
+
533
+ test("drainQueue is called in finally after a successful run", async () => {
534
+ // Verifies Gap 1 fix: messages queued during a wake (because the
535
+ // wake set `processing = true`) must be picked up after the wake
536
+ // completes. Mirrors the canonical user-turn `finally` path which
537
+ // sets `processing = false` then calls `drainQueue`.
538
+ const target = makeTarget({
539
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
540
+ scriptedAssistant: {
541
+ role: "assistant",
542
+ content: [{ type: "text", text: "reply" }],
543
+ },
544
+ });
545
+
546
+ await wakeAgentForOpportunity(
547
+ {
548
+ conversationId: target.conversationId,
549
+ hint: "x",
550
+ source: "unit-test",
551
+ },
552
+ { resolveTarget: async () => target },
553
+ );
554
+
555
+ expect(target.drainQueueCalls).toBe(1);
556
+ // Critical ordering invariant: drain runs after processing=false.
557
+ // If drain ran while processing was still true,
558
+ // `enqueueMessage`'s `if (!ctx.processing) return ...` gate would
559
+ // see processing=true and the drained item would itself just
560
+ // re-enqueue — no progress. Snapshot the live flag *inside* drain
561
+ // (rather than inferring from toggle order) so a future regression
562
+ // that called drain before markProcessing(false) would fail this
563
+ // assertion directly.
564
+ expect(target.processingDuringDrain).toEqual([false]);
565
+ expect(target.processingToggles).toEqual([true, false]);
566
+ expect(target.isProcessing()).toBe(false);
567
+ });
568
+
569
+ test("drainQueue is called in finally even when the agent loop throws", async () => {
570
+ // Verifies the drain is in the finally block, not just on success.
571
+ // A wake that crashes mid-run must still flush queued messages —
572
+ // otherwise a transient LLM error strands every concurrent send.
573
+ const drainProcessingSnapshots: boolean[] = [];
574
+ const toggles: boolean[] = [];
575
+ let processing = false;
576
+ const target: WakeTarget = {
577
+ conversationId: "conv-drain-on-throw",
578
+ agentLoop: {
579
+ run: async () => {
580
+ throw new Error("LLM exploded mid-wake");
581
+ },
582
+ },
583
+ getMessages: () => [],
584
+ pushMessage: () => {},
585
+ emitAgentEvent: () => {},
586
+ isProcessing: () => processing,
587
+ markProcessing: (on) => {
588
+ processing = on;
589
+ toggles.push(on);
590
+ },
591
+ persistTailMessage: async () => {},
592
+ drainQueue: async () => {
593
+ // Snapshot the live `processing` flag *inside* drain rather
594
+ // than inferring from toggle order. This directly observes the
595
+ // state visible to enqueueMessage's gate when a queued message
596
+ // is dequeued.
597
+ drainProcessingSnapshots.push(processing);
598
+ },
599
+ };
600
+
601
+ const result = await wakeAgentForOpportunity(
602
+ { conversationId: "conv-drain-on-throw", hint: "boom", source: "t" },
603
+ { resolveTarget: async () => target },
604
+ );
605
+
606
+ expect(result).toEqual({ invoked: true, producedToolCalls: false });
607
+ // Drain ran AFTER markProcessing(false), satisfying the
608
+ // enqueueMessage gate invariant. Snapshot proves the flag was
609
+ // false at the moment drain ran.
610
+ expect(drainProcessingSnapshots).toEqual([false]);
611
+ expect(toggles).toEqual([true, false]);
612
+ });
613
+
614
+ test("missing drainQueue hook is tolerated (no-op fallback)", async () => {
615
+ // The hook is intentionally optional so test stubs without a queue
616
+ // can omit it. Production daemon adapter always wires it.
617
+ const target = makeTarget({
618
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
619
+ scriptedAssistant: {
620
+ role: "assistant",
621
+ content: [{ type: "text", text: "reply" }],
622
+ },
623
+ omitDrainQueue: true,
624
+ });
625
+
626
+ const result = await wakeAgentForOpportunity(
627
+ {
628
+ conversationId: target.conversationId,
629
+ hint: "x",
630
+ source: "unit-test",
631
+ },
632
+ { resolveTarget: async () => target },
633
+ );
634
+
635
+ expect(result.invoked).toBe(true);
636
+ // No throw, no drain attempt recorded.
637
+ expect(target.drainQueueCalls).toBe(0);
638
+ });
639
+
640
+ test("drainQueue rejection does not propagate from the wake", async () => {
641
+ // Defense in depth: if the queue drain throws (e.g. a poisoned
642
+ // message), the wake itself must still resolve normally — the
643
+ // drain failure is logged but never surfaced.
644
+ const target = makeTarget({
645
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
646
+ scriptedAssistant: {
647
+ role: "assistant",
648
+ content: [{ type: "text", text: "reply" }],
649
+ },
650
+ });
651
+ target.drainQueue = async () => {
652
+ throw new Error("drain blew up");
653
+ };
654
+
655
+ const result = await wakeAgentForOpportunity(
656
+ {
657
+ conversationId: target.conversationId,
658
+ hint: "x",
659
+ source: "unit-test",
660
+ },
661
+ { resolveTarget: async () => target },
662
+ );
663
+
664
+ expect(result.invoked).toBe(true);
665
+ });
666
+
667
+ test("persistTailMessage called for each tail message in order", async () => {
668
+ // Verifies Gap 2 fix: the wake delegates persistence to the target
669
+ // so the daemon adapter can build channel/interface metadata. We
670
+ // only check the call ordering / arguments here — the daemon
671
+ // adapter's metadata composition is exercised separately.
672
+ const firstAssistant: Message = {
673
+ role: "assistant",
674
+ content: [
675
+ {
676
+ type: "tool_use",
677
+ id: "tu-1",
678
+ name: "some_tool",
679
+ input: {},
680
+ },
681
+ ],
682
+ };
683
+ const toolResultUserMsg: Message = {
684
+ role: "user",
685
+ content: [
686
+ { type: "tool_result", tool_use_id: "tu-1", content: "ok" },
687
+ ],
688
+ };
689
+ const followup: Message = {
690
+ role: "assistant",
691
+ content: [{ type: "text", text: "All set." }],
692
+ };
693
+ const target = makeTarget({
694
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
695
+ scriptedAssistant: firstAssistant,
696
+ scriptedTail: [toolResultUserMsg, followup],
697
+ });
698
+
699
+ await wakeAgentForOpportunity(
700
+ {
701
+ conversationId: target.conversationId,
702
+ hint: "x",
703
+ source: "meet-chat-opportunity",
704
+ },
705
+ { resolveTarget: async () => target },
706
+ );
707
+
708
+ expect(target.persistedTailCalls).toEqual([
709
+ firstAssistant,
710
+ toolResultUserMsg,
711
+ followup,
712
+ ]);
713
+ });
714
+
715
+ test(
716
+ "tail messages are pushed and persisted BEFORE drainQueue runs " +
717
+ "(so dequeued turns see updated history)",
718
+ async () => {
719
+ // Locks in the round-3 fix: a user message queued during the wake
720
+ // is drained against `conversation.messages`, so the wake's tail
721
+ // MUST be appended (push) and persisted to DB (persist) before the
722
+ // queue is drained. Otherwise `drainSingleMessage` reads stale
723
+ // history and writes a DB row that lands out of chronological
724
+ // order (queued user msg before the wake's just-produced
725
+ // assistant outputs).
726
+ //
727
+ // Mirrors the canonical user-turn pattern in
728
+ // conversation-agent-loop.ts:1860,2106-2126: messages updated →
729
+ // processing=false → drainQueue.
730
+ const firstAssistant: Message = {
731
+ role: "assistant",
732
+ content: [
733
+ { type: "tool_use", id: "tu-1", name: "some_tool", input: {} },
734
+ ],
735
+ };
736
+ const toolResultUserMsg: Message = {
737
+ role: "user",
738
+ content: [
739
+ { type: "tool_result", tool_use_id: "tu-1", content: "ok" },
740
+ ],
741
+ };
742
+ const followup: Message = {
743
+ role: "assistant",
744
+ content: [{ type: "text", text: "All done." }],
745
+ };
746
+ const target = makeTarget({
747
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
748
+ scriptedAssistant: firstAssistant,
749
+ scriptedTail: [toolResultUserMsg, followup],
750
+ });
751
+
752
+ await wakeAgentForOpportunity(
753
+ {
754
+ conversationId: target.conversationId,
755
+ hint: "x",
756
+ source: "meet-chat-opportunity",
757
+ },
758
+ { resolveTarget: async () => target },
759
+ );
760
+
761
+ // Full call sequence: processing toggled true → 3 pushes →
762
+ // 3 persists → processing toggled false → drain. Specifically,
763
+ // every push and every persist must precede the single drain.
764
+ expect(target.callSequence).toEqual([
765
+ "processing:true",
766
+ "push",
767
+ "push",
768
+ "push",
769
+ "persist",
770
+ "persist",
771
+ "persist",
772
+ "processing:false",
773
+ "drain",
774
+ ]);
775
+
776
+ // Belt-and-braces: cross-check via index lookups so the failure
777
+ // mode (drain before push/persist) shows up clearly even if the
778
+ // exact sequence ever picks up additional entries.
779
+ const drainIdx = target.callSequence.indexOf("drain");
780
+ const lastPushIdx = target.callSequence.lastIndexOf("push");
781
+ const lastPersistIdx = target.callSequence.lastIndexOf("persist");
782
+ expect(drainIdx).toBeGreaterThan(lastPushIdx);
783
+ expect(drainIdx).toBeGreaterThan(lastPersistIdx);
784
+
785
+ // And processing was false when drain ran.
786
+ expect(target.processingDuringDrain).toEqual([false]);
787
+ },
788
+ );
789
+
790
+ test(
791
+ "silent no-op: drainQueue still runs (in finally) but nothing is " +
792
+ "pushed, persisted, or emitted",
793
+ async () => {
794
+ // The wake's silent-no-op semantics must be preserved by the
795
+ // round-3 reordering: an empty assistant reply produces no
796
+ // visible text and no tool calls, so no push/persist/emit should
797
+ // happen. drainQueue must still run in the finally block so a
798
+ // racy queued message is not stranded.
799
+ const target = makeTarget({
800
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
801
+ scriptedAssistant: {
802
+ role: "assistant",
803
+ content: [{ type: "text", text: "" }],
804
+ },
805
+ });
806
+
807
+ await wakeAgentForOpportunity(
808
+ {
809
+ conversationId: target.conversationId,
810
+ hint: "x",
811
+ source: "unit-test",
812
+ },
813
+ { resolveTarget: async () => target },
814
+ );
815
+
816
+ // No push, no persist, no emit.
817
+ expect(target.pushedMessages).toHaveLength(0);
818
+ expect(target.persistedTailCalls).toHaveLength(0);
819
+ expect(target.emittedEvents).toHaveLength(0);
820
+
821
+ // But drain still ran exactly once, after processing flipped to
822
+ // false. Sequence: toggle true → toggle false → drain.
823
+ expect(target.callSequence).toEqual([
824
+ "processing:true",
825
+ "processing:false",
826
+ "drain",
827
+ ]);
828
+ expect(target.processingDuringDrain).toEqual([false]);
829
+ },
830
+ );
831
+ });