@vellumai/assistant 0.8.1 → 0.8.2

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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -107,18 +107,20 @@ mock.module("@qdrant/js-client-rest", () => ({
107
107
  QdrantClient: MockQdrantClient,
108
108
  }));
109
109
 
110
+ const embedWithBackendMock = mock(async (_config, texts: string[]) => ({
111
+ provider: "local",
112
+ model: "test-model",
113
+ vectors: texts.map(() => [0.1, 0.2, 0.3]) as number[][],
114
+ }));
115
+ const generateSparseEmbeddingMock = mock((_text: string) => ({
116
+ indices: [1, 2, 3],
117
+ values: [0.5, 0.5, 0.5] as number[],
118
+ }));
110
119
  const realEmbeddingBackend = await import("../../embedding-backend.js");
111
120
  mock.module("../../embedding-backend.js", () => ({
112
121
  ...realEmbeddingBackend,
113
- embedWithBackend: async () => ({
114
- provider: "local",
115
- model: "test-model",
116
- vectors: [[0.1, 0.2, 0.3]] as number[][],
117
- }),
118
- generateSparseEmbedding: () => ({
119
- indices: [1, 2, 3],
120
- values: [0.5, 0.5, 0.5] as number[],
121
- }),
122
+ embedWithBackend: embedWithBackendMock,
123
+ generateSparseEmbedding: generateSparseEmbeddingMock,
122
124
  }));
123
125
 
124
126
  const realQdrantClient = await import("../../qdrant-client.js");
@@ -293,6 +295,8 @@ beforeEach(() => {
293
295
  qdrantState.queryResponses.sparse.length = 0;
294
296
  loadContextMemoryMock.mockClear();
295
297
  retrieveForTurnMock.mockClear();
298
+ embedWithBackendMock.mockClear();
299
+ generateSparseEmbeddingMock.mockClear();
296
300
  _resetMemoryV2QdrantForTests();
297
301
  });
298
302
 
@@ -390,6 +394,59 @@ describe("ConversationGraphMemory.prepareMemory — v2 routing (per-turn path)",
390
394
  expect(firstBlock.text).toContain("# memory/concepts/alice-vscode.md");
391
395
  });
392
396
 
