@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
@@ -225,8 +225,7 @@ describe("host_browser cloud-hosted e2e round-trip", () => {
225
225
  // Same fixture as the HTTP happy path, but configured to return
226
226
  // results over the /v1/browser-relay WebSocket instead of POSTing
227
227
  // /v1/host-browser-result. This exercises the runtime WS
228
- // `message` handler's host_browser_result dispatch path added in
229
- // PR2 of the browser-use remediation plan.
228
+ // `message` handler's host_browser_result dispatch path.
230
229
  const mockExt = createMockChromeExtension({
231
230
  runtimeBaseUrl,
232
231
  token,
@@ -309,8 +308,7 @@ describe("host_browser cloud-hosted e2e round-trip", () => {
309
308
  });
310
309
 
311
310
  test("abort: late /v1/host-browser-result POST after cancel is ignored (no ghost completion)", async () => {
312
- // Regression for PR6 of the browser-use remediation plan. The
313
- // daemon-side proxy must treat a late result POST — arriving
311
+ // The daemon-side proxy must treat a late result POST — arriving
314
312
  // after the caller has already been resolved with "Aborted" —
315
313
  // as a benign race, not a noisy false-positive timeout. It must
316
314
  // also NOT resolve the caller a second time.
@@ -434,6 +432,142 @@ describe("host_browser cloud-hosted e2e round-trip", () => {
434
432
  });
435
433
  });
436
434
 
435
+ // ── macOS message ingress with connected extension ──────────────────
436
+ //
437
+ // Verifies the end-to-end path for macOS-originated turns when the user
438
+ // has the chrome extension connected. On macOS, browser commands should
439
+ // route through the registry-backed host browser flow (extension → user's
440
+ // real Chrome session) rather than falling back to local Playwright.
441
+ //
442
+ // The macOS browser backend preference order is:
443
+ //
444
+ // macOS + extension connected → extension backend (registry-routed)
445
+ // macOS + extension absent → cdp-inspect (desktop-auto) → local
446
+ //
447
+ // NOTE: These tests construct a HostBrowserProxy directly and call
448
+ // proxy.request(), which validates the extension relay round-trip but
449
+ // bypasses handleSendMessage / conversation-routes. Full ingress-path
450
+ // coverage (interface propagation, resolveHostBrowserSender wiring, and
451
+ // CDP factory candidate selection) is exercised by the route-level tests
452
+ // in conversation-routes-disk-view.test.ts.
453
+ //
454
+ // If future refactors break the wiring between conversation-routes
455
+ // (`resolveHostBrowserSender`) and the CDP factory's candidate list, those
456
+ // route-level tests will fail.
457
+
458
+ describe("macOS message ingress with connected extension", () => {
459
+ let server: RuntimeHttpServer;
460
+ let port: number;
461
+ let runtimeBaseUrl: string;
462
+
463
+ beforeEach(async () => {
464
+ const db = getDb();
465
+ db.run("DELETE FROM contact_channels");
466
+ db.run("DELETE FROM contacts");
467
+ pendingInteractions.clear();
468
+ __resetChromeExtensionRegistryForTests();
469
+
470
+ port = 20000 + Math.floor(Math.random() * 200);
471
+ runtimeBaseUrl = `http://127.0.0.1:${port}`;
472
+ server = new RuntimeHttpServer({ port });
473
+ await server.start();
474
+ });
475
+
476
+ afterEach(async () => {
477
+ await server?.stop();
478
+ pendingInteractions.clear();
479
+ __resetChromeExtensionRegistryForTests();
480
+ });
481
+
482
+ test("macOS turn routes Browser.getVersion through the registry-backed extension, not local Playwright", async () => {
483
+ // Arrange: connect a mock extension for a given guardianId.
484
+ const guardianId = `test-guardian-macos-${crypto.randomUUID()}`;
485
+ const token = mintActorToken(guardianId);
486
+
487
+ const { createMockChromeExtension } =
488
+ await import("./fixtures/mock-chrome-extension.js");
489
+ const mockExt = createMockChromeExtension({
490
+ runtimeBaseUrl,
491
+ token,
492
+ });
493
+ await mockExt.start();
494
+ await mockExt.waitForConnection();
495
+ await waitForRegistryEntry(guardianId);
496
+
497
+ // Build a proxy bound to the guardian's extension connection, mimicking
498
+ // the wiring that conversation-routes.ts performs for macOS turns when
499
+ // the ChromeExtensionRegistry has an active entry for the guardian.
500
+ const { proxy } = createBoundProxy(guardianId, "conv-macos-ext");
501
+
502
+ // Act: issue a CDP command through the proxy (same as how browser tools
503
+ // dispatch commands during a macOS turn with extension override).
504
+ const result = await proxy.request(
505
+ { cdpMethod: "Browser.getVersion" },
506
+ "conv-macos-ext",
507
+ );
508
+
509
+ // Assert: the command reached the mock extension (not local Playwright)
510
+ // and the round-trip completed successfully.
511
+ expect(result.isError).toBe(false);
512
+ expect(result.content).toContain("Chrome/MockTest");
513
+
514
+ const received = mockExt.receivedRequests();
515
+ expect(received).toHaveLength(1);
516
+ expect(received[0].cdpMethod).toBe("Browser.getVersion");
517
+ expect(received[0].conversationId).toBe("conv-macos-ext");
518
+
519
+ proxy.dispose();
520
+ await mockExt.stop();
521
+ });
522
+
523
+ test("macOS turn with extension disconnected mid-conversation does not hang (proxy detects unavailability)", async () => {
524
+ // Arrange: connect a mock extension then forcibly disconnect it.
525
+ const guardianId = `test-guardian-macos-disco-${crypto.randomUUID()}`;
526
+ const token = mintActorToken(guardianId);
527
+
528
+ const { createMockChromeExtension } =
529
+ await import("./fixtures/mock-chrome-extension.js");
530
+ const mockExt = createMockChromeExtension({
531
+ runtimeBaseUrl,
532
+ token,
533
+ });
534
+ await mockExt.start();
535
+ await mockExt.waitForConnection();
536
+ await waitForRegistryEntry(guardianId);
537
+
538
+ // The proxy is bound while the extension is still connected.
539
+ const { proxy } = createBoundProxy(guardianId, "conv-macos-disco");
540
+
541
+ // Disconnect the extension before sending any commands.
542
+ mockExt.forceDisconnect();
543
+
544
+ // Wait for the registry to notice the close event.
545
+ await waitFor(
546
+ () => getChromeExtensionRegistry().get(guardianId) === undefined,
547
+ );
548
+
549
+ // Act: attempt a CDP command through the proxy. The registry send should
550
+ // fail because the connection is gone, and the proxy's sendToClient
551
+ // wrapper throws immediately.
552
+ try {
553
+ await proxy.request(
554
+ { cdpMethod: "Browser.getVersion", timeout_seconds: 0.5 },
555
+ "conv-macos-disco",
556
+ );
557
+ // If we reach here, the test should still verify the result indicates
558
+ // an error rather than a successful extension round-trip.
559
+ expect(true).toBe(false); // Should not reach here
560
+ } catch {
561
+ // Expected: the send failed because the extension is disconnected.
562
+ // This confirms the macOS path detects disconnection rather than
563
+ // silently routing to the wrong backend.
564
+ }
565
+
566
+ proxy.dispose();
567
+ await mockExt.stop();
568
+ });
569
+ });
570
+
437
571
  // ── Local wait helpers ──────────────────────────────────────────────
438
572
 
439
573
  async function waitFor(
@@ -321,7 +321,7 @@ describe("host-browser E2E — self-hosted native messaging path", () => {
321
321
  }
322
322
 
323
323
  // -------------------------------------------------------------------------
324
- // Dev-only `~/.vellum/daemon-token` fallback
324
+ // Dev-only daemon-token fallback (per-instance protected dir)
325
325
  // -------------------------------------------------------------------------
326
326
 
327
327
  describe("dev daemon-token fallback path", () => {
@@ -338,9 +338,9 @@ describe("host-browser E2E — self-hosted native messaging path", () => {
338
338
  test("a token written to a local file round-trips through verifyHostBrowserCapability", () => {
339
339
  // Emulate the `writeDaemonTokenFallback` lifecycle: mint a fresh
340
340
  // capability token, persist it to a 0600 file (the production
341
- // helper writes to `~/.vellum/daemon-token`, but we use a tempdir
342
- // so the test doesn't clobber real dev state), then read it back
343
- // and verify.
341
+ // helper writes to `<protectedDir>/daemon-token`, but we use a
342
+ // tempdir so the test doesn't clobber real dev state), then read
343
+ // it back and verify.
344
344
  //
345
345
  // This path is what the Mac app's manual "paste daemon token"
346
346
  // pairing UI ends up exercising — the file on disk is the only
@@ -284,6 +284,109 @@ describe("host_browser WS event + invalidation e2e", () => {
284
284
  await mockExt.stop();
285
285
  });
286
286
 
287
+ test("keepalive frames are accepted without closing the socket or producing warnings", async () => {
288
+ const guardianId = `guardian-${crypto.randomUUID()}`;
289
+ const { token } = mintHostBrowserCapability(guardianId);
290
+
291
+ const { createMockChromeExtension } =
292
+ await import("./fixtures/mock-chrome-extension.js");
293
+ const mockExt = createMockChromeExtension({
294
+ runtimeBaseUrl,
295
+ token,
296
+ resultTransport: "ws",
297
+ });
298
+ await mockExt.start();
299
+ await mockExt.waitForConnection();
300
+ await waitForRegistryEntry(guardianId);
301
+
302
+ // Grab the initial timestamps so we can verify that keepalive
303
+ // bumps lastKeepaliveAt but NOT lastActiveAt (routing field).
304
+ const connBefore = getChromeExtensionRegistry().get(guardianId)!;
305
+ const lastActiveBefore = connBefore.lastActiveAt;
306
+
307
+ // Small delay to ensure Date.now() advances at least 1ms.
308
+ await new Promise((r) => setTimeout(r, 15));
309
+
310
+ // Send a keepalive frame (the extension sends these periodically
311
+ // to prevent the runtime from considering the connection stale).
312
+ // The frame may contain extra keys (e.g. timestamp) that the
313
+ // runtime should silently ignore (lenient validation).
314
+ mockExt.sendRaw(JSON.stringify({ type: "keepalive", ts: Date.now() }));
315
+
316
+ // Wait for the touch to propagate — touch() updates
317
+ // lastKeepaliveAt (not lastActiveAt) to avoid routing interference.
318
+ await waitFor(() => {
319
+ const conn = getChromeExtensionRegistry().get(guardianId);
320
+ return conn !== undefined && (conn.lastKeepaliveAt ?? 0) > 0;
321
+ });
322
+
323
+ const connAfter = getChromeExtensionRegistry().get(guardianId)!;
324
+ expect(connAfter.lastKeepaliveAt).toBeGreaterThan(0);
325
+ // lastActiveAt must remain unchanged — keepalives must not affect routing.
326
+ expect(connAfter.lastActiveAt).toBe(lastActiveBefore);
327
+
328
+ // Verify the socket is still alive by sending a normal host_browser_event
329
+ // frame after the keepalive — if the socket had been torn down, this
330
+ // would never arrive.
331
+ const observed: ForwardedCdpEvent[] = [];
332
+ const unsubscribe = onCdpEvent((event) => observed.push(event));
333
+
334
+ mockExt.sendHostBrowserEvent({ method: "Page.loadEventFired" });
335
+ await waitFor(() => observed.length === 1);
336
+ expect(observed[0].method).toBe("Page.loadEventFired");
337
+
338
+ unsubscribe();
339
+ await mockExt.stop();
340
+ });
341
+
342
+ test("normal host_browser flows still pass after keepalive traffic", async () => {
343
+ const guardianId = `guardian-${crypto.randomUUID()}`;
344
+ const { token } = mintHostBrowserCapability(guardianId);
345
+
346
+ const { createMockChromeExtension } =
347
+ await import("./fixtures/mock-chrome-extension.js");
348
+ const mockExt = createMockChromeExtension({
349
+ runtimeBaseUrl,
350
+ token,
351
+ resultTransport: "ws",
352
+ });
353
+ await mockExt.start();
354
+ await mockExt.waitForConnection();
355
+ await waitForRegistryEntry(guardianId);
356
+
357
+ // Simulate a burst of keepalive frames (as would happen during an
358
+ // idle period with the extension's alarm-based keepalive ticker).
359
+ for (let i = 0; i < 5; i++) {
360
+ mockExt.sendRaw(JSON.stringify({ type: "keepalive" }));
361
+ }
362
+
363
+ // Small delay to let all keepalive frames process.
364
+ await new Promise((r) => setTimeout(r, 50));
365
+
366
+ // Now send a host_browser_event and verify it still fans out
367
+ // correctly — proving keepalive traffic does not interfere with
368
+ // normal message processing.
369
+ const observed: ForwardedCdpEvent[] = [];
370
+ const unsubscribe = onCdpEvent((event) => observed.push(event));
371
+
372
+ mockExt.sendHostBrowserEvent({
373
+ method: "Network.requestWillBeSent",
374
+ params: { requestId: "req-42", url: "https://example.com/api" },
375
+ cdpSessionId: "session-xyz",
376
+ });
377
+
378
+ await waitFor(() => observed.length === 1);
379
+ expect(observed[0].method).toBe("Network.requestWillBeSent");
380
+ expect(observed[0].params).toEqual({
381
+ requestId: "req-42",
382
+ url: "https://example.com/api",
383
+ });
384
+ expect(observed[0].cdpSessionId).toBe("session-xyz");
385
+
386
+ unsubscribe();
387
+ await mockExt.stop();
388
+ });
389
+
287
390
  test("malformed host_browser_event frames are dropped without tearing down the socket", async () => {
288
391
  const guardianId = `guardian-${crypto.randomUUID()}`;
289
392
  const { token } = mintHostBrowserCapability(guardianId);
@@ -792,26 +792,21 @@ describe("HostCuProxy", () => {
792
792
  function spySignal(source: AbortSignal): Spied {
793
793
  const addCalls: string[] = [];
794
794
  const removeCalls: string[] = [];
795
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
796
795
  const s = source as any;
797
796
  const origAdd = source.addEventListener.bind(source);
798
797
  const origRemove = source.removeEventListener.bind(source);
799
798
  s.addEventListener = (
800
799
  type: string,
801
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
802
800
  ...rest: any[]
803
801
  ) => {
804
802
  addCalls.push(type);
805
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
806
803
  return (origAdd as any)(type, ...rest);
807
804
  };
808
805
  s.removeEventListener = (
809
806
  type: string,
810
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
811
807
  ...rest: any[]
812
808
  ) => {
813
809
  removeCalls.push(type);
814
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
815
810
  return (origRemove as any)(type, ...rest);
816
811
  };
817
812
  return { signal: source, addCalls, removeCalls };
@@ -2,7 +2,8 @@
2
2
  * Unit tests for the identity intro cache (identity-intro-cache.ts).
3
3
  *
4
4
  * Validates TTL-based expiration, content-hash-based invalidation when
5
- * workspace identity files change, and round-trip get/set behavior.
5
+ * workspace identity files or the guardian persona content change, and
6
+ * round-trip get/set behavior.
6
7
  */
7
8
 
8
9
  import { afterEach, describe, expect, mock, test } from "bun:test";
@@ -44,6 +45,14 @@ mock.module("node:fs", () => ({
44
45
  },
45
46
  }));
46
47
 
48
+ // Mocked guardian persona — mutable so tests can change it and verify cache
49
+ // invalidation based on the per-user persona file content.
50
+ let guardianPersonaContent: string | null = null;
51
+
52
+ mock.module("../prompts/persona-resolver.js", () => ({
53
+ resolveGuardianPersona: () => guardianPersonaContent,
54
+ }));
55
+
47
56
  // ---------------------------------------------------------------------------
48
57
  // Imports (after mocks)
49
58
  // ---------------------------------------------------------------------------
@@ -63,6 +72,7 @@ afterEach(() => {
63
72
  for (const key of Object.keys(workspaceFiles)) {
64
73
  delete workspaceFiles[key];
65
74
  }
75
+ guardianPersonaContent = null;
66
76
  });
67
77
 
68
78
  // ---------------------------------------------------------------------------
@@ -77,7 +87,7 @@ describe("identity intro cache", () => {
77
87
  test("round-trip: set then get returns cached text", () => {
78
88
  workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
79
89
  workspaceFiles["SOUL.md"] = "Be playful.";
80
- workspaceFiles["USER.md"] = "The user likes coffee.";
90
+ guardianPersonaContent = "The user likes coffee.";
81
91
 
82
92
  setCachedIntro("Hey, I'm Atlas.");
83
93
  const cached = getCachedIntro();
@@ -131,24 +141,24 @@ describe("identity intro cache", () => {
131
141
  expect(getCachedIntro()).toBeNull();
132
142
  });
133
143
 
134
- test("busts cache when USER.md changes", () => {
135
- workspaceFiles["USER.md"] = "Likes coffee.";
144
+ test("busts cache when guardian persona content changes", () => {
145
+ guardianPersonaContent = "Likes coffee.";
136
146
  setCachedIntro("Good morning!");
137
147
 
138
- // Change USER.md
139
- workspaceFiles["USER.md"] = "Likes tea.";
148
+ // Change guardian persona (e.g. user edited users/<slug>.md)
149
+ guardianPersonaContent = "Likes tea.";
140
150
 
141
151
  expect(getCachedIntro()).toBeNull();
142
152
  });
143
153
 
144
- test("cache survives when files are unchanged", () => {
154
+ test("cache remains valid when guardian persona is unchanged", () => {
145
155
  workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
146
156
  workspaceFiles["SOUL.md"] = "Be chill.";
147
- workspaceFiles["USER.md"] = "Likes sunsets.";
157
+ guardianPersonaContent = "Likes sunsets.";
148
158
 
149
159
  setCachedIntro("Atlas here.");
150
160
 
151
- // Read twice — both should return the cached value
161
+ // Read twice with the same guardian persona — both should return the cached value
152
162
  expect(getCachedIntro()?.text).toBe("Atlas here.");
153
163
  expect(getCachedIntro()?.text).toBe("Atlas here.");
154
164
  });
@@ -156,7 +166,7 @@ describe("identity intro cache", () => {
156
166
  test("computeIdentityContentHash is deterministic", () => {
157
167
  workspaceFiles["IDENTITY.md"] = "test";
158
168
  workspaceFiles["SOUL.md"] = "test2";
159
- workspaceFiles["USER.md"] = "test3";
169
+ guardianPersonaContent = "test3";
160
170
 
161
171
  const hash1 = computeIdentityContentHash();
162
172
  const hash2 = computeIdentityContentHash();
@@ -164,6 +174,26 @@ describe("identity intro cache", () => {
164
174
  expect(hash1).toMatch(/^[a-f0-9]{64}$/); // SHA-256 hex
165
175
  });
166
176
 
177
+ test("computeIdentityContentHash changes when guardian persona changes", () => {
178
+ workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
179
+ workspaceFiles["SOUL.md"] = "Be playful.";
180
+ guardianPersonaContent = "Likes coffee.";
181
+ const hash1 = computeIdentityContentHash();
182
+
183
+ guardianPersonaContent = "Likes tea.";
184
+ const hash2 = computeIdentityContentHash();
185
+
186
+ expect(hash1).not.toBe(hash2);
187
+ });
188
+
189
+ test("computeIdentityContentHash handles null guardian persona", () => {
190
+ workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
191
+ guardianPersonaContent = null;
192
+
193
+ const hash = computeIdentityContentHash();
194
+ expect(hash).toMatch(/^[a-f0-9]{64}$/);
195
+ });
196
+
167
197
  test("computeIdentityContentHash changes when file content changes", () => {
168
198
  workspaceFiles["IDENTITY.md"] = "v1";
169
199
  const hash1 = computeIdentityContentHash();
@@ -1,102 +1,45 @@
1
1
  /**
2
- * Tests for initFeatureFlagOverrides() — the async gateway fetch that
2
+ * Tests for initFeatureFlagOverrides() — the async IPC call that
3
3
  * pre-populates the feature flag cache before CLI program construction.
4
+ *
5
+ * Uses the shared mock-gateway-ipc utility (installed in test-preload.ts)
6
+ * which mocks node:net so no test connects to a real gateway socket.
4
7
  */
5
8
  import { afterEach, beforeEach, describe, expect, it } from "bun:test";
6
9
 
10
+ import {
11
+ mockGatewayIpc,
12
+ resetMockGatewayIpc,
13
+ } from "../__tests__/mock-gateway-ipc.js";
7
14
  import {
8
15
  clearFeatureFlagOverridesCache,
9
16
  initFeatureFlagOverrides,
10
17
  isAssistantFeatureFlagEnabled,
11
18
  } from "../config/assistant-feature-flags.js";
12
- import * as tokenService from "../runtime/auth/token-service.js";
13
- import { getMockFetchCalls, mockFetch, resetMockFetch } from "./mock-fetch.js";
14
-
15
- const VALID_HEX_KEY = "ab".repeat(32);
16
19
 
17
20
  beforeEach(() => {
18
21
  clearFeatureFlagOverridesCache();
19
- tokenService._resetSigningKeyForTesting();
20
-
21
- // Set up a signing key so mintEdgeRelayToken() works
22
- process.env.ACTOR_TOKEN_SIGNING_KEY = VALID_HEX_KEY;
23
- tokenService.initAuthSigningKey(tokenService.resolveSigningKey());
22
+ resetMockGatewayIpc();
24
23
  });
25
24
 
26
25
  afterEach(() => {
27
- resetMockFetch();
28
26
  clearFeatureFlagOverridesCache();
29
- tokenService._resetSigningKeyForTesting();
30
- delete process.env.ACTOR_TOKEN_SIGNING_KEY;
27
+ resetMockGatewayIpc();
31
28
  });
32
29
 
33
30
  describe("initFeatureFlagOverrides", () => {
34
- it("populates cache from gateway fetch response", async () => {
35
- mockFetch(
36
- "/v1/feature-flags",
37
- { method: "GET" },
38
- {
39
- body: {
40
- flags: [
41
- {
42
- key: "foo-enabled",
43
- enabled: true,
44
- label: "Foo",
45
- defaultEnabled: false,
46
- description: "",
47
- },
48
- {
49
- key: "bar-enabled",
50
- enabled: true,
51
- label: "Bar",
52
- defaultEnabled: true,
53
- description: "",
54
- },
55
- ],
56
- },
57
- status: 200,
58
- },
59
- );
31
+ it("populates cache from gateway IPC response", async () => {
32
+ mockGatewayIpc({ "foo-enabled": true, "bar-enabled": true });
60
33
 
61
34
  await initFeatureFlagOverrides();
62
35
 
63
36
  const config = {} as any;
64
37
  expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
65
38
  expect(isAssistantFeatureFlagEnabled("bar-enabled", config)).toBe(true);
66
-
67
- // Verify fetch was called with correct URL and auth header
68
- const calls = getMockFetchCalls();
69
- expect(calls.length).toBe(1);
70
- expect(calls[0].path).toContain("/v1/feature-flags");
71
- const headers = calls[0].init.headers as Record<string, string> | undefined;
72
- expect(headers).toHaveProperty("Authorization");
73
- });
74
-
75
- it("sends a valid Bearer JWT in the Authorization header", async () => {
76
- mockFetch(
77
- "/v1/feature-flags",
78
- { method: "GET" },
79
- { body: { flags: [] }, status: 200 },
80
- );
81
-
82
- await initFeatureFlagOverrides();
83
-
84
- const calls = getMockFetchCalls();
85
- expect(calls.length).toBe(1);
86
- const headers = calls[0].init.headers as Record<string, string> | undefined;
87
- const authHeader = headers?.Authorization;
88
-
89
- expect(authHeader).toBeDefined();
90
- expect(authHeader).toMatch(/^Bearer /);
91
-
92
- // Verify it's a valid JWT (three dot-separated base64url segments)
93
- const token = authHeader!.replace("Bearer ", "");
94
- const parts = token.split(".");
95
- expect(parts.length).toBe(3);
96
39
  });
97
40
 
98
- it("falls back gracefully when gateway is unreachable", async () => {
99
- mockFetch("/v1/feature-flags", { method: "GET" }, { status: 500 });
41
+ it("falls back gracefully when gateway socket is unavailable", async () => {
42
+ mockGatewayIpc(null, { error: true });
100
43
 
101
44
  // Should not throw
102
45
  await initFeatureFlagOverrides();
@@ -106,62 +49,45 @@ describe("initFeatureFlagOverrides", () => {
106
49
  expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
107
50
  });
108
51
 
109
- it("falls back gracefully on non-OK HTTP status", async () => {
110
- mockFetch(
111
- "/v1/feature-flags",
112
- { method: "GET" },
113
- { body: "Unauthorized", status: 401 },
114
- );
52
+ it("respects false values from gateway IPC", async () => {
53
+ mockGatewayIpc({ "gated-feature": true, "disabled-feature": false });
115
54
 
116
55
  await initFeatureFlagOverrides();
117
56
 
118
- // Undeclared flags default to true without overrides
119
57
  const config = {} as any;
120
- expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
58
+ expect(isAssistantFeatureFlagEnabled("gated-feature", config)).toBe(true);
59
+ expect(isAssistantFeatureFlagEnabled("disabled-feature", config)).toBe(
60
+ false,
61
+ );
121
62
  });
122
63
 
123
- it("initializes signing key lazily when not yet set", async () => {
124
- // Reset signing key to simulate fresh CLI subprocess
125
- tokenService._resetSigningKeyForTesting();
126
- delete process.env.ACTOR_TOKEN_SIGNING_KEY;
127
-
128
- expect(tokenService.isSigningKeyInitialized()).toBe(false);
129
-
130
- mockFetch(
131
- "/v1/feature-flags",
132
- { method: "GET" },
133
- {
134
- body: {
135
- flags: [{ key: "expected-enabled", enabled: true }],
136
- },
137
- status: 200,
138
- },
139
- );
64
+ it("does not cache empty gateway response", async () => {
65
+ mockGatewayIpc({});
140
66
 
141
67
  await initFeatureFlagOverrides();
142
68
 
143
- // Signing key should have been initialized during the fetch
144
- expect(tokenService.isSigningKeyInitialized()).toBe(true);
145
-
146
- // And the flag should be resolved correctly
69
+ // Undeclared flags without overrides default to true (not false from
70
+ // a cached empty map)
147
71
  const config = {} as any;
148
- expect(isAssistantFeatureFlagEnabled("expected-enabled", config)).toBe(
149
- true,
150
- );
72
+ expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
151
73
  });
152
74
 
153
- it("does not cache empty gateway response", async () => {
154
- mockFetch(
155
- "/v1/feature-flags",
156
- { method: "GET" },
157
- { body: { flags: [] }, status: 200 },
158
- );
75
+ it("does not re-fetch when cache is already populated", async () => {
76
+ mockGatewayIpc({ "first-call": true });
77
+
78
+ await initFeatureFlagOverrides();
79
+
80
+ // Change what IPC would return — if the guard is broken and init
81
+ // re-fetches, "first-call" would flip to false.
82
+ resetMockGatewayIpc();
83
+ mockGatewayIpc({ "first-call": false, "second-call": true });
159
84
 
160
85
  await initFeatureFlagOverrides();
161
86
 
162
- // Undeclared flags without overrides default to true (not false from
163
- // a cached empty map)
164
87
  const config = {} as any;
165
- expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
88
+ // first-call must still be true (from the cached first fetch)
89
+ expect(isAssistantFeatureFlagEnabled("first-call", config)).toBe(true);
90
+ // second-call should not be in the cache since init was a no-op
91
+ expect(isAssistantFeatureFlagEnabled("second-call", config)).toBe(true); // defaults to true (undeclared)
166
92
  });
167
93
  });