@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
@@ -2,6 +2,13 @@ import { getConfig } from "../config/loader.js";
2
2
  import type { AssistantConfig } from "../config/types.js";
3
3
  import { getLogger } from "../util/logger.js";
4
4
  import { getMemoryCheckpoint, setMemoryCheckpoint } from "./checkpoints.js";
5
+ import {
6
+ getLastScheduledCleanupEnqueueMs,
7
+ markScheduledCleanupEnqueued,
8
+ resetCleanupScheduleThrottle as resetCleanupScheduleThrottleImpl,
9
+ } from "./cleanup-schedule-state.js";
10
+ import { conversationAnalyzeJob } from "./conversation-analyze-job.js";
11
+ import { maybeRunDbMaintenance } from "./db-maintenance.js";
5
12
  import { bootstrapFromHistory } from "./graph/bootstrap.js";
6
13
  import { runConsolidation } from "./graph/consolidation.js";
7
14
  import { runDecayTick } from "./graph/decay.js";
@@ -160,6 +167,7 @@ export async function runMemoryJobsOnce(
160
167
  maybeEnqueueScheduledCleanupJobs(config);
161
168
  }
162
169
  maybeEnqueueGraphMaintenanceJobs();
170
+ maybeRunDbMaintenance();
163
171
  return 0;
164
172
  }
165
173
 
@@ -256,6 +264,7 @@ export async function runMemoryJobsOnce(
256
264
  maybeEnqueueScheduledCleanupJobs(config);
257
265
  }
258
266
  maybeEnqueueGraphMaintenanceJobs();
267
+ maybeRunDbMaintenance();
259
268
  return processed;
260
269
  }
261
270
 