397
+ test("per-turn dense embedding is computed from combined assistant+user text", async () => {
398
+ // Short referential follow-ups ("do that one") carry no semantic signal
399
+ // on their own — the dense PKB query embedding must mirror v1's
400
+ // `retrieveForTurn` and combine the prior assistant turn so hint search
401
+ // still resolves what "that one" refers to. The sparse vector matches
402
+ // v1 by using the user message alone so lexical signal isn't diluted.
403
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
404
+
405
+ const memory = makeMemory();
406
+ const config = makeConfig(true);
407
+ const assistantText =
408
+ "Alice prefers VS Code as her editor — she finds the extension ecosystem unmatched.";
409
+ const userText = "do that one";
410
+ const messages: Message[] = [
411
+ {
412
+ role: "user",
413
+ content: [
414
+ { type: "text" as const, text: "what editors did we cover?" },
415
+ ],
416
+ },
417
+ {
418
+ role: "assistant",
419
+ content: [{ type: "text" as const, text: assistantText }],
420
+ },
421
+ { role: "user", content: [{ type: "text" as const, text: userText }] },
422
+ ];
423
+
424
+ await memory.prepareMemory(
425
+ messages,
426
+ config,
427
+ new AbortController().signal,
428
+ noopEvent,
429
+ );
430
+
431
+ // v1's `retrieveForTurn` joins assistantLast + userLast with "\n\n" and
432
+ // embeds the combined string as the dense query vector. Assert the v2
433
+ // path makes the exact same embed call somewhere during this turn.
434
+ const expectedCombined = `${assistantText}\n\n${userText}`;
435
+ const matchingCall = embedWithBackendMock.mock.calls.find((call) => {
436
+ const texts = call[1] as string[];
437
+ return texts.length === 1 && texts[0] === expectedCombined;
438
+ });
439
+ expect(matchingCall).toBeDefined();
440
+
441
+ // Sparse embedding for the per-turn query uses userLast only.
442
+ expect(generateSparseEmbeddingMock.mock.calls).toContainEqual([userText]);
443
+ expect(
444
+ generateSparseEmbeddingMock.mock.calls.some((call) =>
445
+ (call[0] as string).includes(assistantText),
446
+ ),
447
+ ).toBe(false);
448
+ });
449
+
393
450
  test("config on with empty Qdrant hits → no v2 block, v1 fallback skipped", async () => {
394
451
  // No `stageTurn` call — every channel returns `{ points: [] }` so the
395
452
  // candidate set is empty and `injectMemoryV2Block` returns block=null.
@@ -438,8 +438,10 @@ export class ConversationGraphMemory {
438
438
  // turns. v1's `loadContextMemory` produced these as a side effect of
439
439
  // hybrid retrieval; the v2 path skips that retrieval, so embed
440
440
  // explicitly here.
441
+ const userQueryText = rawUserText ?? userQuery ?? "";
441
442
  const userQueryEmbed = await this.computeQueryVectors(
442
- rawUserText ?? userQuery ?? "",
443
+ userQueryText,
444
+ userQueryText,
443
445
  config,
444
446
  signal,
445
447
  );
@@ -598,8 +600,16 @@ export class ConversationGraphMemory {
598
600
  if (v2.routed) {
599
601
  // Surface a per-turn query embedding so PKB hint search still runs
600
602
  // on v2 turns. v1's `retrieveForTurn` produced these as a side effect;
601
- // the v2 path skips that retrieval, so embed explicitly here.
603
+ // the v2 path skips that retrieval, so embed explicitly here. Match
604
+ // v1's split: dense embeds the combined assistant+user text (short
605
+ // referential follow-ups like "do that one" need the assistant turn
606
+ // for semantic grounding), while sparse uses the user message alone
607
+ // to keep lexical signal focused on what the user actually said.
608
+ const denseQueryText = [assistantLast, userLast]
609
+ .filter((m) => m.length > 0)
610
+ .join("\n\n");
602
611
  const perTurnEmbed = await this.computeQueryVectors(
612
+ denseQueryText,
603
613
  userLast,
604
614
  config,
605
615
  signal,
@@ -697,24 +707,30 @@ export class ConversationGraphMemory {
697
707
  * static fallback rather than blocking the turn.
698
708
  */
699
709
  private async computeQueryVectors(
700
- text: string,
710
+ denseText: string,
711
+ sparseText: string,
701
712
  config: AssistantConfig,
702
713
  signal: AbortSignal,
703
714
  ): Promise<{ dense?: number[]; sparse?: QdrantSparseVector }> {
704
- const trimmed = text.trim();
705
- if (trimmed.length === 0) return {};
715
+ const trimmedDense = denseText.trim();
716
+ const trimmedSparse = sparseText.trim();
706
717
  let dense: number[] | undefined;
707
- try {
708
- const result = await embedWithRetry(config, [trimmed], { signal });
709
- dense = result.vectors[0];
710
- } catch (err) {
711
- log.warn(
712
- { err: err instanceof Error ? err.message : String(err) },
713
- "Failed to embed query for PKB hints on v2 path",
714
- );
718
+ if (trimmedDense.length > 0) {
719
+ try {
720
+ const result = await embedWithRetry(config, [trimmedDense], { signal });
721
+ dense = result.vectors[0];
722
+ } catch (err) {
723
+ log.warn(
724
+ { err: err instanceof Error ? err.message : String(err) },
725
+ "Failed to embed query for PKB hints on v2 path",
726
+ );
727
+ }
728
+ }
729
+ let sparse: QdrantSparseVector | undefined;
730
+ if (trimmedSparse.length > 0) {
731
+ const sparseRaw = generateSparseEmbedding(trimmedSparse);
732
+ sparse = sparseRaw.indices.length > 0 ? sparseRaw : undefined;
715
733
  }
716
- const sparseRaw = generateSparseEmbedding(trimmed);
717
- const sparse = sparseRaw.indices.length > 0 ? sparseRaw : undefined;
718
734
  return { dense, sparse };
719
735
  }
720
736
 
@@ -20,7 +20,7 @@ const RECALL_DEPTHS = ["fast", "standard", "deep"] as const;
20
20
  export const graphRecallDefinition: ToolDefinition = {
21
21
  name: "recall",
22
22
  description:
23
- 'Search local information the moment you feel uncertain. Use recall for memory, past conversations, and workspace files — before you guess, before you ask, before you hedge. Auto-injection is incomplete by design; it surfaces patterns, not the specifics you need to answer well. If you catch yourself reaching for "I think", "I believe", "if I remember", "didn\'t we", "last time" — that\'s the signal. Recall. If the user references someone, a place, a decision, a document, or prior work you should be able to find locally — recall. Call it multiple times per conversation if the turn warrants it. Be specific in your query for best results.',
23
+ 'Search local information the moment you feel uncertain. Use recall for memory, past conversations, and workspace files — before you guess, before you ask, before you hedge. Auto-injection is incomplete by design; it surfaces patterns, not the specifics you need to answer well. If you catch yourself reaching for "I think", "I believe", "if I remember", "didn\'t we", "last time" — that\'s the signal. Recall. If a turn references someone, a place, a decision, a document, or prior work you should be able to find locally — recall. Call it multiple times per conversation if the turn warrants it. Be specific in your query for best results.',
24
24
  input_schema: {
25
25
  type: "object",
26
26
  properties: {
@@ -72,7 +72,7 @@ const REMEMBER_DESCRIPTION_DEFAULT =
72
72
  * something feels worth marking, not because the volume is required.
73
73
  */
74
74
  const REMEMBER_DESCRIPTION_RELAXED =
75
- "Remember anything concrete the user shared: corrections, plans, decisions, felt moments, names, dates, commitments, preferences. Corrections are the highest priority — call `remember` the same turn the correction lands. You don't have to call this on every turn; a retrospective pass reviews the conversation after each message-count / time interval and saves what you didn't capture. Use judgment: pause and remember when something feels worth marking, not because the volume is required.";
75
+ "Remember anything concrete shared in conversation: corrections, plans, decisions, felt moments, names, dates, commitments, preferences. Corrections are the highest priority — call `remember` the same turn the correction lands. You don't have to call this on every turn; a retrospective pass reviews the conversation after each message-count / time interval and saves what you didn't capture. Use judgment: pause and remember when something feels worth marking, not because the volume is required.";
76
76
 
77
77
  /**
78
78
  * Return the description that should appear in the `remember` tool
@@ -115,7 +115,7 @@ export const graphRememberDefinition: ToolDefinition = {
115
115
  finish_turn: {
116
116
  type: "boolean",
117
117
  description:
118
- "When you have nothing else to say and want to hand control back to the user you MUST set this to true. When true, your turn ends after this tool call. It's critical that you do this in order to avoid unnecessary LLM calls.",
118
+ "When you have nothing else to say and want to yield the turn you MUST set this to true. When true, your turn ends after this tool call. It's critical that you do this in order to avoid unnecessary LLM calls.",
119
119
  },
120
120
  },
121
121
  required: ["content"],
@@ -169,32 +169,30 @@ export async function indexMessageNow(
169
169
  const batchSize = config.extraction.batchSize ?? 10;
170
170
  const idleTimeoutMs = config.extraction.idleTimeoutMs ?? 300_000;
171
171
 
172
+ // Reading config here is best-effort: when it fails we treat v2 as
173
+ // inactive (failing-open to v1) so a config error never silently
174
+ // drops the extraction or summarization paths.
175
+ let triggerConfig: ReturnType<typeof getConfig> | null = null;
176
+ try {
177
+ triggerConfig = getConfig();
178
+ } catch (err) {
179
+ log.debug(
180
+ { err, conversationId: input.conversationId },
181
+ "Skipping feature-gated extraction triggers: failed to load config",
182
+ );
183
+ }
184
+
185
+ const v2Config =
186
+ triggerConfig != null && triggerConfig.memory.v2.enabled
187
+ ? triggerConfig
188
+ : null;
189
+
172
190
  // Recursion guard: skip graph extraction + auto-analysis enqueues
173
191
  // when the source conversation is itself an auto-analysis
174
192
  // conversation. The analysis agent writes memory directly via tools,
175
193
  // so extracting from its reflective musings would double-count and
176
194
  // analyzing its own output would loop indefinitely.
177
- // Summaries still run — they feed the graph retrieval pipeline and
178
- // are not recursion-prone.
179
195
  if (!isAutoAnalysisSource) {
180
- // Reading config here is best-effort: when it fails we treat v2 as
181
- // inactive (failing-open to v1) so a config error never silently
182
- // drops both extraction paths.
183
- let triggerConfig: ReturnType<typeof getConfig> | null = null;
184
- try {
185
- triggerConfig = getConfig();
186
- } catch (err) {
187
- log.debug(
188
- { err, conversationId: input.conversationId },
189
- "Skipping feature-gated extraction triggers: failed to load config",
190
- );
191
- }
192
-
193
- const v2Config =
194
- triggerConfig != null && triggerConfig.memory.v2.enabled
195
- ? triggerConfig
196
- : null;
197
-
198
196
  // ── Graph extraction (v1) ───────────────────────────────────────
199
197
  // Suppressed when v2 is active — v2 reads memory from buffer.md
200
198
  // and concept pages, so the v1 graph would be stale data nobody
@@ -302,15 +300,22 @@ export async function indexMessageNow(
302
300
  }
303
301
  }
304
302
 
305
- // ── Conversation summarization (independent of extraction) ────────
306
- // Summaries feed the graph retrieval pipeline via fetchRecentSummaries().
307
- // Debounced on the same idle timeoutno threshold trigger needed since
308
- // summaries compress the whole conversation, not incremental batches.
309
- upsertDebouncedJob(
310
- "build_conversation_summary",
311
- { conversationId: input.conversationId },
312
- Date.now() + idleTimeoutMs,
313
- );
303
+ // ── Conversation summarization (v1) ───────────────────────────────
304
+ // Summaries feed the v1 graph retrieval pipeline (fetchRecentSummaries,
305
+ // semantic search). Suppressed when v2 is active v2 readers (concept
306
+ // pages, activation pipeline) do not consume `memorySummaries`, so the
307
+ // summarization LLM call would produce rows nothing reads. Stale rows
308
+ // from before v2 was enabled are short-circuited at dispatch in
309
+ // jobs-worker.ts. Debounced on the same idle timeout — no threshold
310
+ // trigger needed since summaries compress the whole conversation, not
311
+ // incremental batches.
312
+ if (v2Config == null) {
313
+ upsertDebouncedJob(
314
+ "build_conversation_summary",
315
+ { conversationId: input.conversationId },
316
+ Date.now() + idleTimeoutMs,
317
+ );
318
+ }
314
319
  }
315
320
 
316
321
  if (skippedShortSegments > 0) {
@@ -151,6 +151,8 @@ type MemoryJob = ReturnType<MemoryJobMod["claimMemoryJobs"]>[number];
151
151
  const { embedConceptPageJob, enqueueEmbedConceptPageJob } =
152
152
  await import("../embed-concept-page.js");
153
153
  const { writePage } = await import("../../v2/page-store.js");
154
+ const { _resetQdrantBreaker, isQdrantBreakerOpen, withQdrantBreaker } =
155
+ await import("../../qdrant-circuit-breaker.js");
154
156
 
155
157
  // Use a tiny vectorSize so the cache-dim check matches our stub vector.
156
158
  const TEST_CONFIG = {
@@ -186,6 +188,7 @@ beforeEach(() => {
186
188
  embedWithBackendCalls.length = 0;
187
189
  upsertCalls.length = 0;
188
190
  deleteCalls.length = 0;
191
+ _resetQdrantBreaker();
189
192
  });
190
193
 
191
194
  afterEach(() => {
@@ -436,6 +439,76 @@ describe("embedConceptPageJob — defensive", () => {
436
439
  });
437
440
  });
438
441
 
442
+ describe("embedConceptPageJob — Qdrant breaker integration", () => {
443
+ test("half-open probe success closes the breaker so embed catch-up unthrottles", async () => {
444
+ // Trip the breaker by recording 5 consecutive Qdrant failures. Without
445
+ // this fix, `embed_concept_page` bypassed the breaker entirely — winning
446
+ // the half-open probe slot did not transition state back to closed and
447
+ // the embed lane stayed throttled at one job per tick indefinitely.
448
+ for (let i = 0; i < 5; i++) {
449
+ try {
450
+ await withQdrantBreaker(async () => {
451
+ throw new Error("simulated qdrant failure");
452
+ });
453
+ } catch {
454
+ // expected
455
+ }
456
+ }
457
+ expect(isQdrantBreakerOpen()).toBe(true);
458
+
459
+ // Advance time past the 30s cooldown so the next breaker call transitions
460
+ // open → half-open and allows the probe through.
461
+ const originalNow = Date.now;
462
+ Date.now = () => originalNow() + 60_000;
463
+ try {
464
+ await writePage(tmpWorkspace, {
465
+ slug: "probe-success",
466
+ frontmatter: { edges: [], ref_files: [], ref_urls: [] },
467
+ body: "Probe body.\n",
468
+ });
469
+
470
+ await embedConceptPageJob(
471
+ makeJob({ slug: "probe-success" }),
472
+ TEST_CONFIG,
473
+ );
474
+ } finally {
475
+ Date.now = originalNow;
476
+ }
477
+
478
+ expect(upsertCalls).toHaveLength(1);
479
+ // Probe succeeded → breaker should now be closed (not open, not
480
+ // half-open), restoring full embed-lane concurrency.
481
+ expect(isQdrantBreakerOpen()).toBe(false);
482
+ });
483
+
484
+ test("half-open probe success on the delete path also closes the breaker", async () => {
485
+ // Same flow as above but exercising the missing-page branch — both v2
486
+ // Qdrant calls (`upsert` and `delete`) must close the breaker on success.
487
+ for (let i = 0; i < 5; i++) {
488
+ try {
489
+ await withQdrantBreaker(async () => {
490
+ throw new Error("simulated qdrant failure");
491
+ });
492
+ } catch {
493
+ // expected
494
+ }
495
+ }
496
+ expect(isQdrantBreakerOpen()).toBe(true);
497
+
498
+ const originalNow = Date.now;
499
+ Date.now = () => originalNow() + 60_000;
500
+ try {
501
+ // No `writePage` — the handler takes the delete branch.
502
+ await embedConceptPageJob(makeJob({ slug: "missing-slug" }), TEST_CONFIG);
503
+ } finally {
504
+ Date.now = originalNow;
505
+ }
506
+
507
+ expect(deleteCalls).toEqual(["missing-slug"]);
508
+ expect(isQdrantBreakerOpen()).toBe(false);
509
+ });
510
+ });
511
+
439
512
  describe("enqueueEmbedConceptPageJob", () => {
440
513
  test("enqueues a pending embed_concept_page job with the slug payload", () => {
441
514
  const id = enqueueEmbedConceptPageJob({ slug: "alice-prefers-vs-code" });
@@ -34,6 +34,7 @@ import {
34
34
  import { embeddingInputContentHash } from "../embedding-types.js";
35
35
  import { asString, blobToVector, vectorToBlob } from "../job-utils.js";
36
36
  import { enqueueMemoryJob, type MemoryJob } from "../jobs-store.js";
37
+ import { withQdrantBreaker } from "../qdrant-circuit-breaker.js";
37
38
  import { memoryEmbeddings } from "../schema.js";
38
39
  import { readPage } from "../v2/page-store.js";
39
40
  import {
@@ -81,7 +82,10 @@ export async function embedConceptPageJob(
81
82
  if (!page) {
82
83
  // Page was deleted out from under us — clean up the prior embedding so
83
84
  // retrieval no longer surfaces a slug whose disk-side prose is gone.
84
- await deleteConceptPageEmbedding(slug);
85
+ // Route through the Qdrant breaker so success on the half-open probe
86
+ // slot transitions the breaker back to closed and unthrottles embed
87
+ // catch-up.
88
+ await withQdrantBreaker(() => deleteConceptPageEmbedding(slug));
85
89
  return;
86
90
  }
87
91
 
@@ -280,16 +284,21 @@ export async function embedConceptPageJob(
280
284
  ? await applyCorrectionIfCalibrated(summaryDense, writeProvider, writeModel)
281
285
  : undefined;
282
286
 
283
- await upsertConceptPageEmbedding({
284
- slug,
285
- dense: correctedDense,
286
- sparse,
287
- summary:
288
- correctedSummaryDense && summarySparse
289
- ? { dense: correctedSummaryDense, sparse: summarySparse }
290
- : undefined,
291
- updatedAt: now,
292
- });
287
+ // Route through the Qdrant breaker so a probe-slot success transitions the
288
+ // breaker back to closed; without this wrapper the embed lane stays
289
+ // throttled at one job per tick indefinitely after a half-open success.
290
+ await withQdrantBreaker(() =>
291
+ upsertConceptPageEmbedding({
292
+ slug,
293
+ dense: correctedDense,
294
+ sparse,
295
+ summary:
296
+ correctedSummaryDense && summarySparse
297
+ ? { dense: correctedSummaryDense, sparse: summarySparse }
298
+ : undefined,
299
+ updatedAt: now,
300
+ }),
301
+ );
293
302
  }
294
303
 
295
304
  /** SQLite cache row shape returned by `readEmbeddingCache`. */
@@ -95,7 +95,6 @@ const V1_QDRANT_JOB_TYPES = new Set<MemoryJobType>([
95
95
  "embed_attachment",
96
96
  "embed_graph_node",
97
97
  "embed_pkb_file",
98
- "graph_trigger_embed",
99
98
  "rebuild_index",
100
99
  "delete_qdrant_vectors",
101
100
  ]);
@@ -525,6 +524,12 @@ async function processJob(
525
524
  pruneOldTraceEventsJob(job, config);
526
525
  return;
527
526
  case "build_conversation_summary":
527
+ // Stale rows enqueued before v2 was enabled must not consume the
528
+ // `conversationSummarization` LLM budget — v2 readers do not consume
529
+ // `memorySummaries`, mirroring the `graph_extract` gate below.
530
+ if (config.memory.v2.enabled) {
531
+ return;
532
+ }
528
533
  await buildConversationSummaryJob(job, config);
529
534
  return;
530
535
  case "backfill":
@@ -172,14 +172,21 @@ export class ClickHouseLlmRequestLogSource implements LlmRequestLogSource {
172
172
  private async selectByMessageIds(ids: string[]): Promise<LogRow[]> {
173
173
  if (ids.length === 0) return [];
174
174
  const aid = await this.assistantId();
175
- // ClickHouse Array(String) URL encoding is fiddly; the message IDs are
176
- // server-generated UUIDs (or safe internal strings), so inline the
177
- // literal after escaping single quotes. No SQL-injection surfacethe
178
- // values originate from our own SQLite messages table.
179
- const idLiteral =
180
- "[" +
181
- ids.map((id) => `'${id.replace(/'/g, "''")}'`).join(",") +
182
- "]";
175
+ // Bind each id as its own {id_N:String} placeholder. The IDs ultimately
176
+ // come from a caller-supplied path parameter `getAssistantMessageIdsInTurn`
177
+ // passes the input straight through when the message lookup misses so
178
+ // inline literal building (even with quote-doubling) is unsafe: ClickHouse
179
+ // honors `\'` as an escaped quote inside string literals, letting a
180
+ // backslash-suffixed id break out of the IN clause and bypass the
181
+ // `assistant_id` scope filter. Type-bound parameters carry value, not
182
+ // syntax, regardless of content.
183
+ const params: Record<string, string> = { assistant_id: aid };
184
+ const placeholders: string[] = [];
185
+ for (let i = 0; i < ids.length; i++) {
186
+ const key = `id_${i}`;
187
+ params[key] = ids[i]!;
188
+ placeholders.push(`{${key}:String}`);
189
+ }
183
190
  const sql = `SELECT
184
191
  id,
185
192
  conversation_id,
@@ -190,11 +197,11 @@ export class ClickHouseLlmRequestLogSource implements LlmRequestLogSource {
190
197
  toUnixTimestamp64Milli(created_at) AS created_at
191
198
  FROM ${this.tableRef()}
192
199
  WHERE assistant_id = {assistant_id:String}
193
- AND message_id IN ${idLiteral}
200
+ AND message_id IN (${placeholders.join(",")})
194
201
  ORDER BY created_at ASC, id ASC
195
202
  LIMIT 1 BY id
196
203
  FORMAT JSONEachRow`;
197
- const rows = await this.exec(sql, { assistant_id: aid });
204
+ const rows = await this.exec(sql, params);
198
205
  return rows.map((r) => this.toLogRow(r));
199
206
  }
200
207
 
@@ -12,16 +12,13 @@
12
12
  * only sees rows the mirror cron has flushed). See
13
13
  * `llm-request-log-source-clickhouse.ts`.
14
14
  *
15
- * The active source is cached at module level and invalidated on config
16
- * change (see `daemon/config-watcher.ts`) so a config edit takes effect
17
- * without restarting the daemon.
15
+ * Implementations are cheap to instantiate, so there's no module-level
16
+ * cache each call resolves config fresh and constructs a new instance.
17
+ * Config edits take effect on the next request without an invalidation hook.
18
18
  */
19
19
  import { getConfig } from "../config/loader.js";
20
- import { getLogger } from "../util/logger.js";
21
20
  import type { LogRow } from "./llm-request-log-store.js";
22
21
 
23
- const log = getLogger("llm-request-log-source");
24
-
25
22
  export interface LlmRequestLogSource {
26
23
  /** Fetch a single log row by its primary key. Returns null if not found. */
27
24
  getRequestLogById(logId: string): Promise<LogRow | null>;
@@ -36,62 +33,32 @@ export interface LlmRequestLogSource {
36
33
  getRequestLogsByMessageId(messageId: string): Promise<LogRow[]>;
37
34
  }
38
35
 
39
- let cached: LlmRequestLogSource | null = null;
40
- let cachedKind: "local" | "clickhouse" | null = null;
41
-
42
36
  /**
43
- * Return the currently configured LLM request log source.
37
+ * Return the configured LLM request log source.
44
38
  *
45
- * The result is cached for the lifetime of the process. Callers should
46
- * never hang on to the instance across config reloads always re-resolve
47
- * through this function. The factory is async because BOTH implementations
48
- * are loaded via dynamic `import()` on first use. This is deliberate: it
49
- * keeps the static module graph for `llm-request-log-source.ts` (and for
50
- * everything that transitively imports it, including `config-watcher`)
51
- * free of `llm-request-log-store → conversation-crud → indexer → embedding-backend`,
39
+ * The factory is async because both implementations are loaded via
40
+ * dynamic `import()` on first use. This is deliberate: it keeps the
41
+ * static module graph for `llm-request-log-source.ts` (and for
42
+ * everything that transitively imports it) free of
43
+ * `llm-request-log-store conversation-crud → indexer → embedding-backend`,
52
44
  * which would otherwise force test files that stub `embedding-backend.js`
53
- * to also stub every export `indexer.ts` reaches for. Callers MUST `await`
54
- * the source methods because the active source may swap to one with real
55
- * I/O at any time.
45
+ * to also stub every export `indexer.ts` reaches for.
46
+ *
47
+ * Callers must `await` both the factory and the source methods.
56
48
  */
57
49
  export async function getLlmRequestLogSource(): Promise<LlmRequestLogSource> {
58
- if (cached) return cached;
59
-
60
50
  const config = getConfig();
61
- const kind = config.llmRequestLogs?.readSource ?? "local";
51
+ const cfg = config.llmRequestLogs ?? { readSource: "local" as const };
62
52
 
63
- if (kind === "clickhouse") {
53
+ if (cfg.readSource === "clickhouse") {
64
54
  const { ClickHouseLlmRequestLogSource } = await import(
65
55
  "./llm-request-log-source-clickhouse.js"
66
56
  );
67
- cached = new ClickHouseLlmRequestLogSource(config.llmRequestLogs.clickhouse);
68
- cachedKind = "clickhouse";
69
- log.info(
70
- { table: config.llmRequestLogs.clickhouse.table },
71
- "Using ClickHouse for LLM request log reads",
72
- );
73
- } else {
74
- const { LocalLlmRequestLogSource } = await import(
75
- "./llm-request-log-source-local.js"
76
- );
77
- cached = new LocalLlmRequestLogSource();
78
- cachedKind = "local";
57
+ return new ClickHouseLlmRequestLogSource(cfg.clickhouse);
79
58
  }
80
59
 
81
- return cached;
82
- }
83
-
84
- /**
85
- * Drop the cached source so the next `getLlmRequestLogSource()` call
86
- * resolves fresh from config. Called on workspace config reload.
87
- */
88
- export function invalidateLlmRequestLogSourceCache(): void {
89
- if (cached !== null) {
90
- log.debug(
91
- { previousKind: cachedKind },
92
- "Invalidating LLM request log source cache",
93
- );
94
- }
95
- cached = null;
96
- cachedKind = null;
60
+ const { LocalLlmRequestLogSource } = await import(
61
+ "./llm-request-log-source-local.js"
62
+ );
63
+ return new LocalLlmRequestLogSource();
97
64
  }