@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,396 @@
1
+ /**
2
+ * Timezone-aware bucketing for usage events.
3
+ *
4
+ * SQLite's `strftime(..., 'unixepoch')` is UTC-only, which means bucket
5
+ * boundaries (both daily and hourly) can't respect the user's timezone when
6
+ * computed in SQL. This module performs bucketing in JavaScript using
7
+ * `Intl.DateTimeFormat` so boundaries align to local-hour / local-day in any
8
+ * IANA timezone — including fractional offsets (e.g. Asia/Kolkata, UTC+5:30)
9
+ * and DST transitions.
10
+ */
11
+
12
+ import type { UsageDayBucket, UsageTimeRange } from "./llm-usage-store.js";
13
+
14
+ /** Minimal raw row shape needed for bucketing. */
15
+ export interface UsageEventBucketRow {
16
+ created_at: number;
17
+ input_tokens: number;
18
+ output_tokens: number;
19
+ estimated_cost_usd: number | null;
20
+ llm_call_count: number | null;
21
+ }
22
+
23
+ /** Parts extracted from a single Date in a target timezone. */
24
+ interface LocalParts {
25
+ year: number;
26
+ month: number;
27
+ day: number;
28
+ hour: number;
29
+ /** UTC offset in minutes at this instant in the target tz. */
30
+ offsetMinutes: number;
31
+ }
32
+
33
+ /**
34
+ * Validate that `tz` is a recognized IANA timezone identifier.
35
+ * Throws a tagged error the route layer can surface as 400.
36
+ */
37
+ export function validateTimezone(tz: string): void {
38
+ try {
39
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
40
+ } catch {
41
+ const err = new Error(
42
+ `Invalid IANA timezone identifier: "${tz}". Expected a value like "America/Los_Angeles" or "UTC".`,
43
+ );
44
+ (err as Error & { code?: string }).code = "INVALID_TIMEZONE";
45
+ throw err;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Extract local wall-clock parts + UTC offset for a given instant in `tz`.
51
+ *
52
+ * Uses `formatToParts` with a fixed format to reliably get numeric fields and
53
+ * the short GMT offset. The GMT offset disambiguates the duplicate DST
54
+ * fall-back hour (e.g. 1am EDT and 1am EST both format to "01" in local time
55
+ * but have different offsets).
56
+ */
57
+ function getLocalParts(epochMillis: number, tz: string): LocalParts {
58
+ const formatter = new Intl.DateTimeFormat("en-US", {
59
+ timeZone: tz,
60
+ year: "numeric",
61
+ month: "2-digit",
62
+ day: "2-digit",
63
+ hour: "2-digit",
64
+ minute: "2-digit",
65
+ second: "2-digit",
66
+ hour12: false,
67
+ timeZoneName: "shortOffset",
68
+ });
69
+ const parts = formatter.formatToParts(new Date(epochMillis));
70
+ let year = 0;
71
+ let month = 0;
72
+ let day = 0;
73
+ let hour = 0;
74
+ let offsetMinutes = 0;
75
+ for (const part of parts) {
76
+ switch (part.type) {
77
+ case "year":
78
+ year = Number(part.value);
79
+ break;
80
+ case "month":
81
+ month = Number(part.value);
82
+ break;
83
+ case "day":
84
+ day = Number(part.value);
85
+ break;
86
+ case "hour":
87
+ // `hour12: false` can still yield "24" at midnight in some locales; clamp.
88
+ hour = Number(part.value) % 24;
89
+ break;
90
+ case "timeZoneName":
91
+ offsetMinutes = parseShortOffset(part.value);
92
+ break;
93
+ }
94
+ }
95
+ return { year, month, day, hour, offsetMinutes };
96
+ }
97
+
98
+ /**
99
+ * Parse the `shortOffset` string returned by `Intl.DateTimeFormat` into minutes.
100
+ *
101
+ * Examples: "GMT-8" → -480, "GMT+5:30" → 330, "GMT" → 0, "UTC" → 0.
102
+ */
103
+ function parseShortOffset(value: string): number {
104
+ const match = value.match(/GMT([+-])(\d{1,2})(?::(\d{2}))?/);
105
+ if (!match) return 0;
106
+ const sign = match[1] === "-" ? -1 : 1;
107
+ const hours = Number(match[2]);
108
+ const minutes = match[3] ? Number(match[3]) : 0;
109
+ return sign * (hours * 60 + minutes);
110
+ }
111
+
112
+ function pad2(n: number): string {
113
+ return n < 10 ? `0${n}` : `${n}`;
114
+ }
115
+
116
+ /** Build the canonical daily bucket key in `tz`: "YYYY-MM-DD". */
117
+ function dayKey(parts: LocalParts): string {
118
+ return `${parts.year}-${pad2(parts.month)}-${pad2(parts.day)}`;
119
+ }
120
+
121
+ /**
122
+ * Build the canonical hourly bucket key in `tz`: "YYYY-MM-DD HH:00".
123
+ *
124
+ * For the DST fall-back hour we need to keep the two physically distinct
125
+ * hours separate even though they share the same local wall-clock label.
126
+ * We track them by their UTC offset internally via a separate map key,
127
+ * but the returned `date` string is identical for both so the display label
128
+ * matches. The caller uses a compound map key that includes the offset.
129
+ */
130
+ function hourKey(parts: LocalParts): string {
131
+ return `${parts.year}-${pad2(parts.month)}-${pad2(parts.day)} ${pad2(parts.hour)}:00`;
132
+ }
133
+
134
+ /** Compose a map key that separates duplicate DST fall-back hours. */
135
+ function hourMapKey(parts: LocalParts): string {
136
+ return `${hourKey(parts)}|${parts.offsetMinutes}`;
137
+ }
138
+
139
+ /**
140
+ * Advance `parts` by one local hour, returning the UTC instant of the next
141
+ * hour's start. Used to walk a range and fill empty buckets.
142
+ *
143
+ * We compute the next hour by taking the current instant, adding 1 hour of
144
+ * UTC wall time, and then projecting it back into local parts. This skips the
145
+ * spring-forward hour (23-hour day) and correctly emits both halves of the
146
+ * fall-back hour (25-hour day).
147
+ */
148
+ function addOneHourUtc(epochMillis: number): number {
149
+ return epochMillis + 60 * 60 * 1000;
150
+ }
151
+
152
+ function addOneDayUtc(epochMillis: number): number {
153
+ return epochMillis + 24 * 60 * 60 * 1000;
154
+ }
155
+
156
+ /**
157
+ * Find the UTC instant corresponding to the start of the local hour that
158
+ * contains `epochMillis` in `tz`.
159
+ *
160
+ * Because Intl doesn't give us this directly, we approximate by:
161
+ * 1. Getting the local parts of `epochMillis`
162
+ * 2. Subtracting the minutes/seconds of the wall clock
163
+ *
164
+ * This returns a UTC instant that, when formatted in `tz`, has hour == parts.hour
165
+ * and minute/second == 0.
166
+ */
167
+ function alignToLocalHourStart(epochMillis: number, tz: string): number {
168
+ const formatter = new Intl.DateTimeFormat("en-US", {
169
+ timeZone: tz,
170
+ minute: "2-digit",
171
+ second: "2-digit",
172
+ hour12: false,
173
+ });
174
+ const parts = formatter.formatToParts(new Date(epochMillis));
175
+ let minute = 0;
176
+ let second = 0;
177
+ for (const part of parts) {
178
+ if (part.type === "minute") minute = Number(part.value);
179
+ if (part.type === "second") second = Number(part.value);
180
+ }
181
+ return epochMillis - (minute * 60 + second) * 1000;
182
+ }
183
+
184
+ /**
185
+ * Find the UTC instant corresponding to local midnight of the day containing
186
+ * `epochMillis` in `tz`.
187
+ *
188
+ * DST-aware: the UTC offset at local midnight can differ from the offset at
189
+ * `epochMillis` when a DST transition falls earlier in the same local day.
190
+ * Naively subtracting the current wall-clock hours/minutes/seconds would land
191
+ * on a UTC instant that formats as the wrong local date (e.g. 23:00 of the
192
+ * previous day after spring-forward). We instead derive midnight by locating
193
+ * the UTC instant whose local formatting is Y-M-D 00:00 in `tz`.
194
+ */
195
+ function alignToLocalDayStart(epochMillis: number, tz: string): number {
196
+ const parts = getLocalParts(epochMillis, tz);
197
+ // UTC midnight of the same Y-M-D is a close-but-incorrect guess. Its offset
198
+ // approximates the offset at local midnight — one iteration of correction
199
+ // handles the common case where a DST transition sits between the two.
200
+ const utcMidnightGuess = Date.UTC(parts.year, parts.month - 1, parts.day);
201
+ const offset1 = getLocalParts(utcMidnightGuess, tz).offsetMinutes;
202
+ let candidate = utcMidnightGuess - offset1 * 60_000;
203
+ const offset2 = getLocalParts(candidate, tz).offsetMinutes;
204
+ if (offset2 !== offset1) {
205
+ candidate = utcMidnightGuess - offset2 * 60_000;
206
+ }
207
+ return candidate;
208
+ }
209
+
210
+ /** Format a short human-readable hour label in `tz`, e.g. "3pm". */
211
+ function formatHourLabel(epochMillis: number, tz: string): string {
212
+ const formatter = new Intl.DateTimeFormat("en-US", {
213
+ timeZone: tz,
214
+ hour: "numeric",
215
+ hour12: true,
216
+ });
217
+ // "3 PM" → "3pm"
218
+ return formatter.format(new Date(epochMillis)).replace(/\s/g, "").toLowerCase();
219
+ }
220
+
221
+ /** Format a short human-readable day label in `tz`, e.g. "Apr 11". */
222
+ function formatDayLabel(epochMillis: number, tz: string): string {
223
+ const formatter = new Intl.DateTimeFormat("en-US", {
224
+ timeZone: tz,
225
+ month: "short",
226
+ day: "numeric",
227
+ });
228
+ return formatter.format(new Date(epochMillis));
229
+ }
230
+
231
+ interface MutableBucket {
232
+ bucketId: string;
233
+ date: string;
234
+ displayLabel: string;
235
+ totalInputTokens: number;
236
+ totalOutputTokens: number;
237
+ totalEstimatedCostUsd: number;
238
+ eventCount: number;
239
+ /** Sort key — the UTC instant of the bucket start. */
240
+ sortKey: number;
241
+ }
242
+
243
+ function emptyBucket(
244
+ bucketId: string,
245
+ date: string,
246
+ displayLabel: string,
247
+ sortKey: number,
248
+ ): MutableBucket {
249
+ return {
250
+ bucketId,
251
+ date,
252
+ displayLabel,
253
+ totalInputTokens: 0,
254
+ totalOutputTokens: 0,
255
+ totalEstimatedCostUsd: 0,
256
+ eventCount: 0,
257
+ sortKey,
258
+ };
259
+ }
260
+
261
+ function addEventToBucket(
262
+ bucket: MutableBucket,
263
+ row: UsageEventBucketRow,
264
+ ): void {
265
+ bucket.totalInputTokens += row.input_tokens;
266
+ bucket.totalOutputTokens += row.output_tokens;
267
+ bucket.totalEstimatedCostUsd += row.estimated_cost_usd ?? 0;
268
+ bucket.eventCount += row.llm_call_count ?? 1;
269
+ }
270
+
271
+ function finalize(buckets: Map<string, MutableBucket>): UsageDayBucket[] {
272
+ return Array.from(buckets.values())
273
+ .sort((a, b) => a.sortKey - b.sortKey)
274
+ .map(({ bucketId, date, displayLabel, ...rest }) => ({
275
+ bucketId,
276
+ date,
277
+ displayLabel,
278
+ totalInputTokens: rest.totalInputTokens,
279
+ totalOutputTokens: rest.totalOutputTokens,
280
+ totalEstimatedCostUsd: rest.totalEstimatedCostUsd,
281
+ eventCount: rest.eventCount,
282
+ }));
283
+ }
284
+
285
+ /** Options for bucketing behavior. */
286
+ export interface BucketingOptions {
287
+ /**
288
+ * When true, emit a zero-value bucket for every hour (or day) in the
289
+ * requested range even if no events fall inside it. This produces a
290
+ * continuous chart axis for empty periods but adds noise to text output
291
+ * like the CLI. Defaults to false.
292
+ */
293
+ fillEmpty?: boolean;
294
+ }
295
+
296
+ /**
297
+ * Bucket raw usage events by local hour in the given timezone.
298
+ *
299
+ * DST fall-back duplicate hours are preserved as separate buckets with
300
+ * identical display labels. When `options.fillEmpty` is true, the returned
301
+ * array contains a zero-value bucket for every local hour in the range.
302
+ */
303
+ export function bucketEventsByHour(
304
+ events: UsageEventBucketRow[],
305
+ range: UsageTimeRange,
306
+ tz: string,
307
+ options: BucketingOptions = {},
308
+ ): UsageDayBucket[] {
309
+ validateTimezone(tz);
310
+ const buckets = new Map<string, MutableBucket>();
311
+
312
+ if (options.fillEmpty) {
313
+ let cursor = alignToLocalHourStart(range.from, tz);
314
+ let safety = 0;
315
+ const maxIterations = 200_000;
316
+ while (cursor <= range.to && safety++ < maxIterations) {
317
+ const parts = getLocalParts(cursor, tz);
318
+ const key = hourMapKey(parts);
319
+ if (!buckets.has(key)) {
320
+ buckets.set(
321
+ key,
322
+ emptyBucket(key, hourKey(parts), formatHourLabel(cursor, tz), cursor),
323
+ );
324
+ }
325
+ cursor = addOneHourUtc(cursor);
326
+ }
327
+ }
328
+
329
+ for (const row of events) {
330
+ const parts = getLocalParts(row.created_at, tz);
331
+ const key = hourMapKey(parts);
332
+ let bucket = buckets.get(key);
333
+ if (!bucket) {
334
+ const hourStart = alignToLocalHourStart(row.created_at, tz);
335
+ bucket = emptyBucket(
336
+ key,
337
+ hourKey(parts),
338
+ formatHourLabel(hourStart, tz),
339
+ hourStart,
340
+ );
341
+ buckets.set(key, bucket);
342
+ }
343
+ addEventToBucket(bucket, row);
344
+ }
345
+
346
+ return finalize(buckets);
347
+ }
348
+
349
+ /**
350
+ * Bucket raw usage events by local day in the given timezone.
351
+ *
352
+ * When `options.fillEmpty` is true, the returned array contains a zero-value
353
+ * bucket for every local day in the range.
354
+ */
355
+ export function bucketEventsByDay(
356
+ events: UsageEventBucketRow[],
357
+ range: UsageTimeRange,
358
+ tz: string,
359
+ options: BucketingOptions = {},
360
+ ): UsageDayBucket[] {
361
+ validateTimezone(tz);
362
+ const buckets = new Map<string, MutableBucket>();
363
+
364
+ if (options.fillEmpty) {
365
+ let cursor = alignToLocalDayStart(range.from, tz);
366
+ let safety = 0;
367
+ const maxIterations = 10_000;
368
+ while (cursor <= range.to && safety++ < maxIterations) {
369
+ const parts = getLocalParts(cursor, tz);
370
+ const key = dayKey(parts);
371
+ if (!buckets.has(key)) {
372
+ buckets.set(
373
+ key,
374
+ emptyBucket(key, key, formatDayLabel(cursor, tz), cursor),
375
+ );
376
+ }
377
+ // Advance by 24 UTC hours, then realign to local midnight. Handles
378
+ // DST transitions where a "day" is 23 or 25 hours long in local time.
379
+ cursor = alignToLocalDayStart(addOneDayUtc(cursor), tz);
380
+ }
381
+ }
382
+
383
+ for (const row of events) {
384
+ const parts = getLocalParts(row.created_at, tz);
385
+ const key = dayKey(parts);
386
+ let bucket = buckets.get(key);
387
+ if (!bucket) {
388
+ const dayStart = alignToLocalDayStart(row.created_at, tz);
389
+ bucket = emptyBucket(key, key, formatDayLabel(dayStart, tz), dayStart);
390
+ buckets.set(key, bucket);
391
+ }
392
+ addEventToBucket(bucket, row);
393
+ }
394
+
395
+ return finalize(buckets);
396
+ }
@@ -53,6 +53,30 @@ function isIdempotent(options?: RequestInit): boolean {
53
53
  return IDEMPOTENT_METHODS.has(method);
54
54
  }
55
55
 
56
+ /** Sleep that wakes immediately when the abort signal fires. */
57
+ async function signalAwareSleep(
58
+ ms: number,
59
+ signal?: AbortSignal,
60
+ ): Promise<void> {
61
+ if (!signal) {
62
+ await new Promise<void>((resolve) => setTimeout(resolve, ms));
63
+ return;
64
+ }
65
+ const s = signal; // narrow for closures
66
+ s.throwIfAborted();
67
+ await new Promise<void>((resolve, reject) => {
68
+ const timer = setTimeout(() => {
69
+ s.removeEventListener("abort", onAbort);
70
+ resolve();
71
+ }, ms);
72
+ function onAbort() {
73
+ clearTimeout(timer);
74
+ reject(s.reason ?? new Error("aborted"));
75
+ }
76
+ s.addEventListener("abort", onAbort, { once: true });
77
+ });
78
+ }
79
+
56
80
  interface GmailRequestOptions extends RequestInit {
57
81
  /** Override method-based retry eligibility. When true, retries on 429/5xx even for POST requests. */
58
82
  retryable?: boolean;
@@ -117,11 +141,13 @@ async function request<T>(
117
141
  path: string,
118
142
  options?: GmailRequestOptions,
119
143
  query?: Record<string, string | string[]>,
144
+ signal?: AbortSignal,
120
145
  ): Promise<T> {
121
146
  const canRetry = options?.retryable ?? isIdempotent(options);
122
147
  const method = (options?.method ?? "GET").toUpperCase();
123
148
 
124
149
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
150
+ signal?.throwIfAborted();
125
151
  let resp: OAuthConnectionResponse;
126
152
  try {
127
153
  resp = await connection.request({
@@ -133,9 +159,21 @@ async function request<T>(
133
159
  ...extractNonAuthHeaders(options),
134
160
  },
135
161
  body: extractBody(options),
162
+ signal,
136
163
  });
137
164
  } catch (err) {
138
- // Network-level errors from connection.request() are not retryable
165
+ // Retry thrown errors that indicate a retryable status (e.g. platform
166
+ // proxy throws BackendError on 429 after exhausting its own retries)
167
+ if (
168
+ canRetry &&
169
+ attempt < MAX_RETRIES &&
170
+ err instanceof Error &&
171
+ /\b(429|5\d{2})\b/.test(err.message)
172
+ ) {
173
+ const delayMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
174
+ await signalAwareSleep(delayMs, signal);
175
+ continue;
176
+ }
139
177
  throw err;
140
178
  }
141
179
 
@@ -146,7 +184,7 @@ async function request<T>(
146
184
  const delayMs = retryAfter
147
185
  ? parseInt(retryAfter, 10) * 1000
148
186
  : INITIAL_BACKOFF_MS * Math.pow(2, attempt);
149
- await new Promise((resolve) => setTimeout(resolve, delayMs));
187
+ await signalAwareSleep(delayMs, signal);
150
188
  continue;
151
189
  }
152
190
  const bodyStr =
@@ -202,6 +240,7 @@ export async function getMessage(
202
240
  format: GmailMessageFormat = "full",
203
241
  metadataHeaders?: string[],
204
242
  fields?: string,
243
+ signal?: AbortSignal,
205
244
  ): Promise<GmailMessage> {
206
245
  const params = new URLSearchParams({ format });
207
246
  if (format === "metadata" && metadataHeaders) {
@@ -213,6 +252,7 @@ export async function getMessage(
213
252
  `/messages/${messageId}`,
214
253
  undefined,
215
254
  paramsToQuery(params),
255
+ signal,
216
256
  );
217
257
  }
218
258
 
@@ -256,6 +296,7 @@ async function executeBatchCall(
256
296
  format: GmailMessageFormat,
257
297
  metadataHeaders: string[] | undefined,
258
298
  fields?: string,
299
+ signal?: AbortSignal,
259
300
  ): Promise<{
260
301
  messages: Array<{ index: number; msg: GmailMessage }>;
261
302
  failedIds: Array<{ index: number; id: string }>;
@@ -281,9 +322,13 @@ async function executeBatchCall(
281
322
 
282
323
  const doBatchFetch = async (token: string) => {
283
324
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
325
+ const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS * 2);
326
+ const combinedSignal = signal
327
+ ? AbortSignal.any([signal, timeoutSignal])
328
+ : timeoutSignal;
284
329
  const resp = await fetch(GMAIL_BATCH_URL, {
285
330
  method: "POST",
286
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS * 2),
331
+ signal: combinedSignal,
287
332
  headers: {
288
333
  Authorization: `Bearer ${token}`,
289
334
  "Content-Type": `multipart/mixed; boundary=${boundary}`,
@@ -297,7 +342,7 @@ async function executeBatchCall(
297
342
  const delayMs = retryAfter
298
343
  ? parseInt(retryAfter, 10) * 1000
299
344
  : INITIAL_BACKOFF_MS * Math.pow(2, attempt);
300
- await new Promise((r) => setTimeout(r, delayMs));
345
+ await signalAwareSleep(delayMs, signal);
301
346
  continue;
302
347
  }
303
348
  const errBody = await resp.text().catch(() => "");
@@ -375,13 +420,15 @@ async function fetchMessagesIndividually(
375
420
  format: GmailMessageFormat,
376
421
  metadataHeaders?: string[],
377
422
  fields?: string,
423
+ signal?: AbortSignal,
378
424
  ): Promise<GmailMessage[]> {
379
425
  const results: GmailMessage[] = [];
380
426
  for (let i = 0; i < messageIds.length; i += INDIVIDUAL_CONCURRENCY) {
427
+ signal?.throwIfAborted();
381
428
  const wave = messageIds.slice(i, i + INDIVIDUAL_CONCURRENCY);
382
429
  const waveResults = await Promise.all(
383
430
  wave.map((id) =>
384
- getMessage(connection, id, format, metadataHeaders, fields),
431
+ getMessage(connection, id, format, metadataHeaders, fields, signal),
385
432
  ),
386
433
  );
387
434
  results.push(...waveResults);
@@ -403,6 +450,7 @@ export async function batchGetMessages(
403
450
  format: GmailMessageFormat = "full",
404
451
  metadataHeaders?: string[],
405
452
  fields?: string,
453
+ signal?: AbortSignal,
406
454
  ): Promise<GmailMessage[]> {
407
455
  if (messageIds.length === 0) return [];
408
456
 
@@ -415,6 +463,7 @@ export async function batchGetMessages(
415
463
  format,
416
464
  metadataHeaders,
417
465
  fields,
466
+ signal,
418
467
  ),
419
468
  ];
420
469
  }
@@ -436,6 +485,7 @@ export async function batchGetMessages(
436
485
  format,
437
486
  metadataHeaders,
438
487
  fields,
488
+ signal,
439
489
  );
440
490
  }
441
491
 
@@ -462,6 +512,7 @@ export async function batchGetMessages(
462
512
  format,
463
513
  metadataHeaders,
464
514
  fields,
515
+ signal,
465
516
  ),
466
517
  ),
467
518
  );
@@ -479,7 +530,7 @@ export async function batchGetMessages(
479
530
  if (failedIds.length > 0) {
480
531
  const retried = await Promise.all(
481
532
  failedIds.map(({ id }) =>
482
- getMessage(connection, id, format, metadataHeaders, fields),
533
+ getMessage(connection, id, format, metadataHeaders, fields, signal),
483
534
  ),
484
535
  );
485
536
  for (let r = 0; r < failedIds.length; r++) {