@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
@@ -77,10 +77,13 @@ let getOrCreateSessionPageMock: ReturnType<typeof mock>;
77
77
  let clearSnapshotBackendNodeMapMock: ReturnType<typeof mock>;
78
78
  let positionWindowSidebarMock: ReturnType<typeof mock>;
79
79
 
80
+ const preferredBackendKinds = new Map<string, string>();
81
+
80
82
  mock.module("../tools/browser/browser-manager.js", () => {
81
83
  getOrCreateSessionPageMock = mock(async () => mockPage);
82
84
  clearSnapshotBackendNodeMapMock = mock(() => {});
83
85
  positionWindowSidebarMock = mock(async () => {});
86
+ preferredBackendKinds.clear();
84
87
  return {
85
88
  browserManager: {
86
89
  getOrCreateSessionPage: getOrCreateSessionPageMock,
@@ -88,6 +91,14 @@ mock.module("../tools/browser/browser-manager.js", () => {
88
91
  supportsRouteInterception: true,
89
92
  isInteractive: () => false,
90
93
  positionWindowSidebar: positionWindowSidebarMock,
94
+ getPreferredBackendKind: (conversationId: string) =>
95
+ preferredBackendKinds.get(conversationId) ?? null,
96
+ setPreferredBackendKind: (conversationId: string, kind: string) => {
97
+ preferredBackendKinds.set(conversationId, kind);
98
+ },
99
+ clearPreferredBackendKind: (conversationId: string) => {
100
+ preferredBackendKinds.delete(conversationId);
101
+ },
91
102
  },
92
103
  };
93
104
  });
@@ -101,12 +112,14 @@ mock.module("../tools/browser/browser-screencast.js", () => ({
101
112
 
102
113
  // Default url-safety: allow everything
103
114
  let parseUrlResult: URL | null = null;
115
+ let parseUrlMock: (input: unknown) => URL | null = () => parseUrlResult;
104
116
  let isPrivateResult = false;
117
+ let isPrivateHostMock: (hostname: string) => boolean = () => isPrivateResult;
105
118
  let resolveResult: { blockedAddress?: string } = {};
106
119
 
107
120
  mock.module("../tools/network/url-safety.js", () => ({
108
- parseUrl: (_input: unknown) => parseUrlResult,
109
- isPrivateOrLocalHost: () => isPrivateResult,
121
+ parseUrl: (input: unknown) => parseUrlMock(input),
122
+ isPrivateOrLocalHost: (hostname: string) => isPrivateHostMock(hostname),
110
123
  resolveHostAddresses: async () => [],
111
124
  resolveRequestAddress: async () => resolveResult,
112
125
  sanitizeUrlForOutput: (url: URL) => url.href,
@@ -195,7 +208,9 @@ function resetCdp() {
195
208
  describe("executeBrowserNavigate", () => {
196
209
  beforeEach(() => {
197
210
  parseUrlResult = null;
211
+ parseUrlMock = () => parseUrlResult;
198
212
  isPrivateResult = false;
213
+ isPrivateHostMock = () => isPrivateResult;
199
214
  resolveResult = {};
200
215
  resetMockPage();
201
216
  resetCdp();
@@ -557,9 +572,9 @@ describe("executeBrowserNavigate", () => {
557
572
  expect(cdpDisposed).toBe(true);
558
573
  });
559
574
 
560
- // ── Extension path (no browserManager / route interception) ───
575
+ // ── Extension path (no Playwright route interception) ──────────
561
576
 
562
- test("extension path skips getOrCreateSessionPage and route interception", async () => {
577
+ test("extension path skips Playwright route interception", async () => {
563
578
  parseUrlResult = new URL("https://example.com/page");
564
579
  // Supplying a non-null hostBrowserProxy on the context routes the
565
580
  // mocked getCdpClient to the extension path (it mirrors the real
@@ -578,11 +593,133 @@ describe("executeBrowserNavigate", () => {
578
593
  );
579
594
 
580
595
  expect(result.isError).toBe(false);
581
- // Extension path never installs or removes a Playwright route.
596
+ // Extension path never installs or removes a Playwright route
597
+ // (route interception only works on the local Playwright path).
582
598
  expect(mockPage.route.mock.calls.length).toBe(routeCallsBefore);
583
599
  expect(mockPage.unroute.mock.calls.length).toBe(unrouteCallsBefore);
584
600
  // Page.navigate still goes through the CdpClient.
585
601
  expect(cdpSendCalls.some((c) => c.method === "Page.navigate")).toBe(true);
586
602
  expect(cdpDisposed).toBe(true);
587
603
  });
604
+
605
+ test("extension path blocks redirects via post-navigation final URL check", async () => {
606
+ // The initial URL is public and passes pre-flight checks. The
607
+ // extension path has no Playwright route interception, but the
608
+ // post-navigation defense-in-depth check catches the private final URL.
609
+ parseUrlResult = new URL("https://public.example.com/start");
610
+ isPrivateResult = false;
611
+
612
+ // Configure mocks to return different results for the initial URL
613
+ // vs. the final URL returned by navigateAndWait.
614
+ parseUrlMock = (input: unknown) => {
615
+ if (typeof input === "string" && input.includes("127.0.0.1")) {
616
+ return new URL(input);
617
+ }
618
+ return parseUrlResult;
619
+ };
620
+ isPrivateHostMock = (hostname: string) => {
621
+ return hostname === "127.0.0.1";
622
+ };
623
+
624
+ // navigateAndWait returns a private final URL (simulating a
625
+ // server-side redirect).
626
+ cdpSendHandler = (method, params) => {
627
+ if (method === "Page.navigate") return { frameId: "f1" };
628
+ if (method === "Runtime.evaluate") {
629
+ const expression = String(params?.["expression"] ?? "");
630
+ if (expression === "document.location.href") {
631
+ return { result: { value: "about:blank" } };
632
+ }
633
+ if (
634
+ expression.includes("readyState") &&
635
+ expression.includes("document.location.href")
636
+ ) {
637
+ return {
638
+ result: {
639
+ value: {
640
+ readyState: "complete",
641
+ href: "http://127.0.0.1/admin",
642
+ },
643
+ },
644
+ };
645
+ }
646
+ return { result: { value: null } };
647
+ }
648
+ return {};
649
+ };
650
+
651
+ const extensionCtx: ToolContext = {
652
+ ...ctx,
653
+ hostBrowserProxy: {} as unknown as ToolContext["hostBrowserProxy"],
654
+ };
655
+ const result = await executeBrowserNavigate(
656
+ { url: "https://public.example.com/start" },
657
+ extensionCtx,
658
+ );
659
+ expect(result.isError).toBe(true);
660
+ expect(result.content).toContain("Navigation blocked");
661
+ expect(result.content).toContain("Final URL resolved to a local/private");
662
+ expect(result.content).toContain("allow_private_network=true");
663
+ expect(cdpDisposed).toBe(true);
664
+ });
665
+
666
+ // ── Defense-in-depth: post-navigation final URL check ─────────
667
+
668
+ test("post-nav check blocks when final URL resolves to private target", async () => {
669
+ // The initial URL is public and passes pre-flight checks, but the
670
+ // final URL (after redirect) resolves to a private address. The
671
+ // route handler is NOT triggered (navigation succeeds), so only the
672
+ // post-navigation defense-in-depth check catches it.
673
+ parseUrlResult = new URL("https://public.example.com/redirect");
674
+ isPrivateResult = false;
675
+
676
+ // Configure parseUrlMock to return different results for the initial
677
+ // URL vs. the final URL returned by navigateAndWait.
678
+ parseUrlMock = (input: unknown) => {
679
+ if (typeof input === "string" && input.includes("192.168")) {
680
+ return new URL(input);
681
+ }
682
+ return parseUrlResult;
683
+ };
684
+ isPrivateHostMock = (hostname: string) => {
685
+ return hostname === "192.168.1.1";
686
+ };
687
+
688
+ // navigateAndWait returns a private final URL (simulating a
689
+ // server-side redirect that the route handler didn't catch).
690
+ cdpSendHandler = (method, params) => {
691
+ if (method === "Page.navigate") return { frameId: "f1" };
692
+ if (method === "Runtime.evaluate") {
693
+ const expression = String(params?.["expression"] ?? "");
694
+ if (expression === "document.location.href") {
695
+ return { result: { value: "about:blank" } };
696
+ }
697
+ if (
698
+ expression.includes("readyState") &&
699
+ expression.includes("document.location.href")
700
+ ) {
701
+ return {
702
+ result: {
703
+ value: {
704
+ readyState: "complete",
705
+ href: "http://192.168.1.1/admin",
706
+ },
707
+ },
708
+ };
709
+ }
710
+ return { result: { value: null } };
711
+ }
712
+ return {};
713
+ };
714
+
715
+ const result = await executeBrowserNavigate(
716
+ { url: "https://public.example.com/redirect" },
717
+ ctx,
718
+ );
719
+ expect(result.isError).toBe(true);
720
+ expect(result.content).toContain("Navigation blocked");
721
+ expect(result.content).toContain("Final URL resolved to a local/private");
722
+ expect(result.content).toContain("allow_private_network=true");
723
+ expect(cdpDisposed).toBe(true);
724
+ });
588
725
  });
@@ -76,12 +76,23 @@ let mockPage: {
76
76
  };
77
77
  };
78
78
 
79
+ const preferredBackendKinds = new Map<string, string>();
80
+
79
81
  mock.module("../tools/browser/browser-manager.js", () => {
82
+ preferredBackendKinds.clear();
80
83
  return {
81
84
  browserManager: {
82
85
  getOrCreateSessionPage: async () => mockPage,
83
86
  closeSessionPage: async () => {},
84
87
  closeAllPages: async () => {},
88
+ getPreferredBackendKind: (conversationId: string) =>
89
+ preferredBackendKinds.get(conversationId) ?? null,
90
+ setPreferredBackendKind: (conversationId: string, kind: string) => {
91
+ preferredBackendKinds.set(conversationId, kind);
92
+ },
93
+ clearPreferredBackendKind: (conversationId: string) => {
94
+ preferredBackendKinds.delete(conversationId);
95
+ },
85
96
  },
86
97
  };
87
98
  });
@@ -35,9 +35,11 @@ let closeAllPagesMock: ReturnType<typeof mock>;
35
35
  let clearSnapshotBackendNodeMapMock: ReturnType<typeof mock>;
36
36
  let storeSnapshotBackendNodeMapMock: ReturnType<typeof mock>;
37
37
  let storedBackendNodeMaps: Map<string, Map<string, number>>;
38
+ const preferredBackendKinds = new Map<string, string>();
38
39
 
39
40
  mock.module("../tools/browser/browser-manager.js", () => {
40
41
  storedBackendNodeMaps = new Map();
42
+ preferredBackendKinds.clear();
41
43
  closeSessionPageMock = mock(async () => {});
42
44
  closeAllPagesMock = mock(async () => {});
43
45
  clearSnapshotBackendNodeMapMock = mock((conversationId: string) => {
@@ -80,6 +82,14 @@ mock.module("../tools/browser/browser-manager.js", () => {
80
82
  if (!map) return null;
81
83
  return map.get(elementId) ?? null;
82
84
  },
85
+ getPreferredBackendKind: (conversationId: string) =>
86
+ preferredBackendKinds.get(conversationId) ?? null,
87
+ setPreferredBackendKind: (conversationId: string, kind: string) => {
88
+ preferredBackendKinds.set(conversationId, kind);
89
+ },
90
+ clearPreferredBackendKind: (conversationId: string) => {
91
+ preferredBackendKinds.delete(conversationId);
92
+ },
83
93
  },
84
94
  };
85
95
  });
@@ -23,6 +23,39 @@ mock.module("../config/loader.js", () => ({
23
23
  invalidateConfigCache: () => {},
24
24
  }));
25
25
 
26
+ // ── Guardian persona mock ─────────────────────────────────────────
27
+ //
28
+ // `heartbeat-service.isShallowProfile` reads the guardian persona via
29
+ // `resolveGuardianPersona()` and compares against the exported
30
+ // `GUARDIAN_PERSONA_TEMPLATE` scaffold. We mock the module so each
31
+ // test can seed whatever persona content it needs; the scaffold text
32
+ // below is kept byte-identical to the real template in
33
+ // `persona-resolver.ts` so the "scaffold-only" path triggers a match.
34
+ const GUARDIAN_PERSONA_TEMPLATE = `_ Lines starting with _ are comments - they won't appear in the system prompt
35
+
36
+ # User Profile
37
+
38
+ Store details about your user here. Edit freely - build this over time as you learn about them. Don't be pushy about seeking details, but when you learn something, write it down. More context makes you more useful.
39
+
40
+ - Preferred name/reference:
41
+ - Pronouns:
42
+ - Locale:
43
+ - Work role:
44
+ - Goals:
45
+ - Hobbies/fun:
46
+ - Daily tools:
47
+ `;
48
+
49
+ // `resolveGuardianPersona` returns already-stripped + trimmed content
50
+ // (or null for missing/empty files). Tests mutate this variable to
51
+ // drive `isShallowProfile`.
52
+ let mockGuardianPersona: string | null = null;
53
+
54
+ mock.module("../prompts/persona-resolver.js", () => ({
55
+ GUARDIAN_PERSONA_TEMPLATE,
56
+ resolveGuardianPersona: () => mockGuardianPersona,
57
+ }));
58
+
26
59
  // Mock conversation store
27
60
  const createdConversations: Array<{ title: string; conversationType: string }> =
28
61
  [];
@@ -85,7 +118,13 @@ const IDENTITY_TEMPLATE = readFileSync(
85
118
  join(templatesDir, "IDENTITY.md"),
86
119
  "utf-8",
87
120
  );
88
- const USER_TEMPLATE = readFileSync(join(templatesDir, "USER.md"), "utf-8");
121
+
122
+ // Stripped/trimmed form of the guardian persona scaffold — mirrors
123
+ // the transformation applied by `resolveGuardianPersona` (which runs
124
+ // `stripCommentLines` internally). Used to simulate a freshly-seeded,
125
+ // never-edited persona file.
126
+ const { stripCommentLines } = await import("../util/strip-comment-lines.js");
127
+ const SCAFFOLD_PERSONA = stripCommentLines(GUARDIAN_PERSONA_TEMPLATE).trim();
89
128
 
90
129
  describe("HeartbeatService", () => {
91
130
  let processMessageCalls: Array<{
@@ -99,7 +138,6 @@ describe("HeartbeatService", () => {
99
138
  // Clean up workspace files between tests so file-existence tests don't leak
100
139
  rmSync(join(testWorkspaceDir, "HEARTBEAT.md"), { force: true });
101
140
  rmSync(join(testWorkspaceDir, "IDENTITY.md"), { force: true });
102
- rmSync(join(testWorkspaceDir, "USER.md"), { force: true });
103
141
  rmSync(join(testWorkspaceDir, ".reengagement-ts"), { force: true });
104
142
  });
105
143
 
@@ -108,6 +146,7 @@ describe("HeartbeatService", () => {
108
146
  alerterCalls = [];
109
147
  createdConversations.length = 0;
110
148
  conversationIdCounter = 0;
149
+ mockGuardianPersona = null;
111
150
 
112
151
  mockConfig = {
113
152
  heartbeat: {
@@ -494,9 +533,23 @@ describe("HeartbeatService", () => {
494
533
  });
495
534
 
496
535
  describe("isShallowProfile", () => {
497
- test("returns true when both IDENTITY.md and USER.md are unmodified templates", () => {
536
+ test("returns true when IDENTITY.md is template and guardian persona is missing", () => {
537
+ writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
538
+ mockGuardianPersona = null;
539
+
540
+ expect(isShallowProfile()).toBe(true);
541
+ });
542
+
543
+ test("returns true when IDENTITY.md is template and guardian persona has only scaffold fields", () => {
498
544
  writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
499
- writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
545
+ mockGuardianPersona = SCAFFOLD_PERSONA;
546
+
547
+ expect(isShallowProfile()).toBe(true);
548
+ });
549
+
550
+ test("returns true when IDENTITY.md is template and guardian persona is empty string", () => {
551
+ writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
552
+ mockGuardianPersona = "";
500
553
 
501
554
  expect(isShallowProfile()).toBe(true);
502
555
  });
@@ -506,22 +559,22 @@ describe("HeartbeatService", () => {
506
559
  join(testWorkspaceDir, "IDENTITY.md"),
507
560
  "# IDENTITY.md\n\n- **Name:** Jarvis\n",
508
561
  );
509
- writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
562
+ mockGuardianPersona = SCAFFOLD_PERSONA;
510
563
 
511
564
  expect(isShallowProfile()).toBe(false);
512
565
  });
513
566
 
514
- test("returns false when USER.md has been customized", () => {
567
+ test("returns false when guardian persona has real content", () => {
515
568
  writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
516
- writeFileSync(
517
- join(testWorkspaceDir, "USER.md"),
518
- "# USER.md\n\n- Preferred name/reference: Alice\n",
519
- );
569
+ mockGuardianPersona =
570
+ "# User Profile\n\n- Preferred name/reference: Alice\n- Work role: designer";
520
571
 
521
572
  expect(isShallowProfile()).toBe(false);
522
573
  });
523
574
 
524
- test("returns false when neither file exists", () => {
575
+ test("returns false when IDENTITY.md does not exist", () => {
576
+ mockGuardianPersona = null;
577
+
525
578
  expect(isShallowProfile()).toBe(false);
526
579
  });
527
580
  });
@@ -529,7 +582,7 @@ describe("HeartbeatService", () => {
529
582
  describe("relationship-depth prompt injection", () => {
530
583
  test("includes <relationship-depth> when profile is shallow", () => {
531
584
  writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
532
- writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
585
+ mockGuardianPersona = SCAFFOLD_PERSONA;
533
586
 
534
587
  const service = createService();
535
588
  const { prompt, includedReengagement } =
@@ -545,7 +598,7 @@ describe("HeartbeatService", () => {
545
598
  join(testWorkspaceDir, "IDENTITY.md"),
546
599
  "# IDENTITY.md\n\n- **Name:** Jarvis\n",
547
600
  );
548
- writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
601
+ mockGuardianPersona = SCAFFOLD_PERSONA;
549
602
 
550
603
  const service = createService();
551
604
  const { prompt, includedReengagement } =
@@ -557,7 +610,7 @@ describe("HeartbeatService", () => {
557
610
 
558
611
  test("omits <relationship-depth> when cooldown has not elapsed", () => {
559
612
  writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
560
- writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
613
+ mockGuardianPersona = SCAFFOLD_PERSONA;
561
614
  // Write a recent timestamp to simulate cooldown not elapsed
562
615
  writeFileSync(
563
616
  join(testWorkspaceDir, ".reengagement-ts"),
@@ -574,7 +627,7 @@ describe("HeartbeatService", () => {
574
627
 
575
628
  test("includes <relationship-depth> when cooldown has elapsed", () => {
576
629
  writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
577
- writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
630
+ mockGuardianPersona = SCAFFOLD_PERSONA;
578
631
  // Write a timestamp from 19 hours ago
579
632
  const nineteenHoursAgo = Date.now() - 19 * 60 * 60 * 1000;
580
633
  writeFileSync(
@@ -592,7 +645,7 @@ describe("HeartbeatService", () => {
592
645
 
593
646
  test("does not record timestamp when processMessage fails", async () => {
594
647
  writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
595
- writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
648
+ mockGuardianPersona = SCAFFOLD_PERSONA;
596
649
 
597
650
  const service = createService({
598
651
  processMessage: async () => {
@@ -609,7 +662,7 @@ describe("HeartbeatService", () => {
609
662
 
610
663
  test("records timestamp after successful delivery", async () => {
611
664
  writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), IDENTITY_TEMPLATE);
612
- writeFileSync(join(testWorkspaceDir, "USER.md"), USER_TEMPLATE);
665
+ mockGuardianPersona = SCAFFOLD_PERSONA;
613
666
 
614
667
  const service = createService();
615
668
  await service.runOnce();
@@ -0,0 +1,162 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ // Stub the OAuth connection store before importing anything that
13
+ // transitively pulls in the writer — otherwise importing the route
14
+ // module would try to open the real OAuth DB.
15
+ mock.module("../oauth/oauth-store.js", () => ({
16
+ listConnections: () => [],
17
+ }));
18
+
19
+ // Stub the DB-authoritative conversation count helper so the writer
20
+ // (invoked by the read-through fallback) does not lazy-open a real
21
+ // sqlite handle against a stale or deleted per-test tmpdir.
22
+ mock.module("../memory/conversation-queries.js", () => ({
23
+ countConversations: () => 0,
24
+ }));
25
+
26
+ const { handleGetHomeState, homeStateRouteDefinitions } =
27
+ await import("../runtime/routes/home-state-routes.js");
28
+ const { writeRelationshipState, getRelationshipStatePath } =
29
+ await import("../home/relationship-state-writer.js");
30
+
31
+ interface RelationshipStateWire {
32
+ version: number;
33
+ assistantId: string;
34
+ tier: number;
35
+ progressPercent: number;
36
+ facts: unknown[];
37
+ capabilities: Array<{ id: string; tier: string }>;
38
+ conversationCount: number;
39
+ hatchedDate: string;
40
+ assistantName: string;
41
+ userName?: string;
42
+ updatedAt: string;
43
+ }
44
+
45
+ let workspaceDir: string;
46
+ let origWorkspaceDir: string | undefined;
47
+
48
+ beforeEach(() => {
49
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-hsr-"));
50
+ origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
51
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
52
+ });
53
+
54
+ afterEach(() => {
55
+ if (origWorkspaceDir === undefined) {
56
+ delete process.env.VELLUM_WORKSPACE_DIR;
57
+ } else {
58
+ process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
59
+ }
60
+ try {
61
+ rmSync(workspaceDir, { recursive: true, force: true });
62
+ } catch {
63
+ // best-effort cleanup
64
+ }
65
+ });
66
+
67
+ describe("home-state-routes", () => {
68
+ describe("route registration", () => {
69
+ test("exposes GET /v1/home/state", () => {
70
+ const routes = homeStateRouteDefinitions();
71
+ expect(routes).toHaveLength(1);
72
+ expect(routes[0].endpoint).toBe("home/state");
73
+ expect(routes[0].method).toBe("GET");
74
+ });
75
+ });
76
+
77
+ describe("handleGetHomeState", () => {
78
+ test("returns persisted state when the JSON file exists", async () => {
79
+ // Seed a minimal USER.md so the writer produces a non-empty
80
+ // state, then let the writer persist it.
81
+ mkdirSync(workspaceDir, { recursive: true });
82
+ writeFileSync(
83
+ join(workspaceDir, "USER.md"),
84
+ "- Preferred name: Casey\n- Work role: Engineer\n",
85
+ "utf-8",
86
+ );
87
+ await writeRelationshipState();
88
+ expect(existsSync(getRelationshipStatePath())).toBe(true);
89
+
90
+ const res = await handleGetHomeState();
91
+ expect(res.status).toBe(200);
92
+ const body = (await res.json()) as RelationshipStateWire;
93
+ expect(body.version).toBe(1);
94
+ expect(body.assistantId).toBe("default");
95
+ expect(body.capabilities).toHaveLength(6);
96
+ expect(body.userName).toBe("Casey");
97
+ expect(typeof body.updatedAt).toBe("string");
98
+ expect(Number.isNaN(Date.parse(body.updatedAt))).toBe(false);
99
+ });
100
+
101
+ test("read-through fallback when the file is missing", async () => {
102
+ // Do NOT call the writer — the file should not exist. The
103
+ // route must still succeed via computeRelationshipState().
104
+ expect(existsSync(getRelationshipStatePath())).toBe(false);
105
+
106
+ const res = await handleGetHomeState();
107
+ expect(res.status).toBe(200);
108
+ const body = (await res.json()) as RelationshipStateWire;
109
+ expect(body.version).toBe(1);
110
+ expect(body.tier).toBe(1);
111
+ expect(body.progressPercent).toBe(0);
112
+ expect(body.capabilities).toHaveLength(6);
113
+ expect(body.conversationCount).toBe(0);
114
+
115
+ // Fallback must NOT write the file — that's the writer's job.
116
+ expect(existsSync(getRelationshipStatePath())).toBe(false);
117
+ });
118
+
119
+ test("falls back to compute when the persisted file is malformed", async () => {
120
+ // Write a deliberately broken file at the state path.
121
+ const path = getRelationshipStatePath();
122
+ mkdirSync(join(path, ".."), { recursive: true });
123
+ writeFileSync(path, "this is not json", "utf-8");
124
+
125
+ const res = await handleGetHomeState();
126
+ expect(res.status).toBe(200);
127
+ const body = (await res.json()) as RelationshipStateWire;
128
+ // Parsed body is a fresh compute, not the garbage on disk.
129
+ expect(body.version).toBe(1);
130
+ expect(body.capabilities).toHaveLength(6);
131
+ });
132
+
133
+ test("GET returns fresh state even when the persisted file is stale", async () => {
134
+ // Persist a snapshot with userName=Casey.
135
+ mkdirSync(workspaceDir, { recursive: true });
136
+ writeFileSync(
137
+ join(workspaceDir, "USER.md"),
138
+ "- Preferred name: Casey\n",
139
+ "utf-8",
140
+ );
141
+ await writeRelationshipState();
142
+ expect(existsSync(getRelationshipStatePath())).toBe(true);
143
+
144
+ // Mutate USER.md outside of any turn-boundary writer call. This
145
+ // simulates: a user editing their persona file, OAuth connecting
146
+ // a provider, or a conversation delete flow touching state
147
+ // outside the turn-boundary writer.
148
+ writeFileSync(
149
+ join(workspaceDir, "USER.md"),
150
+ "- Preferred name: Jamie\n",
151
+ "utf-8",
152
+ );
153
+
154
+ // The route must return the FRESH value (Jamie), not the
155
+ // cached value (Casey) from the persisted file.
156
+ const res = await handleGetHomeState();
157
+ expect(res.status).toBe(200);
158
+ const body = (await res.json()) as RelationshipStateWire;
159
+ expect(body.userName).toBe("Jamie");
160
+ });
161
+ });
162
+ });
@@ -405,26 +405,21 @@ describe("HostBashProxy", () => {
405
405
  function spySignal(source: AbortSignal): Spied {
406
406
  const addCalls: string[] = [];
407
407
  const removeCalls: string[] = [];
408
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
409
408
  const s = source as any;
410
409
  const origAdd = source.addEventListener.bind(source);
411
410
  const origRemove = source.removeEventListener.bind(source);
412
411
  s.addEventListener = (
413
412
  type: string,
414
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
415
413
  ...rest: any[]
416
414
  ) => {
417
415
  addCalls.push(type);
418
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
419
416
  return (origAdd as any)(type, ...rest);
420
417
  };
421
418
  s.removeEventListener = (
422
419
  type: string,
423
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
424
420
  ...rest: any[]
425
421
  ) => {
426
422
  removeCalls.push(type);
427
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
428
423
  return (origRemove as any)(type, ...rest);
429
424
  };
430
425
  return { signal: source, addCalls, removeCalls };