@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,212 @@
1
+ /**
2
+ * Phase 5 exit-criteria end-to-end test.
3
+ *
4
+ * Exercises the full home-feed flow in-process against a tmpdir
5
+ * workspace: the platform-baseline generator writes a digest, the
6
+ * assistant-authoring helper writes a nudge, the HTTP route serves
7
+ * both with a context banner, and the patch endpoint flips an item
8
+ * to `seen` — all without spinning up the macOS app.
9
+ *
10
+ * Exit-criteria mapping (from the TDD checklist):
11
+ *
12
+ * [x] platform-baseline Gmail digest surfaces via the feed route
13
+ * → `generateGmailDigest(now)` + `GET /v1/home/feed` → 1 item
14
+ * [x] assistant-authored nudges coexist with platform digests
15
+ * → `writeAssistantFeedItem(...)` → 2 items visible
16
+ * [x] `minTimeAway` filtering behaves at route boundary
17
+ * → seed an item with `minTimeAway: 3600` and query with
18
+ * `timeAwaySeconds: 12 * 3600` → included
19
+ * [x] context banner reports `newCount` across all authors
20
+ * → 2 new items → banner.newCount === 2
21
+ * [x] `PATCH /v1/home/feed/:id` flips status and mutates banner
22
+ * → patchFeedItemStatus(nudgeId, "seen") → newCount drops to 1
23
+ *
24
+ * This test is a wiring-regression safety net — the per-PR unit
25
+ * tests in this directory (feed-writer, feed-types, authoring,
26
+ * gmail-digest, plus the route tests under `runtime/routes/__tests__`)
27
+ * still carry the per-component coverage. Keep this file fast (<5s)
28
+ * by doing all persistence against a single tmpdir workspace and
29
+ * calling route handlers directly instead of standing up the HTTP
30
+ * server.
31
+ */
32
+
33
+ import { mkdtempSync, rmSync } from "node:fs";
34
+ import { tmpdir } from "node:os";
35
+ import { join } from "node:path";
36
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
37
+
38
+ // ─── assistantEventHub mock ────────────────────────────────────────────
39
+ // The real feed writer publishes `home_feed_updated` via the in-process
40
+ // event hub on every write. This test doesn't assert on those events —
41
+ // it asserts on the feed file + HTTP-route output — so we stub the hub
42
+ // to an inert publisher. Must run before the first dynamic import of
43
+ // anything that transitively imports `feed-writer.js`.
44
+ const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
45
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
46
+ assistantEventHub: {
47
+ publish: publishSpy,
48
+ subscribe: () => () => {},
49
+ },
50
+ }));
51
+
52
+ // ─── createConversation / addMessage stub ──────────────────────────────
53
+ // The feed action endpoint (`POST /v1/home/feed/:id/actions/:actionId`)
54
+ // calls into the real conversation store, which drags in a large chunk
55
+ // of daemon state we don't want this test to depend on. The exit-
56
+ // criteria flow we care about is GET + PATCH — not the action POST —
57
+ // so we stub the conversation module to a trivial in-memory fake and
58
+ // never exercise it. If a future exit-criteria item needs the action
59
+ // path, swap this for a real-ish fake.
60
+ mock.module("../../runtime/conversation-store.js", () => ({
61
+ createConversation: (args: { title: string; source: string }) => ({
62
+ id: "stub-conversation",
63
+ title: args.title,
64
+ source: args.source,
65
+ }),
66
+ addMessage: async () => {},
67
+ }));
68
+
69
+ const { generateGmailDigest } = await import("../platform-gmail-digest.js");
70
+ const { writeAssistantFeedItem } =
71
+ await import("../assistant-feed-authoring.js");
72
+ const { patchFeedItemStatus, readHomeFeed } = await import("../feed-writer.js");
73
+ const { handleGetHomeFeed } =
74
+ await import("../../runtime/routes/home-feed-routes.js");
75
+
76
+ // ─── tmpdir workspace lifecycle ────────────────────────────────────────
77
+
78
+ let workspaceDir: string;
79
+ let origWorkspaceDir: string | undefined;
80
+
81
+ beforeEach(() => {
82
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-phase5-e2e-"));
83
+ origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
84
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
85
+ publishSpy.mockClear();
86
+ });
87
+
88
+ afterEach(() => {
89
+ if (origWorkspaceDir === undefined) {
90
+ delete process.env.VELLUM_WORKSPACE_DIR;
91
+ } else {
92
+ process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
93
+ }
94
+ try {
95
+ rmSync(workspaceDir, { recursive: true, force: true });
96
+ } catch {
97
+ // best-effort — tests must not fail on cleanup
98
+ }
99
+ });
100
+
101
+ // ─── Helpers ───────────────────────────────────────────────────────────
102
+
103
+ /** Hit the GET handler directly with a synthetic Request object. */
104
+ async function getFeed(timeAwaySeconds: number): Promise<{
105
+ items: Array<{
106
+ id: string;
107
+ status: string;
108
+ type: string;
109
+ source?: string;
110
+ author: string;
111
+ }>;
112
+ updatedAt: string;
113
+ contextBanner: { greeting: string; timeAwayLabel: string; newCount: number };
114
+ }> {
115
+ const req = new Request(
116
+ `http://daemon.local/v1/home/feed?timeAwaySeconds=${timeAwaySeconds}`,
117
+ );
118
+ const res = await handleGetHomeFeed(req);
119
+ expect(res.status).toBe(200);
120
+ return (await res.json()) as Awaited<ReturnType<typeof getFeed>>;
121
+ }
122
+
123
+ // ─── Tests ─────────────────────────────────────────────────────────────
124
+
125
+ describe("Phase 5 exit criteria — end-to-end", () => {
126
+ test("platform digest + assistant nudge surface via the feed route, and patch flips newCount", async () => {
127
+ const now = new Date("2026-04-14T12:00:00.000Z");
128
+
129
+ // 1. Platform baseline: Gmail digest — mechanical "3 new emails"
130
+ const digest = await generateGmailDigest(now, async () => 3);
131
+ expect(digest).not.toBeNull();
132
+ expect(digest!.source).toBe("gmail");
133
+ expect(digest!.author).toBe("platform");
134
+ expect(digest!.type).toBe("digest");
135
+
136
+ // 2. Assistant-authored nudge — the hybrid-authoring surface
137
+ const nudge = await writeAssistantFeedItem({
138
+ type: "nudge",
139
+ source: "assistant",
140
+ title: "Follow up on the Figma file",
141
+ summary: "Alice is asking when the deck lands.",
142
+ actions: [
143
+ {
144
+ id: "open",
145
+ label: "Open conversation",
146
+ prompt: "Remind me about the Figma file.",
147
+ },
148
+ ],
149
+ minTimeAway: 3600,
150
+ });
151
+ expect(nudge.author).toBe("assistant");
152
+ expect(nudge.type).toBe("nudge");
153
+
154
+ // 3. On-disk sanity: both items made it into home-feed.json and
155
+ // the digest coexists with the assistant nudge (different
156
+ // (type, source) so the one-per-source replacement does not
157
+ // collapse them into a single item).
158
+ const onDisk = readHomeFeed();
159
+ const ids = onDisk.items.map((i) => i.id).sort();
160
+ expect(ids.length).toBe(2);
161
+ expect(ids).toContain(digest!.id);
162
+ expect(ids).toContain(nudge.id);
163
+
164
+ // 4. Route: `GET /v1/home/feed?timeAwaySeconds=43200` — both items
165
+ // pass the `minTimeAway` gate (3600 <= 43200). Banner reports
166
+ // `newCount: 2` since both are still `.new`.
167
+ const initial = await getFeed(12 * 3600);
168
+ expect(initial.items.length).toBe(2);
169
+ expect(initial.contextBanner.newCount).toBe(2);
170
+ expect(initial.contextBanner.timeAwayLabel.length).toBeGreaterThan(0);
171
+ expect(initial.contextBanner.greeting.length).toBeGreaterThan(0);
172
+
173
+ // 5. Flip the nudge to `seen` via the writer (the route's PATCH
174
+ // handler is a thin wrapper around this — we skip the HTTP
175
+ // layer here so the test stays at the in-process seam).
176
+ const patched = await patchFeedItemStatus(nudge.id, "seen");
177
+ expect(patched).not.toBeNull();
178
+ expect(patched!.status).toBe("seen");
179
+
180
+ // 6. Re-fetch: `newCount` drops to 1 (only the digest is still
181
+ // `.new`). Total item count is unchanged — patching status
182
+ // must never remove items from the feed.
183
+ const afterPatch = await getFeed(12 * 3600);
184
+ expect(afterPatch.items.length).toBe(2);
185
+ expect(afterPatch.contextBanner.newCount).toBe(1);
186
+ const seenItem = afterPatch.items.find((i) => i.id === nudge.id);
187
+ expect(seenItem?.status).toBe("seen");
188
+ });
189
+
190
+ test("minTimeAway gate excludes items when the user has not been away long enough", async () => {
191
+ // Assistant nudge with a 1-hour away gate
192
+ const nudge = await writeAssistantFeedItem({
193
+ type: "nudge",
194
+ source: "assistant",
195
+ title: "Come back after lunch",
196
+ summary: "This only shows after an hour away.",
197
+ minTimeAway: 3600,
198
+ });
199
+
200
+ // User has only been away 10 minutes → item must be filtered
201
+ const brief = await getFeed(600);
202
+ const briefIds = brief.items.map((i) => i.id);
203
+ expect(briefIds).not.toContain(nudge.id);
204
+ expect(brief.contextBanner.newCount).toBe(0);
205
+
206
+ // User has been away 2 hours → item must now appear
207
+ const long = await getFeed(2 * 3600);
208
+ const longIds = long.items.map((i) => i.id);
209
+ expect(longIds).toContain(nudge.id);
210
+ expect(long.contextBanner.newCount).toBe(1);
211
+ });
212
+ });
@@ -0,0 +1,222 @@
1
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ // ─── assistantEventHub mock ────────────────────────────────────────────
7
+ // The real feed-writer publishes `home_feed_updated` via the in-process
8
+ // event hub on every write; tests don't care about that side effect, so
9
+ // we stub the hub to an inert publisher. Must be in place before the
10
+ // first dynamic import of feed-writer.js so the module graph picks up
11
+ // the mock.
12
+ const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
13
+
14
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
15
+ assistantEventHub: {
16
+ publish: publishSpy,
17
+ subscribe: () => () => {},
18
+ },
19
+ }));
20
+
21
+ // Dynamic import so the mock above resolves before the real module is
22
+ // evaluated.
23
+ const { generateGmailDigest } = await import("../platform-gmail-digest.js");
24
+ const { getHomeFeedPath, readHomeFeed } = await import("../feed-writer.js");
25
+
26
+ // ─── tmpdir workspace lifecycle ────────────────────────────────────────
27
+
28
+ let workspaceDir: string;
29
+ let origWorkspaceDir: string | undefined;
30
+
31
+ beforeEach(() => {
32
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-pgd-"));
33
+ origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
34
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
35
+ publishSpy.mockClear();
36
+ });
37
+
38
+ afterEach(() => {
39
+ if (origWorkspaceDir === undefined) {
40
+ delete process.env.VELLUM_WORKSPACE_DIR;
41
+ } else {
42
+ process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
43
+ }
44
+ try {
45
+ rmSync(workspaceDir, { recursive: true, force: true });
46
+ } catch {
47
+ // best-effort
48
+ }
49
+ });
50
+
51
+ // ─── Tests ─────────────────────────────────────────────────────────────
52
+
53
+ describe("generateGmailDigest", () => {
54
+ test("returns null and does not write when count is 0", async () => {
55
+ const result = await generateGmailDigest(
56
+ new Date("2026-04-14T12:00:00.000Z"),
57
+ async () => 0,
58
+ );
59
+ expect(result).toBeNull();
60
+
61
+ // No file should have been written — readHomeFeed returns the
62
+ // empty sentinel with the Unix-epoch updatedAt.
63
+ const feed = readHomeFeed();
64
+ expect(feed.items).toEqual([]);
65
+ expect(feed.updatedAt).toBe(new Date(0).toISOString());
66
+ });
67
+
68
+ test("returns a digest FeedItem with the expected shape when count > 0", async () => {
69
+ const now = new Date("2026-04-14T12:00:00.000Z");
70
+ const result = await generateGmailDigest(now, async () => 7);
71
+
72
+ expect(result).not.toBeNull();
73
+ expect(result!.type).toBe("digest");
74
+ expect(result!.source).toBe("gmail");
75
+ expect(result!.author).toBe("platform");
76
+ expect(result!.priority).toBe(40);
77
+ expect(result!.minTimeAway).toBe(3600);
78
+ expect(result!.status).toBe("new");
79
+ expect(result!.title).toContain("7");
80
+ expect(result!.timestamp).toBe(now.toISOString());
81
+ expect(result!.createdAt).toBe(now.toISOString());
82
+ });
83
+
84
+ test("minTimeAway is exactly 3600 seconds (1 hour)", async () => {
85
+ const result = await generateGmailDigest(
86
+ new Date("2026-04-14T12:00:00.000Z"),
87
+ async () => 1,
88
+ );
89
+ expect(result).not.toBeNull();
90
+ expect(result!.minTimeAway).toBe(3600);
91
+ });
92
+
93
+ test("title uses singular form for count of 1, plural otherwise", async () => {
94
+ const now = new Date("2026-04-14T12:00:00.000Z");
95
+
96
+ const one = await generateGmailDigest(now, async () => 1);
97
+ expect(one!.title).toBe("1 new email");
98
+
99
+ const many = await generateGmailDigest(now, async () => 3);
100
+ expect(many!.title).toBe("3 new emails");
101
+ });
102
+
103
+ test("returns null when count source throws", async () => {
104
+ const result = await generateGmailDigest(
105
+ new Date("2026-04-14T12:00:00.000Z"),
106
+ async () => {
107
+ throw new Error("boom");
108
+ },
109
+ );
110
+ expect(result).toBeNull();
111
+
112
+ const feed = readHomeFeed();
113
+ expect(feed.items).toEqual([]);
114
+ });
115
+
116
+ test("writes the item to home-feed.json via appendFeedItem", async () => {
117
+ const now = new Date("2026-04-14T12:00:00.000Z");
118
+ const result = await generateGmailDigest(now, async () => 5);
119
+ expect(result).not.toBeNull();
120
+
121
+ const raw = readFileSync(getHomeFeedPath(), "utf-8");
122
+ const file = JSON.parse(raw) as {
123
+ items: Array<{
124
+ id: string;
125
+ type: string;
126
+ source?: string;
127
+ author: string;
128
+ priority: number;
129
+ title: string;
130
+ }>;
131
+ };
132
+
133
+ expect(file.items.length).toBe(1);
134
+ expect(file.items[0]).toMatchObject({
135
+ id: result!.id,
136
+ type: "digest",
137
+ source: "gmail",
138
+ author: "platform",
139
+ priority: 40,
140
+ title: "5 new emails",
141
+ });
142
+ });
143
+
144
+ test("summary uses generic check-in copy on first-ever digest", async () => {
145
+ const result = await generateGmailDigest(
146
+ new Date("2026-04-14T12:00:00.000Z"),
147
+ async () => 4,
148
+ );
149
+ expect(result).not.toBeNull();
150
+ expect(result!.summary).toBe("Since your last check-in");
151
+ });
152
+
153
+ test("summary references the prior digest timestamp on subsequent call", async () => {
154
+ const first = await generateGmailDigest(
155
+ new Date("2026-04-14T14:32:00.000Z"),
156
+ async () => 2,
157
+ );
158
+ expect(first).not.toBeNull();
159
+
160
+ // Same-day follow-up → "Since h:mm AM/PM" pulled from the first
161
+ // digest's timestamp (2:32 PM UTC rendered via en-US locale).
162
+ const sameDay = await generateGmailDigest(
163
+ new Date("2026-04-14T16:00:00.000Z"),
164
+ async () => 5,
165
+ );
166
+ expect(sameDay).not.toBeNull();
167
+ expect(sameDay!.summary).toMatch(/^Since \d{1,2}:\d{2}\s?(AM|PM)$/);
168
+ });
169
+
170
+ test("summary includes weekday when prior digest is on a different day", async () => {
171
+ const first = await generateGmailDigest(
172
+ new Date("2026-04-13T22:00:00.000Z"),
173
+ async () => 1,
174
+ );
175
+ expect(first).not.toBeNull();
176
+
177
+ const nextDay = await generateGmailDigest(
178
+ new Date("2026-04-14T14:00:00.000Z"),
179
+ async () => 3,
180
+ );
181
+ expect(nextDay).not.toBeNull();
182
+ // Cross-day → "Since <Weekday> h:mm AM/PM" — weekday prefix set.
183
+ expect(nextDay!.summary).toMatch(
184
+ /^Since (Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{1,2}:\d{2}\s?(AM|PM)$/,
185
+ );
186
+ });
187
+
188
+ // Integration: exercise the writer's one-per-source rule. Two
189
+ // successive generator calls should leave exactly one Gmail digest
190
+ // on disk — the newer call replaces the earlier one in place.
191
+ test("second call replaces the first via one-per-source rule", async () => {
192
+ const first = await generateGmailDigest(
193
+ new Date("2026-04-14T12:00:00.000Z"),
194
+ async () => 2,
195
+ );
196
+ expect(first).not.toBeNull();
197
+
198
+ const second = await generateGmailDigest(
199
+ new Date("2026-04-14T13:00:00.000Z"),
200
+ async () => 9,
201
+ );
202
+ expect(second).not.toBeNull();
203
+ expect(second!.id).not.toBe(first!.id);
204
+
205
+ const raw = readFileSync(getHomeFeedPath(), "utf-8");
206
+ const file = JSON.parse(raw) as {
207
+ items: Array<{
208
+ id: string;
209
+ type: string;
210
+ source?: string;
211
+ title: string;
212
+ }>;
213
+ };
214
+
215
+ const gmailDigests = file.items.filter(
216
+ (i) => i.type === "digest" && i.source === "gmail",
217
+ );
218
+ expect(gmailDigests.length).toBe(1);
219
+ expect(gmailDigests[0]!.id).toBe(second!.id);
220
+ expect(gmailDigests[0]!.title).toBe("9 new emails");
221
+ });
222
+ });
@@ -0,0 +1,213 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ computeProgressPercent,
5
+ computeTier,
6
+ PROGRESS_TARGETS,
7
+ PROGRESS_WEIGHTS,
8
+ } from "../progress-formula.js";
9
+ import type { Capability, Fact } from "../relationship-state.js";
10
+
11
+ function fact(id: string): Fact {
12
+ return {
13
+ id,
14
+ category: "world",
15
+ text: "placeholder",
16
+ confidence: "strong",
17
+ source: "inferred",
18
+ };
19
+ }
20
+
21
+ function cap(id: string, tier: Capability["tier"]): Capability {
22
+ return {
23
+ id,
24
+ name: id,
25
+ description: "",
26
+ tier,
27
+ gate: "",
28
+ };
29
+ }
30
+
31
+ function facts(n: number): Fact[] {
32
+ return Array.from({ length: n }, (_, i) => fact(`f-${i}`));
33
+ }
34
+
35
+ function unlockedCaps(n: number): Capability[] {
36
+ return Array.from({ length: n }, (_, i) => cap(`c-${i}`, "unlocked"));
37
+ }
38
+
39
+ describe("progress-formula", () => {
40
+ describe("PROGRESS_WEIGHTS", () => {
41
+ test("sum to exactly 1 so a fully saturated input yields 100", () => {
42
+ const sum =
43
+ PROGRESS_WEIGHTS.facts +
44
+ PROGRESS_WEIGHTS.capabilities +
45
+ PROGRESS_WEIGHTS.conversations;
46
+ // Use toBeCloseTo for float safety.
47
+ expect(sum).toBeCloseTo(1, 10);
48
+ });
49
+ });
50
+
51
+ describe("computeProgressPercent", () => {
52
+ test("zero state -> 0", () => {
53
+ expect(
54
+ computeProgressPercent({
55
+ facts: [],
56
+ capabilities: [],
57
+ conversationCount: 0,
58
+ }),
59
+ ).toBe(0);
60
+ });
61
+
62
+ test("all saturated -> 100", () => {
63
+ expect(
64
+ computeProgressPercent({
65
+ facts: facts(PROGRESS_TARGETS.facts),
66
+ capabilities: unlockedCaps(PROGRESS_TARGETS.capabilities),
67
+ conversationCount: PROGRESS_TARGETS.conversations,
68
+ }),
69
+ ).toBe(100);
70
+ });
71
+
72
+ test("saturation clamps: inputs beyond target do not exceed 100", () => {
73
+ expect(
74
+ computeProgressPercent({
75
+ facts: facts(PROGRESS_TARGETS.facts * 3),
76
+ capabilities: unlockedCaps(PROGRESS_TARGETS.capabilities * 3),
77
+ conversationCount: PROGRESS_TARGETS.conversations * 3,
78
+ }),
79
+ ).toBe(100);
80
+ });
81
+
82
+ test("facts-only at target contributes exactly its weight", () => {
83
+ expect(
84
+ computeProgressPercent({
85
+ facts: facts(PROGRESS_TARGETS.facts),
86
+ capabilities: [],
87
+ conversationCount: 0,
88
+ }),
89
+ ).toBe(Math.round(PROGRESS_WEIGHTS.facts * 100));
90
+ });
91
+
92
+ test("capabilities-only at target contributes exactly its weight", () => {
93
+ expect(
94
+ computeProgressPercent({
95
+ facts: [],
96
+ capabilities: unlockedCaps(PROGRESS_TARGETS.capabilities),
97
+ conversationCount: 0,
98
+ }),
99
+ ).toBe(Math.round(PROGRESS_WEIGHTS.capabilities * 100));
100
+ });
101
+
102
+ test("conversations-only at target contributes exactly its weight", () => {
103
+ expect(
104
+ computeProgressPercent({
105
+ facts: [],
106
+ capabilities: [],
107
+ conversationCount: PROGRESS_TARGETS.conversations,
108
+ }),
109
+ ).toBe(Math.round(PROGRESS_WEIGHTS.conversations * 100));
110
+ });
111
+
112
+ test("non-unlocked capabilities do not contribute", () => {
113
+ const mixed: Capability[] = [
114
+ cap("a", "next-up"),
115
+ cap("b", "earned"),
116
+ cap("c", "unlocked"),
117
+ ];
118
+ // Only 1 of PROGRESS_TARGETS.capabilities counts toward the caps weight.
119
+ const expected = Math.round(
120
+ ((PROGRESS_WEIGHTS.capabilities * 1) / PROGRESS_TARGETS.capabilities) *
121
+ 100,
122
+ );
123
+ expect(
124
+ computeProgressPercent({
125
+ facts: [],
126
+ capabilities: mixed,
127
+ conversationCount: 0,
128
+ }),
129
+ ).toBe(expected);
130
+ });
131
+
132
+ test("specific mix: half facts, half caps, half conversations -> 50", () => {
133
+ // Each signal at half its target contributes half its weight; total
134
+ // is half of the combined weight (1) * 100 = 50.
135
+ expect(
136
+ computeProgressPercent({
137
+ facts: facts(PROGRESS_TARGETS.facts / 2),
138
+ capabilities: unlockedCaps(PROGRESS_TARGETS.capabilities / 2),
139
+ conversationCount: PROGRESS_TARGETS.conversations / 2,
140
+ }),
141
+ ).toBe(50);
142
+ });
143
+ });
144
+
145
+ describe("computeTier", () => {
146
+ test("zero state -> tier 1 (Getting to know you)", () => {
147
+ expect(
148
+ computeTier({ facts: [], capabilities: [], conversationCount: 0 }),
149
+ ).toBe(1);
150
+ });
151
+
152
+ test("5 conversations + 3 facts -> tier 2 (Finding my footing)", () => {
153
+ expect(
154
+ computeTier({
155
+ facts: facts(3),
156
+ capabilities: [],
157
+ conversationCount: 5,
158
+ }),
159
+ ).toBe(2);
160
+ });
161
+
162
+ test("just below tier 2 threshold on facts -> still tier 1", () => {
163
+ expect(
164
+ computeTier({
165
+ facts: facts(2),
166
+ capabilities: [],
167
+ conversationCount: 5,
168
+ }),
169
+ ).toBe(1);
170
+ });
171
+
172
+ test("just below tier 2 threshold on conversations -> still tier 1", () => {
173
+ expect(
174
+ computeTier({
175
+ facts: facts(3),
176
+ capabilities: [],
177
+ conversationCount: 4,
178
+ }),
179
+ ).toBe(1);
180
+ });
181
+
182
+ test("20 conversations + 3 unlocked caps -> tier 3 (Hitting our stride)", () => {
183
+ expect(
184
+ computeTier({
185
+ facts: facts(5),
186
+ capabilities: unlockedCaps(3),
187
+ conversationCount: 20,
188
+ }),
189
+ ).toBe(3);
190
+ });
191
+
192
+ test("20 conversations + 2 unlocked caps -> tier 2 (fallthrough)", () => {
193
+ expect(
194
+ computeTier({
195
+ facts: facts(3),
196
+ capabilities: unlockedCaps(2),
197
+ conversationCount: 20,
198
+ }),
199
+ ).toBe(2);
200
+ });
201
+
202
+ test("tier 4 is reserved and unreachable from the default heuristic", () => {
203
+ // Even a fully-saturated input should not produce tier 4 today.
204
+ const result = computeTier({
205
+ facts: facts(50),
206
+ capabilities: unlockedCaps(6),
207
+ conversationCount: 1_000,
208
+ });
209
+ expect(result).not.toBe(4);
210
+ expect(result).toBe(3);
211
+ });
212
+ });
213
+ });