@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
@@ -243,6 +243,10 @@ export const ROUTES: RouteDefinition[] = [
243
243
  summary: "Export a document as PDF",
244
244
  description: "Render a document to PDF and return the binary content.",
245
245
  tags: ["documents"],
246
+ responseBody: {
247
+ contentType: "application/pdf",
248
+ schema: { type: "string", format: "binary" },
249
+ },
246
250
  handler: async ({ pathParams }) => {
247
251
  const doc = getDocumentById(pathParams!.id);
248
252
  if (!doc) {
@@ -5,6 +5,8 @@
5
5
  * the subdomain to local config so getAssistantDomain() can use it.
6
6
  */
7
7
 
8
+ import { z } from "zod";
9
+
8
10
  import { getApexDomain } from "../../config/env.js";
9
11
  import {
10
12
  loadRawConfig,
@@ -19,18 +21,36 @@ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
19
21
 
20
22
  const log = getLogger("domain-routes");
21
23
 
22
- // ── Helpers ───────────────────────────────────────────────────────────
24
+ // ── Schemas ───────────────────────────────────────────────────────────
23
25
 
24
- interface DomainListResponse {
25
- next?: string | null;
26
- results: {
27
- id: string;
28
- subdomain?: string;
29
- domain?: string;
30
- created_at?: string;
31
- created?: string;
32
- }[];
33
- }
26
+ const DomainEntrySchema = z.object({
27
+ id: z.string(),
28
+ subdomain: z.string().optional(),
29
+ domain: z.string().optional(),
30
+ created_at: z.string().optional(),
31
+ created: z.string().optional(),
32
+ });
33
+
34
+ const DomainListResponseSchema = z.object({
35
+ count: z.number(),
36
+ next: z.string().nullable().optional(),
37
+ previous: z.string().nullable().optional(),
38
+ results: z.array(DomainEntrySchema),
39
+ });
40
+ type DomainListResponse = z.infer<typeof DomainListResponseSchema>;
41
+
42
+ const DomainRegisterResponseSchema = DomainEntrySchema.extend({
43
+ email_error: z.object({ detail: z.string(), code: z.string() }).optional(),
44
+ });
45
+ type DomainRegisterResponse = z.infer<typeof DomainRegisterResponseSchema>;
46
+
47
+ const DomainVerificationStatusResponseSchema = z.object({
48
+ domain: z.string(),
49
+ status: z.string(),
50
+ message: z.string(),
51
+ });
52
+
53
+ // ── Helpers ───────────────────────────────────────────────────────────
34
54
 
35
55
  async function requireClient(): Promise<VellumPlatformClient> {
36
56
  const client = await VellumPlatformClient.create();
@@ -113,20 +133,11 @@ async function handleDomainRegister({ body = {} }: RouteHandlerArgs) {
113
133
  throw new BadRequestError(String(detail));
114
134
  }
115
135
 
116
- const data = (await response.json()) as {
117
- id: string;
118
- subdomain?: string;
119
- domain?: string;
120
- created_at?: string;
121
- created?: string;
122
- email_error?: { detail: string; code: string };
123
- };
136
+ const data = (await response.json()) as DomainRegisterResponse;
124
137
 
125
138
  // Persist the subdomain to config so getAssistantDomain() can use it
126
139
  const registeredSubdomain =
127
- data.subdomain ??
128
- data.domain?.replace(`.${apexDomain}`, "") ??
129
- subdomain;
140
+ data.subdomain ?? data.domain?.replace(`.${apexDomain}`, "") ?? subdomain;
130
141
  if (registeredSubdomain) {
131
142
  const raw = loadRawConfig();
132
143
  setNestedValue(raw, "platform.subdomain", registeredSubdomain);
@@ -146,12 +157,11 @@ async function handleDomainStatus(_args: RouteHandlerArgs) {
146
157
  // Sync subdomain to config if not already cached
147
158
  if (domains.length > 0) {
148
159
  const first = domains[0];
149
- const sub =
150
- first.subdomain ?? first.domain?.replace(`.${apexDomain}`, "");
160
+ const sub = first.subdomain ?? first.domain?.replace(`.${apexDomain}`, "");
151
161
  if (sub) {
152
162
  const raw = loadRawConfig();
153
- const existing = (raw as Record<string, Record<string, unknown>>)
154
- .platform?.subdomain;
163
+ const existing = (raw as Record<string, Record<string, unknown>>).platform
164
+ ?.subdomain;
155
165
  if (existing !== sub) {
156
166
  setNestedValue(raw, "platform.subdomain", sub);
157
167
  saveRawConfig(raw);
@@ -162,9 +172,7 @@ async function handleDomainStatus(_args: RouteHandlerArgs) {
162
172
  return data;
163
173
  }
164
174
 
165
- async function handleDomainVerificationStatus({
166
- body = {},
167
- }: RouteHandlerArgs) {
175
+ async function handleDomainVerificationStatus({ body = {} }: RouteHandlerArgs) {
168
176
  const { domain_id } = body as { domain_id?: string };
169
177
  if (!domain_id) {
170
178
  throw new BadRequestError("domain_id is required");
@@ -179,9 +187,7 @@ async function handleDomainVerificationStatus({
179
187
  );
180
188
  }
181
189
  if (!results?.some((d) => d.id === domain_id)) {
182
- throw new BadRequestError(
183
- "domain_id is not registered for this assistant",
184
- );
190
+ throw new BadRequestError("domain_id is not registered for this assistant");
185
191
  }
186
192
 
187
193
  const response = await client.fetch(
@@ -201,11 +207,9 @@ async function handleDomainVerificationStatus({
201
207
  );
202
208
  }
203
209
 
204
- return (await response.json()) as {
205
- domain: string;
206
- status: string;
207
- message: string;
208
- };
210
+ return (await response.json()) as z.infer<
211
+ typeof DomainVerificationStatusResponseSchema
212
+ >;
209
213
  }
210
214
 
211
215
  // ── Route definitions ─────────────────────────────────────────────────
@@ -222,6 +226,11 @@ export const ROUTES: RouteDefinition[] = [
222
226
  handler: handleDomainRegister,
223
227
  summary: "Register a subdomain for this assistant",
224
228
  tags: ["domain"],
229
+ requestBody: z.object({
230
+ subdomain: z.string().optional(),
231
+ email_username: z.string().optional(),
232
+ }),
233
+ responseBody: DomainRegisterResponseSchema,
225
234
  },
226
235
  {
227
236
  operationId: "domain_status",
@@ -234,6 +243,7 @@ export const ROUTES: RouteDefinition[] = [
234
243
  handler: handleDomainStatus,
235
244
  summary: "Show domain registration and health",
236
245
  tags: ["domain"],
246
+ responseBody: DomainListResponseSchema,
237
247
  },
238
248
  {
239
249
  operationId: "domain_verification_status",
@@ -246,5 +256,9 @@ export const ROUTES: RouteDefinition[] = [
246
256
  handler: handleDomainVerificationStatus,
247
257
  summary: "Get live DNS verification status for a domain",
248
258
  tags: ["domain"],
259
+ requestBody: z.object({
260
+ domain_id: z.string(),
261
+ }),
262
+ responseBody: DomainVerificationStatusResponseSchema,
249
263
  },
250
264
  ];
@@ -0,0 +1,34 @@
1
+ import { BadRequestError } from "./errors.js";
2
+
3
+ export interface EpochMillisRange {
4
+ from: number;
5
+ to: number;
6
+ }
7
+
8
+ export function parseEpochMillisRange(
9
+ queryParams: Record<string, string>,
10
+ ): EpochMillisRange {
11
+ const fromRaw = queryParams.from;
12
+ const toRaw = queryParams.to;
13
+
14
+ if (!fromRaw || !toRaw) {
15
+ throw new BadRequestError(
16
+ 'Missing required query parameters: "from" and "to" (epoch milliseconds)',
17
+ );
18
+ }
19
+
20
+ const from = Number(fromRaw);
21
+ const to = Number(toRaw);
22
+
23
+ if (!Number.isFinite(from) || !Number.isFinite(to)) {
24
+ throw new BadRequestError(
25
+ '"from" and "to" must be valid numbers (epoch milliseconds)',
26
+ );
27
+ }
28
+
29
+ if (from > to) {
30
+ throw new BadRequestError('"from" must be less than or equal to "to"');
31
+ }
32
+
33
+ return { from, to };
34
+ }
@@ -39,9 +39,9 @@ import {
39
39
  AssistantEventHub,
40
40
  assistantEventHub,
41
41
  } from "../assistant-event-hub.js";
42
+ import type { ReplaySubscriber } from "../assistant-stream-state.js";
43
+ import { getReplayWindow } from "../assistant-stream-state.js";
42
44
  import { ACTOR_PRINCIPALS, GATEWAY_PRINCIPALS } from "../auth/route-policy.js";
43
- import type { ReplaySubscriber } from "../conversation-stream-state.js";
44
- import { getReplayWindow } from "../conversation-stream-state.js";
45
45
  import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
46
46
  import {
47
47
  BadRequestError,
@@ -143,6 +143,12 @@ export interface SseSubscriberInstrumentation {
143
143
  clientId: string | null;
144
144
  interfaceId: string | null;
145
145
  conversationKey: string | null;
146
+ /**
147
+ * Per-connection id assigned by the hub. Distinguishes connections
148
+ * sharing a `clientId` (old vs reconnected) so a shed can be attributed
149
+ * to a specific connection. `null` until the hub subscription is created.
150
+ */
151
+ connectionId: string | null;
146
152
  }
147
153
 
148
154
  export type SseShedReason = "callback_backpressure" | "heartbeat_backpressure";
@@ -177,6 +183,7 @@ export function buildSseShedSentryContext(
177
183
  heartbeats_sent: inst.heartbeatsSent,
178
184
  client_id: inst.clientId,
179
185
  interface_id: inst.interfaceId,
186
+ connection_id: inst.connectionId,
180
187
  event_loop_delay_mean_ms: elDelay.mean_ms,
181
188
  event_loop_delay_p99_ms: elDelay.p99_ms,
182
189
  event_loop_delay_max_ms: elDelay.max_ms,
@@ -216,6 +223,7 @@ const defaultSseShedReporter: SseShedReporter = (reason, inst) => {
216
223
  scope.setTag("sse_shed_reason", reason);
217
224
  if (inst.clientId) scope.setTag("client_id", inst.clientId);
218
225
  if (inst.interfaceId) scope.setTag("interface_id", inst.interfaceId);
226
+ if (inst.connectionId) scope.setTag("connection_id", inst.connectionId);
219
227
  scope.setContext("sse_shed", sentryContext);
220
228
  Sentry.captureMessage(`sse_subscriber_shed:${reason}`);
221
229
  });
@@ -371,6 +379,7 @@ export function handleSubscribeAssistantEvents(
371
379
  clientId,
372
380
  interfaceId,
373
381
  conversationKey: scopeConversationKey,
382
+ connectionId: null,
374
383
  };
375
384
 
376
385
  ensureEventLoopDelayMonitorStarted();
@@ -394,19 +403,6 @@ export function handleSubscribeAssistantEvents(
394
403
  // replay window we just drained.
395
404
  let highWaterReplaySeq = -1;
396
405
 
397
- // Per-conversation subscriber-filtered sequence counters. Incremented
398
- // for each conversation-scoped event this specific subscriber receives
399
- // (after capability/client/interface targeting), producing a gap-free
400
- // sequence from the subscriber's perspective. Clients use `clientSeq`
401
- // for gap detection instead of the global `seq` to avoid false
402
- // positives from targeted events they never receive.
403
- const clientSeqCounters = new Map<string, number>();
404
- function nextClientSeqFor(conversationId: string): number {
405
- const next = (clientSeqCounters.get(conversationId) ?? 0) + 1;
406
- clientSeqCounters.set(conversationId, next);
407
- return next;
408
- }
409
-
410
406
  const callback: AssistantEventCallback = (event) => {
411
407
  const controller = controllerRef;
412
408
  if (!controller) return;
@@ -425,11 +421,7 @@ export function handleSubscribeAssistantEvents(
425
421
  cleanup();
426
422
  return;
427
423
  }
428
- const frame =
429
- event.conversationId != null && event.seq != null
430
- ? { ...event, clientSeq: nextClientSeqFor(event.conversationId) }
431
- : event;
432
- controller.enqueue(encoder.encode(formatSseFrame(frame)));
424
+ controller.enqueue(encoder.encode(formatSseFrame(event)));
433
425
  instrumentation.eventsDelivered += 1;
434
426
  } catch {
435
427
  sub.dispose();
@@ -461,6 +453,9 @@ export function handleSubscribeAssistantEvents(
461
453
  ...subscriberBase,
462
454
  type: "process" as const,
463
455
  });
456
+ // Stamp the hub-assigned connection id so a later backpressure shed can be
457
+ // tied back to this specific connection in logs and Sentry.
458
+ instrumentation.connectionId = sub.connectionId;
464
459
  } catch (err) {
465
460
  if (err instanceof RangeError) {
466
461
  throw new ServiceUnavailableError("Too many concurrent connections");
@@ -479,16 +474,22 @@ export function handleSubscribeAssistantEvents(
479
474
  return;
480
475
  }
481
476
 
482
- // Reconnect replay: when the caller passed lastSeenSeq and the
483
- // subscription is scoped to a single conversation, deliver any
484
- // buffered events the client missed before the first heartbeat.
477
+ // Reconnect replay: when the caller passed lastSeenSeq, deliver
478
+ // any buffered events the client missed before the first
479
+ // heartbeat. `seq` is a single global per-assistant counter, so
480
+ // one cursor resumes the stream regardless of how many
481
+ // conversations are multiplexed on an unfiltered connection.
482
+ // Replay re-applies the subscriber's targeting filter; a
483
+ // conversation-scoped subscription additionally scopes replay to
484
+ // its own conversation (other conversations are never delivered
485
+ // live on that connection, so replaying them would be wrong).
485
486
  //
486
487
  // If the cursor is older than the ring's oldest entry,
487
488
  // `getReplayWindow` returns `null`. We do not surface that to
488
489
  // the client over the wire -- the connection just goes live.
489
490
  // The client detects the gap from the seq jump on its first
490
491
  // live event and refetches via the existing messages API.
491
- if (lastSeenSeq != null && filter.conversationId) {
492
+ if (lastSeenSeq != null) {
492
493
  const replaySubscriber: ReplaySubscriber =
493
494
  clientId && interfaceId
494
495
  ? {
@@ -501,20 +502,13 @@ export function handleSubscribeAssistantEvents(
501
502
  }
502
503
  : { type: "process" };
503
504
  const window = getReplayWindow(
504
- filter.conversationId,
505
505
  lastSeenSeq,
506
506
  replaySubscriber,
507
+ filter.conversationId,
507
508
  );
508
509
  if (window !== null) {
509
510
  for (const replayed of window) {
510
- const frame =
511
- replayed.conversationId != null && replayed.seq != null
512
- ? {
513
- ...replayed,
514
- clientSeq: nextClientSeqFor(replayed.conversationId),
515
- }
516
- : replayed;
517
- controller.enqueue(encoder.encode(formatSseFrame(frame)));
511
+ controller.enqueue(encoder.encode(formatSseFrame(replayed)));
518
512
  instrumentation.eventsDelivered += 1;
519
513
  if (replayed.seq != null && replayed.seq > highWaterReplaySeq) {
520
514
  highWaterReplaySeq = replayed.seq;
@@ -621,7 +615,7 @@ export const ROUTES: RouteDefinition[] = [
621
615
  {
622
616
  name: "lastSeenSeq",
623
617
  description:
624
- "Optional reconnect cursor: the highest per-conversation event seq the client has already applied. When set together with a conversation scope, the daemon replays any buffered events with seq > lastSeenSeq before going live. If the cursor is older than the ring buffer's oldest entry the connection simply goes live; the client is expected to detect the gap from the next event's seq and refetch via the messages API. Must be a non-negative integer.",
618
+ "Optional reconnect cursor: the highest global event seq the client has already applied. `seq` is a single per-assistant counter shared across all conversations, so one cursor resumes the stream regardless of how many conversations are multiplexed on the connection. When set, the daemon replays any buffered events with seq > lastSeenSeq (re-applying the subscriber's targeting/scope filter) before going live. If the cursor is older than the ring buffer's oldest entry the connection simply goes live; the client is expected to detect the gap from the next event's seq and refetch via the messages API. Must be a non-negative integer.",
625
619
  },
626
620
  ],
627
621
  responseHeaders: {
@@ -35,7 +35,14 @@ async function gatewayFetch(
35
35
 
36
36
  // ── Schemas ─────────────────────────────────────────────────────────────
37
37
 
38
- const LEVEL_NAMES = ["trace", "debug", "info", "warn", "error", "fatal"] as const;
38
+ const LEVEL_NAMES = [
39
+ "trace",
40
+ "debug",
41
+ "info",
42
+ "warn",
43
+ "error",
44
+ "fatal",
45
+ ] as const;
39
46
 
40
47
  const GatewayLogsTailParams = z
41
48
  .object({
@@ -45,9 +52,18 @@ const GatewayLogsTailParams = z
45
52
  })
46
53
  .strict();
47
54
 
55
+ const GatewayLogsTailResponseSchema = z.object({
56
+ lines: z.array(z.record(z.string(), z.unknown())),
57
+ truncated: z.boolean(),
58
+ });
59
+ type GatewayLogsTailResponse = z.infer<typeof GatewayLogsTailResponseSchema>;
60
+
48
61
  // ── Handlers ────────────────────────────────────────────────────────────
49
62
 
50
- async function handleGatewayLogsTail({ queryParams = {}, body = {} }: RouteHandlerArgs) {
63
+ async function handleGatewayLogsTail({
64
+ queryParams = {},
65
+ body = {},
66
+ }: RouteHandlerArgs): Promise<GatewayLogsTailResponse> {
51
67
  // HTTP GET delivers filters via queryParams; CLI IPC puts them in body.
52
68
  const source = Object.keys(queryParams).length > 0 ? queryParams : body;
53
69
  const p = GatewayLogsTailParams.parse(source);
@@ -56,7 +72,9 @@ async function handleGatewayLogsTail({ queryParams = {}, body = {} }: RouteHandl
56
72
  if (p.level !== undefined) qs.set("level", p.level);
57
73
  if (p.module !== undefined) qs.set("module", p.module);
58
74
  const query = qs.toString();
59
- return gatewayFetch(`/v1/logs/tail${query ? `?${query}` : ""}`);
75
+ return gatewayFetch(
76
+ `/v1/logs/tail${query ? `?${query}` : ""}`,
77
+ ) as Promise<GatewayLogsTailResponse>;
60
78
  }
61
79
 
62
80
  // ── Route definitions ───────────────────────────────────────────────────
@@ -75,8 +93,12 @@ export const ROUTES: RouteDefinition[] = [
75
93
  description:
76
94
  "Return the last N structured log entries from the gateway log files.",
77
95
  tags: ["gateway-logs"],
96
+ responseBody: GatewayLogsTailResponseSchema,
78
97
  queryParams: [
79
- { name: "n", description: "Number of lines to return (1–1000, default: 10)" },
98
+ {
99
+ name: "n",
100
+ description: "Number of lines to return (1–1000, default: 10)",
101
+ },
80
102
  { name: "level", description: "Minimum pino level name (default: info)" },
81
103
  { name: "module", description: "Filter to exact pino module name" },
82
104
  ],
@@ -18,6 +18,8 @@ import {
18
18
  } from "../../config/loader.js";
19
19
  import { listHeartbeatRuns } from "../../heartbeat/heartbeat-run-store.js";
20
20
  import { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
21
+ import { getConversation } from "../../memory/conversation-crud.js";
22
+ import { getUsageCostForConversationWindow } from "../../memory/llm-usage-store.js";
21
23
  import { readTextFileSync } from "../../util/fs.js";
22
24
  import { getLogger } from "../../util/logger.js";
23
25
  import { getWorkspacePromptPath } from "../../util/platform.js";
@@ -38,19 +40,34 @@ function handleListRuns(queryParams: Record<string, string>) {
38
40
  : 20;
39
41
 
40
42
  const runs = listHeartbeatRuns(limit);
43
+ const now = Date.now();
41
44
  return {
42
- runs: runs.map((r) => ({
43
- id: r.id,
44
- scheduledFor: r.scheduledFor,
45
- startedAt: r.startedAt,
46
- finishedAt: r.finishedAt,
47
- durationMs: r.durationMs,
48
- status: r.status,
49
- skipReason: r.skipReason,
50
- error: r.error,
51
- conversationId: r.conversationId,
52
- createdAt: r.createdAt,
53
- })),
45
+ runs: runs.map((r) => {
46
+ const conversation = r.conversationId
47
+ ? getConversation(r.conversationId)
48
+ : null;
49
+ return {
50
+ id: r.id,
51
+ scheduledFor: r.scheduledFor,
52
+ startedAt: r.startedAt,
53
+ finishedAt: r.finishedAt,
54
+ durationMs: r.durationMs,
55
+ status: r.status,
56
+ skipReason: r.skipReason,
57
+ error: r.error,
58
+ conversationId: r.conversationId,
59
+ conversationExists: conversation != null,
60
+ conversationArchivedAt: conversation?.archivedAt ?? null,
61
+ estimatedCostUsd: r.conversationId
62
+ ? getUsageCostForConversationWindow({
63
+ conversationId: r.conversationId,
64
+ from: r.startedAt ?? r.scheduledFor,
65
+ to: r.finishedAt ?? now,
66
+ })
67
+ : 0,
68
+ createdAt: r.createdAt,
69
+ };
70
+ }),
54
71
  };
55
72
  }
56
73
 
@@ -116,6 +133,9 @@ export const ROUTES: RouteDefinition[] = [
116
133
  skipReason: z.string().nullable(),
117
134
  error: z.string().nullable(),
118
135
  conversationId: z.string().nullable(),
136
+ conversationExists: z.boolean(),
137
+ conversationArchivedAt: z.number().nullable(),
138
+ estimatedCostUsd: z.number(),
119
139
  createdAt: z.number(),
120
140
  }),
121
141
  )
@@ -3,38 +3,33 @@
3
3
  *
4
4
  * Greetings are sourced from (in priority order):
5
5
  * 1. A `## Greetings` section in SOUL.md (user-defined bullet list)
6
- * 2. A cached greetings array (populated by the BTW side-chain LLM call)
7
- * 3. Fallback: the assistant name from IDENTITY.md
6
+ * 2. A cached greetings array (populated by the empty-state greeting callsite)
7
+ * 3. A generic fallback when generation is unavailable
8
8
  *
9
- * Cache uses TTL + content-hash invalidation: when IDENTITY.md, SOUL.md, or
10
- * the guardian persona change, the cache is busted.
9
+ * Cache invalidation is intentionally TTL-only. User-authored SOUL.md
10
+ * greetings are read before cache, so manual overrides still take priority
11
+ * without coupling cache validity to prompt-file edits.
11
12
  *
12
13
  * Storage uses the existing `memory_checkpoints` table (simple key-value store).
13
14
  */
14
15
 
15
- import { createHash } from "node:crypto";
16
16
  import { existsSync, readFileSync } from "node:fs";
17
17
 
18
18
  import {
19
19
  getMemoryCheckpoint,
20
20
  setMemoryCheckpoint,
21
21
  } from "../../memory/checkpoints.js";
22
- import { resolveGuardianPersona } from "../../prompts/persona-resolver.js";
23
22
  import { getWorkspacePromptPath } from "../../util/platform.js";
24
23
 
25
24
  // ---------------------------------------------------------------------------
26
25
  // Constants
27
26
  // ---------------------------------------------------------------------------
28
27
 
29
- const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
28
+ const CACHE_TTL_MS = 2 * 24 * 60 * 60 * 1000; // 2 days
30
29
 
31
30
  const CHECKPOINT_KEY_GREETINGS = "identity:intro:greetings";
32
- const CHECKPOINT_KEY_HASH = "identity:intro:content_hash";
33
31
  const CHECKPOINT_KEY_TIMESTAMP = "identity:intro:cached_at";
34
32
 
35
- /** Workspace files whose content influences the identity intro. */
36
- const IDENTITY_FILES = ["IDENTITY.md", "SOUL.md"] as const;
37
-
38
33
  // ---------------------------------------------------------------------------
39
34
  // Helpers
40
35
  // ---------------------------------------------------------------------------
@@ -125,14 +120,6 @@ export function readWorkspaceGreetings(): string[] | null {
125
120
  return parseGreetingsSection(soulContent);
126
121
  }
127
122
 
128
- /** Compute a SHA-256 hex hash of the concatenated identity file contents. */
129
- export function computeIdentityContentHash(): string {
130
- const staticFiles = IDENTITY_FILES.map(readWorkspaceFile).join("\n---\n");
131
- const guardianPersona = resolveGuardianPersona() ?? "";
132
- const combined = staticFiles + "\n---\n" + guardianPersona;
133
- return createHash("sha256").update(combined).digest("hex");
134
- }
135
-
136
123
  // ---------------------------------------------------------------------------
137
124
  // Public API
138
125
  // ---------------------------------------------------------------------------
@@ -142,27 +129,22 @@ export interface CachedIntro {
142
129
  }
143
130
 
144
131
  /**
145
- * Retrieve the cached greetings array if it exists, is within the TTL window,
146
- * and the identity files have not changed since it was generated.
132
+ * Retrieve the cached greetings array if it exists and is within the TTL
133
+ * window.
147
134
  *
148
- * Returns `null` when the cache is missing, expired, or invalidated.
135
+ * Returns `null` when the cache is missing or expired.
149
136
  */
150
137
  export function getCachedIntro(): CachedIntro | null {
151
138
  try {
152
139
  const raw = getMemoryCheckpoint(CHECKPOINT_KEY_GREETINGS);
153
- const hash = getMemoryCheckpoint(CHECKPOINT_KEY_HASH);
154
140
  const timestampStr = getMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP);
155
141
 
156
- if (!raw || !hash || !timestampStr) return null;
142
+ if (!raw || !timestampStr) return null;
157
143
 
158
144
  // TTL check
159
145
  const cachedAt = Number(timestampStr);
160
146
  if (isNaN(cachedAt) || Date.now() - cachedAt > CACHE_TTL_MS) return null;
161
147
 
162
- // Content-hash check — bust cache when identity files change
163
- const currentHash = computeIdentityContentHash();
164
- if (currentHash !== hash) return null;
165
-
166
148
  // Parse stored value — handles both JSON array and legacy single string
167
149
  let greetings: string[];
168
150
  try {
@@ -178,16 +160,11 @@ export function getCachedIntro(): CachedIntro | null {
178
160
  }
179
161
  }
180
162
 
181
- /**
182
- * Store a greetings array in the cache along with the current content hash
183
- * and timestamp.
184
- */
163
+ /** Store a greetings array in the cache along with the current timestamp. */
185
164
  export function setCachedIntro(greetings: string[]): void {
186
165
  try {
187
- const hash = computeIdentityContentHash();
188
166
  const now = String(Date.now());
189
167
  setMemoryCheckpoint(CHECKPOINT_KEY_GREETINGS, JSON.stringify(greetings));
190
- setMemoryCheckpoint(CHECKPOINT_KEY_HASH, hash);
191
168
  setMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP, now);
192
169
  } catch {
193
170
  // Cache write failure is non-fatal — next request will regenerate.