@@ -417,6 +426,9 @@ async function processJob(
417
426
  case "graph_extract":
418
427
  await graphExtractJob(job, config);
419
428
  return;
429
+ case "conversation_analyze":
430
+ await conversationAnalyzeJob(job, config);
431
+ return;
420
432
  case "graph_decay":
421
433
  graphDecayJob(job);
422
434
  return;
@@ -449,12 +461,13 @@ async function processJob(
449
461
 
450
462
  // ── Cleanup scheduling ─────────────────────────────────────────────
451
463
 
452
- let lastScheduledCleanupEnqueueMs = 0;
453
-
454
- /** Reset the cleanup enqueue throttle so tests can run deterministic checks. */
455
- export function resetCleanupScheduleThrottle(): void {
456
- lastScheduledCleanupEnqueueMs = 0;
457
- }
464
+ /**
465
+ * Re-export of the shared throttle-reset helper. The underlying state lives
466
+ * in cleanup-schedule-state.ts so that lighter-weight callers (e.g.
467
+ * ConfigWatcher) can reset it without pulling in jobs-worker's transitive
468
+ * imports.
469
+ */
470
+ export const resetCleanupScheduleThrottle = resetCleanupScheduleThrottleImpl;
458
471
 
459
472
  /**
460
473
  * Enqueue periodic cleanup jobs using config-driven retention windows.
@@ -466,7 +479,7 @@ export function maybeEnqueueScheduledCleanupJobs(
466
479
  ): boolean {
467
480
  const cleanup = config.memory.cleanup;
468
481
  if (!cleanup.enabled) return false;
469
- if (nowMs - lastScheduledCleanupEnqueueMs < cleanup.enqueueIntervalMs)
482
+ if (nowMs - getLastScheduledCleanupEnqueueMs() < cleanup.enqueueIntervalMs)
470
483
  return false;
471
484
 
472
485
  const pruneConversationsJobId =
@@ -474,10 +487,10 @@ export function maybeEnqueueScheduledCleanupJobs(
474
487
  ? enqueuePruneOldConversationsJob(cleanup.conversationRetentionDays)
475
488
  : null;
476
489
  const pruneLlmRequestLogsJobId =
477
- cleanup.llmRequestLogRetentionMs > 0
490
+ cleanup.llmRequestLogRetentionMs !== null
478
491
  ? enqueuePruneOldLlmRequestLogsJob(cleanup.llmRequestLogRetentionMs)
479
492
  : null;
480
- lastScheduledCleanupEnqueueMs = nowMs;
493
+ markScheduledCleanupEnqueued(nowMs);
481
494
  log.debug(
482
495
  {
483
496
  pruneConversationsJobId,
@@ -9,6 +9,11 @@ import type {
9
9
  import { getDb } from "./db.js";
10
10
  import { rawAll } from "./raw-query.js";
11
11
  import { llmUsageEvents } from "./schema.js";
12
+ import {
13
+ bucketEventsByDay,
14
+ bucketEventsByHour,
15
+ type UsageEventBucketRow,
16
+ } from "./usage-buckets.js";
12
17
 
13
18
  // ---------------------------------------------------------------------------
14
19
  // Write
@@ -152,8 +157,25 @@ export type UsageGranularity = "daily" | "hourly";
152
157
 
153
158
  /** A single time bucket with its aggregate totals. */
154
159
  export interface UsageDayBucket {
155
- /** ISO date string: "YYYY-MM-DD" (daily) or "YYYY-MM-DD HH:00" (hourly), UTC. */
160
+ /**
161
+ * Stable unique identifier for the bucket. Safe for use as a SwiftUI/React
162
+ * list key. Distinct even for DST fall-back duplicate hours (which share the
163
+ * same `date` string). Daily buckets use `date` directly; hourly buckets use
164
+ * "YYYY-MM-DD HH:00|<offsetMinutes>" to disambiguate repeated local hours.
165
+ */
166
+ bucketId: string;
167
+ /**
168
+ * Local-time bucket key in the requested tz:
169
+ * "YYYY-MM-DD" (daily) or "YYYY-MM-DD HH:00" (hourly).
170
+ * NOT unique: on DST fall-back days, two 01:00 hourly buckets share this key.
171
+ * Use `bucketId` as a list identifier and `date` for display/sort only.
172
+ */
156
173
  date: string;
174
+ /**
175
+ * Human-readable label for the bucket, formatted in the requested tz.
176
+ * Hourly: "3pm". Daily: "Apr 11".
177
+ */
178
+ displayLabel?: string;
157
179
  /** Direct input tokens only; cache traffic is tracked separately in totals. */
158
180
  totalInputTokens: number;
159
181
  totalOutputTokens: number;
@@ -164,6 +186,14 @@ export interface UsageDayBucket {
164
186
  /** A grouped breakdown row (by actor, provider, or model). */
165
187
  export interface UsageGroupBreakdown {
166
188
  group: string;
189
+ /**
190
+ * Stable identifier for the group. Populated with the conversation id when
191
+ * `groupBy === "conversation"` (and `null` for that mode's "Other" bucket,
192
+ * which aggregates events with no conversation id). For all other group-bys
193
+ * (`actor`, `provider`, `model`) this is always `null` — the raw grouping
194
+ * column is already exposed via `group`.
195
+ */
196
+ groupId: string | null;
167
197
  /** Direct input tokens only; cache traffic is reported separately below. */
168
198
  totalInputTokens: number;
169
199
  totalOutputTokens: number;
@@ -186,16 +216,10 @@ interface TotalsRow {
186
216
  unpriced_event_count: number;
187
217
  }
188
218
 
189
- interface DayBucketRow {
190
- date: string;
191
- total_input_tokens: number;
192
- total_output_tokens: number;
193
- total_estimated_cost_usd: number | null;
194
- event_count: number;
195
- }
196
219
 
197
220
  interface GroupRow {
198
221
  group_key: string;
222
+ group_id: string | null;
199
223
  total_input_tokens: number;
200
224
  total_output_tokens: number;
201
225
  total_cache_creation_tokens: number;
@@ -238,67 +262,68 @@ export function getUsageTotals(range: UsageTimeRange): UsageTotals {
238
262
  };
239
263
  }
240
264
 
241
- /**
242
- * Return per-day aggregates (UTC) within the given time range, ordered by date ascending.
243
- *
244
- * Each bucket key is a YYYY-MM-DD string derived by dividing the epoch-millis
245
- * timestamp by 86400000 and formatting as a date.
246
- */
247
- export function getUsageDayBuckets(range: UsageTimeRange): UsageDayBucket[] {
248
- const rows = rawAll<DayBucketRow>(
265
+ /** Fetch raw events in a time range for in-memory bucketing. */
266
+ function fetchRawBucketRows(range: UsageTimeRange): UsageEventBucketRow[] {
267
+ return rawAll<UsageEventBucketRow>(
249
268
  /*sql*/ `
250
269
  SELECT
251
- strftime('%Y-%m-%d', created_at / 1000, 'unixepoch') AS date,
252
- COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
253
- COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
254
- COALESCE(SUM(estimated_cost_usd), 0) AS total_estimated_cost_usd,
255
- COALESCE(SUM(COALESCE(llm_call_count, 1)), 0) AS event_count
270
+ created_at,
271
+ input_tokens,
272
+ output_tokens,
273
+ estimated_cost_usd,
274
+ llm_call_count
256
275
  FROM llm_usage_events
257
276
  WHERE created_at >= ?1 AND created_at <= ?2
258
- GROUP BY date
259
- ORDER BY date ASC
260
277
  `,
261
278
  range.from,
262
279
  range.to,
263
280
  );
264
- return rows.map((r) => ({
265
- date: r.date,
266
- totalInputTokens: r.total_input_tokens,
267
- totalOutputTokens: r.total_output_tokens,
268
- totalEstimatedCostUsd: r.total_estimated_cost_usd ?? 0,
269
- eventCount: r.event_count,
270
- }));
281
+ }
282
+
283
+ /** Options for bucket aggregation. */
284
+ export interface UsageBucketOptions {
285
+ /**
286
+ * When true, emit a zero-value bucket for every day (or hour) in the range
287
+ * even if no events fall inside it. Defaults to false so the CLI and other
288
+ * callers only see active periods; the chart route opts in.
289
+ */
290
+ fillEmpty?: boolean;
271
291
  }
272
292
 
273
293
  /**
274
- * Return per-hour aggregates (UTC) within the given time range, ordered ascending.
294
+ * Return per-day aggregates within the given time range, keyed by local date
295
+ * in the requested timezone (default UTC).
275
296
  *
276
- * Each bucket key is a "YYYY-MM-DD HH:00" string.
297
+ * Each bucket key is a "YYYY-MM-DD" string anchored on local midnight in `tz`.
298
+ * When `options.fillEmpty` is true, empty days within the range are filled
299
+ * with zero-value buckets. DST-short and DST-long local days are handled
300
+ * correctly.
277
301
  */
278
- export function getUsageHourBuckets(range: UsageTimeRange): UsageDayBucket[] {
279
- const rows = rawAll<DayBucketRow>(
280
- /*sql*/ `
281
- SELECT
282
- strftime('%Y-%m-%d %H:00', created_at / 1000, 'unixepoch') AS date,
283
- COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
284
- COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
285
- COALESCE(SUM(estimated_cost_usd), 0) AS total_estimated_cost_usd,
286
- COALESCE(SUM(COALESCE(llm_call_count, 1)), 0) AS event_count
287
- FROM llm_usage_events
288
- WHERE created_at >= ?1 AND created_at <= ?2
289
- GROUP BY date
290
- ORDER BY date ASC
291
- `,
292
- range.from,
293
- range.to,
294
- );
295
- return rows.map((r) => ({
296
- date: r.date,
297
- totalInputTokens: r.total_input_tokens,
298
- totalOutputTokens: r.total_output_tokens,
299
- totalEstimatedCostUsd: r.total_estimated_cost_usd ?? 0,
300
- eventCount: r.event_count,
301
- }));
302
+ export function getUsageDayBuckets(
303
+ range: UsageTimeRange,
304
+ tz: string = "UTC",
305
+ options: UsageBucketOptions = {},
306
+ ): UsageDayBucket[] {
307
+ const rows = fetchRawBucketRows(range);
308
+ return bucketEventsByDay(rows, range, tz, options);
309
+ }
310
+
311
+ /**
312
+ * Return per-hour aggregates within the given time range, keyed by local hour
313
+ * in the requested timezone (default UTC).
314
+ *
315
+ * Each bucket key is a "YYYY-MM-DD HH:00" string anchored on local hour starts.
316
+ * When `options.fillEmpty` is true, empty hours are filled with zero-value
317
+ * buckets. DST fall-back produces two distinct buckets for the duplicated hour;
318
+ * DST spring-forward produces 23 buckets for the affected day.
319
+ */
320
+ export function getUsageHourBuckets(
321
+ range: UsageTimeRange,
322
+ tz: string = "UTC",
323
+ options: UsageBucketOptions = {},
324
+ ): UsageDayBucket[] {
325
+ const rows = fetchRawBucketRows(range);
326
+ return bucketEventsByHour(rows, range, tz, options);
302
327
  }
303
328
 
304
329
  type GroupByDimension = "actor" | "provider" | "model" | "conversation";
@@ -330,6 +355,7 @@ export function getUsageGroupBreakdown(
330
355
  CASE WHEN e.conversation_id IS NULL THEN 'Other'
331
356
  ELSE COALESCE(c.title, 'Untitled')
332
357
  END AS group_key,
358
+ e.conversation_id AS group_id,
333
359
  COALESCE(SUM(e.input_tokens), 0) AS total_input_tokens,
334
360
  COALESCE(SUM(e.output_tokens), 0) AS total_output_tokens,
335
361
  COALESCE(SUM(e.cache_creation_input_tokens), 0) AS total_cache_creation_tokens,
@@ -348,6 +374,10 @@ export function getUsageGroupBreakdown(
348
374
  );
349
375
  return rows.map((r) => ({
350
376
  group: r.group_key,
377
+ // `GROUP BY e.conversation_id` makes `e.conversation_id` unambiguous
378
+ // inside each group — it is the seeded conversation id for real rows
379
+ // and `null` for the "Other" bucket (events with no conversation).
380
+ groupId: r.group_id,
351
381
  totalInputTokens: r.total_input_tokens,
352
382
  totalOutputTokens: r.total_output_tokens,
353
383
  totalCacheCreationTokens: r.total_cache_creation_tokens,
@@ -362,6 +392,7 @@ export function getUsageGroupBreakdown(
362
392
  /*sql*/ `
363
393
  SELECT
364
394
  ${column} AS group_key,
395
+ NULL AS group_id,
365
396
  COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
366
397
  COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
367
398
  COALESCE(SUM(cache_creation_input_tokens), 0) AS total_cache_creation_tokens,
@@ -378,6 +409,11 @@ export function getUsageGroupBreakdown(
378
409
  );
379
410
  return rows.map((r) => ({
380
411
  group: r.group_key,
412
+ // Non-conversation group-bys (actor/provider/model) don't have a
413
+ // separate stable id — the grouping column itself is the identifier
414
+ // and is already exposed via `group`. The SELECT projects
415
+ // `NULL AS group_id` so the runtime shape matches `GroupRow`.
416
+ groupId: r.group_id,
381
417
  totalInputTokens: r.total_input_tokens,
382
418
  totalOutputTokens: r.total_output_tokens,
383
419
  totalCacheCreationTokens: r.total_cache_creation_tokens,
@@ -0,0 +1,15 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateOAuthProvidersTokenExchangeBodyFormat(
5
+ database: DrizzleDb,
6
+ ): void {
7
+ const raw = getSqliteFrom(database);
8
+ try {
9
+ raw.exec(
10
+ /*sql*/ `ALTER TABLE oauth_providers ADD COLUMN token_exchange_body_format TEXT NOT NULL DEFAULT 'form'`,
11
+ );
12
+ } catch {
13
+ // Column already exists — nothing to do.
14
+ }
15
+ }
@@ -0,0 +1,190 @@
1
+ import { computeUserFileBaseSlug } from "../../contacts/contact-store.js";
2
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
3
+ import { withCrashRecovery } from "./validate-migration-state.js";
4
+
5
+ /**
6
+ * Reverse is a no-op. This migration only consolidates `user_file` across
7
+ * contacts sharing the same `principal_id`; the pre-migration split values
8
+ * cannot be reconstructed after normalization, and no schema changes.
9
+ */
10
+ export function downNormalizeUserFileByPrincipal(_database: DrizzleDb): void {
11
+ /* no-op */
12
+ }
13
+
14
+ /**
15
+ * Heuristic: does `userFile` look like an auto-incremented persona slug?
16
+ *
17
+ * `generateUserFileSlug` appends `-<N>.md` where N is any positive integer
18
+ * (the loop is unbounded, so a very dense principal space could reach 4+
19
+ * digits). Matching is anchored to the end so a slug that happens to contain
20
+ * digits earlier (e.g. `alex-2024.md` for display name "Alex 2024") is not
21
+ * affected.
22
+ *
23
+ * The final-integer suffix also matches year-like or date-like names
24
+ * (`-2025.md`, `-2025-04-13.md` via the trailing `-13.md`). Those must NOT
25
+ * be classified as auto-increments, so we exclude any filename that ends
26
+ * with a date-shaped tail: `-YYYY.md`, `-YYYY-MM.md`, or `-YYYY-MM-DD.md`
27
+ * where YYYY is a 4-digit year starting with 19, 20, or 21. A counter that
28
+ * happens to fall in that range (e.g. `-1999.md`) is indistinguishable from
29
+ * a year by filename alone, so we conservatively treat it as non-auto.
30
+ *
31
+ * Month/day segments must be 2 digits (ISO style) to discriminate them from
32
+ * single-digit collision counters: `generateUserFileSlug` emits `-2.md`,
33
+ * `-3.md`, etc. without leading zeros, so `alex-2025-2.md` is a counter on
34
+ * base `alex-2025.md` — not a date — and must remain classified as auto.
35
+ *
36
+ * Filename-only classification is still ambiguous at the margins: a display
37
+ * name like "Alex 2025 4" legitimately produces `alex-2025-4.md` as a base
38
+ * slug, which looks identical to a year-prefixed counter. When the caller can
39
+ * supply the row's display name, we disambiguate by recomputing the expected
40
+ * base slug: if it matches the filename, the name is a base slug and we
41
+ * classify as non-auto. This closes the only remaining false-positive hole.
42
+ */
43
+ const DATE_LIKE_SUFFIX = /-(19|20|21)\d{2}((-\d{2}){1,2})?\.md$/;
44
+ const INTEGER_SUFFIX = /-\d+\.md$/;
45
+
46
+ export function isAutoIncrementedUserFile(
47
+ userFile: string,
48
+ displayName?: string,
49
+ ): boolean {
50
+ if (DATE_LIKE_SUFFIX.test(userFile)) return false;
51
+ if (!INTEGER_SUFFIX.test(userFile)) return false;
52
+ if (displayName !== undefined) {
53
+ const expectedBase = `${computeUserFileBaseSlug(displayName)}.md`;
54
+ if (expectedBase === userFile) return false;
55
+ }
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * Normalize `contacts.user_file` across contact rows that share the same
61
+ * `principal_id`.
62
+ *
63
+ * Multiple contact rows may represent the same principal (one per channel:
64
+ * desktop, phone, Slack, etc.). When a new row was created for a second
65
+ * channel, `generateUserFileSlug(displayName)` auto-incremented to avoid a
66
+ * filename collision (e.g. `sidd.md` → `sidd-2.md`), even though no
67
+ * `sidd-2.md` file ever existed on disk. The persona resolver then silently
68
+ * fell back to `users/default.md` for that channel's messages — and the same
69
+ * slug is used for the journal directory, so the user lost per-principal
70
+ * continuity on every non-primary channel.
71
+ *
72
+ * This migration picks one canonical `user_file` per principal and updates
73
+ * every sibling row to match. Selection heuristic:
74
+ *
75
+ * 1. Prefer values that do NOT look auto-incremented (see
76
+ * `isAutoIncrementedUserFile`).
77
+ * 2. Among those, prefer the oldest contact row (earliest `created_at`).
78
+ * 3. Ties broken by `id` for determinism.
79
+ *
80
+ * Skips principals where only one distinct (non-null) value exists — nothing
81
+ * to normalize. Principals whose contacts all have `user_file = NULL` are
82
+ * left untouched; the code path in `upsertContact` will populate them on the
83
+ * next write.
84
+ */
85
+ export function migrateNormalizeUserFileByPrincipal(
86
+ database: DrizzleDb,
87
+ ): void {
88
+ withCrashRecovery(
89
+ database,
90
+ "migration_normalize_user_file_by_principal_v1",
91
+ () => {
92
+ const raw = getSqliteFrom(database);
93
+
94
+ const tableExists = raw
95
+ .query(
96
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'contacts'`,
97
+ )
98
+ .get();
99
+ if (!tableExists) return;
100
+
101
+ const userFileColExists = raw
102
+ .query(
103
+ `SELECT 1 FROM pragma_table_info('contacts') WHERE name = 'user_file'`,
104
+ )
105
+ .get();
106
+ const principalColExists = raw
107
+ .query(
108
+ `SELECT 1 FROM pragma_table_info('contacts') WHERE name = 'principal_id'`,
109
+ )
110
+ .get();
111
+ if (!userFileColExists || !principalColExists) return;
112
+
113
+ try {
114
+ raw.exec("BEGIN");
115
+
116
+ const principals = raw
117
+ .query(
118
+ /*sql*/ `
119
+ SELECT principal_id
120
+ FROM contacts
121
+ WHERE principal_id IS NOT NULL
122
+ GROUP BY principal_id
123
+ HAVING COUNT(DISTINCT COALESCE(user_file, '')) > 1
124
+ `,
125
+ )
126
+ .all() as Array<{ principal_id: string }>;
127
+
128
+ // Fetch all non-null candidates and rank in JS. The auto-increment
129
+ // classification is a regex that SQLite's GLOB can't express cleanly
130
+ // (unbounded digit count, date-pattern exclusion), and keeping the
131
+ // logic in one place avoids SQL/JS drift.
132
+ const selectCandidates = raw.prepare(
133
+ /*sql*/ `
134
+ SELECT user_file, display_name, created_at, id FROM contacts
135
+ WHERE principal_id = ? AND user_file IS NOT NULL
136
+ `,
137
+ );
138
+
139
+ const updateSiblings = raw.prepare(
140
+ /*sql*/ `
141
+ UPDATE contacts
142
+ SET user_file = ?, updated_at = ?
143
+ WHERE principal_id = ?
144
+ AND (user_file IS NULL OR user_file != ?)
145
+ `,
146
+ );
147
+
148
+ for (const { principal_id } of principals) {
149
+ const candidates = selectCandidates.all(principal_id) as Array<{
150
+ user_file: string;
151
+ display_name: string;
152
+ created_at: number;
153
+ id: string;
154
+ }>;
155
+ if (candidates.length === 0) continue;
156
+
157
+ candidates.sort((a, b) => {
158
+ const aAuto = isAutoIncrementedUserFile(a.user_file, a.display_name)
159
+ ? 1
160
+ : 0;
161
+ const bAuto = isAutoIncrementedUserFile(b.user_file, b.display_name)
162
+ ? 1
163
+ : 0;
164
+ if (aAuto !== bAuto) return aAuto - bAuto;
165
+ if (a.created_at !== b.created_at)
166
+ return a.created_at - b.created_at;
167
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
168
+ });
169
+
170
+ const canonical = candidates[0]!.user_file;
171
+ updateSiblings.run(
172
+ canonical,
173
+ Date.now(),
174
+ principal_id,
175
+ canonical,
176
+ );
177
+ }
178
+
179
+ raw.exec("COMMIT");
180
+ } catch (e) {
181
+ try {
182
+ raw.exec("ROLLBACK");
183
+ } catch {
184
+ /* no active transaction */
185
+ }
186
+ throw e;
187
+ }
188
+ },
189
+ );
190
+ }
@@ -0,0 +1,16 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateConversationsArchivedAt(database: DrizzleDb): void {
5
+ const raw = getSqliteFrom(database);
6
+ try {
7
+ raw.exec(
8
+ `ALTER TABLE conversations ADD COLUMN archived_at INTEGER DEFAULT NULL`,
9
+ );
10
+ } catch {
11
+ // Column already exists — nothing to do.
12
+ }
13
+ raw.exec(
14
+ `CREATE INDEX IF NOT EXISTS idx_conversations_archived_at ON conversations (archived_at)`,
15
+ );
16
+ }
@@ -160,6 +160,12 @@ export { migrateOAuthProvidersRevoke } from "./215-oauth-providers-revoke.js";
160
160
  export { migrateOAuthProvidersTokenAuthMethodDefault } from "./216-oauth-providers-token-auth-method.js";
161
161
  export { migrateConversationHostAccess } from "./217-conversation-host-access.js";
162
162
  export { migrateOAuthProvidersLogoUrl } from "./218-oauth-providers-logo-url.js";
163
+ export { migrateOAuthProvidersTokenExchangeBodyFormat } from "./219-oauth-providers-token-exchange-body-format.js";
164
+ export {
165
+ downNormalizeUserFileByPrincipal,
166
+ migrateNormalizeUserFileByPrincipal,
167
+ } from "./220-normalize-user-file-by-principal.js";
168
+ export { migrateConversationsArchivedAt } from "./221-conversations-archived-at.js";
163
169
  export {
164
170
  MIGRATION_REGISTRY,
165
171
  type MigrationRegistryEntry,
@@ -42,6 +42,7 @@ import { migrateStripIntegrationPrefixFromProviderKeysDown } from "./196-strip-i
42
42
  import { migrateRenameMemoryGraphTypeValuesDown } from "./204-rename-memory-graph-type-values.js";
43
43
  import { migrateScrubCorruptedImageAttachmentsDown } from "./206-scrub-corrupted-image-attachments.js";
44
44
  import { downConversationHostAccess } from "./217-conversation-host-access.js";
45
+ import { downNormalizeUserFileByPrincipal } from "./220-normalize-user-file-by-principal.js";
45
46
 
46
47
  export interface MigrationRegistryEntry {
47
48
  /** The checkpoint key written to memory_checkpoints on completion. */
@@ -365,6 +366,13 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
365
366
  "Add a host_access column to conversations so computer access is persisted per conversation with a safe default of disabled",
366
367
  down: downConversationHostAccess,
367
368
  },
369
+ {
370
+ key: "migration_normalize_user_file_by_principal_v1",
371
+ version: 42,
372
+ description:
373
+ "Normalize contacts.user_file across rows sharing the same principal_id so every channel for one principal loads the same users/<slug>.md persona and journal directory",
374
+ down: downNormalizeUserFileByPrincipal,
375
+ },
368
376
  ];
369
377
 
370
378
  export function getMaxMigrationVersion(): number {
@@ -269,23 +269,50 @@ export class QdrantManager {
269
269
 
270
270
  private async waitForReady(): Promise<void> {
271
271
  const start = Date.now();
272
+ // Build a single exited-promise once so each race reuses the same handle.
273
+ // Reading `proc.exitCode` synchronously inside the poll loop is unreliable
274
+ // in Bun: while the loop is busy with fetch() + Bun.sleep(), the
275
+ // subprocess-exit event may not be processed on the event loop, so
276
+ // `exitCode` stays null even after the process has died. Racing
277
+ // `proc.exited` directly forces the loop to yield and observe the exit.
278
+ type ExitedOutcome = { type: "exited"; code: number };
279
+ const exitedRace: Promise<ExitedOutcome> =
280
+ this.process != null
281
+ ? this.process.exited.then((code) => ({ type: "exited", code }))
282
+ : new Promise<ExitedOutcome>(() => {});
283
+
284
+ const throwOnExit = async (code: number): Promise<never> => {
285
+ await this.stderrDrained;
286
+ const stderr = this.stderrBuffer.trim();
287
+ throw new Error(
288
+ `Qdrant process exited with code ${code} before becoming ready` +
289
+ (stderr ? `\nstderr:\n${stderr}` : ""),
290
+ );
291
+ };
292
+
272
293
  while (Date.now() - start < this.readyzTimeoutMs) {
273
- // Fail fast if the managed process exited before becoming ready
274
- if (this.process != null && this.process.exitCode != null) {
275
- await this.stderrDrained;
276
- const stderr = this.stderrBuffer.trim();
277
- throw new Error(
278
- `Qdrant process exited with code ${this.process.exitCode} before becoming ready` +
279
- (stderr ? `\nstderr:\n${stderr}` : ""),
280
- );
281
- }
282
- try {
283
- const res = await fetch(`${this.url}/readyz`);
284
- if (res.ok) return;
285
- } catch {
286
- // Not ready yet
287
- }
288
- await Bun.sleep(this.readyzPollIntervalMs);
294
+ const fetchOutcome = await Promise.race([
295
+ exitedRace,
296
+ fetch(`${this.url}/readyz`).then(
297
+ (r) => ({ type: "fetch" as const, ok: r.ok }),
298
+ () => ({ type: "fetch" as const, ok: false }),
299
+ ),
300
+ ]);
301
+ if (fetchOutcome.type === "exited") await throwOnExit(fetchOutcome.code);
302
+ if (fetchOutcome.type === "fetch" && fetchOutcome.ok) return;
303
+
304
+ // Race the poll-interval sleep with process exit so we don't waste time
305
+ // sleeping after the subprocess has already died.
306
+ const sleepOutcome = await Promise.race([
307
+ exitedRace,
308
+ new Promise<{ type: "timeout" }>((resolve) =>
309
+ setTimeout(
310
+ () => resolve({ type: "timeout" }),
311
+ this.readyzPollIntervalMs,
312
+ ),
313
+ ),
314
+ ]);
315
+ if (sleepOutcome.type === "exited") await throwOnExit(sleepOutcome.code);
289
316
  }
290
317
  const stderr = this.stderrBuffer.trim();
291
318
  throw new Error(
@@ -32,11 +32,13 @@ export const conversations = sqliteTable(
32
32
  isAutoTitle: integer("is_auto_title").notNull().default(1),
33
33
  scheduleJobId: text("schedule_job_id"),
34
34
  lastMessageAt: integer("last_message_at"),
35
+ archivedAt: integer("archived_at"),
35
36
  },
36
37
  (table) => [
37
38
  index("idx_conversations_updated_at").on(table.updatedAt),
38
39
  index("idx_conversations_last_message_at").on(table.lastMessageAt),
39
40
  index("idx_conversations_conversation_type").on(table.conversationType),
41
+ index("idx_conversations_archived_at").on(table.archivedAt),
40
42
  index("idx_conversations_fork_parent_conversation_id").on(
41
43
  table.forkParentConversationId,
42
44
  ),
@@ -14,6 +14,9 @@ export const oauthProviders = sqliteTable("oauth_providers", {
14
14
  tokenEndpointAuthMethod: text("token_endpoint_auth_method")
15
15
  .notNull()
16
16
  .default("client_secret_post"),
17
+ tokenExchangeBodyFormat: text("token_exchange_body_format")
18
+ .notNull()
19
+ .default("form"),
17
20
  userinfoUrl: text("userinfo_url"),
18
21
  baseUrl: text("base_url"),
19
22
  defaultScopes: text("default_scopes").notNull().default("[]"),