@vellumai/assistant 0.8.7 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (387) hide show
  1. package/Dockerfile +20 -4
  2. package/docker-entrypoint.sh +4 -2
  3. package/docker-init-apt-root.sh +3 -1
  4. package/docker-kata-apt-env.sh +3 -1
  5. package/docker-kata-runtime-family.sh +12 -0
  6. package/docs/architecture/memory.md +1 -1
  7. package/docs/plugins.md +75 -79
  8. package/examples/plugins/echo/README.md +6 -12
  9. package/examples/plugins/echo/register.ts +0 -41
  10. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  11. package/openapi.yaml +3381 -348
  12. package/package.json +1 -1
  13. package/scripts/generate-openapi.ts +68 -41
  14. package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
  15. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  16. package/src/__tests__/agent-loop.test.ts +37 -87
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  18. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  19. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  20. package/src/__tests__/anthropic-provider.test.ts +95 -2
  21. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  22. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  23. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  24. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  25. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  26. package/src/__tests__/btw-routes.test.ts +62 -3
  27. package/src/__tests__/build-persisted-content.test.ts +184 -0
  28. package/src/__tests__/catalog-files.test.ts +1 -1
  29. package/src/__tests__/clawhub-files.test.ts +1 -1
  30. package/src/__tests__/compaction-pipeline.test.ts +1 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  32. package/src/__tests__/config-watcher.test.ts +1 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  34. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
  35. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
  37. package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
  38. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  39. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  40. package/src/__tests__/conversation-pairing.test.ts +4 -31
  41. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
  43. package/src/__tests__/conversation-queue.test.ts +2 -0
  44. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  45. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
  48. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  49. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  50. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  51. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  52. package/src/__tests__/conversation-title-service.test.ts +135 -2
  53. package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
  54. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  55. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  56. package/src/__tests__/dm-persistence.test.ts +5 -1
  57. package/src/__tests__/empty-response-hook.test.ts +304 -0
  58. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  59. package/src/__tests__/gemini-image-service.test.ts +13 -0
  60. package/src/__tests__/helpers/mock-provider.ts +110 -0
  61. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  62. package/src/__tests__/history-repair-hook.test.ts +1 -0
  63. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  64. package/src/__tests__/identity-routes.test.ts +248 -7
  65. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  66. package/src/__tests__/injector-background-turn.test.ts +2 -8
  67. package/src/__tests__/injector-chain.test.ts +106 -270
  68. package/src/__tests__/injector-disk-pressure.test.ts +3 -12
  69. package/src/__tests__/injector-document-comments.test.ts +2 -2
  70. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  71. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  72. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  73. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  74. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  75. package/src/__tests__/llm-usage-store.test.ts +223 -1
  76. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  77. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  78. package/src/__tests__/native-web-search.test.ts +191 -0
  79. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  80. package/src/__tests__/openai-image-service.test.ts +17 -0
  81. package/src/__tests__/openai-provider.test.ts +31 -1
  82. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  83. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  84. package/src/__tests__/pipeline-runner.test.ts +29 -39
  85. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  86. package/src/__tests__/plugin-bootstrap.test.ts +13 -28
  87. package/src/__tests__/plugin-registry.test.ts +0 -27
  88. package/src/__tests__/plugin-types.test.ts +2 -125
  89. package/src/__tests__/process-message-display-content.test.ts +6 -2
  90. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  91. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  92. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  93. package/src/__tests__/schedule-routes.test.ts +603 -2
  94. package/src/__tests__/schedule-store.test.ts +41 -0
  95. package/src/__tests__/schedule-tools.test.ts +35 -0
  96. package/src/__tests__/server-history-render.test.ts +314 -1
  97. package/src/__tests__/skillssh-files.test.ts +1 -1
  98. package/src/__tests__/system-prompt.test.ts +20 -0
  99. package/src/__tests__/task-scheduler.test.ts +162 -1
  100. package/src/__tests__/terminal-tools.test.ts +6 -1
  101. package/src/__tests__/title-generate-hook.test.ts +319 -0
  102. package/src/__tests__/tool-error-hook.test.ts +278 -0
  103. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  104. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  106. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  107. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  108. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  109. package/src/__tests__/usage-routes.test.ts +285 -1
  110. package/src/__tests__/user-plugin-loader.test.ts +2 -2
  111. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  112. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  113. package/src/agent/loop.ts +346 -442
  114. package/src/api/events/assistant-thinking-delta.ts +33 -0
  115. package/src/api/events/tool-output-chunk.ts +45 -0
  116. package/src/api/events/tool-use-preview-start.ts +32 -0
  117. package/src/api/events/trace-event.ts +69 -0
  118. package/src/api/index.ts +48 -13
  119. package/src/api/responses/conversation-message.ts +368 -0
  120. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  121. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  122. package/src/cli/commands/notifications.ts +112 -60
  123. package/src/config/assistant-feature-flags.ts +22 -11
  124. package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
  125. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  126. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  127. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  128. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  129. package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
  130. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  131. package/src/config/feature-flag-cache.ts +3 -3
  132. package/src/config/feature-flag-registry.json +35 -3
  133. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  134. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  135. package/src/config/schemas/llm.ts +1 -0
  136. package/src/config/schemas/memory-v2.ts +8 -0
  137. package/src/config/schemas/memory-v3.ts +8 -0
  138. package/src/config/schemas/platform.ts +8 -0
  139. package/src/config/seed-inference-profiles.ts +2 -2
  140. package/src/config/skills.ts +13 -0
  141. package/src/context/compactor.ts +1 -1
  142. package/src/context/strip-injections.ts +122 -0
  143. package/src/context/token-estimator.ts +23 -0
  144. package/src/context/tool-result-truncation.ts +0 -23
  145. package/src/context/window-manager.ts +3 -6
  146. package/src/credential-execution/executable-discovery.ts +16 -0
  147. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  148. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  149. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  150. package/src/daemon/assistant-attachments.ts +1 -1
  151. package/src/daemon/config-watcher.ts +2 -2
  152. package/src/daemon/context-overflow-reducer.ts +0 -1
  153. package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
  154. package/src/daemon/conversation-agent-loop.ts +281 -760
  155. package/src/daemon/conversation-history.ts +5 -4
  156. package/src/daemon/conversation-lifecycle.ts +3 -4
  157. package/src/daemon/conversation-messaging.ts +7 -6
  158. package/src/daemon/conversation-process.ts +11 -16
  159. package/src/daemon/conversation-runtime-assembly.ts +130 -347
  160. package/src/daemon/conversation-slash.ts +6 -25
  161. package/src/daemon/conversation-surfaces.ts +222 -4
  162. package/src/daemon/conversation-tool-setup.ts +2 -29
  163. package/src/daemon/conversation.ts +32 -14
  164. package/src/daemon/external-plugins-bootstrap.ts +9 -10
  165. package/src/daemon/handlers/config-a2a.ts +51 -36
  166. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  167. package/src/daemon/handlers/config-telegram.ts +16 -2
  168. package/src/daemon/handlers/shared.ts +156 -84
  169. package/src/daemon/handlers/skills.ts +39 -10
  170. package/src/daemon/lifecycle.ts +4 -0
  171. package/src/daemon/message-types/apps.ts +1 -29
  172. package/src/daemon/message-types/messages.ts +9 -57
  173. package/src/daemon/message-types/skills.ts +2 -0
  174. package/src/daemon/message-types/surfaces.ts +136 -3
  175. package/src/daemon/now-scratchpad.ts +21 -0
  176. package/src/daemon/orphan-reaper.test.ts +210 -0
  177. package/src/daemon/orphan-reaper.ts +240 -0
  178. package/src/daemon/persist-unsendable-image.ts +117 -0
  179. package/src/daemon/process-message.ts +1 -3
  180. package/src/daemon/trace-emitter.ts +6 -4
  181. package/src/daemon/trust-context.ts +19 -0
  182. package/src/daemon/wake-target-adapter.ts +3 -1
  183. package/src/home/home-greeting-cache.ts +24 -1
  184. package/src/ipc/gateway-client.test.ts +2 -2
  185. package/src/ipc/gateway-client.ts +3 -3
  186. package/src/media/gemini-image-service.ts +15 -0
  187. package/src/media/openai-image-service.ts +14 -0
  188. package/src/media/types.ts +34 -0
  189. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  190. package/src/memory/auth-fallback-events-store.ts +94 -0
  191. package/src/memory/conversation-title-service.ts +65 -41
  192. package/src/memory/db-init.ts +4 -0
  193. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  194. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  195. package/src/memory/jobs-store.ts +33 -0
  196. package/src/memory/jobs-worker.ts +31 -4
  197. package/src/memory/llm-usage-store.ts +224 -50
  198. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  199. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  200. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  201. package/src/memory/migrations/index.ts +2 -0
  202. package/src/memory/pkb/autoinject.ts +61 -0
  203. package/src/memory/pkb/context.ts +50 -0
  204. package/src/memory/pkb/types.ts +14 -0
  205. package/src/memory/schedule-attribution-sql.ts +104 -0
  206. package/src/memory/schema/infrastructure.ts +16 -0
  207. package/src/memory/usage-grouped-buckets.ts +6 -1
  208. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
  209. package/src/memory/v2/consolidation-job.ts +1 -1
  210. package/src/memory/v3/__tests__/health.test.ts +16 -0
  211. package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
  212. package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
  213. package/src/memory/v3/__tests__/router.test.ts +101 -29
  214. package/src/memory/v3/__tests__/selector.test.ts +93 -27
  215. package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
  216. package/src/memory/v3/health.ts +0 -0
  217. package/src/memory/v3/llm-retry.ts +32 -0
  218. package/src/memory/v3/orchestrate.ts +26 -14
  219. package/src/memory/v3/provider-blocks.ts +15 -5
  220. package/src/memory/v3/router.ts +48 -42
  221. package/src/memory/v3/selector.ts +57 -42
  222. package/src/memory/v3/shadow-plugin.ts +47 -15
  223. package/src/memory/v3/types.ts +8 -0
  224. package/src/notifications/conversation-pairing.ts +8 -15
  225. package/src/notifications/decision-engine.ts +6 -3
  226. package/src/notifications/home-feed-side-effect.ts +12 -1
  227. package/src/permissions/prompter.ts +4 -0
  228. package/src/plugin-api/constants.ts +4 -0
  229. package/src/plugin-api/index.ts +8 -1
  230. package/src/plugin-api/types.ts +151 -1
  231. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  232. package/src/plugins/defaults/empty-response/register.ts +8 -13
  233. package/src/plugins/defaults/index.ts +1 -15
  234. package/src/plugins/defaults/injectors/register.ts +243 -74
  235. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
  236. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  237. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  238. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  239. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  240. package/src/plugins/defaults/title-generate/package.json +1 -1
  241. package/src/plugins/defaults/title-generate/register.ts +18 -18
  242. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  243. package/src/plugins/defaults/tool-error/package.json +1 -1
  244. package/src/plugins/defaults/tool-error/register.ts +9 -21
  245. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  246. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  247. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  248. package/src/plugins/pipeline.ts +6 -18
  249. package/src/plugins/registry.ts +8 -25
  250. package/src/plugins/types.ts +43 -474
  251. package/src/proactive-artifact/aux-message-injector.ts +3 -3
  252. package/src/proactive-artifact/job.test.ts +7 -12
  253. package/src/prompts/__tests__/system-prompt.test.ts +36 -0
  254. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
  255. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  256. package/src/prompts/templates/system-sections.ts +15 -0
  257. package/src/providers/anthropic/client.ts +37 -29
  258. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  259. package/src/providers/openai/chat-completions-provider.ts +44 -0
  260. package/src/providers/openrouter/client.ts +1 -0
  261. package/src/providers/placeholder-sentinels.ts +35 -0
  262. package/src/runtime/__tests__/agent-wake.test.ts +5 -1
  263. package/src/runtime/agent-wake.ts +2 -2
  264. package/src/runtime/assistant-event-hub.ts +36 -6
  265. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  266. package/src/runtime/http-router.ts +16 -21
  267. package/src/runtime/http-types.ts +16 -70
  268. package/src/runtime/pending-interactions.ts +1 -0
  269. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  270. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  271. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  272. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  273. package/src/runtime/routes/app-management-routes.ts +6 -117
  274. package/src/runtime/routes/app-routes.ts +13 -15
  275. package/src/runtime/routes/attachment-routes.ts +26 -15
  276. package/src/runtime/routes/avatar-routes.ts +26 -0
  277. package/src/runtime/routes/btw-routes.ts +29 -23
  278. package/src/runtime/routes/consolidation-routes.ts +120 -20
  279. package/src/runtime/routes/conversation-query-routes.ts +2 -0
  280. package/src/runtime/routes/conversation-routes.ts +358 -184
  281. package/src/runtime/routes/documents-routes.ts +4 -0
  282. package/src/runtime/routes/domain-routes.ts +51 -37
  283. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  284. package/src/runtime/routes/events-routes.ts +28 -34
  285. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  286. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  287. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  288. package/src/runtime/routes/identity-routes.ts +208 -17
  289. package/src/runtime/routes/image-generation-routes.ts +40 -2
  290. package/src/runtime/routes/index.ts +2 -0
  291. package/src/runtime/routes/integrations/a2a.ts +12 -10
  292. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  293. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  294. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  295. package/src/runtime/routes/integrations/telegram.ts +6 -0
  296. package/src/runtime/routes/integrations/twilio.ts +42 -0
  297. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  298. package/src/runtime/routes/log-export-routes.ts +8 -0
  299. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  300. package/src/runtime/routes/memory-v3-routes.ts +50 -28
  301. package/src/runtime/routes/oauth-apps.ts +66 -12
  302. package/src/runtime/routes/oauth-providers.ts +44 -5
  303. package/src/runtime/routes/platform-routes.ts +81 -5
  304. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  305. package/src/runtime/routes/playground/force-compact.ts +1 -1
  306. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  307. package/src/runtime/routes/schedule-routes.ts +152 -42
  308. package/src/runtime/routes/secret-routes.ts +14 -2
  309. package/src/runtime/routes/skills-routes.ts +43 -14
  310. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  311. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  312. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  313. package/src/runtime/routes/tts-routes.ts +35 -0
  314. package/src/runtime/routes/types.ts +66 -8
  315. package/src/runtime/routes/usage-routes.ts +47 -39
  316. package/src/runtime/routes/webhook-routes.ts +41 -2
  317. package/src/runtime/routes/workspace-routes.ts +4 -0
  318. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  319. package/src/runtime/services/analyze-conversation.ts +2 -2
  320. package/src/schedule/schedule-store.ts +20 -1
  321. package/src/schedule/schedule-usage-store.ts +83 -0
  322. package/src/schedule/scheduler.ts +12 -5
  323. package/src/skills/catalog-files.ts +2 -2
  324. package/src/skills/catalog-install.ts +3 -0
  325. package/src/skills/categories-cache.ts +118 -0
  326. package/src/skills/clawhub-files.ts +1 -2
  327. package/src/skills/skillssh-files.ts +1 -2
  328. package/src/telemetry/types.ts +29 -1
  329. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  330. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  331. package/src/tools/executor.ts +1 -53
  332. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  333. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  334. package/src/tools/network/web-search-error.test.ts +248 -0
  335. package/src/tools/network/web-search-error.ts +267 -0
  336. package/src/tools/network/web-search.ts +207 -48
  337. package/src/tools/schedule/create.ts +2 -0
  338. package/src/tools/terminal/safe-env.ts +10 -1
  339. package/src/tools/ui-surface/definitions.ts +9 -1
  340. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  341. package/src/tts/provider-catalog.ts +76 -1
  342. package/src/util/mutex.ts +47 -0
  343. package/src/workspace/git-service.ts +1 -42
  344. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  345. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  346. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
  347. package/src/workspace/migrations/registry.ts +6 -0
  348. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  349. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  350. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  351. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  352. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  353. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  354. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  355. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  356. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  357. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  358. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  359. package/src/gallery/default-gallery.ts +0 -1359
  360. package/src/gallery/gallery-manifest.ts +0 -28
  361. package/src/home/feature-gate.ts +0 -22
  362. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  363. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  364. package/src/plugins/defaults/injectors/package.json +0 -15
  365. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  366. package/src/plugins/defaults/llm-call/package.json +0 -15
  367. package/src/plugins/defaults/llm-call/register.ts +0 -45
  368. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  369. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  370. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  371. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  372. package/src/plugins/defaults/persistence/package.json +0 -15
  373. package/src/plugins/defaults/persistence/register.ts +0 -38
  374. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  375. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  376. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  377. package/src/plugins/defaults/token-estimate/package.json +0 -15
  378. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  379. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  380. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  381. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  382. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  383. package/src/plugins/defaults/tool-execute/package.json +0 -15
  384. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  385. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  386. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  387. package/src/skills/category-inference.ts +0 -111
