@vellumai/assistant 0.8.7 → 0.8.8

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 (387) hide show
  1. package/Dockerfile +20 -4
  2. package/docker-entrypoint.sh +4 -2
  3. package/docker-init-apt-root.sh +3 -1
  4. package/docker-kata-apt-env.sh +3 -1
  5. package/docker-kata-runtime-family.sh +12 -0
  6. package/docs/architecture/memory.md +1 -1
  7. package/docs/plugins.md +75 -79
  8. package/examples/plugins/echo/README.md +6 -12
  9. package/examples/plugins/echo/register.ts +0 -41
  10. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  11. package/openapi.yaml +3381 -348
  12. package/package.json +1 -1
  13. package/scripts/generate-openapi.ts +68 -41
  14. package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
  15. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  16. package/src/__tests__/agent-loop.test.ts +37 -87
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  18. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  19. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  20. package/src/__tests__/anthropic-provider.test.ts +95 -2
  21. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  22. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  23. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  24. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  25. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  26. package/src/__tests__/btw-routes.test.ts +62 -3
  27. package/src/__tests__/build-persisted-content.test.ts +184 -0
  28. package/src/__tests__/catalog-files.test.ts +1 -1
  29. package/src/__tests__/clawhub-files.test.ts +1 -1
  30. package/src/__tests__/compaction-pipeline.test.ts +1 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  32. package/src/__tests__/config-watcher.test.ts +1 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  34. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
  35. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
  37. package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
  38. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  39. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  40. package/src/__tests__/conversation-pairing.test.ts +4 -31
  41. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
  43. package/src/__tests__/conversation-queue.test.ts +2 -0
  44. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  45. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
  48. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  49. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  50. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  51. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  52. package/src/__tests__/conversation-title-service.test.ts +135 -2
  53. package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
  54. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  55. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  56. package/src/__tests__/dm-persistence.test.ts +5 -1
  57. package/src/__tests__/empty-response-hook.test.ts +304 -0
  58. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  59. package/src/__tests__/gemini-image-service.test.ts +13 -0
  60. package/src/__tests__/helpers/mock-provider.ts +110 -0
  61. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  62. package/src/__tests__/history-repair-hook.test.ts +1 -0
  63. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  64. package/src/__tests__/identity-routes.test.ts +248 -7
  65. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  66. package/src/__tests__/injector-background-turn.test.ts +2 -8
  67. package/src/__tests__/injector-chain.test.ts +106 -270
  68. package/src/__tests__/injector-disk-pressure.test.ts +3 -12
  69. package/src/__tests__/injector-document-comments.test.ts +2 -2
  70. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  71. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  72. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  73. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  74. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  75. package/src/__tests__/llm-usage-store.test.ts +223 -1
  76. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  77. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  78. package/src/__tests__/native-web-search.test.ts +191 -0
  79. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  80. package/src/__tests__/openai-image-service.test.ts +17 -0
  81. package/src/__tests__/openai-provider.test.ts +31 -1
  82. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  83. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  84. package/src/__tests__/pipeline-runner.test.ts +29 -39
  85. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  86. package/src/__tests__/plugin-bootstrap.test.ts +13 -28
  87. package/src/__tests__/plugin-registry.test.ts +0 -27
  88. package/src/__tests__/plugin-types.test.ts +2 -125
  89. package/src/__tests__/process-message-display-content.test.ts +6 -2
  90. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  91. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  92. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  93. package/src/__tests__/schedule-routes.test.ts +603 -2
  94. package/src/__tests__/schedule-store.test.ts +41 -0
  95. package/src/__tests__/schedule-tools.test.ts +35 -0
  96. package/src/__tests__/server-history-render.test.ts +314 -1
  97. package/src/__tests__/skillssh-files.test.ts +1 -1
  98. package/src/__tests__/system-prompt.test.ts +20 -0
  99. package/src/__tests__/task-scheduler.test.ts +162 -1
  100. package/src/__tests__/terminal-tools.test.ts +6 -1
  101. package/src/__tests__/title-generate-hook.test.ts +319 -0
  102. package/src/__tests__/tool-error-hook.test.ts +278 -0
  103. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  104. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  106. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  107. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  108. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  109. package/src/__tests__/usage-routes.test.ts +285 -1
  110. package/src/__tests__/user-plugin-loader.test.ts +2 -2
  111. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  112. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  113. package/src/agent/loop.ts +346 -442
  114. package/src/api/events/assistant-thinking-delta.ts +33 -0
  115. package/src/api/events/tool-output-chunk.ts +45 -0
  116. package/src/api/events/tool-use-preview-start.ts +32 -0
  117. package/src/api/events/trace-event.ts +69 -0
  118. package/src/api/index.ts +48 -13
  119. package/src/api/responses/conversation-message.ts +368 -0
  120. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  121. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  122. package/src/cli/commands/notifications.ts +112 -60
  123. package/src/config/assistant-feature-flags.ts +22 -11
  124. package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
  125. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  126. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  127. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  128. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  129. package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
  130. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  131. package/src/config/feature-flag-cache.ts +3 -3
  132. package/src/config/feature-flag-registry.json +35 -3
  133. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  134. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  135. package/src/config/schemas/llm.ts +1 -0
  136. package/src/config/schemas/memory-v2.ts +8 -0
  137. package/src/config/schemas/memory-v3.ts +8 -0
  138. package/src/config/schemas/platform.ts +8 -0
  139. package/src/config/seed-inference-profiles.ts +2 -2
  140. package/src/config/skills.ts +13 -0
  141. package/src/context/compactor.ts +1 -1
  142. package/src/context/strip-injections.ts +122 -0
  143. package/src/context/token-estimator.ts +23 -0
  144. package/src/context/tool-result-truncation.ts +0 -23
  145. package/src/context/window-manager.ts +3 -6
  146. package/src/credential-execution/executable-discovery.ts +16 -0
  147. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  148. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  149. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  150. package/src/daemon/assistant-attachments.ts +1 -1
  151. package/src/daemon/config-watcher.ts +2 -2
  152. package/src/daemon/context-overflow-reducer.ts +0 -1
  153. package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
  154. package/src/daemon/conversation-agent-loop.ts +281 -760
  155. package/src/daemon/conversation-history.ts +5 -4
  156. package/src/daemon/conversation-lifecycle.ts +3 -4
  157. package/src/daemon/conversation-messaging.ts +7 -6
  158. package/src/daemon/conversation-process.ts +11 -16
  159. package/src/daemon/conversation-runtime-assembly.ts +130 -347
  160. package/src/daemon/conversation-slash.ts +6 -25
  161. package/src/daemon/conversation-surfaces.ts +222 -4
  162. package/src/daemon/conversation-tool-setup.ts +2 -29
  163. package/src/daemon/conversation.ts +32 -14
  164. package/src/daemon/external-plugins-bootstrap.ts +9 -10
  165. package/src/daemon/handlers/config-a2a.ts +51 -36
  166. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  167. package/src/daemon/handlers/config-telegram.ts +16 -2
  168. package/src/daemon/handlers/shared.ts +156 -84
  169. package/src/daemon/handlers/skills.ts +39 -10
  170. package/src/daemon/lifecycle.ts +4 -0
  171. package/src/daemon/message-types/apps.ts +1 -29
  172. package/src/daemon/message-types/messages.ts +9 -57
  173. package/src/daemon/message-types/skills.ts +2 -0
  174. package/src/daemon/message-types/surfaces.ts +136 -3
  175. package/src/daemon/now-scratchpad.ts +21 -0
  176. package/src/daemon/orphan-reaper.test.ts +210 -0
  177. package/src/daemon/orphan-reaper.ts +240 -0
  178. package/src/daemon/persist-unsendable-image.ts +117 -0
  179. package/src/daemon/process-message.ts +1 -3
  180. package/src/daemon/trace-emitter.ts +6 -4
  181. package/src/daemon/trust-context.ts +19 -0
  182. package/src/daemon/wake-target-adapter.ts +3 -1
  183. package/src/home/home-greeting-cache.ts +24 -1
  184. package/src/ipc/gateway-client.test.ts +2 -2
  185. package/src/ipc/gateway-client.ts +3 -3
  186. package/src/media/gemini-image-service.ts +15 -0
  187. package/src/media/openai-image-service.ts +14 -0
  188. package/src/media/types.ts +34 -0
  189. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  190. package/src/memory/auth-fallback-events-store.ts +94 -0
  191. package/src/memory/conversation-title-service.ts +65 -41
  192. package/src/memory/db-init.ts +4 -0
  193. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  194. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  195. package/src/memory/jobs-store.ts +33 -0
  196. package/src/memory/jobs-worker.ts +31 -4
  197. package/src/memory/llm-usage-store.ts +224 -50
  198. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  199. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  200. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  201. package/src/memory/migrations/index.ts +2 -0
  202. package/src/memory/pkb/autoinject.ts +61 -0
  203. package/src/memory/pkb/context.ts +50 -0
  204. package/src/memory/pkb/types.ts +14 -0
  205. package/src/memory/schedule-attribution-sql.ts +104 -0
  206. package/src/memory/schema/infrastructure.ts +16 -0
  207. package/src/memory/usage-grouped-buckets.ts +6 -1
  208. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
  209. package/src/memory/v2/consolidation-job.ts +1 -1
  210. package/src/memory/v3/__tests__/health.test.ts +16 -0
  211. package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
  212. package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
  213. package/src/memory/v3/__tests__/router.test.ts +101 -29
  214. package/src/memory/v3/__tests__/selector.test.ts +93 -27
  215. package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
  216. package/src/memory/v3/health.ts +0 -0
  217. package/src/memory/v3/llm-retry.ts +32 -0
  218. package/src/memory/v3/orchestrate.ts +26 -14
  219. package/src/memory/v3/provider-blocks.ts +15 -5
  220. package/src/memory/v3/router.ts +48 -42
  221. package/src/memory/v3/selector.ts +57 -42
  222. package/src/memory/v3/shadow-plugin.ts +47 -15
  223. package/src/memory/v3/types.ts +8 -0
  224. package/src/notifications/conversation-pairing.ts +8 -15
  225. package/src/notifications/decision-engine.ts +6 -3
  226. package/src/notifications/home-feed-side-effect.ts +12 -1
  227. package/src/permissions/prompter.ts +4 -0
  228. package/src/plugin-api/constants.ts +4 -0
  229. package/src/plugin-api/index.ts +8 -1
  230. package/src/plugin-api/types.ts +151 -1
  231. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  232. package/src/plugins/defaults/empty-response/register.ts +8 -13
  233. package/src/plugins/defaults/index.ts +1 -15
  234. package/src/plugins/defaults/injectors/register.ts +243 -74
  235. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
  236. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  237. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  238. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  239. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  240. package/src/plugins/defaults/title-generate/package.json +1 -1
  241. package/src/plugins/defaults/title-generate/register.ts +18 -18
  242. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  243. package/src/plugins/defaults/tool-error/package.json +1 -1
  244. package/src/plugins/defaults/tool-error/register.ts +9 -21
  245. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  246. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  247. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  248. package/src/plugins/pipeline.ts +6 -18
  249. package/src/plugins/registry.ts +8 -25
  250. package/src/plugins/types.ts +43 -474
  251. package/src/proactive-artifact/aux-message-injector.ts +3 -3
  252. package/src/proactive-artifact/job.test.ts +7 -12
  253. package/src/prompts/__tests__/system-prompt.test.ts +36 -0
  254. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
  255. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  256. package/src/prompts/templates/system-sections.ts +15 -0
  257. package/src/providers/anthropic/client.ts +37 -29
  258. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  259. package/src/providers/openai/chat-completions-provider.ts +44 -0
  260. package/src/providers/openrouter/client.ts +1 -0
  261. package/src/providers/placeholder-sentinels.ts +35 -0
  262. package/src/runtime/__tests__/agent-wake.test.ts +5 -1
  263. package/src/runtime/agent-wake.ts +2 -2
  264. package/src/runtime/assistant-event-hub.ts +36 -6
  265. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  266. package/src/runtime/http-router.ts +16 -21
  267. package/src/runtime/http-types.ts +16 -70
  268. package/src/runtime/pending-interactions.ts +1 -0
  269. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  270. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  271. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  272. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  273. package/src/runtime/routes/app-management-routes.ts +6 -117
  274. package/src/runtime/routes/app-routes.ts +13 -15
  275. package/src/runtime/routes/attachment-routes.ts +26 -15
  276. package/src/runtime/routes/avatar-routes.ts +26 -0
  277. package/src/runtime/routes/btw-routes.ts +29 -23
  278. package/src/runtime/routes/consolidation-routes.ts +120 -20
  279. package/src/runtime/routes/conversation-query-routes.ts +2 -0
  280. package/src/runtime/routes/conversation-routes.ts +358 -184
  281. package/src/runtime/routes/documents-routes.ts +4 -0
  282. package/src/runtime/routes/domain-routes.ts +51 -37
  283. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  284. package/src/runtime/routes/events-routes.ts +28 -34
  285. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  286. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  287. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  288. package/src/runtime/routes/identity-routes.ts +208 -17
  289. package/src/runtime/routes/image-generation-routes.ts +40 -2
  290. package/src/runtime/routes/index.ts +2 -0
  291. package/src/runtime/routes/integrations/a2a.ts +12 -10
  292. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  293. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  294. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  295. package/src/runtime/routes/integrations/telegram.ts +6 -0
  296. package/src/runtime/routes/integrations/twilio.ts +42 -0
  297. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  298. package/src/runtime/routes/log-export-routes.ts +8 -0
  299. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  300. package/src/runtime/routes/memory-v3-routes.ts +50 -28
  301. package/src/runtime/routes/oauth-apps.ts +66 -12
  302. package/src/runtime/routes/oauth-providers.ts +44 -5
  303. package/src/runtime/routes/platform-routes.ts +81 -5
  304. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  305. package/src/runtime/routes/playground/force-compact.ts +1 -1
  306. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  307. package/src/runtime/routes/schedule-routes.ts +152 -42
  308. package/src/runtime/routes/secret-routes.ts +14 -2
  309. package/src/runtime/routes/skills-routes.ts +43 -14
  310. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  311. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  312. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  313. package/src/runtime/routes/tts-routes.ts +35 -0
  314. package/src/runtime/routes/types.ts +66 -8
  315. package/src/runtime/routes/usage-routes.ts +47 -39
  316. package/src/runtime/routes/webhook-routes.ts +41 -2
  317. package/src/runtime/routes/workspace-routes.ts +4 -0
  318. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  319. package/src/runtime/services/analyze-conversation.ts +2 -2
  320. package/src/schedule/schedule-store.ts +20 -1
  321. package/src/schedule/schedule-usage-store.ts +83 -0
  322. package/src/schedule/scheduler.ts +12 -5
  323. package/src/skills/catalog-files.ts +2 -2
  324. package/src/skills/catalog-install.ts +3 -0
  325. package/src/skills/categories-cache.ts +118 -0
  326. package/src/skills/clawhub-files.ts +1 -2
  327. package/src/skills/skillssh-files.ts +1 -2
  328. package/src/telemetry/types.ts +29 -1
  329. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  330. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  331. package/src/tools/executor.ts +1 -53
  332. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  333. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  334. package/src/tools/network/web-search-error.test.ts +248 -0
  335. package/src/tools/network/web-search-error.ts +267 -0
  336. package/src/tools/network/web-search.ts +207 -48
  337. package/src/tools/schedule/create.ts +2 -0
  338. package/src/tools/terminal/safe-env.ts +10 -1
  339. package/src/tools/ui-surface/definitions.ts +9 -1
  340. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  341. package/src/tts/provider-catalog.ts +76 -1
  342. package/src/util/mutex.ts +47 -0
  343. package/src/workspace/git-service.ts +1 -42
  344. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  345. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  346. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
  347. package/src/workspace/migrations/registry.ts +6 -0
  348. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  349. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  350. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  351. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  352. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  353. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  354. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  355. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  356. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  357. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  358. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  359. package/src/gallery/default-gallery.ts +0 -1359
  360. package/src/gallery/gallery-manifest.ts +0 -28
  361. package/src/home/feature-gate.ts +0 -22
  362. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  363. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  364. package/src/plugins/defaults/injectors/package.json +0 -15
  365. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  366. package/src/plugins/defaults/llm-call/package.json +0 -15
  367. package/src/plugins/defaults/llm-call/register.ts +0 -45
  368. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  369. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  370. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  371. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  372. package/src/plugins/defaults/persistence/package.json +0 -15
  373. package/src/plugins/defaults/persistence/register.ts +0 -38
  374. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  375. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  376. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  377. package/src/plugins/defaults/token-estimate/package.json +0 -15
  378. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  379. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  380. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  381. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  382. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  383. package/src/plugins/defaults/tool-execute/package.json +0 -15
  384. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  385. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  386. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  387. package/src/skills/category-inference.ts +0 -111
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Tests for render-time enrichment of history tool calls with confirmation
3
+ * context: derived scope ladders for scope-aware tools and outstanding prompts
4
+ * read from the pending-interactions registry.
5
+ */
6
+ import { afterEach, describe, expect, test } from "bun:test";
7
+
8
+ import type { ConversationMessageToolCall } from "../../api/responses/conversation-message.js";
9
+ import { clear, register } from "../pending-interactions.js";
10
+ import {
11
+ collectPendingConfirmations,
12
+ enrichToolCallsWithConfirmation,
13
+ } from "./tool-call-confirmation-enrichment.js";
14
+
15
+ const WORKSPACE = "/home/user/project";
16
+
17
+ function toolCall(
18
+ overrides: Partial<ConversationMessageToolCall>,
19
+ ): ConversationMessageToolCall {
20
+ return {
21
+ name: "file_read",
22
+ input: {},
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ afterEach(() => {
28
+ clear();
29
+ });
30
+
31
+ describe("collectPendingConfirmations", () => {
32
+ test("keys confirmation interactions by toolUseId", () => {
33
+ // GIVEN a confirmation interaction registered for a conversation with a
34
+ // tool-use id and confirmation details
35
+ register("req-1", {
36
+ conversationId: "conv-1",
37
+ kind: "confirmation",
38
+ toolUseId: "tool-abc",
39
+ confirmationDetails: {
40
+ toolName: "file_read",
41
+ input: { path: "/home/user/project/a.txt" },
42
+ riskLevel: "low",
43
+ allowlistOptions: [],
44
+ scopeOptions: [],
45
+ },
46
+ });
47
+
48
+ // WHEN we collect the conversation's pending confirmations
49
+ const byToolUseId = collectPendingConfirmations("conv-1");
50
+
51
+ // THEN the interaction is keyed by its tool-use id
52
+ expect(byToolUseId.size).toBe(1);
53
+ expect(byToolUseId.get("tool-abc")?.requestId).toBe("req-1");
54
+ });
55
+
56
+ test("ignores interactions lacking a toolUseId or confirmation details", () => {
57
+ // GIVEN a confirmation without a toolUseId AND a non-confirmation
58
+ // interaction in the same conversation
59
+ register("req-no-tool", {
60
+ conversationId: "conv-2",
61
+ kind: "confirmation",
62
+ confirmationDetails: {
63
+ toolName: "file_read",
64
+ input: {},
65
+ riskLevel: "low",
66
+ allowlistOptions: [],
67
+ scopeOptions: [],
68
+ },
69
+ });
70
+ register("req-secret", {
71
+ conversationId: "conv-2",
72
+ kind: "secret",
73
+ toolUseId: "tool-xyz",
74
+ });
75
+
76
+ // WHEN we collect the conversation's pending confirmations
77
+ const byToolUseId = collectPendingConfirmations("conv-2");
78
+
79
+ // THEN neither is included — one has no toolUseId, the other is not a
80
+ // confirmation
81
+ expect(byToolUseId.size).toBe(0);
82
+ });
83
+ });
84
+
85
+ describe("enrichToolCallsWithConfirmation", () => {
86
+ test("derives the scope ladder for scope-aware tools", () => {
87
+ // GIVEN a completed scope-aware tool call with no registry entry
88
+ const calls = [toolCall({ id: "tool-1", name: "file_read" })];
89
+
90
+ // WHEN we enrich it
91
+ const [enriched] = enrichToolCallsWithConfirmation(calls, {
92
+ workspaceDir: WORKSPACE,
93
+ pendingConfirmations: new Map(),
94
+ });
95
+
96
+ // THEN the scope ladder is derived from the workspace and tool name
97
+ expect(enriched?.scopeOptions?.[0]).toEqual({
98
+ label: WORKSPACE,
99
+ scope: WORKSPACE,
100
+ });
101
+ // AND no pending confirmation is stamped
102
+ expect(enriched?.pendingConfirmation).toBeUndefined();
103
+ });
104
+
105
+ test("leaves non-scope-aware tool calls untouched", () => {
106
+ // GIVEN a tool call for a tool that has no scope ladder
107
+ const original = toolCall({ id: "tool-2", name: "web_search" });
108
+
109
+ // WHEN we enrich it with no matching registry entry
110
+ const [enriched] = enrichToolCallsWithConfirmation([original], {
111
+ workspaceDir: WORKSPACE,
112
+ pendingConfirmations: new Map(),
113
+ });
114
+
115
+ // THEN the tool call is returned unchanged (same reference)
116
+ expect(enriched).toBe(original);
117
+ });
118
+
119
+ test("stamps the pending confirmation when the registry has a match", () => {
120
+ // GIVEN a registry entry matching the tool call by id
121
+ const pendingConfirmations = collectPendingConfirmationsFixture();
122
+ const calls = [toolCall({ id: "tool-abc", name: "file_read" })];
123
+
124
+ // WHEN we enrich it
125
+ const [enriched] = enrichToolCallsWithConfirmation(calls, {
126
+ workspaceDir: WORKSPACE,
127
+ pendingConfirmations,
128
+ });
129
+
130
+ // THEN the outstanding prompt is projected onto the tool call
131
+ expect(enriched?.pendingConfirmation?.requestId).toBe("req-1");
132
+ expect(enriched?.pendingConfirmation?.toolName).toBe("file_read");
133
+ expect(enriched?.pendingConfirmation?.riskLevel).toBe("high");
134
+ // AND the directory scope ladder carries through from the registry so a
135
+ // restored prompt offers the same scope the live event did
136
+ expect(enriched?.pendingConfirmation?.directoryScopeOptions).toEqual([
137
+ { label: "Anywhere in project/", scope: "/home/user/project" },
138
+ ]);
139
+ // AND the derived scope ladder is still present
140
+ expect(enriched?.scopeOptions?.length).toBeGreaterThan(0);
141
+ });
142
+ });
143
+
144
+ function collectPendingConfirmationsFixture() {
145
+ register("req-1", {
146
+ conversationId: "conv-fixture",
147
+ kind: "confirmation",
148
+ toolUseId: "tool-abc",
149
+ confirmationDetails: {
150
+ toolName: "file_read",
151
+ input: { path: "/home/user/project/a.txt" },
152
+ riskLevel: "high",
153
+ allowlistOptions: [],
154
+ scopeOptions: [],
155
+ directoryScopeOptions: [
156
+ { label: "Anywhere in project/", scope: "/home/user/project" },
157
+ ],
158
+ },
159
+ });
160
+ return collectPendingConfirmations("conv-fixture");
161
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Render-time enrichment of history tool calls with confirmation context.
3
+ *
4
+ * Two pieces of state the persisted content does not itself carry are layered
5
+ * onto each rendered tool call here so the web/API clients can render the same
6
+ * confirmation UI on a cold reconnect (or a history reopen after the live
7
+ * event buffer has aged out) that the live `confirmation_request` SSE stream
8
+ * would have produced:
9
+ *
10
+ * 1. `scopeOptions` — the confirmation scope ladder for scope-aware tools
11
+ * (file/bash). It is a pure function of the workspace directory and the
12
+ * tool name, so it is *derived* at render rather than persisted. Completed
13
+ * tool calls regain the ladder the rule editor's trust-rule suggestion
14
+ * fallback consumes.
15
+ *
16
+ * 2. `pendingConfirmation` — the in-flight prompt for a tool call still
17
+ * awaiting a user decision. It is read from the in-memory
18
+ * `pending-interactions` registry (the authoritative store of unresolved
19
+ * prompts), so it appears only while the prompt is genuinely outstanding.
20
+ */
21
+
22
+ import type { ConversationMessageToolCall } from "../../api/responses/conversation-message.js";
23
+ import { generateScopeOptions } from "../../permissions/checker.js";
24
+ import {
25
+ type ConfirmationDetails,
26
+ getByConversation,
27
+ } from "../pending-interactions.js";
28
+
29
+ /** A pending confirmation matched to the tool call it prompts for, keyed by `toolUseId`. */
30
+ interface PendingConfirmationMatch {
31
+ requestId: string;
32
+ details: ConfirmationDetails;
33
+ }
34
+
35
+ /**
36
+ * Build the `toolUseId → pending confirmation` lookup for a conversation from
37
+ * the registry. Only confirmation interactions that carry both a `toolUseId`
38
+ * and `confirmationDetails` can be stamped onto a wire tool call.
39
+ */
40
+ export function collectPendingConfirmations(
41
+ conversationId: string,
42
+ ): Map<string, PendingConfirmationMatch> {
43
+ const byToolUseId = new Map<string, PendingConfirmationMatch>();
44
+ for (const interaction of getByConversation(conversationId)) {
45
+ if (
46
+ interaction.kind === "confirmation" &&
47
+ interaction.confirmationDetails &&
48
+ interaction.toolUseId
49
+ ) {
50
+ byToolUseId.set(interaction.toolUseId, {
51
+ requestId: interaction.requestId,
52
+ details: interaction.confirmationDetails,
53
+ });
54
+ }
55
+ }
56
+ return byToolUseId;
57
+ }
58
+
59
+ /** Project a registry `ConfirmationDetails` into the wire `pendingConfirmation` shape. */
60
+ function toPendingConfirmation(
61
+ requestId: string,
62
+ details: ConfirmationDetails,
63
+ ): NonNullable<ConversationMessageToolCall["pendingConfirmation"]> {
64
+ return {
65
+ requestId,
66
+ toolName: details.toolName,
67
+ riskLevel: details.riskLevel,
68
+ input: details.input,
69
+ allowlistOptions: details.allowlistOptions,
70
+ scopeOptions: details.scopeOptions,
71
+ directoryScopeOptions: details.directoryScopeOptions,
72
+ persistentDecisionsAllowed: details.persistentDecisionsAllowed,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Layer derived `scopeOptions` and any outstanding `pendingConfirmation` onto a
78
+ * message's rendered tool calls. Returns a new array; tool calls without
79
+ * enrichment are returned unchanged.
80
+ */
81
+ export function enrichToolCallsWithConfirmation(
82
+ toolCalls: ConversationMessageToolCall[],
83
+ opts: {
84
+ workspaceDir: string;
85
+ pendingConfirmations: ReadonlyMap<string, PendingConfirmationMatch>;
86
+ },
87
+ ): ConversationMessageToolCall[] {
88
+ return toolCalls.map((tc) => {
89
+ const scopeOptions = generateScopeOptions(opts.workspaceDir, tc.name);
90
+ const match = tc.id ? opts.pendingConfirmations.get(tc.id) : undefined;
91
+ if (scopeOptions.length === 0 && !match) {
92
+ return tc;
93
+ }
94
+ return {
95
+ ...tc,
96
+ ...(scopeOptions.length > 0 ? { scopeOptions } : {}),
97
+ ...(match
98
+ ? {
99
+ pendingConfirmation: toPendingConfirmation(
100
+ match.requestId,
101
+ match.details,
102
+ ),
103
+ }
104
+ : {}),
105
+ };
106
+ });
107
+ }
@@ -43,9 +43,30 @@ const TrustRulesListParams = z
43
43
  })
44
44
  .strict();
45
45
 
46
+ const TrustRuleSchema = z.object({
47
+ id: z.string(),
48
+ tool: z.string(),
49
+ pattern: z.string(),
50
+ risk: z.enum(["low", "medium", "high"]),
51
+ description: z.string(),
52
+ origin: z.enum(["default", "user_defined"]),
53
+ userModified: z.boolean(),
54
+ deleted: z.boolean(),
55
+ createdAt: z.string(),
56
+ updatedAt: z.string(),
57
+ });
58
+
59
+ const TrustRulesListResponseSchema = z.object({
60
+ rules: z.array(TrustRuleSchema),
61
+ });
62
+ type TrustRulesListResponse = z.infer<typeof TrustRulesListResponseSchema>;
63
+
46
64
  // ── Handlers ────────────────────────────────────────────────────────────
47
65
 
48
- async function handleList({ queryParams = {}, body = {} }: RouteHandlerArgs) {
66
+ async function handleList({
67
+ queryParams = {},
68
+ body = {},
69
+ }: RouteHandlerArgs): Promise<TrustRulesListResponse> {
49
70
  // HTTP GET delivers filters via queryParams; CLI IPC puts them in body.
50
71
  const source = Object.keys(queryParams).length > 0 ? queryParams : body;
51
72
  const p = TrustRulesListParams.parse(source);
@@ -54,7 +75,9 @@ async function handleList({ queryParams = {}, body = {} }: RouteHandlerArgs) {
54
75
  if (p.origin) qs.set("origin", p.origin);
55
76
  if (p.include_all) qs.set("include_all", "true");
56
77
  const query = qs.toString();
57
- return gatewayFetch(`/v1/trust-rules${query ? `?${query}` : ""}`);
78
+ return gatewayFetch(
79
+ `/v1/trust-rules${query ? `?${query}` : ""}`,
80
+ ) as Promise<TrustRulesListResponse>;
58
81
  }
59
82
 
60
83
  // ── Route definitions ───────────────────────────────────────────────────
@@ -73,6 +96,7 @@ export const ROUTES: RouteDefinition[] = [
73
96
  description:
74
97
  "List trust rules, optionally filtered by tool, origin, or include_all.",
75
98
  tags: ["trust-rules"],
99
+ responseBody: TrustRulesListResponseSchema,
76
100
  queryParams: [
77
101
  { name: "tool", description: "Filter by tool name" },
78
102
  { name: "origin", description: "Filter by origin" },
@@ -15,6 +15,7 @@ import { sanitizeForTts } from "../../calls/tts-text-sanitizer.js";
15
15
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
16
16
  import { getConfig } from "../../config/loader.js";
17
17
  import { getMessageContent } from "../../daemon/handlers/conversation-history.js";
18
+ import { listCatalogProvidersForDisplay } from "../../tts/provider-catalog.js";
18
19
  import {
19
20
  synthesizeText,
20
21
  TtsSynthesisError,
@@ -136,6 +137,10 @@ const ttsResponseHeaders = () => ({
136
137
  // Handlers
137
138
  // ---------------------------------------------------------------------------
138
139
 
140
+ function handleListTtsProviders() {
141
+ return { providers: listCatalogProvidersForDisplay() };
142
+ }
143
+
139
144
  async function handleMessageTts({ pathParams, queryParams }: RouteHandlerArgs) {
140
145
  const config = getConfig();
141
146
 
@@ -226,6 +231,36 @@ async function handleSynthesizeCliTts({ body }: RouteHandlerArgs) {
226
231
  // ---------------------------------------------------------------------------
227
232
 
228
233
  export const ROUTES: RouteDefinition[] = [
234
+ {
235
+ operationId: "tts_providers",
236
+ endpoint: "tts/providers",
237
+ method: "GET",
238
+ policy: {
239
+ requiredScopes: ["settings.read"],
240
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
241
+ },
242
+ summary: "List TTS providers",
243
+ description:
244
+ "Return the catalog of available TTS providers with client-facing metadata.",
245
+ tags: ["tts"],
246
+ responseBody: z.object({
247
+ providers: z.array(
248
+ z.object({
249
+ id: z.string(),
250
+ displayName: z.string(),
251
+ subtitle: z.string(),
252
+ supportsVoiceSelection: z.boolean(),
253
+ apiKeyPlaceholder: z.string(),
254
+ credentialsGuide: z.object({
255
+ description: z.string(),
256
+ url: z.string(),
257
+ linkLabel: z.string(),
258
+ }),
259
+ }),
260
+ ),
261
+ }),
262
+ handler: handleListTtsProviders,
263
+ },
229
264
  {
230
265
  operationId: "messages_tts",
231
266
  endpoint: "messages/:messageId/tts",
@@ -31,6 +31,70 @@ export interface RoutePathParam {
31
31
  description?: string;
32
32
  }
33
33
 
34
+ /**
35
+ * Content types a route can declare a request body for. `application/json`
36
+ * is the implicit default when `requestBody` is a bare Zod schema, so it is
37
+ * only spelled out here for the explicit `{ contentType, schema }` form.
38
+ */
39
+ export type RouteRequestContentType =
40
+ | "application/json"
41
+ | "application/octet-stream"
42
+ | "multipart/form-data";
43
+
44
+ /**
45
+ * A route's request body. Either:
46
+ * - a bare Zod schema, which is advertised as `application/json`, or
47
+ * - an explicit `{ contentType, schema }` pair for non-JSON bodies (e.g. a
48
+ * raw `application/octet-stream` upload). `schema` may be a Zod schema or a
49
+ * plain JSON Schema fragment (e.g. `{ type: "string", format: "binary" }`).
50
+ *
51
+ * The OpenAPI generator turns this into the operation's `requestBody`, so the
52
+ * generated client SDK describes a real body type instead of `never`. The HTTP
53
+ * adapter parses the body off the request `Content-Type` header, so this field
54
+ * is a codegen signal only and does not change runtime request handling.
55
+ */
56
+ export type RouteRequestBody =
57
+ | z.ZodType
58
+ | {
59
+ contentType: RouteRequestContentType;
60
+ schema: z.ZodType | Record<string, unknown>;
61
+ };
62
+
63
+ /**
64
+ * Content types a route can declare a success response body for.
65
+ * `application/json` is the implicit default when `responseBody` is a bare
66
+ * Zod schema, so it is only spelled out here for the explicit
67
+ * `{ contentType, schema }` form (e.g. a binary `application/octet-stream`
68
+ * download or an `application/gzip` archive).
69
+ */
70
+ export type RouteResponseContentType =
71
+ | "application/json"
72
+ | "application/octet-stream"
73
+ | "application/gzip"
74
+ | "application/pdf"
75
+ | "application/zip";
76
+
77
+ /**
78
+ * A route's success response body. Either:
79
+ * - a bare Zod schema, which is advertised as `application/json`, or
80
+ * - an explicit `{ contentType, schema }` pair for non-JSON responses (e.g. a
81
+ * binary download). `schema` may be a Zod schema or a plain JSON Schema
82
+ * fragment (e.g. `{ type: "string", format: "binary" }`, which is not
83
+ * expressible as a bare Zod type).
84
+ *
85
+ * The OpenAPI generator turns this into the operation's success response, so
86
+ * the generated client SDK describes a real response type (e.g. `Blob`)
87
+ * instead of `unknown`. Handlers serialize their own bytes via `RouteResponse`,
88
+ * so this field is a codegen signal only and does not change runtime
89
+ * response handling.
90
+ */
91
+ export type RouteResponseBody =
92
+ | z.ZodType
93
+ | {
94
+ contentType: RouteResponseContentType;
95
+ schema: z.ZodType | Record<string, unknown>;
96
+ };
97
+
34
98
  export interface RouteHandlerArgs {
35
99
  pathParams?: Record<string, string>;
36
100
  queryParams?: Record<string, string>;
@@ -102,8 +166,8 @@ export interface RouteDefinition {
102
166
  tags?: string[];
103
167
  pathParams?: RoutePathParam[];
104
168
  queryParams?: RouteQueryParam[];
105
- requestBody?: z.ZodType;
106
- responseBody?: z.ZodType;
169
+ requestBody?: RouteRequestBody;
170
+ responseBody?: RouteResponseBody;
107
171
  /**
108
172
  * HTTP status code for the success response. Defaults to "200".
109
173
  * Use "201" for resource creation, "204" for no-content responses.
@@ -145,12 +209,6 @@ export interface RouteDefinition {
145
209
  * RouteError subclasses rather than explicit Response objects.
146
210
  */
147
211
  additionalResponses?: Record<string, { description: string }>;
148
- /**
149
- * When true, the route expects a raw binary body (e.g. file uploads).
150
- * The HTTP adapter already reads `rawBody` for non-JSON content types;
151
- * this flag is a declarative signal for documentation and tooling.
152
- */
153
- rawBody?: boolean;
154
212
  /**
155
213
  * Per-route request-log control. Routes that opt in can suppress the
156
214
  * per-request INFO log line after a confirmed run of successful
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Route handlers for usage and cost summary endpoints.
3
3
  *
4
- * GET /v1/usage/totals?from=&to= — aggregate totals for a time range
5
- * GET /v1/usage/daily?from=&to= — per-day buckets for a time range
6
- * GET /v1/usage/breakdown?from=&to=&groupBy= — grouped breakdown
7
- * GET /v1/usage/series?from=&to=&granularity=&groupBy= — grouped time-series buckets
4
+ * GET /v1/usage/totals?from=&to=&scheduleId= — aggregate totals for a time range
5
+ * GET /v1/usage/daily?from=&to=&scheduleId= — per-day buckets for a time range
6
+ * GET /v1/usage/breakdown?from=&to=&groupBy=&scheduleId= — grouped breakdown
7
+ * GET /v1/usage/series?from=&to=&granularity=&groupBy=&scheduleId= — grouped time-series buckets
8
8
  */
9
9
 
10
10
  import { z } from "zod";
@@ -18,10 +18,12 @@ import {
18
18
  type GroupByDimension,
19
19
  USAGE_GROUP_BY_DIMENSIONS,
20
20
  USAGE_SERIES_GROUP_BY_DIMENSIONS,
21
+ type UsageAggregationFilter,
21
22
  type UsageGranularity,
22
23
  } from "../../memory/llm-usage-store.js";
23
24
  import { validateTimezone } from "../../memory/usage-buckets.js";
24
25
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
26
+ import { parseEpochMillisRange } from "./epoch-millis-range.js";
25
27
  import { BadRequestError } from "./errors.js";
26
28
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
27
29
 
@@ -29,6 +31,8 @@ const VALID_GROUP_BY = new Set<string>(USAGE_GROUP_BY_DIMENSIONS);
29
31
  const VALID_SERIES_GROUP_BY = new Set<string>(USAGE_SERIES_GROUP_BY_DIMENSIONS);
30
32
  const GROUP_BY_DESCRIPTION = USAGE_GROUP_BY_DIMENSIONS.join(", ");
31
33
  const SERIES_GROUP_BY_DESCRIPTION = USAGE_SERIES_GROUP_BY_DIMENSIONS.join(", ");
34
+ const SCHEDULE_ID_FILTER_DESCRIPTION =
35
+ "Optional schedule id. When set, usage is attributed by cron run windows for that schedule.";
32
36
 
33
37
  const usageTotalsSchema = z.object({
34
38
  totalInputTokens: z.number(),
@@ -86,43 +90,23 @@ function resolveTimezone(queryParams: Record<string, string>): string {
86
90
  return tz;
87
91
  }
88
92
 
89
- function parseTimeRange(queryParams: Record<string, string>): {
90
- from: number;
91
- to: number;
92
- } {
93
- const fromRaw = queryParams.from;
94
- const toRaw = queryParams.to;
95
-
96
- if (!fromRaw || !toRaw) {
97
- throw new BadRequestError(
98
- 'Missing required query parameters: "from" and "to" (epoch milliseconds)',
99
- );
100
- }
101
-
102
- const from = Number(fromRaw);
103
- const to = Number(toRaw);
104
-
105
- if (!Number.isFinite(from) || !Number.isFinite(to)) {
106
- throw new BadRequestError(
107
- '"from" and "to" must be valid numbers (epoch milliseconds)',
108
- );
109
- }
110
-
111
- if (from > to) {
112
- throw new BadRequestError('"from" must be less than or equal to "to"');
113
- }
114
-
115
- return { from, to };
93
+ function parseUsageAggregationFilter(
94
+ queryParams: Record<string, string>,
95
+ ): UsageAggregationFilter {
96
+ const scheduleId = queryParams.scheduleId?.trim();
97
+ return scheduleId ? { scheduleId } : {};
116
98
  }
117
99
 
118
100
  function handleUsageTotals({ queryParams }: RouteHandlerArgs) {
119
- const range = parseTimeRange(queryParams ?? {});
120
- return getUsageTotals(range);
101
+ const qp = queryParams ?? {};
102
+ const range = parseEpochMillisRange(qp);
103
+ const filter = parseUsageAggregationFilter(qp);
104
+ return getUsageTotals(range, filter);
121
105
  }
122
106
 
123
107
  function handleUsageDaily({ queryParams }: RouteHandlerArgs) {
124
108
  const qp = queryParams ?? {};
125
- const range = parseTimeRange(qp);
109
+ const range = parseEpochMillisRange(qp);
126
110
  const granularity = qp.granularity ?? "daily";
127
111
  if (granularity !== "daily" && granularity !== "hourly") {
128
112
  throw new BadRequestError(
@@ -130,16 +114,17 @@ function handleUsageDaily({ queryParams }: RouteHandlerArgs) {
130
114
  );
131
115
  }
132
116
  const tz = resolveTimezone(qp);
117
+ const filter = parseUsageAggregationFilter(qp);
133
118
  const buckets =
134
119
  granularity === "hourly"
135
- ? getUsageHourBuckets(range, tz, { fillEmpty: true })
136
- : getUsageDayBuckets(range, tz, { fillEmpty: true });
120
+ ? getUsageHourBuckets(range, tz, { fillEmpty: true }, filter)
121
+ : getUsageDayBuckets(range, tz, { fillEmpty: true }, filter);
137
122
  return { buckets };
138
123
  }
139
124
 
140
125
  function handleUsageBreakdown({ queryParams }: RouteHandlerArgs) {
141
126
  const qp = queryParams ?? {};
142
- const range = parseTimeRange(qp);
127
+ const range = parseEpochMillisRange(qp);
143
128
 
144
129
  const groupBy = qp.groupBy;
145
130
  if (!groupBy) {
@@ -153,13 +138,18 @@ function handleUsageBreakdown({ queryParams }: RouteHandlerArgs) {
153
138
  );
154
139
  }
155
140
 
156
- const breakdown = getUsageGroupBreakdown(range, groupBy as GroupByDimension);
141
+ const filter = parseUsageAggregationFilter(qp);
142
+ const breakdown = getUsageGroupBreakdown(
143
+ range,
144
+ groupBy as GroupByDimension,
145
+ filter,
146
+ );
157
147
  return { breakdown };
158
148
  }
159
149
 
160
150
  function handleUsageSeries({ queryParams }: RouteHandlerArgs) {
161
151
  const qp = queryParams ?? {};
162
- const range = parseTimeRange(qp);
152
+ const range = parseEpochMillisRange(qp);
163
153
  const granularity = qp.granularity ?? "daily";
164
154
  if (granularity !== "daily" && granularity !== "hourly") {
165
155
  throw new BadRequestError(
@@ -180,12 +170,14 @@ function handleUsageSeries({ queryParams }: RouteHandlerArgs) {
180
170
  }
181
171
 
182
172
  const tz = resolveTimezone(qp);
173
+ const filter = parseUsageAggregationFilter(qp);
183
174
  const buckets = getUsageGroupedSeries(
184
175
  range,
185
176
  groupBy as Exclude<GroupByDimension, "conversation">,
186
177
  granularity as UsageGranularity,
187
178
  tz,
188
179
  { fillEmpty: true },
180
+ filter,
189
181
  );
190
182
  return { buckets };
191
183
  }
@@ -213,6 +205,10 @@ export const ROUTES: RouteDefinition[] = [
213
205
  type: "integer",
214
206
  description: "End epoch millis (required)",
215
207
  },
208
+ {
209
+ name: "scheduleId",
210
+ description: SCHEDULE_ID_FILTER_DESCRIPTION,
211
+ },
216
212
  ],
217
213
  responseBody: usageTotalsSchema,
218
214
  handler: handleUsageTotals,
@@ -249,6 +245,10 @@ export const ROUTES: RouteDefinition[] = [
249
245
  description:
250
246
  'IANA timezone identifier (e.g. "America/Los_Angeles"). Bucket boundaries and display labels are computed in this timezone. Defaults to "UTC" for backwards compatibility.',
251
247
  },
248
+ {
249
+ name: "scheduleId",
250
+ description: SCHEDULE_ID_FILTER_DESCRIPTION,
251
+ },
252
252
  ],
253
253
  responseBody: z.object({
254
254
  buckets: z.array(usageDayBucketSchema).describe("Usage bucket objects"),
@@ -282,6 +282,10 @@ export const ROUTES: RouteDefinition[] = [
282
282
  name: "groupBy",
283
283
  description: `Group by: ${GROUP_BY_DESCRIPTION} (required)`,
284
284
  },
285
+ {
286
+ name: "scheduleId",
287
+ description: SCHEDULE_ID_FILTER_DESCRIPTION,
288
+ },
285
289
  ],
286
290
  responseBody: z.object({
287
291
  breakdown: z
@@ -327,6 +331,10 @@ export const ROUTES: RouteDefinition[] = [
327
331
  description:
328
332
  'IANA timezone identifier (e.g. "America/Los_Angeles"). Bucket boundaries and display labels are computed in this timezone. Defaults to "UTC".',
329
333
  },
334
+ {
335
+ name: "scheduleId",
336
+ description: SCHEDULE_ID_FILTER_DESCRIPTION,
337
+ },
330
338
  ],
331
339
  responseBody: z.object({
332
340
  buckets: z