@vellumai/assistant 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -0,0 +1,469 @@
1
+ /**
2
+ * Home activity feed writer.
3
+ *
4
+ * Owns `<workspace>/data/home-feed.json`, the daemon-side source of
5
+ * truth for the macOS Home page activity feed. Handles the merge
6
+ * semantics defined by the TDD / plan:
7
+ *
8
+ * - Digest replacement: at most one digest per `source`. A fresh
9
+ * digest for a source replaces any prior digest for the same
10
+ * source in place.
11
+ * - Thread in-place update: if an incoming `thread` item shares its
12
+ * `id` with an existing item, replace that item while preserving
13
+ * its array position so the UI does not jitter on updates.
14
+ * - Author resolution: for matching `(type, source)` pairs the
15
+ * hybrid-authoring precedence is `assistant` beats `platform` —
16
+ * an assistant item overwrites an existing platform item for the
17
+ * same pair, but a platform item never overwrites an existing
18
+ * assistant item (no-op). Applies to nudges; actions are exempt
19
+ * (see next bullet).
20
+ * - Action append-without-replace: `action` items are the feed's
21
+ * activity log and never merge by `(type, source)` — each append
22
+ * becomes a distinct entry so successive background-job events
23
+ * don't collapse onto each other. A same-`id` action is the one
24
+ * exception: it performs an in-place update (same semantics as
25
+ * threads) so callers using a deterministic dedup id via
26
+ * `emit-feed-event.ts` can refresh an entry without appending a
27
+ * duplicate. Callers that want to auto-expire an action item
28
+ * must set `expiresAt` explicitly; the writer does NOT fill in
29
+ * a default expiry.
30
+ * - Per-source action cap: after merge, each source keeps at most
31
+ * {@link MAX_ACTIONS_PER_SOURCE} action items (most recent by
32
+ * `createdAt`). Older actions for that source are dropped so the
33
+ * on-disk file can't balloon as background jobs emit events.
34
+ * Action items without a `source` are unbounded and passed
35
+ * through untouched.
36
+ * - TTL filter on read: `readHomeFeed` drops any item whose
37
+ * `expiresAt` is in the past. This is a stateless sweep — the
38
+ * writer does not rewrite the file on read, so concurrent reads
39
+ * never race the writer.
40
+ *
41
+ * Concurrent writers are coalesced with the exact same "latest wins"
42
+ * pattern as `relationship-state-writer.ts`: at most one compute+write
43
+ * runs at a time, and overlapping calls during an in-flight write all
44
+ * resolve off a single tail write that reflects the final state.
45
+ *
46
+ * Each successful write publishes a `home_feed_updated` SSE event via
47
+ * the in-process `assistantEventHub`, carrying the post-filter count
48
+ * of items with `status === "new"` so subscribers can update unread
49
+ * badges without a full refetch.
50
+ */
51
+
52
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
53
+ import { join } from "node:path";
54
+
55
+ import { buildAssistantEvent } from "../runtime/assistant-event.js";
56
+ import { assistantEventHub } from "../runtime/assistant-event-hub.js";
57
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
58
+ import { getLogger } from "../util/logger.js";
59
+ import { getDataDir } from "../util/platform.js";
60
+ import {
61
+ type FeedItem,
62
+ type FeedItemStatus,
63
+ type HomeFeedFile,
64
+ parseFeedFile,
65
+ } from "./feed-types.js";
66
+
67
+ const log = getLogger("home-feed-writer");
68
+
69
+ /** Filename for the on-disk home feed. Lives under the workspace data dir. */
70
+ export const HOME_FEED_FILENAME = "home-feed.json";
71
+
72
+ /** On-disk file-format version. Bump + migrate if the shape changes. */
73
+ export const HOME_FEED_VERSION = 1;
74
+
75
+ /**
76
+ * Per-source volume cap for `action` items. When the post-merge item
77
+ * list has more than this many action items for a single source, the
78
+ * oldest (by `createdAt`) are dropped until the count is back within
79
+ * the cap. Other item types are unaffected, and action items without
80
+ * a `source` are also unaffected.
81
+ */
82
+ export const MAX_ACTIONS_PER_SOURCE = 20;
83
+
84
+ /**
85
+ * Canonical path to the home-feed snapshot
86
+ * (`<workspace>/data/home-feed.json`).
87
+ */
88
+ export function getHomeFeedPath(): string {
89
+ return join(getDataDir(), HOME_FEED_FILENAME);
90
+ }
91
+
92
+ /**
93
+ * Read the on-disk feed file, applying the stateless TTL filter.
94
+ *
95
+ * Returns an empty `HomeFeedFile` when the file is missing, unreadable,
96
+ * or fails Zod validation — callers never see a throw from this path.
97
+ * Items whose `expiresAt` is in the past are dropped from the returned
98
+ * `items` array but are NOT rewritten to disk; the next append cycle
99
+ * will persist the post-filter view naturally.
100
+ */
101
+ export function readHomeFeed(): HomeFeedFile {
102
+ const path = getHomeFeedPath();
103
+ const empty: HomeFeedFile = {
104
+ version: HOME_FEED_VERSION,
105
+ items: [],
106
+ updatedAt: new Date(0).toISOString(),
107
+ };
108
+
109
+ if (!existsSync(path)) {
110
+ return empty;
111
+ }
112
+
113
+ let raw: unknown;
114
+ try {
115
+ raw = JSON.parse(readFileSync(path, "utf-8"));
116
+ } catch (err) {
117
+ log.warn({ err, path }, "Failed to read home-feed.json; returning empty");
118
+ return empty;
119
+ }
120
+
121
+ let parsed: HomeFeedFile;
122
+ try {
123
+ parsed = parseFeedFile(raw);
124
+ } catch (err) {
125
+ log.warn(
126
+ { err, path },
127
+ "home-feed.json failed schema validation; returning empty",
128
+ );
129
+ return empty;
130
+ }
131
+
132
+ const now = Date.now();
133
+ const items = parsed.items.filter((item) => !isExpired(item, now));
134
+ return {
135
+ version: parsed.version,
136
+ items,
137
+ updatedAt: parsed.updatedAt,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Append (or merge) a single feed item and persist the result.
143
+ *
144
+ * See the module comment for the precise merge semantics. Never
145
+ * throws — all failures degrade to a warn-log so fire-and-forget
146
+ * callers in the daemon don't need a try/catch wrapper. Concurrent
147
+ * calls are coalesced via the in-module `writeInFlight` / `writeDirty`
148
+ * pattern so at most one write is in flight at a time.
149
+ */
150
+ export async function appendFeedItem(item: FeedItem): Promise<void> {
151
+ pendingAppends.push(item);
152
+ return scheduleWrite();
153
+ }
154
+
155
+ /**
156
+ * Update the `status` field of a single feed item by id.
157
+ *
158
+ * Returns the updated `FeedItem` on success, or `null` if no item with
159
+ * the given id exists. This is the path the HTTP route uses when the
160
+ * client marks an item as `"seen"` or `"acted_on"`. Concurrent patches
161
+ * go through the same coalescing queue as `appendFeedItem` so two
162
+ * overlapping status flips can't race each other.
163
+ *
164
+ * The patch is applied inside `runWrite()` so the existence check
165
+ * reads from the same state snapshot the mutation will land on —
166
+ * callers never observe a "phantom success" where we return an
167
+ * updated item for an id that no longer exists on disk by the time
168
+ * the queued write runs.
169
+ */
170
+ export async function patchFeedItemStatus(
171
+ id: string,
172
+ status: FeedItemStatus,
173
+ ): Promise<FeedItem | null> {
174
+ let resolveResult!: (value: FeedItem | null) => void;
175
+ const resultPromise = new Promise<FeedItem | null>((resolve) => {
176
+ resolveResult = resolve;
177
+ });
178
+ pendingPatches.push({ id, status, resolve: resolveResult });
179
+ void scheduleWrite();
180
+ return resultPromise;
181
+ }
182
+
183
+ // ─── Internal: coalescing queue ────────────────────────────────────────
184
+
185
+ /**
186
+ * Pending operations that land in the next coalesced write cycle.
187
+ * Appends and patches drain together so overlapping callers share a
188
+ * single compute+write tail.
189
+ */
190
+ const pendingAppends: FeedItem[] = [];
191
+ const pendingPatches: Array<{
192
+ id: string;
193
+ status: FeedItemStatus;
194
+ resolve: (value: FeedItem | null) => void;
195
+ }> = [];
196
+
197
+ let writeInFlight: Promise<void> | null = null;
198
+ let writeDirty = false;
199
+
200
+ /**
201
+ * Enqueue a write cycle. Mirrors the `relationship-state-writer.ts`
202
+ * coalescing pattern exactly: the first caller kicks off a run; any
203
+ * callers that arrive during an in-flight run mark dirty and resolve
204
+ * off the same tail promise, so N overlapping callers produce at most
205
+ * two runs (the initial + one coalesced tail).
206
+ */
207
+ function scheduleWrite(): Promise<void> {
208
+ if (writeInFlight) {
209
+ writeDirty = true;
210
+ return writeInFlight;
211
+ }
212
+ writeInFlight = (async () => {
213
+ try {
214
+ await runWrite();
215
+ while (writeDirty) {
216
+ writeDirty = false;
217
+ await runWrite();
218
+ }
219
+ } finally {
220
+ writeInFlight = null;
221
+ }
222
+ })();
223
+ return writeInFlight;
224
+ }
225
+
226
+ /**
227
+ * Drain the pending-operations queue into a fresh on-disk snapshot
228
+ * and publish the SSE event. Never throws — the write error is caught
229
+ * + logged so the coalescing loop can still move on to the next cycle.
230
+ */
231
+ async function runWrite(): Promise<void> {
232
+ const appendsToApply = pendingAppends.splice(0, pendingAppends.length);
233
+ const patchesToApply = pendingPatches.splice(0, pendingPatches.length);
234
+
235
+ const current = readHomeFeed();
236
+ let items = current.items.slice();
237
+
238
+ for (const incoming of appendsToApply) {
239
+ items = mergeIncoming(items, incoming);
240
+ }
241
+
242
+ items = pruneActionsPerSource(items);
243
+
244
+ // Track the per-patch result so callers can distinguish an update
245
+ // from an unknown-id no-op. We collect resolvers first and fire them
246
+ // after the write lands so the resolved `FeedItem` matches on-disk
247
+ // state exactly.
248
+ const patchResults: Array<{
249
+ resolve: (v: FeedItem | null) => void;
250
+ value: FeedItem | null;
251
+ }> = [];
252
+ for (const patch of patchesToApply) {
253
+ const idx = items.findIndex((i) => i.id === patch.id);
254
+ if (idx === -1) {
255
+ patchResults.push({ resolve: patch.resolve, value: null });
256
+ continue;
257
+ }
258
+ const updated: FeedItem = { ...items[idx]!, status: patch.status };
259
+ items[idx] = updated;
260
+ patchResults.push({ resolve: patch.resolve, value: updated });
261
+ }
262
+
263
+ items.sort(compareFeedItems);
264
+
265
+ const updatedAt = new Date().toISOString();
266
+ const next: HomeFeedFile = {
267
+ version: HOME_FEED_VERSION,
268
+ items,
269
+ updatedAt,
270
+ };
271
+
272
+ let wrote = false;
273
+ try {
274
+ const path = getHomeFeedPath();
275
+ mkdirSync(getDataDir(), { recursive: true });
276
+ writeFileSync(path, JSON.stringify(next, null, 2), "utf-8");
277
+ wrote = true;
278
+ log.info({ path, items: items.length }, "Wrote home-feed.json");
279
+ } catch (err) {
280
+ log.warn({ err }, "Failed to write home-feed.json");
281
+ }
282
+
283
+ if (wrote) {
284
+ const newItemCount = items.filter((i) => i.status === "new").length;
285
+ publishHomeFeedUpdated(updatedAt, newItemCount);
286
+ }
287
+
288
+ // Resolve pending patch promises AFTER we've emitted the SSE event
289
+ // so callers awaiting `patchFeedItemStatus` observe a fully
290
+ // consistent world: the on-disk file, the SSE event, and the
291
+ // returned `FeedItem` all reflect the same write.
292
+ //
293
+ // If the write failed, resolve all patch promises with `null` — the
294
+ // state was not persisted, and callers (e.g. HTTP route handlers)
295
+ // must not report success when the underlying write failed.
296
+ for (const { resolve, value } of patchResults) {
297
+ resolve(wrote ? value : null);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Apply the merge semantics for a single incoming item against the
303
+ * current item list and return a new list. Pure function — the input
304
+ * array is not mutated.
305
+ */
306
+ function mergeIncoming(items: FeedItem[], incoming: FeedItem): FeedItem[] {
307
+ // Digest replacement: one digest per source wins.
308
+ if (incoming.type === "digest" && incoming.source) {
309
+ const filtered = items.filter(
310
+ (i) => !(i.type === "digest" && i.source === incoming.source),
311
+ );
312
+ filtered.push(incoming);
313
+ return filtered;
314
+ }
315
+
316
+ // Thread in-place update: same id wins, preserve position.
317
+ if (incoming.type === "thread") {
318
+ const idx = items.findIndex(
319
+ (i) => i.type === "thread" && i.id === incoming.id,
320
+ );
321
+ if (idx !== -1) {
322
+ const copy = items.slice();
323
+ copy[idx] = incoming;
324
+ return copy;
325
+ }
326
+ }
327
+
328
+ // Action append-without-replace: each action item is a distinct
329
+ // activity-log entry and must NOT collapse onto an existing action
330
+ // for the same (type, source) pair. The per-source volume cap in
331
+ // `pruneActionsPerSource` keeps the log from growing unbounded.
332
+ //
333
+ // Exception: same-id in-place update. Callers that want
334
+ // deterministic dedup (e.g. via `emit-feed-event.ts`'s `dedupKey`)
335
+ // produce a stable id per logical event; a second emit with the
336
+ // same id refreshes the existing entry in place rather than
337
+ // appending a duplicate.
338
+ if (incoming.type === "action") {
339
+ const idx = items.findIndex(
340
+ (i) => i.type === "action" && i.id === incoming.id,
341
+ );
342
+ if (idx !== -1) {
343
+ const copy = items.slice();
344
+ copy[idx] = incoming;
345
+ return copy;
346
+ }
347
+ return [...items, incoming];
348
+ }
349
+
350
+ // Author resolution: for matching (type, source) pairs, assistant
351
+ // beats platform. A platform-authored incoming item against an
352
+ // existing assistant item is a no-op. Applies to nudges (actions
353
+ // short-circuit above).
354
+ if (incoming.source) {
355
+ const existingIdx = items.findIndex(
356
+ (i) => i.type === incoming.type && i.source === incoming.source,
357
+ );
358
+ if (existingIdx !== -1) {
359
+ const existing = items[existingIdx]!;
360
+ if (existing.author === "assistant" && incoming.author === "platform") {
361
+ // Platform can't overwrite assistant — no-op.
362
+ return items;
363
+ }
364
+ if (existing.author === "platform" && incoming.author === "assistant") {
365
+ const copy = items.slice();
366
+ copy[existingIdx] = incoming;
367
+ return copy;
368
+ }
369
+ }
370
+ }
371
+
372
+ return [...items, incoming];
373
+ }
374
+
375
+ /**
376
+ * Enforce the per-source volume cap on `action` items. For each
377
+ * source that has more than {@link MAX_ACTIONS_PER_SOURCE} actions in
378
+ * the post-merge list, keep the most recent by `createdAt` and drop
379
+ * the rest. Other item types and action items without a `source` are
380
+ * passed through untouched. Stable with respect to non-affected items.
381
+ */
382
+ function pruneActionsPerSource(items: FeedItem[]): FeedItem[] {
383
+ const actionsBySource = new Map<string, FeedItem[]>();
384
+ for (const item of items) {
385
+ if (item.type !== "action" || !item.source) continue;
386
+ const bucket = actionsBySource.get(item.source);
387
+ if (bucket) {
388
+ bucket.push(item);
389
+ } else {
390
+ actionsBySource.set(item.source, [item]);
391
+ }
392
+ }
393
+
394
+ const overflowing: string[] = [];
395
+ for (const [source, bucket] of actionsBySource) {
396
+ if (bucket.length > MAX_ACTIONS_PER_SOURCE) overflowing.push(source);
397
+ }
398
+ if (overflowing.length === 0) return items;
399
+
400
+ const keepIds = new Set<string>();
401
+ for (const source of overflowing) {
402
+ const bucket = actionsBySource.get(source)!.slice();
403
+ bucket.sort((a, b) => {
404
+ const am = Date.parse(a.createdAt);
405
+ const bm = Date.parse(b.createdAt);
406
+ if (Number.isNaN(am) && Number.isNaN(bm)) return 0;
407
+ if (Number.isNaN(am)) return 1;
408
+ if (Number.isNaN(bm)) return -1;
409
+ return bm - am;
410
+ });
411
+ for (const item of bucket.slice(0, MAX_ACTIONS_PER_SOURCE)) {
412
+ keepIds.add(item.id);
413
+ }
414
+ }
415
+
416
+ return items.filter((item) => {
417
+ if (item.type !== "action") return true;
418
+ if (!item.source) return true;
419
+ if (!overflowing.includes(item.source)) return true;
420
+ return keepIds.has(item.id);
421
+ });
422
+ }
423
+
424
+ /**
425
+ * Return `true` when the item has an `expiresAt` timestamp that is in
426
+ * the past relative to the supplied `nowMs`. Items without
427
+ * `expiresAt`, or with an unparseable value, are treated as not
428
+ * expired (fail-open).
429
+ */
430
+ function isExpired(item: FeedItem, nowMs: number): boolean {
431
+ if (!item.expiresAt) return false;
432
+ const expiresMs = Date.parse(item.expiresAt);
433
+ if (Number.isNaN(expiresMs)) return false;
434
+ return expiresMs <= nowMs;
435
+ }
436
+
437
+ /**
438
+ * Sort comparator: priority DESC, then createdAt DESC. Matches the
439
+ * ordering contract the UI expects so higher-priority and fresher
440
+ * items sort to the top of the feed.
441
+ */
442
+ function compareFeedItems(a: FeedItem, b: FeedItem): number {
443
+ if (a.priority !== b.priority) return b.priority - a.priority;
444
+ const aMs = Date.parse(a.createdAt);
445
+ const bMs = Date.parse(b.createdAt);
446
+ if (Number.isNaN(aMs) && Number.isNaN(bMs)) return 0;
447
+ if (Number.isNaN(aMs)) return 1;
448
+ if (Number.isNaN(bMs)) return -1;
449
+ return bMs - aMs;
450
+ }
451
+
452
+ /**
453
+ * Publish a `home_feed_updated` event to the in-process hub. Wrapped
454
+ * in a `.catch` so a subscriber rejection never bubbles up into the
455
+ * writer coalescing loop.
456
+ */
457
+ function publishHomeFeedUpdated(updatedAt: string, newItemCount: number): void {
458
+ assistantEventHub
459
+ .publish(
460
+ buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, {
461
+ type: "home_feed_updated",
462
+ updatedAt,
463
+ newItemCount,
464
+ }),
465
+ )
466
+ .catch((err) => {
467
+ log.warn({ err }, "Failed to publish home_feed_updated event");
468
+ });
469
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Platform-baseline Gmail digest generator.
3
+ *
4
+ * Produces a mechanical "N new emails" digest FeedItem for the home
5
+ * activity feed. This is the first platform-authored feed source —
6
+ * it writes a digest item via the feed writer. Scheduling/invocation
7
+ * wiring lands in a follow-up PR when the end-to-end feed flow is
8
+ * turned on.
9
+ *
10
+ * Design notes:
11
+ *
12
+ * - No LLM calls. Title and summary are purely mechanical so this
13
+ * path stays cheap and deterministic. Assistant-authored nudges
14
+ * can override a platform digest for the same `(type, source)`
15
+ * pair via the feed writer's hybrid-authoring resolver (see
16
+ * `feed-writer.ts`).
17
+ * - No direct Gmail API fetches. The count is read from whatever
18
+ * integration cache already exists. A dependency-injected count
19
+ * source keeps the function testable and leaves room for the real
20
+ * integration wiring to land in a follow-up PR.
21
+ * - One-per-source replacement is handled by the writer — a fresh
22
+ * digest automatically replaces any prior Gmail digest in place
23
+ * on each call.
24
+ * - `minTimeAway: 3600` (1 hour) avoids showing the digest to users
25
+ * who've only briefly stepped away. Priority `40` is mid-tier so
26
+ * assistant-authored items naturally win on the sort.
27
+ */
28
+
29
+ import { randomUUID } from "node:crypto";
30
+
31
+ import { getLogger } from "../util/logger.js";
32
+ import type { FeedItem } from "./feed-types.js";
33
+ import { appendFeedItem, readHomeFeed } from "./feed-writer.js";
34
+
35
+ const log = getLogger("platform-gmail-digest");
36
+
37
+ /**
38
+ * Count source for pending Gmail emails. Kept as an injectable
39
+ * dependency so tests can supply a deterministic number and so the
40
+ * real wiring (an integration cache / event bus) can be swapped in
41
+ * without touching this module.
42
+ *
43
+ * The default source returns 0 — there is no persistent, platform-
44
+ * wide Gmail inbox count tracked in the daemon today, so the default
45
+ * path is a no-op until a real count source is wired in.
46
+ */
47
+ export type GmailCountSource = () => Promise<number>;
48
+
49
+ async function defaultGmailCountSource(): Promise<number> {
50
+ // No platform-wide Gmail inbox count exists in the daemon yet.
51
+ // Callers pass an explicit `countSource` from whatever integration
52
+ // state is appropriate for their context (tests, watcher store,
53
+ // future integration event bus, etc.).
54
+ return 0;
55
+ }
56
+
57
+ /**
58
+ * Build and append a platform-baseline Gmail digest feed item.
59
+ *
60
+ * Returns `null` when the count is 0 (no-op; we do not write an
61
+ * empty digest). Otherwise returns the constructed `FeedItem` after
62
+ * successfully enqueueing it via `appendFeedItem`.
63
+ *
64
+ * Never throws — all failures degrade to a warn-log so the caller
65
+ * (a scheduler tick) can fire-and-forget without a try/catch.
66
+ */
67
+ export async function generateGmailDigest(
68
+ now: Date,
69
+ countSource: GmailCountSource = defaultGmailCountSource,
70
+ ): Promise<FeedItem | null> {
71
+ let count: number;
72
+ try {
73
+ count = await countSource();
74
+ } catch (err) {
75
+ log.warn({ err }, "Gmail count source threw; skipping digest");
76
+ return null;
77
+ }
78
+
79
+ if (!Number.isFinite(count) || count <= 0) {
80
+ return null;
81
+ }
82
+
83
+ const flooredCount = Math.floor(count);
84
+ const timestamp = now.toISOString();
85
+ const summary = buildDigestSummary(now);
86
+
87
+ const item: FeedItem = {
88
+ id: randomUUID(),
89
+ type: "digest",
90
+ source: "gmail",
91
+ author: "platform",
92
+ title: `${flooredCount} new email${flooredCount === 1 ? "" : "s"}`,
93
+ summary,
94
+ priority: 40,
95
+ minTimeAway: 3600,
96
+ timestamp,
97
+ createdAt: timestamp,
98
+ status: "new",
99
+ };
100
+
101
+ try {
102
+ await appendFeedItem(item);
103
+ } catch (err) {
104
+ log.warn({ err }, "Failed to append Gmail digest to feed");
105
+ return null;
106
+ }
107
+
108
+ return item;
109
+ }
110
+
111
+ /**
112
+ * Builds the digest summary line. Reads the prior Gmail digest's
113
+ * timestamp from the feed and formats it as `"Since <short time>"`
114
+ * so users can anchor "new emails" to a specific moment. Falls back
115
+ * to a generic string on first-ever digest or on any read failure
116
+ * (the writer is authoritative; we never throw out of the generator).
117
+ */
118
+ function buildDigestSummary(now: Date): string {
119
+ const priorTimestamp = readPriorGmailDigestTimestamp();
120
+ if (priorTimestamp == null) {
121
+ return "Since your last check-in";
122
+ }
123
+
124
+ const priorDate = new Date(priorTimestamp);
125
+ if (Number.isNaN(priorDate.getTime())) {
126
+ return "Since your last check-in";
127
+ }
128
+
129
+ return `Since ${formatShortTime(priorDate, now)}`;
130
+ }
131
+
132
+ function readPriorGmailDigestTimestamp(): string | null {
133
+ try {
134
+ const feed = readHomeFeed();
135
+ const prior = feed.items.find(
136
+ (item) => item.type === "digest" && item.source === "gmail",
137
+ );
138
+ return prior?.timestamp ?? null;
139
+ } catch (err) {
140
+ log.warn({ err }, "Failed to read prior Gmail digest timestamp");
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Same-day prior → "10:32 AM". Cross-day prior → "Mon 10:32 AM".
147
+ * Plain `toLocaleTimeString` would conflate yesterday and today.
148
+ */
149
+ function formatShortTime(prior: Date, now: Date): string {
150
+ const time = prior.toLocaleTimeString("en-US", {
151
+ hour: "numeric",
152
+ minute: "2-digit",
153
+ });
154
+ const sameDay =
155
+ prior.getFullYear() === now.getFullYear() &&
156
+ prior.getMonth() === now.getMonth() &&
157
+ prior.getDate() === now.getDate();
158
+ if (sameDay) {
159
+ return time;
160
+ }
161
+ const weekday = prior.toLocaleDateString("en-US", { weekday: "short" });
162
+ return `${weekday} ${time}`;
163
+ }