@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
@@ -67,6 +67,8 @@ import {
67
67
  failMemoryJob,
68
68
  failStalledJobs,
69
69
  hasActiveJobOfType,
70
+ isAutomaticConsolidationJob,
71
+ MEMORY_V2_CONSOLIDATION_JOB_TRIGGERS,
70
72
  type MemoryJob,
71
73
  type MemoryJobType,
72
74
  resetRunningJobsToPending,
@@ -89,6 +91,10 @@ import { maintainJob as memoryV3MaintainJob } from "./v3/maintain-job.js";
89
91
 
90
92
  const log = getLogger("memory-jobs-worker");
91
93
 
94
+ const AUTOMATIC_CONSOLIDATION_JOB_PAYLOAD = {
95
+ trigger: MEMORY_V2_CONSOLIDATION_JOB_TRIGGERS.automatic,
96
+ } as const;
97
+
92
98
  /**
93
99
  * V1 job types that read or write the v1 Qdrant collection via
94
100
  * `getQdrantClient()`. When `memory.v2.enabled` is true, the v1 client is
@@ -609,6 +615,16 @@ async function processJob(
609
615
  await memoryV2SweepJob(job, config);
610
616
  return;
611
617
  case "memory_v2_consolidate":
618
+ if (
619
+ isAutomaticConsolidationJob(job) &&
620
+ !config.memory.v2.consolidation_enabled
621
+ ) {
622
+ log.info(
623
+ { jobId: job.id },
624
+ "Skipping automatic memory v2 consolidation because scheduled consolidation is disabled",
625
+ );
626
+ return;
627
+ }
612
628
  await memoryV2ConsolidateJob(job, config);
613
629
  return;
614
630
  case "memory_v2_migrate":
@@ -734,6 +750,8 @@ export function maybeEnqueueGraphMaintenanceJobs(
734
750
  nowMs = Date.now(),
735
751
  ): void {
736
752
  const v2Active = config.memory.v2.enabled;
753
+ const v2ConsolidationAutomaticEnabled =
754
+ v2Active && config.memory.v2.consolidation_enabled;
737
755
 
738
756
  // The single buffer-drainer entry for the v2-active branch. Referenced again
739
757
  // below by the size-based trigger.
@@ -748,7 +766,9 @@ export function maybeEnqueueGraphMaintenanceJobs(
748
766
  intervalMs: number;
749
767
  jobType: MemoryJobType;
750
768
  }> = v2Active
751
- ? [consolidateEntry]
769
+ ? v2ConsolidationAutomaticEnabled
770
+ ? [consolidateEntry]
771
+ : []
752
772
  : [
753
773
  {
754
774
  key: GRAPH_MAINTENANCE_CHECKPOINTS.decay,
@@ -795,7 +815,11 @@ export function maybeEnqueueGraphMaintenanceJobs(
795
815
  for (const { key, intervalMs, jobType } of schedule) {
796
816
  const lastRun = parseInt(getMemoryCheckpoint(key) ?? "0", 10);
797
817
  if (nowMs - lastRun >= intervalMs) {
798
- enqueueMemoryJob(jobType, {});
818
+ const payload =
819
+ jobType === consolidateEntry.jobType
820
+ ? AUTOMATIC_CONSOLIDATION_JOB_PAYLOAD
821
+ : {};
822
+ enqueueMemoryJob(jobType, payload);
799
823
  setMemoryCheckpoint(key, String(nowMs));
800
824
  if (jobType === consolidateEntry.jobType) enqueuedConsolidate = true;
801
825
  }
@@ -811,14 +835,17 @@ export function maybeEnqueueGraphMaintenanceJobs(
811
835
  // buffer stays over threshold, flooding the queue with redundant LLM work.
812
836
  const maxLines = config.memory.v2.consolidation_max_buffer_lines;
813
837
  if (
814
- v2Active &&
838
+ v2ConsolidationAutomaticEnabled &&
815
839
  !enqueuedConsolidate &&
816
840
  maxLines !== null &&
817
841
  !hasActiveJobOfType(consolidateEntry.jobType)
818
842
  ) {
819
843
  const bufferPath = join(getWorkspaceDir(), "memory", "buffer.md");
820
844
  if (countBufferLines(bufferPath) >= maxLines) {
821
- enqueueMemoryJob(consolidateEntry.jobType, {});
845
+ enqueueMemoryJob(
846
+ consolidateEntry.jobType,
847
+ AUTOMATIC_CONSOLIDATION_JOB_PAYLOAD,
848
+ );
822
849
  setMemoryCheckpoint(consolidateEntry.key, String(nowMs));
823
850
  }
824
851
  }
@@ -9,6 +9,13 @@ import type {
9
9
  import { APP_VERSION } from "../version.js";
10
10
  import { getDb } from "./db-connection.js";
11
11
  import { rawAll } from "./raw-query.js";
12
+ import {
13
+ buildScheduleAttributionSubquery,
14
+ buildScheduleRunWindowExists,
15
+ normalizeScheduleAttributionFilter,
16
+ type ScheduleAttributionFilter,
17
+ type ScheduleAttributionSqlParam,
18
+ } from "./schedule-attribution-sql.js";
12
19
  import { conversations, llmUsageEvents } from "./schema.js";
13
20
  import {
14
21
  bucketEventsByDay,
@@ -282,6 +289,8 @@ export interface UsageTimeRange {
282
289
  to: number;
283
290
  }
284
291
 
292
+ export type UsageAggregationFilter = ScheduleAttributionFilter;
293
+
285
294
  /** Aggregate totals across a time range. */
286
295
  export interface UsageTotals {
287
296
  /** Direct input tokens only; cache traffic is reported separately below. */
@@ -332,14 +341,15 @@ export interface UsageGroupBreakdown {
332
341
  /**
333
342
  * Stable identifier for the group. Populated with the conversation id when
334
343
  * `groupBy === "conversation"` (and `null` for that mode's "Other" bucket,
335
- * which aggregates events with no conversation id). For all other group-bys
336
- * this is always `null`.
344
+ * which aggregates events with no conversation id) or with the schedule id
345
+ * when `groupBy === "schedule"` (and `null` for "Other"). For all other
346
+ * group-bys this is always `null`.
337
347
  */
338
348
  groupId: string | null;
339
349
  /**
340
350
  * Raw stored grouping value for dimensions whose display label may differ
341
- * from storage (`call_site`, `inference_profile`). Omitted for legacy
342
- * dimensions where `group` is already the raw value.
351
+ * from storage (`call_site`, `inference_profile`, `schedule`). Omitted for
352
+ * legacy dimensions where `group` is already the raw value.
343
353
  */
344
354
  groupKey?: string | null;
345
355
  /** Direct input tokens only; cache traffic is reported separately below. */
@@ -367,6 +377,7 @@ interface TotalsRow {
367
377
  interface GroupRow {
368
378
  group_key: string | null;
369
379
  group_id: string | null;
380
+ group_label: string | null;
370
381
  total_input_tokens: number;
371
382
  total_output_tokens: number;
372
383
  total_cache_creation_tokens: number;
@@ -375,6 +386,39 @@ interface GroupRow {
375
386
  event_count: number;
376
387
  }
377
388
 
389
+ type UsageQueryParam = ScheduleAttributionSqlParam;
390
+
391
+ function normalizeUsageAggregationFilter(
392
+ filter?: UsageAggregationFilter,
393
+ ): UsageAggregationFilter {
394
+ return normalizeScheduleAttributionFilter(filter);
395
+ }
396
+
397
+ function buildUsageAggregationWhere(
398
+ range: UsageTimeRange,
399
+ filter?: UsageAggregationFilter,
400
+ eventAlias?: string,
401
+ now: number = Date.now(),
402
+ ): { sql: string; params: UsageQueryParam[] } {
403
+ const normalized = normalizeUsageAggregationFilter(filter);
404
+ const eventTable = eventAlias ?? "llm_usage_events";
405
+ const createdAt = `${eventTable}.created_at`;
406
+ const clauses = [`${createdAt} >= ? AND ${createdAt} <= ?`];
407
+ const params: UsageQueryParam[] = [range.from, range.to];
408
+
409
+ if (normalized.scheduleId) {
410
+ const exists = buildScheduleRunWindowExists({
411
+ eventAlias: eventTable,
412
+ filter: normalized,
413
+ now,
414
+ });
415
+ clauses.push(exists.sql);
416
+ params.push(...exists.params);
417
+ }
418
+
419
+ return { sql: clauses.join(" AND "), params };
420
+ }
421
+
378
422
  /**
379
423
  * Return aggregate usage for a single conversation (e.g. a subagent).
380
424
  */
@@ -406,10 +450,38 @@ export function getConversationUsageTotals(conversationId: string): {
406
450
  };
407
451
  }
408
452
 
453
+ export function getUsageCostForConversationWindow({
454
+ conversationId,
455
+ from,
456
+ to,
457
+ }: {
458
+ conversationId: string;
459
+ from: number;
460
+ to: number;
461
+ }): number {
462
+ const rows = rawAll<{ total_cost: number | null }>(
463
+ /*sql*/ `
464
+ SELECT COALESCE(SUM(estimated_cost_usd), 0) AS total_cost
465
+ FROM llm_usage_events
466
+ WHERE conversation_id = ?1
467
+ AND created_at >= ?2
468
+ AND created_at <= ?3
469
+ `,
470
+ conversationId,
471
+ from,
472
+ to,
473
+ );
474
+ return rows[0]?.total_cost ?? 0;
475
+ }
476
+
409
477
  /**
410
478
  * Return aggregate totals for all usage events within the given time range.
411
479
  */
412
- export function getUsageTotals(range: UsageTimeRange): UsageTotals {
480
+ export function getUsageTotals(
481
+ range: UsageTimeRange,
482
+ filter?: UsageAggregationFilter,
483
+ ): UsageTotals {
484
+ const where = buildUsageAggregationWhere(range, filter);
413
485
  const rows = rawAll<TotalsRow>(
414
486
  /*sql*/ `
415
487
  SELECT
@@ -422,10 +494,9 @@ export function getUsageTotals(range: UsageTimeRange): UsageTotals {
422
494
  COUNT(CASE WHEN pricing_status = 'priced' THEN 1 END) AS priced_event_count,
423
495
  COUNT(CASE WHEN pricing_status = 'unpriced' THEN 1 END) AS unpriced_event_count
424
496
  FROM llm_usage_events
425
- WHERE created_at >= ?1 AND created_at <= ?2
497
+ WHERE ${where.sql}
426
498
  `,
427
- range.from,
428
- range.to,
499
+ ...where.params,
429
500
  );
430
501
  const row = rows[0];
431
502
  return {
@@ -441,7 +512,11 @@ export function getUsageTotals(range: UsageTimeRange): UsageTotals {
441
512
  }
442
513
 
443
514
  /** Fetch raw events in a time range for in-memory bucketing. */
444
- function fetchRawBucketRows(range: UsageTimeRange): UsageEventBucketRow[] {
515
+ function fetchRawBucketRows(
516
+ range: UsageTimeRange,
517
+ filter?: UsageAggregationFilter,
518
+ ): UsageEventBucketRow[] {
519
+ const where = buildUsageAggregationWhere(range, filter);
445
520
  return rawAll<UsageEventBucketRow>(
446
521
  /*sql*/ `
447
522
  SELECT
@@ -451,11 +526,10 @@ function fetchRawBucketRows(range: UsageTimeRange): UsageEventBucketRow[] {
451
526
  estimated_cost_usd,
452
527
  llm_call_count
453
528
  FROM llm_usage_events
454
- WHERE created_at >= ?1 AND created_at <= ?2
529
+ WHERE ${where.sql}
455
530
  ORDER BY created_at ASC
456
531
  `,
457
- range.from,
458
- range.to,
532
+ ...where.params,
459
533
  );
460
534
  }
461
535
 
@@ -482,8 +556,9 @@ export function getUsageDayBuckets(
482
556
  range: UsageTimeRange,
483
557
  tz: string = "UTC",
484
558
  options: UsageBucketOptions = {},
559
+ filter?: UsageAggregationFilter,
485
560
  ): UsageDayBucket[] {
486
- const rows = fetchRawBucketRows(range);
561
+ const rows = fetchRawBucketRows(range, filter);
487
562
  return bucketEventsByDay(rows, range, tz, options);
488
563
  }
489
564
 
@@ -500,8 +575,9 @@ export function getUsageHourBuckets(
500
575
  range: UsageTimeRange,
501
576
  tz: string = "UTC",
502
577
  options: UsageBucketOptions = {},
578
+ filter?: UsageAggregationFilter,
503
579
  ): UsageDayBucket[] {
504
- const rows = fetchRawBucketRows(range);
580
+ const rows = fetchRawBucketRows(range, filter);
505
581
  return bucketEventsByHour(rows, range, tz, options);
506
582
  }
507
583
 
@@ -512,6 +588,7 @@ export const USAGE_GROUP_BY_DIMENSIONS = [
512
588
  "conversation",
513
589
  "call_site",
514
590
  "inference_profile",
591
+ "schedule",
515
592
  ] as const;
516
593
 
517
594
  export type GroupByDimension = (typeof USAGE_GROUP_BY_DIMENSIONS)[number];
@@ -522,10 +599,11 @@ export const USAGE_SERIES_GROUP_BY_DIMENSIONS = [
522
599
  "model",
523
600
  "call_site",
524
601
  "inference_profile",
602
+ "schedule",
525
603
  ] as const satisfies readonly GroupByDimension[];
526
604
 
527
605
  const GROUP_BY_COLUMNS: Record<
528
- Exclude<GroupByDimension, "conversation">,
606
+ Exclude<GroupByDimension, "conversation" | "schedule">,
529
607
  string
530
608
  > = {
531
609
  actor: "actor",
@@ -550,9 +628,11 @@ function mapGroupRow(
550
628
  groupBy: GroupByDimension,
551
629
  ): UsageGroupBreakdown {
552
630
  const includeGroupKey =
553
- groupBy === "call_site" || groupBy === "inference_profile";
631
+ groupBy === "call_site" ||
632
+ groupBy === "inference_profile" ||
633
+ groupBy === "schedule";
554
634
  return {
555
- group: displayUsageGroup(groupBy, row.group_key),
635
+ group: row.group_label ?? displayUsageGroup(groupBy, row.group_key),
556
636
  groupId: row.group_id,
557
637
  ...(includeGroupKey ? { groupKey: row.group_key } : {}),
558
638
  totalInputTokens: row.total_input_tokens,
@@ -571,12 +651,16 @@ function mapGroupRow(
571
651
  export function getUsageGroupBreakdown(
572
652
  range: UsageTimeRange,
573
653
  groupBy: GroupByDimension,
654
+ filter?: UsageAggregationFilter,
574
655
  ): UsageGroupBreakdown[] {
575
656
  // Runtime allowlist — defense-in-depth against SQL injection via type assertions.
576
657
  assertGroupByDimension(groupBy);
577
658
 
659
+ const normalizedFilter = normalizeUsageAggregationFilter(filter);
660
+
578
661
  // Conversation grouping requires a JOIN with conversations to resolve titles.
579
662
  if (groupBy === "conversation") {
663
+ const where = buildUsageAggregationWhere(range, normalizedFilter, "e");
580
664
  const rows = rawAll<GroupRow>(
581
665
  /*sql*/ `
582
666
  SELECT
@@ -584,6 +668,7 @@ export function getUsageGroupBreakdown(
584
668
  ELSE COALESCE(c.title, 'Untitled')
585
669
  END AS group_key,
586
670
  e.conversation_id AS group_id,
671
+ NULL AS group_label,
587
672
  COALESCE(SUM(e.input_tokens), 0) AS total_input_tokens,
588
673
  COALESCE(SUM(e.output_tokens), 0) AS total_output_tokens,
589
674
  COALESCE(SUM(e.cache_creation_input_tokens), 0) AS total_cache_creation_tokens,
@@ -592,36 +677,81 @@ export function getUsageGroupBreakdown(
592
677
  COALESCE(SUM(COALESCE(e.llm_call_count, 1)), 0) AS event_count
593
678
  FROM llm_usage_events e
594
679
  LEFT JOIN conversations c ON e.conversation_id = c.id
595
- WHERE e.created_at >= ?1 AND e.created_at <= ?2
680
+ WHERE ${where.sql}
596
681
  GROUP BY e.conversation_id
597
682
  ORDER BY total_estimated_cost_usd DESC
598
683
  LIMIT 50
599
684
  `,
600
- range.from,
601
- range.to,
685
+ ...where.params,
686
+ );
687
+ return rows.map((row) => mapGroupRow(row, groupBy));
688
+ }
689
+
690
+ if (groupBy === "schedule") {
691
+ const now = Date.now();
692
+ const where = buildUsageAggregationWhere(range, normalizedFilter, "e", now);
693
+ const groupKeySubquery = buildScheduleAttributionSubquery({
694
+ eventAlias: "e",
695
+ filter: normalizedFilter,
696
+ now,
697
+ selectExpression: "schedule_attr_runs.job_id",
698
+ });
699
+ const rows = rawAll<GroupRow>(
700
+ /*sql*/ `
701
+ WITH schedule_usage AS (
702
+ SELECT
703
+ e.input_tokens,
704
+ e.output_tokens,
705
+ e.cache_creation_input_tokens,
706
+ e.cache_read_input_tokens,
707
+ e.estimated_cost_usd,
708
+ e.llm_call_count,
709
+ ${groupKeySubquery.sql} AS group_key
710
+ FROM llm_usage_events e
711
+ WHERE ${where.sql}
712
+ )
713
+ SELECT
714
+ schedule_usage.group_key AS group_key,
715
+ schedule_usage.group_key AS group_id,
716
+ MAX(schedule_group_jobs.name) AS group_label,
717
+ COALESCE(SUM(schedule_usage.input_tokens), 0) AS total_input_tokens,
718
+ COALESCE(SUM(schedule_usage.output_tokens), 0) AS total_output_tokens,
719
+ COALESCE(SUM(schedule_usage.cache_creation_input_tokens), 0) AS total_cache_creation_tokens,
720
+ COALESCE(SUM(schedule_usage.cache_read_input_tokens), 0) AS total_cache_read_tokens,
721
+ COALESCE(SUM(schedule_usage.estimated_cost_usd), 0) AS total_estimated_cost_usd,
722
+ COALESCE(SUM(COALESCE(schedule_usage.llm_call_count, 1)), 0) AS event_count
723
+ FROM schedule_usage
724
+ LEFT JOIN cron_jobs schedule_group_jobs
725
+ ON schedule_group_jobs.id = schedule_usage.group_key
726
+ GROUP BY schedule_usage.group_key
727
+ ORDER BY total_estimated_cost_usd DESC
728
+ `,
729
+ ...groupKeySubquery.params,
730
+ ...where.params,
602
731
  );
603
732
  return rows.map((row) => mapGroupRow(row, groupBy));
604
733
  }
605
734
 
606
735
  const column = GROUP_BY_COLUMNS[groupBy];
736
+ const where = buildUsageAggregationWhere(range, normalizedFilter, "e");
607
737
  const rows = rawAll<GroupRow>(
608
738
  /*sql*/ `
609
739
  SELECT
610
- ${column} AS group_key,
611
- NULL AS group_id,
612
- COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
613
- COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
614
- COALESCE(SUM(cache_creation_input_tokens), 0) AS total_cache_creation_tokens,
615
- COALESCE(SUM(cache_read_input_tokens), 0) AS total_cache_read_tokens,
616
- COALESCE(SUM(estimated_cost_usd), 0) AS total_estimated_cost_usd,
617
- COALESCE(SUM(COALESCE(llm_call_count, 1)), 0) AS event_count
618
- FROM llm_usage_events
619
- WHERE created_at >= ?1 AND created_at <= ?2
620
- GROUP BY ${column}
740
+ e.${column} AS group_key,
741
+ NULL AS group_id,
742
+ NULL AS group_label,
743
+ COALESCE(SUM(e.input_tokens), 0) AS total_input_tokens,
744
+ COALESCE(SUM(e.output_tokens), 0) AS total_output_tokens,
745
+ COALESCE(SUM(e.cache_creation_input_tokens), 0) AS total_cache_creation_tokens,
746
+ COALESCE(SUM(e.cache_read_input_tokens), 0) AS total_cache_read_tokens,
747
+ COALESCE(SUM(e.estimated_cost_usd), 0) AS total_estimated_cost_usd,
748
+ COALESCE(SUM(COALESCE(e.llm_call_count, 1)), 0) AS event_count
749
+ FROM llm_usage_events e
750
+ WHERE ${where.sql}
751
+ GROUP BY e.${column}
621
752
  ORDER BY total_estimated_cost_usd DESC
622
753
  `,
623
- range.from,
624
- range.to,
754
+ ...where.params,
625
755
  );
626
756
  return rows.map((row) => mapGroupRow(row, groupBy));
627
757
  }
@@ -632,29 +762,73 @@ export function getUsageGroupedSeries(
632
762
  granularity: UsageGranularity,
633
763
  tz: string = "UTC",
634
764
  options: UsageBucketOptions = {},
765
+ filter?: UsageAggregationFilter,
635
766
  ): UsageGroupedSeriesBucket[] {
636
767
  assertGroupByDimension(groupBy);
637
768
  if (groupBy === "conversation") {
638
769
  throw new Error("Grouped usage series does not support conversation");
639
770
  }
640
771
 
641
- const column = GROUP_BY_COLUMNS[groupBy];
642
- const rows = rawAll<UsageGroupedBucketRow>(
643
- /*sql*/ `
644
- SELECT
645
- created_at,
646
- input_tokens,
647
- output_tokens,
648
- estimated_cost_usd,
649
- llm_call_count,
650
- ${column} AS group_key
651
- FROM llm_usage_events
652
- WHERE created_at >= ?1 AND created_at <= ?2
653
- ORDER BY created_at ASC
654
- `,
655
- range.from,
656
- range.to,
657
- );
772
+ const normalizedFilter = normalizeUsageAggregationFilter(filter);
773
+ let rows: UsageGroupedBucketRow[];
774
+
775
+ if (groupBy === "schedule") {
776
+ const now = Date.now();
777
+ const where = buildUsageAggregationWhere(range, normalizedFilter, "e", now);
778
+ const groupKeySubquery = buildScheduleAttributionSubquery({
779
+ eventAlias: "e",
780
+ filter: normalizedFilter,
781
+ now,
782
+ selectExpression: "schedule_attr_runs.job_id",
783
+ });
784
+ rows = rawAll<UsageGroupedBucketRow>(
785
+ /*sql*/ `
786
+ WITH schedule_usage AS (
787
+ SELECT
788
+ e.created_at,
789
+ e.input_tokens,
790
+ e.output_tokens,
791
+ e.estimated_cost_usd,
792
+ e.llm_call_count,
793
+ ${groupKeySubquery.sql} AS group_key
794
+ FROM llm_usage_events e
795
+ WHERE ${where.sql}
796
+ )
797
+ SELECT
798
+ schedule_usage.created_at,
799
+ schedule_usage.input_tokens,
800
+ schedule_usage.output_tokens,
801
+ schedule_usage.estimated_cost_usd,
802
+ schedule_usage.llm_call_count,
803
+ schedule_usage.group_key,
804
+ schedule_group_jobs.name AS group_label
805
+ FROM schedule_usage
806
+ LEFT JOIN cron_jobs schedule_group_jobs
807
+ ON schedule_group_jobs.id = schedule_usage.group_key
808
+ ORDER BY schedule_usage.created_at ASC
809
+ `,
810
+ ...groupKeySubquery.params,
811
+ ...where.params,
812
+ );
813
+ } else {
814
+ const column = GROUP_BY_COLUMNS[groupBy];
815
+ const where = buildUsageAggregationWhere(range, normalizedFilter, "e");
816
+ rows = rawAll<UsageGroupedBucketRow>(
817
+ /*sql*/ `
818
+ SELECT
819
+ e.created_at,
820
+ e.input_tokens,
821
+ e.output_tokens,
822
+ e.estimated_cost_usd,
823
+ e.llm_call_count,
824
+ e.${column} AS group_key
825
+ FROM llm_usage_events e
826
+ WHERE ${where.sql}
827
+ ORDER BY e.created_at ASC
828
+ `,
829
+ ...where.params,
830
+ );
831
+ }
658
832
 
659
833
  return bucketGroupedUsageEvents(rows, range, tz, {
660
834
  ...options,
@@ -1,15 +1,16 @@
1
- import { isPlaceholderSentinelText } from "../../providers/anthropic/client.js";
1
+ import { isPlaceholderSentinelText } from "../../providers/placeholder-sentinels.js";
2
2
  import type { DrizzleDb } from "../db-connection.js";
3
3
  import { getSqliteFrom } from "../db-connection.js";
4
4
  import { withCrashRecovery } from "./validate-migration-state.js";
5
5
 
6
6
  /**
7
- * Strip Anthropic provider placeholder sentinel text blocks from persisted
8
- * assistant messages.
7
+ * Strip provider placeholder sentinel text blocks from persisted assistant
8
+ * messages.
9
9
  *
10
10
  * PLACEHOLDER_EMPTY_TURN and PLACEHOLDER_BLOCKS_OMITTED are injected into
11
- * outbound Anthropic request bodies to preserve role alternation when an
12
- * assistant turn would otherwise be empty. They are never supposed to be
11
+ * outbound provider request bodies (Anthropic role alternation, OpenAI-
12
+ * compatible "content or tool_calls" constraint) when an assistant turn would
13
+ * otherwise be empty. They are never supposed to be
13
14
  * persisted, but a leak path caused them to be stored in the messages table
14
15
  * where they render in chat bubbles as bold "PLACEHOLDER[...]" (markdown
15
16
  * interprets the double-underscore surround as bold).
@@ -0,0 +1,13 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateScheduleSourceConversation(database: DrizzleDb): void {
5
+ const raw = getSqliteFrom(database);
6
+ try {
7
+ raw.exec(
8
+ `ALTER TABLE cron_jobs ADD COLUMN created_from_conversation_id TEXT`,
9
+ );
10
+ } catch {
11
+ // Column already exists — nothing to do.
12
+ }
13
+ }
@@ -0,0 +1,21 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+
3
+ /**
4
+ * Create the auth_fallback_events table for tracking legacy-loopback
5
+ * auth-fallback counts forwarded by the gateway. One row per
6
+ * (guard, path, failure_kind) per flush window.
7
+ */
8
+ export function createAuthFallbackEventsTable(database: DrizzleDb): void {
9
+ database.run(/*sql*/ `
10
+ CREATE TABLE IF NOT EXISTS auth_fallback_events (
11
+ id TEXT PRIMARY KEY,
12
+ created_at INTEGER NOT NULL,
13
+ guard TEXT NOT NULL,
14
+ path TEXT NOT NULL,
15
+ failure_kind TEXT NOT NULL,
16
+ count INTEGER NOT NULL,
17
+ window_start INTEGER NOT NULL,
18
+ window_end INTEGER NOT NULL
19
+ )
20
+ `);
21
+ }
@@ -257,6 +257,8 @@ export { migrateLlmUsageEventsAddAssistantVersion } from "./267-llm-usage-events
257
257
  export { migrateAddMemoryV3Selections } from "./268-add-memory-v3-selections.js";
258
258
  export { migrateScheduleScriptTimeout } from "./269-schedule-script-timeout.js";
259
259
  export { migrateMessagesRoleCreatedAtIndex } from "./270-messages-role-created-at-index.js";
260
+ export { migrateScheduleSourceConversation } from "./270-schedule-source-conversation.js";
261
+ export { createAuthFallbackEventsTable } from "./271-create-auth-fallback-events.js";
260
262
  export {
261
263
  MIGRATION_REGISTRY,
262
264
  type MigrationRegistryEntry,
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Resolution of the PKB auto-inject file list (`pkb/_autoinject.md`).
3
+ *
4
+ * Which PKB files are always loaded into context is a PKB-domain concern
5
+ * owned here next to {@link getPkbRoot}, so both the runtime injector and the
6
+ * reminder-hint tracker resolve the list from a single canonical source
7
+ * rather than threading it through the orchestrator.
8
+ */
9
+
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ import { stripCommentLines } from "../../util/strip-comment-lines.js";
14
+
15
+ const PKB_DEFAULT_FILES = [
16
+ "INDEX.md",
17
+ "essentials.md",
18
+ "threads.md",
19
+ "buffer.md",
20
+ ];
21
+
22
+ const AUTOINJECT_FILENAME = "_autoinject.md";
23
+
24
+ /**
25
+ * Read `_autoinject.md` from the PKB directory and return the list of
26
+ * filenames to inject.
27
+ *
28
+ * - Returns `null` when the file is missing or unreadable — callers
29
+ * should fall back to the hardcoded defaults.
30
+ * - Returns `[]` when the file exists but has no entries (empty or
31
+ * comments only) — an explicit opt-out meaning "inject nothing."
32
+ */
33
+ export function readAutoinjectList(pkbDir: string): string[] | null {
34
+ const filePath = join(pkbDir, AUTOINJECT_FILENAME);
35
+ if (!existsSync(filePath)) return null;
36
+ try {
37
+ const raw = stripCommentLines(readFileSync(filePath, "utf-8"));
38
+ const files = raw
39
+ .split("\n")
40
+ .map((l) => l.trim())
41
+ .filter((l) => l.length > 0);
42
+ return files.length > 0 ? files : [];
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Resolve the effective list of auto-inject filenames for a PKB directory.
50
+ *
51
+ * This is the single source of truth used both by `readPkbContext` (which
52
+ * actually injects the files) and by the PKB reminder-hint injector (which
53
+ * needs to know what's already in context so it doesn't redundantly
54
+ * recommend those files).
55
+ *
56
+ * Returns `PKB_DEFAULT_FILES` when `_autoinject.md` is missing/unreadable,
57
+ * or the parsed list (possibly empty) when it is present.
58
+ */
59
+ export function getPkbAutoInjectList(pkbRoot: string): string[] {
60
+ return readAutoinjectList(pkbRoot) ?? PKB_DEFAULT_FILES;
61
+ }