@@ -0,0 +1,50 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+
4
+ import { getWorkspaceDir } from "../../util/platform.js";
5
+ import { stripCommentLines } from "../../util/strip-comment-lines.js";
6
+ import { getPkbAutoInjectList } from "./autoinject.js";
7
+
8
+ /** Max buffer.md lines injected into prompts — keeps context bounded even when filing is off. */
9
+ const MAX_BUFFER_LINES = 50;
10
+
11
+ /**
12
+ * Read the always-loaded PKB files and append a nudge encouraging the
13
+ * assistant to proactively read topic files and use `remember` aggressively.
14
+ *
15
+ * Which files are loaded is determined by `pkb/_autoinject.md` (one filename
16
+ * per line). Falls back to the built-in defaults when that file is absent.
17
+ *
18
+ * Returns the concatenated content ready for injection, or `null` if all
19
+ * files are missing or empty.
20
+ */
21
+ export function readPkbContext(): string | null {
22
+ const pkbDir = join(getWorkspaceDir(), "pkb");
23
+ if (!existsSync(pkbDir)) return null;
24
+
25
+ const filesToInject = getPkbAutoInjectList(pkbDir);
26
+
27
+ const parts: string[] = [];
28
+ for (const file of filesToInject) {
29
+ // Path traversal guard: reject entries that escape the pkb directory
30
+ const filePath = resolve(pkbDir, file);
31
+ if (!filePath.startsWith(pkbDir + "/")) continue;
32
+
33
+ if (!existsSync(filePath)) continue;
34
+ try {
35
+ let content = stripCommentLines(readFileSync(filePath, "utf-8")).trim();
36
+ if (file === "buffer.md" && content.length > 0) {
37
+ // Cap buffer entries to prevent unbounded growth when filing is disabled
38
+ const lines = content.split("\n");
39
+ if (lines.length > MAX_BUFFER_LINES) {
40
+ content = lines.slice(-MAX_BUFFER_LINES).join("\n");
41
+ }
42
+ }
43
+ if (content.length > 0) parts.push(content);
44
+ } catch {
45
+ // Skip unreadable files
46
+ }
47
+ }
48
+
49
+ return parts.length > 0 ? parts.join("\n\n") : null;
50
+ }
@@ -5,8 +5,22 @@
5
5
  * index writer) land in later PRs.
6
6
  */
7
7
 
8
+ import { join } from "node:path";
9
+
10
+ import { getWorkspaceDir } from "../../util/platform.js";
11
+
8
12
  export const PKB_TARGET_TYPE = "pkb_file" as const;
9
13
 
14
+ /**
15
+ * Absolute path to the workspace's PKB directory (`<workspace>/pkb`). PKB is
16
+ * a workspace-shared resource with a single on-disk location, so this is the
17
+ * canonical way to resolve its root rather than re-joining the workspace dir
18
+ * at each call site.
19
+ */
20
+ export function getPkbRoot(): string {
21
+ return join(getWorkspaceDir(), "pkb");
22
+ }
23
+
10
24
  /**
11
25
  * Sentinel `memory_scope_id` under which ALL PKB points are indexed and
12
26
  * searched. PKB files are a workspace-shared resource: one copy on disk is
@@ -0,0 +1,104 @@
1
+ export interface ScheduleAttributionFilter {
2
+ scheduleId?: string;
3
+ }
4
+
5
+ export type ScheduleAttributionSqlParam = string | number;
6
+
7
+ export interface ScheduleAttributionSqlFragment {
8
+ sql: string;
9
+ params: ScheduleAttributionSqlParam[];
10
+ }
11
+
12
+ export function normalizeScheduleAttributionFilter(
13
+ filter?: ScheduleAttributionFilter,
14
+ ): ScheduleAttributionFilter {
15
+ const scheduleId = filter?.scheduleId?.trim();
16
+ return scheduleId ? { scheduleId } : {};
17
+ }
18
+
19
+ function usageColumn(column: string, eventAlias: string): string {
20
+ return `${eventAlias}.${column}`;
21
+ }
22
+
23
+ function buildScheduleRunWindowPredicate({
24
+ eventAlias,
25
+ runAlias,
26
+ filter,
27
+ }: {
28
+ eventAlias: string;
29
+ runAlias: string;
30
+ filter?: ScheduleAttributionFilter;
31
+ }): string {
32
+ const normalized = normalizeScheduleAttributionFilter(filter);
33
+ const scheduleClause = normalized.scheduleId
34
+ ? `${runAlias}.job_id = ? AND `
35
+ : "";
36
+ return `${scheduleClause}${runAlias}.conversation_id = ${usageColumn(
37
+ "conversation_id",
38
+ eventAlias,
39
+ )}
40
+ AND ${usageColumn("created_at", eventAlias)} >= ${runAlias}.started_at
41
+ AND ${usageColumn("created_at", eventAlias)} <= COALESCE(${runAlias}.finished_at, ?)`;
42
+ }
43
+
44
+ function buildScheduleRunWindowParams(
45
+ filter: ScheduleAttributionFilter | undefined,
46
+ now: number,
47
+ ): ScheduleAttributionSqlParam[] {
48
+ const normalized = normalizeScheduleAttributionFilter(filter);
49
+ return normalized.scheduleId ? [normalized.scheduleId, now] : [now];
50
+ }
51
+
52
+ export function buildScheduleRunWindowExists({
53
+ eventAlias,
54
+ filter,
55
+ now,
56
+ runAlias = "schedule_filter_runs",
57
+ }: {
58
+ eventAlias: string;
59
+ filter?: ScheduleAttributionFilter;
60
+ now: number;
61
+ runAlias?: string;
62
+ }): ScheduleAttributionSqlFragment {
63
+ return {
64
+ sql: `EXISTS (
65
+ SELECT 1
66
+ FROM cron_runs ${runAlias}
67
+ WHERE ${buildScheduleRunWindowPredicate({
68
+ eventAlias,
69
+ runAlias,
70
+ filter,
71
+ })}
72
+ )`,
73
+ params: buildScheduleRunWindowParams(filter, now),
74
+ };
75
+ }
76
+
77
+ export function buildScheduleAttributionSubquery({
78
+ eventAlias,
79
+ filter,
80
+ now,
81
+ selectExpression,
82
+ runAlias = "schedule_attr_runs",
83
+ }: {
84
+ eventAlias: string;
85
+ filter?: ScheduleAttributionFilter;
86
+ now: number;
87
+ selectExpression: string;
88
+ runAlias?: string;
89
+ }): ScheduleAttributionSqlFragment {
90
+ return {
91
+ sql: `(
92
+ SELECT ${selectExpression}
93
+ FROM cron_runs ${runAlias}
94
+ WHERE ${buildScheduleRunWindowPredicate({
95
+ eventAlias,
96
+ runAlias,
97
+ filter,
98
+ })}
99
+ ORDER BY ${runAlias}.started_at DESC, ${runAlias}.id DESC
100
+ LIMIT 1
101
+ )`,
102
+ params: buildScheduleRunWindowParams(filter, now),
103
+ };
104
+ }
@@ -22,6 +22,7 @@ export const cronJobs = sqliteTable("cron_jobs", {
22
22
  maxRetries: integer("max_retries").notNull().default(3),
23
23
  retryBackoffMs: integer("retry_backoff_ms").notNull().default(60000),
24
24
  timeoutMs: integer("timeout_ms"), // script-mode execution timeout override (ms); null = use default
25
+ createdFromConversationId: text("created_from_conversation_id"),
25
26
  createdBy: text("created_by").notNull(), // 'agent' | 'user'
26
27
  mode: text("mode").notNull().default("execute"), // 'notify' | 'execute'
27
28
  routingIntent: text("routing_intent").notNull().default("all_channels"), // 'single_channel' | 'multi_channel' | 'all_channels'
@@ -278,6 +279,21 @@ export const onboardingEvents = sqliteTable("onboarding_events", {
278
279
  abVariant: text("ab_variant"),
279
280
  });
280
281
 
282
+ // Aggregated legacy-loopback auth-fallback counts forwarded by the gateway.
283
+ // One row per (guard, path, failure_kind) per flush window; `count` is how many
284
+ // requests fell back to the loopback exemption in that window. Flushed to the
285
+ // platform telemetry endpoint by the usage telemetry reporter.
286
+ export const authFallbackEvents = sqliteTable("auth_fallback_events", {
287
+ id: text("id").primaryKey(),
288
+ createdAt: integer("created_at").notNull(),
289
+ guard: text("guard").notNull(), // 'edge' | 'edge-scoped' | 'edge-guardian'
290
+ path: text("path").notNull(),
291
+ failureKind: text("failure_kind").notNull(),
292
+ count: integer("count").notNull(),
293
+ windowStart: integer("window_start").notNull(),
294
+ windowEnd: integer("window_end").notNull(),
295
+ });
296
+
281
297
  export const traceEvents = sqliteTable(
282
298
  "trace_events",
283
299
  {
@@ -27,6 +27,7 @@ export interface UsageGroupedSeriesBucket extends UsageDayBucket {
27
27
 
28
28
  export interface UsageGroupedBucketRow extends UsageEventBucketRow {
29
29
  group_key: string | null;
30
+ group_label?: string | null;
30
31
  }
31
32
 
32
33
  const VALUE_GROUP_PREFIX = "value:";
@@ -42,6 +43,9 @@ export function displayUsageGroup(
42
43
  if (groupBy === "inference_profile") {
43
44
  return groupKey === null ? "Default / Unset" : groupKey;
44
45
  }
46
+ if (groupBy === "schedule") {
47
+ return groupKey ?? "Other";
48
+ }
45
49
  return groupKey ?? "Other";
46
50
  }
47
51
 
@@ -102,7 +106,8 @@ export function bucketGroupedUsageEvents(
102
106
  let group = groupedBucket.groups[seriesKey];
103
107
  if (!group) {
104
108
  group = {
105
- group: displayUsageGroup(options.groupBy, row.group_key),
109
+ group:
110
+ row.group_label ?? displayUsageGroup(options.groupBy, row.group_key),
106
111
  groupKey: row.group_key,
107
112
  totalInputTokens: 0,
108
113
  totalOutputTokens: 0,
@@ -279,7 +279,7 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
279
279
  expect(runnerLastArgs).not.toBeNull();
280
280
  expect(runnerLastArgs?.jobName).toBe("memory.consolidate");
281
281
  expect(runnerLastArgs?.source).toBe("memory_v2_consolidation");
282
- expect(runnerLastArgs?.callSite).toBe("mainAgent");
282
+ expect(runnerLastArgs?.callSite).toBe("memoryV2Consolidation");
283
283
  expect(runnerLastArgs?.origin).toBe("memory_consolidation");
284
284
  // The whole point of this PR: opt out of activity.failed notifications
285
285
  // because consolidation runs on tight intervals and transient failures
@@ -197,7 +197,7 @@ export async function memoryV2ConsolidateJob(
197
197
  source: MEMORY_V2_CONSOLIDATION_SOURCE,
198
198
  prompt,
199
199
  trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
200
- callSite: "mainAgent",
200
+ callSite: "memoryV2Consolidation",
201
201
  timeoutMs: CONSOLIDATION_TIMEOUT_MS,
202
202
  origin: "memory_consolidation",
203
203
  suppressFailureNotifications: true,
@@ -42,6 +42,22 @@ describe("computeV3Health", () => {
42
42
  expect(report.unassigned).toEqual(["page-b", "page-c"]);
43
43
  });
44
44
 
45
+ test("excludes synthetic capability slugs from unassigned / novel clusters", () => {
46
+ const t = tree({ "domain-a/topic-x": ["page-a"] });
47
+ const report = computeV3Health({
48
+ tree: t,
49
+ allSlugs: ["page-a", "page-b", "cli-commands/example", "skills/example"],
50
+ });
51
+ // Capability slugs are handled by the always-on capabilities leaf (injected
52
+ // into the live lane tree, absent here), not the persisted tree — so they must
53
+ // not be reported as unassigned or grouped into novel clusters. page-b is the
54
+ // one real unassigned concept page.
55
+ expect(report.unassigned).toEqual(["page-b"]);
56
+ expect(report.novelClusters).toEqual([
57
+ { prefix: "page-b", slugs: ["page-b"], count: 1 },
58
+ ]);
59
+ });
60
+
45
61
  test("flags dangling page refs pointing at missing leaves", () => {
46
62
  const t = tree({ "domain-a/topic-x": ["page-a"] });
47
63
  const report = computeV3Health({
@@ -217,6 +217,43 @@ describe("orchestrate — fixture sequence (carry-forward)", () => {
217
217
  expect(t2.currentSelections.map((s) => s.slug)).not.toContain("page-a");
218
218
  expect(t2.finalInjection).toContain("page-a");
219
219
  });
220
+
221
+ test("carry-forward survives a turn whose selections fill the cap", async () => {
222
+ const tree = makeTree();
223
+ // Cap of 1: under a naive record-then-cap order this turn's own selection
224
+ // would evict the carried page before injection. Snapshotting the carry
225
+ // BEFORE recording this turn keeps the earlier page in the injection.
226
+ const workingSet = new WorkingSet(1);
227
+ const needle = fakeNeedle([]);
228
+ const stub = (selectIds: number[]): Provider => ({
229
+ name: "stub",
230
+ sendMessage: async (_messages, options) =>
231
+ options?.tools?.[0]?.name === "open_leaves"
232
+ ? toolUseResponse("open_leaves", { ids: [1] })
233
+ : toolUseResponse("select_pages", { ids: selectIds, pinned_ids: [] }),
234
+ });
235
+
236
+ providerStub = stub([1]); // turn 1 → page-a
237
+ await orchestrate(makeTurn(1, "page a"), {
238
+ tree,
239
+ core: new Set(),
240
+ needle,
241
+ workingSet,
242
+ pageSummary: summaryOf,
243
+ });
244
+
245
+ providerStub = stub([2]); // turn 2 → page-b, never re-selects page-a
246
+ const t2 = await orchestrate(makeTurn(2, "page b"), {
247
+ tree,
248
+ core: new Set(),
249
+ needle,
250
+ workingSet,
251
+ pageSummary: summaryOf,
252
+ });
253
+
254
+ expect(t2.currentSelections.map((s) => s.slug)).toEqual(["page-b"]);
255
+ expect(t2.finalInjection).toContain("page-a"); // carried despite the cap
256
+ });
220
257
  });
221
258
 
222
259
  // ---------------------------------------------------------------------------
@@ -298,14 +335,16 @@ describe("orchestrate — edge cases", () => {
298
335
  expect(result.finalInjection).toEqual(["page-a", "page-b"]);
299
336
  });
300
337
 
301
- test("L1 routing fallback (omitted ids all leaves) still works", async () => {
338
+ test("omitted L1 ids opens only the deterministic lanes, not the whole tree", async () => {
302
339
  const tree = makeTree();
303
- // Omitted ids → routeL1 opens ALL leaves; select everything per leaf.
340
+ // L1 omits ids → routeL1 opens NO routed leaves; only the needle/core lanes
341
+ // drive the open set, so the whole tree is never fanned out (topic-y, which
342
+ // nothing routes or needles to, stays closed).
304
343
  providerStub = {
305
344
  name: "stub",
306
345
  sendMessage: async (_messages, options) => {
307
346
  if (options?.tools?.[0]?.name === "open_leaves") {
308
- return toolUseResponse("open_leaves", {}); // omitted ids
347
+ return toolUseResponse("open_leaves", {}); // omitted ids → []
309
348
  }
310
349
  return toolUseResponse("select_pages", {}); // omitted → all members
311
350
  },
@@ -313,15 +352,12 @@ describe("orchestrate — edge cases", () => {
313
352
  const result = await orchestrate(makeTurn(1, "x"), {
314
353
  tree,
315
354
  core: new Set(),
316
- needle: fakeNeedle([]),
355
+ needle: fakeNeedle(["page-a"]), // needle opens domain-a/topic-x only
317
356
  workingSet: new WorkingSet(),
318
357
  pageSummary: summaryOf,
319
358
  });
320
- expect(result.openedLeaves).toEqual([
321
- "domain-a/topic-x",
322
- "domain-a/topic-y",
323
- ]);
324
- expect(result.finalInjection).toEqual(["page-a", "page-b", "page-c"]);
359
+ expect(result.openedLeaves).toEqual(["domain-a/topic-x"]);
360
+ expect(result.finalInjection).toEqual(["page-a", "page-b"]);
325
361
  });
326
362
 
327
363
  test("pinned current-turn selections land in the working set", async () => {
@@ -0,0 +1,13 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { cachedTextBlock } from "../provider-blocks.js";
4
+
5
+ describe("cachedTextBlock", () => {
6
+ test("stamps an ephemeral cache_control with a 1h TTL", () => {
7
+ const block = cachedTextBlock("stable leaf block");
8
+ expect(block).toMatchObject({ type: "text", text: "stable leaf block" });
9
+ expect(
10
+ (block as unknown as { cache_control?: unknown }).cache_control,
11
+ ).toEqual({ type: "ephemeral", ttl: "1h" });
12
+ });
13
+ });
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * Coverage matrix:
5
5
  * - Returned IDs map to the right leaves by 1-based index, in model order.
6
- * - Omitted `ids` → ALL leaves (recall-safe).
6
+ * - Omitted `ids` → no leaves (the router must name leaves explicitly,
7
+ * never open the whole tree).
7
8
  * - Explicit `ids: []` → no leaves (deliberate abstention).
8
9
  * - Out-of-range / duplicate IDs ignored, no throw.
9
- * - No provider / missing tool_use / schema mismatch / throw → ALL leaves.
10
+ * - No provider / missing tool_use / schema mismatch / throw → no leaves
11
+ * (degrade to the deterministic lanes), the last three after a re-prompt
12
+ * retry; a malformed response that recovers on retry returns its IDs.
10
13
  * - The rendered leaf block is byte-identical across two calls with
11
14
  * different queries (the cache invariant).
12
15
  * - The system prompt mentions "register" (locks the routing commitment).
@@ -76,6 +79,45 @@ function toolUseResponse(input: Record<string, unknown>): ProviderResponse {
76
79
  };
77
80
  }
78
81
 
82
+ /** A 200 response that carries no tool_use — the malformed-but-successful case
83
+ * the re-prompt retry exists to recover from. */
84
+ function noToolResponse(): ProviderResponse {
85
+ return {
86
+ model: "stub-model",
87
+ stopReason: "end_turn",
88
+ usage: { inputTokens: 0, outputTokens: 0 },
89
+ content: [{ type: "text", text: "no tool call" }],
90
+ };
91
+ }
92
+
93
+ /** Provider returning a different response per call (the i-th call returns
94
+ * responses[i], or the last entry once exhausted), recording each call so a
95
+ * test can assert how many attempts were made. */
96
+ function makeSequenceProvider(responses: ProviderResponse[]): Provider {
97
+ let i = 0;
98
+ return {
99
+ name: "sequence",
100
+ sendMessage: async (messages, options) => {
101
+ providerCalls.push({ messages, options });
102
+ const response = responses[Math.min(i, responses.length - 1)];
103
+ i += 1;
104
+ return response;
105
+ },
106
+ };
107
+ }
108
+
109
+ /** Provider that records each call and then throws — for the throw-after-retries
110
+ * path (the provider's own RetryProvider has already exhausted its backoff). */
111
+ function makeThrowingProvider(): Provider {
112
+ return {
113
+ name: "throwing",
114
+ sendMessage: async (messages, options) => {
115
+ providerCalls.push({ messages, options });
116
+ throw new Error("boom");
117
+ },
118
+ };
119
+ }
120
+
79
121
  function makeLeaf(path: LeafPath, description: string): LeafNode {
80
122
  return {
81
123
  path,
@@ -101,8 +143,6 @@ function makeTree(): LeafTree {
101
143
  };
102
144
  }
103
145
 
104
- const SORTED_PATHS = ["people/alice", "people/bob", "projects/atlas"];
105
-
106
146
  function makeTurn(currentMessage: string): TurnContext {
107
147
  return {
108
148
  conversationId: "conv-xyz",
@@ -128,10 +168,10 @@ describe("routeL1 — id mapping", () => {
128
168
  expect(result).toEqual(["projects/atlas", "people/alice"]);
129
169
  });
130
170
 
131
- test("omitted ids opens ALL leaves (recall-safe)", async () => {
171
+ test("omitted ids opens no leaves (must name leaves explicitly)", async () => {
132
172
  providerStub = makeProvider(toolUseResponse({}));
133
173
  const result = await routeL1(makeTurn("anything"), makeTree());
134
- expect(result).toEqual(SORTED_PATHS);
174
+ expect(result).toEqual([]);
135
175
  });
136
176
 
137
177
  test("explicit empty ids opens no leaves (abstention)", async () => {
@@ -157,39 +197,43 @@ describe("routeL1 — id mapping", () => {
157
197
  });
158
198
  });
159
199
 
160
- describe("routeL1 — recall-safe fallbacks", () => {
161
- test("no provider → ALL leaves", async () => {
200
+ describe("routeL1 — degradation on failure", () => {
201
+ test("no provider → no leaves, without calling the provider", async () => {
162
202
  providerStub = null;
163
203
  const result = await routeL1(makeTurn("x"), makeTree());
164
- expect(result).toEqual(SORTED_PATHS);
204
+ expect(result).toEqual([]);
205
+ expect(providerCalls).toHaveLength(0);
165
206
  });
166
207
 
167
- test("missing tool_use → ALL leaves", async () => {
168
- providerStub = makeProvider({
169
- model: "stub-model",
170
- stopReason: "end_turn",
171
- usage: { inputTokens: 0, outputTokens: 0 },
172
- content: [{ type: "text", text: "no tool call" }],
173
- });
208
+ test("missing tool_use → no leaves after retrying", async () => {
209
+ providerStub = makeProvider(noToolResponse());
174
210
  const result = await routeL1(makeTurn("x"), makeTree());
175
- expect(result).toEqual(SORTED_PATHS);
211
+ expect(result).toEqual([]);
212
+ expect(providerCalls).toHaveLength(3);
176
213
  });
177
214
 
178
- test("schema mismatch → ALL leaves", async () => {
215
+ test("schema mismatch → no leaves after retrying", async () => {
179
216
  providerStub = makeProvider(toolUseResponse({ ids: "not-an-array" }));
180
217
  const result = await routeL1(makeTurn("x"), makeTree());
181
- expect(result).toEqual(SORTED_PATHS);
218
+ expect(result).toEqual([]);
219
+ expect(providerCalls).toHaveLength(3);
182
220
  });
183
221
 
184
- test("provider throw → ALL leaves", async () => {
185
- providerStub = {
186
- name: "throwing",
187
- sendMessage: async () => {
188
- throw new Error("boom");
189
- },
190
- };
222
+ test("provider throw → no leaves after retrying", async () => {
223
+ providerStub = makeThrowingProvider();
191
224
  const result = await routeL1(makeTurn("x"), makeTree());
192
- expect(result).toEqual(SORTED_PATHS);
225
+ expect(result).toEqual([]);
226
+ expect(providerCalls).toHaveLength(3);
227
+ });
228
+
229
+ test("a malformed response that recovers on retry returns its IDs", async () => {
230
+ providerStub = makeSequenceProvider([
231
+ noToolResponse(),
232
+ toolUseResponse({ ids: [2] }),
233
+ ]);
234
+ const result = await routeL1(makeTurn("bob?"), makeTree());
235
+ expect(result).toEqual(["people/bob"]);
236
+ expect(providerCalls).toHaveLength(2);
193
237
  });
194
238
  });
195
239
 
@@ -213,11 +257,11 @@ describe("routeL1 — request shape", () => {
213
257
  const [blockA, blockB] = providerCalls[0].messages[0].content as Array<{
214
258
  type: string;
215
259
  text: string;
216
- cache_control?: { type: string };
260
+ cache_control?: { type: string; ttl?: string };
217
261
  }>;
218
262
  expect(blockA.type).toBe("text");
219
263
  expect(blockA.text).toContain("<leaves>");
220
- expect(blockA.cache_control).toEqual({ type: "ephemeral" });
264
+ expect(blockA.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
221
265
 
222
266
  expect(blockB.type).toBe("text");
223
267
  expect(blockB.text).toContain("<current_message>alice?</current_message>");
@@ -225,6 +269,34 @@ describe("routeL1 — request shape", () => {
225
269
  expect(blockB.cache_control).toBeUndefined();
226
270
  });
227
271
 
272
+ test("situational context renders in the per-turn block when present", async () => {
273
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
274
+ await routeL1(
275
+ {
276
+ ...makeTurn("x"),
277
+ situationalContext: "Today is Saturday. Alice's anniversary is today.",
278
+ },
279
+ makeTree(),
280
+ );
281
+ const blockB = providerCalls[0].messages[0].content[1] as { text: string };
282
+ expect(blockB.text).toContain(
283
+ "<situation>Today is Saturday. Alice's anniversary is today.</situation>",
284
+ );
285
+ });
286
+
287
+ test("situational context is omitted when the turn has none", async () => {
288
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
289
+ await routeL1(makeTurn("x"), makeTree());
290
+ const blockB = providerCalls[0].messages[0].content[1] as { text: string };
291
+ expect(blockB.text).not.toContain("<situation>");
292
+ });
293
+
294
+ test("system prompt mentions situation (locks the routing commitment)", async () => {
295
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
296
+ await routeL1(makeTurn("x"), makeTree());
297
+ expect(providerCalls[0].options?.systemPrompt).toMatch(/[Ss]ituation/);
298
+ });
299
+
228
300
  test("system prompt mentions register (locks the routing commitment)", async () => {
229
301
  providerStub = makeProvider(toolUseResponse({ ids: [1] }));
230
302
  await routeL1(makeTurn("x"), makeTree());