@vellumai/assistant 0.8.1 → 0.8.3

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 (630) hide show
  1. package/ARCHITECTURE.md +13 -19
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +17 -0
  5. package/docker-init-apt-root.sh +167 -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 +642 -5
  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-loop-exit-reason.test.ts +272 -0
  21. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  23. package/src/__tests__/anthropic-provider.test.ts +45 -0
  24. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  25. package/src/__tests__/app-executors.test.ts +220 -4
  26. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  27. package/src/__tests__/bundled-asset.test.ts +6 -6
  28. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  29. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  30. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  31. package/src/__tests__/clawhub.test.ts +75 -16
  32. package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
  33. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  34. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  35. package/src/__tests__/config-schema.test.ts +21 -0
  36. package/src/__tests__/config-set-route.test.ts +80 -0
  37. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  38. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  39. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  40. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  41. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  42. package/src/__tests__/context-token-estimator.test.ts +31 -65
  43. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  44. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  45. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  46. package/src/__tests__/conversation-agent-loop.test.ts +59 -1
  47. package/src/__tests__/conversation-error.test.ts +42 -3
  48. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  49. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  50. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  51. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  52. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  53. package/src/__tests__/conversation-pairing.test.ts +54 -0
  54. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  55. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  56. package/src/__tests__/conversation-queue.test.ts +4 -1
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
  58. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  59. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  60. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  61. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  64. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  65. package/src/__tests__/date-context.test.ts +45 -0
  66. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  67. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  68. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  69. package/src/__tests__/dm-backfill.test.ts +121 -10
  70. package/src/__tests__/document-tool-security.test.ts +258 -0
  71. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  72. package/src/__tests__/edit-propagation.test.ts +33 -0
  73. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  74. package/src/__tests__/external-plugin-loader.test.ts +151 -55
  75. package/src/__tests__/filing-service.test.ts +140 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  77. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  78. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  79. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  80. package/src/__tests__/heartbeat-service.test.ts +24 -164
  81. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  82. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  83. package/src/__tests__/helpers/wait-for.ts +21 -0
  84. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  85. package/src/__tests__/history-repair.test.ts +73 -0
  86. package/src/__tests__/host-app-control-proxy.test.ts +507 -10
  87. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  88. package/src/__tests__/image-credentials.test.ts +1 -1
  89. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  90. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  91. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  92. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  93. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  94. package/src/__tests__/injector-background-turn.test.ts +153 -0
  95. package/src/__tests__/injector-chain.test.ts +15 -8
  96. package/src/__tests__/install-skill-routing.test.ts +155 -37
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
  98. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  99. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  100. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  101. package/src/__tests__/llm-catalog-parity.test.ts +58 -13
  102. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  103. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  104. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
  105. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  106. package/src/__tests__/llm-resolver.test.ts +255 -2
  107. package/src/__tests__/llm-usage-store.test.ts +114 -0
  108. package/src/__tests__/managed-profile-guard.test.ts +41 -29
  109. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  110. package/src/__tests__/managed-store.test.ts +84 -192
  111. package/src/__tests__/media-generate-image.test.ts +1 -1
  112. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  113. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  114. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  115. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  116. package/src/__tests__/notification-deep-link.test.ts +15 -0
  117. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  118. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  119. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  120. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  121. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  122. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  123. package/src/__tests__/openai-provider.test.ts +242 -3
  124. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  125. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  126. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  127. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  128. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  129. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
  130. package/src/__tests__/platform.test.ts +2 -0
  131. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  132. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  133. package/src/__tests__/plugin-external-api.test.ts +68 -0
  134. package/src/__tests__/plugin-registry.test.ts +0 -77
  135. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  136. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  137. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  138. package/src/__tests__/plugin-types.test.ts +3 -13
  139. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  140. package/src/__tests__/process-message-display-content.test.ts +421 -0
  141. package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  143. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
  144. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  145. package/src/__tests__/schedule-routes.test.ts +50 -3
  146. package/src/__tests__/schedule-store.test.ts +94 -0
  147. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  148. package/src/__tests__/schema-transforms.test.ts +20 -0
  149. package/src/__tests__/search-skills-unified.test.ts +0 -5
  150. package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
  151. package/src/__tests__/server-history-render.test.ts +43 -0
  152. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  153. package/src/__tests__/skill-load-tool.test.ts +27 -89
  154. package/src/__tests__/skill-memory.test.ts +23 -3
  155. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  156. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  157. package/src/__tests__/skills-install-extract.test.ts +49 -38
  158. package/src/__tests__/skills-install-staging.test.ts +159 -0
  159. package/src/__tests__/skills-uninstall.test.ts +9 -41
  160. package/src/__tests__/skills.test.ts +51 -58
  161. package/src/__tests__/slack-channel-config.test.ts +9 -0
  162. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  163. package/src/__tests__/system-prompt.test.ts +670 -63
  164. package/src/__tests__/terminal-tools.test.ts +28 -1
  165. package/src/__tests__/thread-backfill.test.ts +557 -27
  166. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  167. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  168. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  169. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  170. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  171. package/src/__tests__/tool-executor.test.ts +16 -4
  172. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  173. package/src/__tests__/turn-events-store.test.ts +256 -0
  174. package/src/__tests__/twilio-routes.test.ts +4 -0
  175. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  176. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  177. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  178. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  179. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  180. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  181. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  182. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  183. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  184. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  185. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  186. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  187. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  188. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  189. package/src/a2a/__tests__/task-store.test.ts +246 -0
  190. package/src/a2a/agent-card.ts +58 -0
  191. package/src/a2a/feature-gate.ts +8 -0
  192. package/src/a2a/protocol-constants.ts +21 -0
  193. package/src/a2a/protocol-errors.ts +50 -0
  194. package/src/a2a/protocol-types.ts +162 -0
  195. package/src/a2a/task-store.ts +168 -0
  196. package/src/acp/resolve-agent.ts +1 -1
  197. package/src/agent/image-optimize.ts +13 -5
  198. package/src/agent/loop.ts +167 -18
  199. package/src/calls/voice-session-bridge.ts +61 -42
  200. package/src/channels/config.ts +9 -0
  201. package/src/channels/types.ts +122 -0
  202. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  203. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  204. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  205. package/src/cli/commands/__tests__/schedules.test.ts +960 -0
  206. package/src/cli/commands/changelog.ts +106 -42
  207. package/src/cli/commands/conversations.ts +102 -17
  208. package/src/cli/commands/default-action.ts +10 -53
  209. package/src/cli/commands/notifications.ts +388 -346
  210. package/src/cli/commands/plugins.ts +252 -0
  211. package/src/cli/commands/schedules.ts +683 -0
  212. package/src/cli/commands/telemetry.ts +40 -0
  213. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  214. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  215. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  216. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  217. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  218. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  219. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  220. package/src/cli/lib/cli-colors.ts +12 -0
  221. package/src/cli/lib/confirm-prompt.ts +79 -0
  222. package/src/cli/lib/install-from-github.ts +303 -0
  223. package/src/cli/lib/list-installed-plugins.ts +137 -0
  224. package/src/cli/lib/search-plugins.ts +163 -0
  225. package/src/cli/lib/uninstall-plugin.ts +82 -0
  226. package/src/cli/lib/unknown-command.ts +111 -0
  227. package/src/cli/program.ts +52 -2
  228. package/src/config/assistant-feature-flags.ts +24 -54
  229. package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
  230. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  231. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  232. package/src/config/bundled-skills/document/SKILL.md +23 -3
  233. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  234. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  235. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  236. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  237. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  238. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  239. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  240. package/src/config/bundled-tool-registry.ts +6 -0
  241. package/src/config/call-site-defaults.ts +105 -0
  242. package/src/config/feature-flag-registry.json +41 -9
  243. package/src/config/llm-resolver.ts +52 -1
  244. package/src/config/loader.ts +64 -38
  245. package/src/config/schema.ts +9 -10
  246. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  247. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  248. package/src/config/schemas/channels.ts +17 -0
  249. package/src/config/schemas/compaction.ts +28 -0
  250. package/src/config/schemas/conversations.ts +10 -0
  251. package/src/config/schemas/heartbeat.ts +23 -0
  252. package/src/config/schemas/llm-request-logs.ts +31 -7
  253. package/src/config/schemas/llm.ts +1 -0
  254. package/src/config/schemas/memory-retrieval.ts +18 -0
  255. package/src/config/schemas/memory-retrospective.ts +1 -1
  256. package/src/config/schemas/memory-v2.ts +4 -4
  257. package/src/config/schemas/memory.ts +3 -1
  258. package/src/config/schemas/tools.ts +14 -0
  259. package/src/config/seed-inference-profiles.ts +99 -29
  260. package/src/config/skills.ts +3 -96
  261. package/src/context/compactor.ts +1107 -0
  262. package/src/context/token-estimator.ts +34 -36
  263. package/src/context/window-manager.ts +197 -1520
  264. package/src/credential-execution/managed-catalog.ts +37 -0
  265. package/src/credential-health/credential-health-service.ts +280 -19
  266. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
  267. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  268. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  269. package/src/daemon/approval-generators.ts +8 -6
  270. package/src/daemon/config-watcher.ts +94 -31
  271. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  272. package/src/daemon/conversation-agent-loop.ts +198 -11
  273. package/src/daemon/conversation-error.ts +171 -37
  274. package/src/daemon/conversation-lifecycle.ts +53 -40
  275. package/src/daemon/conversation-messaging.ts +25 -6
  276. package/src/daemon/conversation-process.ts +49 -12
  277. package/src/daemon/conversation-runtime-assembly.ts +25 -1
  278. package/src/daemon/conversation-slash.ts +12 -5
  279. package/src/daemon/conversation-store.ts +11 -4
  280. package/src/daemon/conversation-tool-setup.ts +39 -7
  281. package/src/daemon/conversation.ts +33 -8
  282. package/src/daemon/date-context.ts +40 -0
  283. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  284. package/src/daemon/first-greeting.ts +22 -2
  285. package/src/daemon/guardian-action-generators.ts +1 -125
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  287. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  288. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  289. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  290. package/src/daemon/handlers/config-a2a.ts +289 -0
  291. package/src/daemon/handlers/config-model.ts +6 -5
  292. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  293. package/src/daemon/handlers/conversations.ts +1 -0
  294. package/src/daemon/handlers/shared.ts +14 -5
  295. package/src/daemon/handlers/skills.ts +111 -108
  296. package/src/daemon/history-repair.ts +28 -1
  297. package/src/daemon/host-app-control-proxy.ts +153 -27
  298. package/src/daemon/host-proxy-preactivation.ts +85 -18
  299. package/src/daemon/lifecycle.ts +89 -91
  300. package/src/daemon/meet-host-supervisor.ts +5 -4
  301. package/src/daemon/memory-v2-startup.ts +85 -0
  302. package/src/daemon/message-protocol.ts +1 -0
  303. package/src/daemon/message-types/conversations.ts +25 -0
  304. package/src/daemon/message-types/messages.ts +61 -0
  305. package/src/daemon/message-types/notifications.ts +21 -0
  306. package/src/daemon/message-types/subagents.ts +1 -0
  307. package/src/daemon/message-types/sync.ts +1 -0
  308. package/src/daemon/pkb-reminder-builder.test.ts +11 -54
  309. package/src/daemon/pkb-reminder-builder.ts +5 -20
  310. package/src/daemon/plugin-source-watcher.ts +146 -0
  311. package/src/daemon/process-message.ts +24 -3
  312. package/src/daemon/server.ts +11 -2
  313. package/src/daemon/skill-memory-refresh.ts +33 -0
  314. package/src/daemon/wake-target-adapter.ts +2 -0
  315. package/src/documents/document-store.ts +221 -3
  316. package/src/embedded/plugin-api.ts +40 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  318. package/src/export/transcript-formatter.ts +54 -20
  319. package/src/filing/filing-service.ts +39 -0
  320. package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
  321. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  322. package/src/heartbeat/heartbeat-service.ts +73 -189
  323. package/src/home/__tests__/feed-types.test.ts +80 -0
  324. package/src/home/feed-types.ts +36 -2
  325. package/src/home/post-connect-feed.ts +1 -0
  326. package/src/index.ts +18 -1
  327. package/src/ipc/cli-client.ts +147 -45
  328. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  329. package/src/mcp/client.ts +20 -4
  330. package/src/media/image-credentials.ts +3 -3
  331. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  332. package/src/memory/__tests__/conversation-queries.test.ts +483 -0
  333. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  334. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  335. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  336. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  337. package/src/memory/__tests__/message-content.test.ts +35 -0
  338. package/src/memory/bookmark-crud.ts +42 -10
  339. package/src/memory/context-search/sources/conversations.ts +62 -2
  340. package/src/memory/context-search/sources/workspace.ts +4 -0
  341. package/src/memory/conversation-crud.ts +63 -19
  342. package/src/memory/conversation-queries.ts +197 -11
  343. package/src/memory/conversation-title-service.ts +26 -4
  344. package/src/memory/db-init.ts +12 -0
  345. package/src/memory/delivery-crud.ts +152 -5
  346. package/src/memory/embedding-backend.ts +4 -4
  347. package/src/memory/external-conversation-store.ts +66 -5
  348. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
  349. package/src/memory/graph/conversation-graph-memory.ts +49 -21
  350. package/src/memory/graph/tools.ts +9 -40
  351. package/src/memory/indexer.ts +34 -29
  352. package/src/memory/invite-store.ts +53 -0
  353. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  354. package/src/memory/jobs/embed-concept-page.ts +20 -11
  355. package/src/memory/jobs-worker.ts +6 -1
  356. package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
  357. package/src/memory/llm-request-log-source.ts +19 -52
  358. package/src/memory/llm-request-log-store.ts +92 -1
  359. package/src/memory/llm-usage-store.ts +125 -5
  360. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  361. package/src/memory/memory-retrospective-job.ts +33 -6
  362. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  363. package/src/memory/message-content.ts +1 -1
  364. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  365. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  366. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  367. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  368. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  369. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  370. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  371. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  372. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  373. package/src/memory/migrations/index.ts +9 -0
  374. package/src/memory/migrations/registry.ts +16 -0
  375. package/src/memory/onboarding-events-store.ts +106 -0
  376. package/src/memory/schema/a2a.ts +15 -0
  377. package/src/memory/schema/bookmarks.ts +0 -2
  378. package/src/memory/schema/calls.ts +1 -0
  379. package/src/memory/schema/index.ts +1 -0
  380. package/src/memory/schema/inference.ts +3 -3
  381. package/src/memory/schema/infrastructure.ts +13 -0
  382. package/src/memory/turn-events-store.ts +127 -2
  383. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  384. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  385. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  386. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  387. package/src/memory/v2/__tests__/injection.test.ts +288 -11
  388. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  389. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  390. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  391. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  392. package/src/memory/v2/__tests__/router.test.ts +15 -0
  393. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  394. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  395. package/src/memory/v2/activation-store.ts +14 -16
  396. package/src/memory/v2/cli-command-content.ts +19 -0
  397. package/src/memory/v2/cli-command-store.ts +304 -0
  398. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  399. package/src/memory/v2/injection.ts +81 -26
  400. package/src/memory/v2/migration.ts +49 -19
  401. package/src/memory/v2/page-index.ts +63 -8
  402. package/src/memory/v2/prompts/router.ts +11 -8
  403. package/src/memory/v2/prompts/sweep.ts +2 -2
  404. package/src/memory/v2/qdrant.ts +135 -7
  405. package/src/memory/v2/router.ts +9 -8
  406. package/src/memory/v2/skill-store.ts +120 -35
  407. package/src/memory/v2/static-context.ts +4 -4
  408. package/src/memory/v2/types.ts +23 -0
  409. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  410. package/src/messaging/providers/a2a/deliver.ts +156 -0
  411. package/src/messaging/providers/gmail/client.ts +9 -2
  412. package/src/messaging/providers/index.ts +11 -2
  413. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  414. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  415. package/src/messaging/providers/slack/adapter.ts +43 -5
  416. package/src/messaging/providers/slack/client.ts +27 -0
  417. package/src/messaging/providers/slack/deep-link.ts +65 -0
  418. package/src/messaging/providers/slack/download.ts +104 -0
  419. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  420. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  421. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  422. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  423. package/src/messaging/providers/slack/types.ts +20 -1
  424. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  425. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  426. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  427. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  428. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  429. package/src/notifications/adapters/macos.ts +12 -2
  430. package/src/notifications/broadcaster.ts +29 -4
  431. package/src/notifications/conversation-pairing.ts +2 -1
  432. package/src/notifications/copy-composer.ts +17 -64
  433. package/src/notifications/decision-engine.ts +113 -45
  434. package/src/notifications/deterministic-checks.ts +96 -0
  435. package/src/notifications/emit-signal.ts +21 -1
  436. package/src/notifications/home-feed-side-effect.ts +138 -5
  437. package/src/notifications/signal.ts +3 -5
  438. package/src/notifications/types.ts +8 -0
  439. package/src/oauth/connection-resolver.ts +8 -4
  440. package/src/oauth/platform-connection.test.ts +43 -3
  441. package/src/oauth/platform-connection.ts +19 -6
  442. package/src/oauth/seed-providers.ts +10 -1
  443. package/src/permissions/checker.ts +2 -0
  444. package/src/permissions/ipc-risk-types.ts +1 -0
  445. package/src/permissions/question-prompter.test.ts +416 -0
  446. package/src/permissions/question-prompter.ts +294 -0
  447. package/src/platform/client.test.ts +1 -1
  448. package/src/platform/client.ts +1 -1
  449. package/src/plugin-api/constants.ts +26 -0
  450. package/src/plugin-api/index.ts +34 -1
  451. package/src/plugin-api/types.ts +104 -22
  452. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  453. package/src/plugins/defaults/compaction.ts +0 -4
  454. package/src/plugins/defaults/empty-response.ts +0 -2
  455. package/src/plugins/defaults/history-repair.ts +0 -2
  456. package/src/plugins/defaults/injectors.ts +74 -22
  457. package/src/plugins/defaults/llm-call.ts +0 -2
  458. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  459. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  460. package/src/plugins/defaults/persistence.ts +0 -2
  461. package/src/plugins/defaults/title-generate.ts +0 -5
  462. package/src/plugins/defaults/token-estimate.ts +0 -2
  463. package/src/plugins/defaults/tool-error.ts +0 -7
  464. package/src/plugins/defaults/tool-execute.ts +0 -2
  465. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  466. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  467. package/src/plugins/external-api.ts +104 -0
  468. package/src/plugins/external-plugin-loader.ts +187 -42
  469. package/src/plugins/feature-gate.ts +22 -0
  470. package/src/plugins/pipeline.ts +37 -0
  471. package/src/plugins/registry.ts +48 -80
  472. package/src/plugins/types.ts +40 -26
  473. package/src/plugins/user-loader.ts +21 -2
  474. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  475. package/src/proactive-artifact/job.test.ts +37 -5
  476. package/src/prompts/__tests__/system-prompt.test.ts +10 -43
  477. package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
  478. package/src/prompts/normalize-onboarding.ts +27 -0
  479. package/src/prompts/sections.ts +302 -0
  480. package/src/prompts/system-prompt.ts +63 -174
  481. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  482. package/src/prompts/templates/system-sections.ts +164 -0
  483. package/src/providers/__tests__/inference.test.ts +24 -7
  484. package/src/providers/anthropic/client.ts +28 -28
  485. package/src/providers/call-site-routing.ts +24 -6
  486. package/src/providers/connection-resolution.ts +68 -11
  487. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  488. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  489. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  490. package/src/providers/inference/adapter-factory.ts +32 -6
  491. package/src/providers/inference/auth.ts +12 -0
  492. package/src/providers/inference/backfill.ts +14 -1
  493. package/src/providers/inference/connections.ts +159 -34
  494. package/src/providers/inference/resolve-auth.ts +14 -4
  495. package/src/providers/model-catalog.ts +249 -12
  496. package/src/providers/model-intents.ts +3 -3
  497. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  498. package/src/providers/openai/chat-completions-provider.ts +169 -8
  499. package/src/providers/openrouter/client.ts +49 -4
  500. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
  501. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  502. package/src/providers/provider-availability.ts +17 -2
  503. package/src/providers/provider-catalog-visibility.ts +38 -0
  504. package/src/providers/provider-send-message.ts +27 -12
  505. package/src/providers/registry.ts +52 -15
  506. package/src/providers/retry.ts +47 -1
  507. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  508. package/src/runtime/agent-wake.ts +103 -15
  509. package/src/runtime/auth/route-policy.ts +21 -1
  510. package/src/runtime/btw-sidechain.ts +2 -0
  511. package/src/runtime/http-server.ts +7 -16
  512. package/src/runtime/http-types.ts +19 -47
  513. package/src/runtime/migrations/origin-mode.ts +1 -1
  514. package/src/runtime/pending-interactions.ts +1 -0
  515. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  516. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  517. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  518. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
  519. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  520. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  521. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  522. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  523. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  524. package/src/runtime/routes/acp-routes.ts +5 -3
  525. package/src/runtime/routes/auth-routes.ts +1 -1
  526. package/src/runtime/routes/bookmark-routes.ts +5 -3
  527. package/src/runtime/routes/btw-routes.ts +5 -1
  528. package/src/runtime/routes/channel-availability-routes.ts +126 -0
  529. package/src/runtime/routes/consolidation-routes.ts +100 -0
  530. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  531. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  532. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  533. package/src/runtime/routes/conversation-query-routes.ts +99 -35
  534. package/src/runtime/routes/conversation-routes.ts +97 -11
  535. package/src/runtime/routes/documents-routes.ts +25 -86
  536. package/src/runtime/routes/group-routes.ts +5 -0
  537. package/src/runtime/routes/inbound-conversation.ts +28 -8
  538. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  539. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  540. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  541. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  542. package/src/runtime/routes/index.ts +8 -0
  543. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  544. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  545. package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
  546. package/src/runtime/routes/integrations/a2a.ts +235 -0
  547. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  548. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  549. package/src/runtime/routes/integrations/twilio.ts +6 -13
  550. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  551. package/src/runtime/routes/notification-routes.ts +1 -1
  552. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  553. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  554. package/src/runtime/routes/question-routes.ts +259 -0
  555. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  556. package/src/runtime/routes/schedule-routes.ts +4 -7
  557. package/src/runtime/routes/subagents-routes.ts +98 -18
  558. package/src/runtime/routes/telemetry-routes.ts +27 -0
  559. package/src/runtime/routes/tts-routes.ts +27 -2
  560. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  561. package/src/runtime/routes/workspace-routes.ts +28 -0
  562. package/src/runtime/services/conversation-serializer.ts +39 -7
  563. package/src/runtime/sync/resource-sync-events.ts +93 -1
  564. package/src/schedule/schedule-store.ts +27 -2
  565. package/src/schedule/scheduler.ts +9 -1
  566. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  567. package/src/security/untrusted-content.ts +93 -8
  568. package/src/skills/catalog-files.ts +1 -1
  569. package/src/skills/catalog-install.ts +233 -116
  570. package/src/skills/clawhub.ts +70 -13
  571. package/src/skills/managed-store.ts +4 -119
  572. package/src/skills/skillssh-registry.ts +27 -48
  573. package/src/subagent/manager.ts +17 -7
  574. package/src/telemetry/types.ts +113 -1
  575. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  576. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  577. package/src/tools/apps/executors.ts +58 -7
  578. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  579. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  580. package/src/tools/browser/browser-execution.ts +15 -11
  581. package/src/tools/computer-use/definitions.ts +3 -3
  582. package/src/tools/credentials/vault.ts +1 -1
  583. package/src/tools/document/document-tool.ts +124 -1
  584. package/src/tools/filesystem/edit.ts +1 -1
  585. package/src/tools/filesystem/list.ts +1 -1
  586. package/src/tools/filesystem/read.ts +1 -1
  587. package/src/tools/filesystem/write.ts +5 -2
  588. package/src/tools/host-filesystem/transfer.ts +1 -1
  589. package/src/tools/host-terminal/host-shell.ts +1 -1
  590. package/src/tools/memory/register.ts +1 -9
  591. package/src/tools/permission-checker.ts +1 -1
  592. package/src/tools/registry.ts +17 -7
  593. package/src/tools/schedule/create.ts +2 -2
  594. package/src/tools/schema-transforms.ts +7 -2
  595. package/src/tools/side-effects.ts +1 -0
  596. package/src/tools/skills/delete-managed.ts +4 -4
  597. package/src/tools/skills/execute.ts +1 -1
  598. package/src/tools/skills/scaffold-managed.ts +3 -2
  599. package/src/tools/subagent/notify-parent.ts +1 -1
  600. package/src/tools/system/request-permission.ts +2 -2
  601. package/src/tools/terminal/safe-env.ts +60 -1
  602. package/src/tools/tool-manifest.ts +2 -0
  603. package/src/tools/types.ts +107 -21
  604. package/src/tools/ui-surface/definitions.ts +6 -5
  605. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  606. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  607. package/src/types/onboarding-context.ts +2 -0
  608. package/src/util/errors.ts +17 -0
  609. package/src/util/platform.ts +10 -0
  610. package/src/watcher/__tests__/engine.test.ts +22 -0
  611. package/src/watcher/engine.ts +6 -2
  612. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  613. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  614. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  615. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  616. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  617. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  618. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  619. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  620. package/src/workspace/migrations/registry.ts +10 -0
  621. package/src/workspace/migrations/runner.ts +39 -9
  622. package/src/workspace/migrations/types.ts +4 -0
  623. package/examples/plugins/echo/bun.lock +0 -25
  624. package/src/__tests__/context-window-manager.test.ts +0 -2481
  625. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  626. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  627. package/src/context/prompts/compact.md +0 -26
  628. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  629. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  630. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -121,6 +121,9 @@ const state = {
121
121
  // Throw queue for upsert: first call shifts and throws if non-null;
122
122
  // subsequent calls succeed once the queue is exhausted.
123
123
  upsertThrowQueue: [] as Array<Error | null>,
124
+ // Throw queue for createPayloadIndex: each entry maps to the next call,
125
+ // so tests can simulate index-creation failures (strict-mode, network).
126
+ createIndexThrowQueue: [] as Array<Error | null>,
124
127
  };
125
128
 
126
129
  class MockQdrantClient {
@@ -157,6 +160,10 @@ class MockQdrantClient {
157
160
  params: { field_name: string; field_schema: string },
158
161
  ) {
159
162
  state.createIndexCalls.push(params);
163
+ if (state.createIndexThrowQueue.length > 0) {
164
+ const next = state.createIndexThrowQueue.shift();
165
+ if (next) throw next;
166
+ }
160
167
  return {};
161
168
  }
162
169
  async upsert(_name: string, params: { wait: boolean; points: MockPoint[] }) {
@@ -228,6 +235,7 @@ function resetState(): void {
228
235
  state.countThrows = null;
229
236
  state.countCalls = 0;
230
237
  state.upsertThrowQueue.length = 0;
238
+ state.createIndexThrowQueue.length = 0;
231
239
  _resetMemoryV2QdrantForTests();
232
240
  // Drop any sentinel a prior test left behind so the no-drift default path
233
241
  // doesn't accidentally report `migrated: true`.
@@ -278,9 +286,10 @@ describe("memory v2 qdrant — collection lifecycle", () => {
278
286
  });
279
287
  expect(params.on_disk_payload).toBe(true);
280
288
 
281
- // Slug payload index is created up front.
289
+ // Slug + kind payload indexes are created up front.
282
290
  expect(state.createIndexCalls).toEqual([
283
291
  { field_name: "slug", field_schema: "keyword" },
292
+ { field_name: "kind", field_schema: "keyword" },
284
293
  ]);
285
294
  });
286
295
 
@@ -295,9 +304,14 @@ describe("memory v2 qdrant — collection lifecycle", () => {
295
304
  await ensureConceptPageCollection();
296
305
 
297
306
  // Existence check fired exactly once thanks to the in-memory readiness
298
- // cache; createCollection / createPayloadIndex never ran.
307
+ // cache; createCollection never ran. Payload indexes are (idempotently)
308
+ // ensured on the existing-collection path to backfill long-lived installs
309
+ // that predate the `kind` index.
299
310
  expect(state.createCollectionCalls).toBe(0);
300
- expect(state.createIndexCalls).toEqual([]);
311
+ expect(state.createIndexCalls).toEqual([
312
+ { field_name: "slug", field_schema: "keyword" },
313
+ { field_name: "kind", field_schema: "keyword" },
314
+ ]);
301
315
  expect(state.collectionExistsCalls).toBe(1);
302
316
  });
303
317
 
@@ -314,6 +328,7 @@ describe("memory v2 qdrant — collection lifecycle", () => {
314
328
  expect(state.createCollectionCalls).toBe(1);
315
329
  expect(state.createIndexCalls).toEqual([
316
330
  { field_name: "slug", field_schema: "keyword" },
331
+ { field_name: "kind", field_schema: "keyword" },
317
332
  ]);
318
333
  });
319
334
 
@@ -433,6 +448,54 @@ describe("memory v2 qdrant — collection lifecycle", () => {
433
448
  expect(existsSync(REEMBED_SENTINEL_PATH)).toBe(false);
434
449
  });
435
450
 
451
+ test("swallows 'already exists' on createPayloadIndex but propagates other failures without latching readiness", async () => {
452
+ // Existing collection that already has the full schema — the ensure
453
+ // path goes through `ensurePayloadIndexes` to backfill long-lived
454
+ // installs. The first index call hits an "already exists" race
455
+ // (benign; swallow); the second hits a strict-mode rejection (must
456
+ // propagate so readiness is not latched).
457
+ state.collectionExistsBeforeCreate = true;
458
+ state.createIndexThrowQueue.push(
459
+ Object.assign(
460
+ new Error("Wrong input: Payload field 'slug' already exists"),
461
+ { status: 400 },
462
+ ),
463
+ Object.assign(
464
+ new Error(
465
+ "Strict mode prohibits creating payload indexes on this deployment",
466
+ ),
467
+ { status: 400 },
468
+ ),
469
+ );
470
+
471
+ let caught: unknown = null;
472
+ try {
473
+ await ensureConceptPageCollection();
474
+ } catch (err) {
475
+ caught = err;
476
+ }
477
+ expect((caught as Error | null)?.message).toMatch(/strict mode/i);
478
+
479
+ // Both attempts ran; the strict-mode failure was not swallowed.
480
+ expect(state.createIndexCalls).toEqual([
481
+ { field_name: "slug", field_schema: "keyword" },
482
+ { field_name: "kind", field_schema: "keyword" },
483
+ ]);
484
+
485
+ // Readiness must NOT be latched after a non-benign failure — otherwise
486
+ // later slug/kind-filtered queries (e.g. skill backfill) would keep
487
+ // failing until a daemon restart. A follow-up ensure must retry.
488
+ const result = await ensureConceptPageCollection();
489
+ expect(result).toEqual({ migrated: false });
490
+ // Indexes attempted again on the retry (no throws queued this time).
491
+ expect(state.createIndexCalls).toEqual([
492
+ { field_name: "slug", field_schema: "keyword" },
493
+ { field_name: "kind", field_schema: "keyword" },
494
+ { field_name: "slug", field_schema: "keyword" },
495
+ { field_name: "kind", field_schema: "keyword" },
496
+ ]);
497
+ });
498
+
436
499
  test("concurrent ensure during a schema rebuild only deletes/creates once", async () => {
437
500
  state.collectionExistsBeforeCreate = true;
438
501
  state.getCollectionInfo = {
@@ -460,6 +460,21 @@ describe("runRouter — failure modes", () => {
460
460
  expect(warnSeen).toBe(true);
461
461
  });
462
462
 
463
+ test("duplicate-heavy IDs are deduped before the cap is applied", async () => {
464
+ // [1, 1, 2] with max=2 must yield two distinct slugs, not collapse to one
465
+ // after a pre-dedupe slice trims away the only other unique ID.
466
+ providerStub = makeProvider(toolUseResponse([1, 1, 2]));
467
+
468
+ const result = await runRouter({
469
+ workspaceDir,
470
+ ...COMMON_PARAMS,
471
+ config: makeConfig({ maxPageIds: 2 }),
472
+ });
473
+
474
+ expect(result.failureReason).toBeNull();
475
+ expect(result.selectedSlugs).toEqual(["alpha", "bravo"]);
476
+ });
477
+
463
478
  test("more than max_page_ids → truncated with warn", async () => {
464
479
  providerStub = makeProvider(toolUseResponse([1, 2, 3]));
465
480
 
@@ -15,10 +15,13 @@
15
15
  * - It swallows errors from the embedding backend — the function resolves
16
16
  * and the cache is unchanged from prior state.
17
17
  *
18
- * Hermetic by design: the catalog loader, state resolver, embedding backend,
19
- * Qdrant module, and feature-flag resolver are all module-mocked so the suite
20
- * never reaches a real backend or filesystem.
18
+ * Hermetic by design: the embedding backend, Qdrant module, and feature-flag
19
+ * resolver are module-mocked so the suite never reaches a real backend. One
20
+ * regression case uses a temp workspace to exercise disk-discovered skills.
21
21
  */
22
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
22
25
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
23
26
 
24
27
  import { makeMockLogger } from "../../../__tests__/helpers/mock-logger.js";
@@ -48,9 +51,15 @@ interface PruneCall {
48
51
  options?: { kind?: string };
49
52
  }
50
53
 
54
+ interface BackfillCall {
55
+ prefix: string;
56
+ kind: string;
57
+ allowedSuffixes: ReadonlySet<string>;
58
+ }
59
+
51
60
  interface TestState {
52
- catalog: SkillSummary[];
53
- resolved: ResolvedSkill[];
61
+ catalog: SkillSummary[] | null;
62
+ resolved: ResolvedSkill[] | null;
54
63
  fullCatalog: CatalogSkill[];
55
64
  fullCatalogThrows: Error | null;
56
65
  flagsEnabled: Record<string, boolean>;
@@ -60,6 +69,10 @@ interface TestState {
60
69
  upsertCalls: UpsertCall[];
61
70
  pruneCalls: PruneCall[];
62
71
  upsertThrows: Error | null;
72
+ backfillCalls: BackfillCall[];
73
+ backfillReturn: number;
74
+ backfillThrows: Error | null;
75
+ callSequence: Array<"upsert" | "prune" | "backfill">;
63
76
  }
64
77
 
65
78
  const state: TestState = {
@@ -74,6 +87,10 @@ const state: TestState = {
74
87
  upsertCalls: [],
75
88
  pruneCalls: [],
76
89
  upsertThrows: null,
90
+ backfillCalls: [],
91
+ backfillReturn: 0,
92
+ backfillThrows: null,
93
+ callSequence: [],
77
94
  };
78
95
 
79
96
  // Stub config so resolveSkillStates / mcp augmentation have something to read.
@@ -83,16 +100,39 @@ mock.module("../../../config/loader.js", () => ({
83
100
  qdrant: { url: "http://127.0.0.1:6333", vectorSize: 3, onDisk: false },
84
101
  },
85
102
  mcp: { servers: {} },
86
- skills: { entries: {}, allowBundled: null },
103
+ skills: { entries: {}, allowBundled: [] },
87
104
  }),
88
105
  }));
89
106
 
90
107
  mock.module("../../../config/skills.js", () => ({
91
- loadSkillCatalog: () => state.catalog,
108
+ loadSkillCatalog: () => state.catalog ?? [],
92
109
  }));
93
110
 
94
111
  mock.module("../../../config/skill-state.js", () => ({
95
- resolveSkillStates: () => state.resolved,
112
+ resolveSkillStates: (
113
+ catalog: SkillSummary[],
114
+ config: { skills?: { allowBundled?: string[] | null } },
115
+ ) => {
116
+ if (state.resolved) return state.resolved;
117
+ return catalog
118
+ .filter((summary) => {
119
+ const allowBundled = config.skills?.allowBundled;
120
+ return !(
121
+ summary.source === "bundled" &&
122
+ allowBundled != null &&
123
+ !allowBundled.includes(summary.id)
124
+ );
125
+ })
126
+ .map((summary) => ({
127
+ summary,
128
+ state:
129
+ summary.source === "managed" ||
130
+ summary.source === "bundled" ||
131
+ summary.source === "plugin"
132
+ ? "enabled"
133
+ : "disabled",
134
+ }));
135
+ },
96
136
  }));
97
137
 
98
138
  mock.module("../../../config/assistant-feature-flags.js", () => ({
@@ -115,6 +155,7 @@ mock.module("../../embedding-backend.js", () => ({
115
155
  mock.module("../qdrant.js", () => ({
116
156
  upsertConceptPageEmbedding: async (params: UpsertCall) => {
117
157
  if (state.upsertThrows) throw state.upsertThrows;
158
+ state.callSequence.push("upsert");
118
159
  state.upsertCalls.push(params);
119
160
  },
120
161
  pruneSlugsWithPrefixExcept: async (
@@ -122,8 +163,19 @@ mock.module("../qdrant.js", () => ({
122
163
  activeSuffixes: readonly string[],
123
164
  options?: { kind?: string },
124
165
  ) => {
166
+ state.callSequence.push("prune");
125
167
  state.pruneCalls.push({ prefix, activeSuffixes, options });
126
168
  },
169
+ backfillKindOnPointsWithPrefix: async (
170
+ prefix: string,
171
+ kind: string,
172
+ allowedSuffixes: ReadonlySet<string>,
173
+ ) => {
174
+ if (state.backfillThrows) throw state.backfillThrows;
175
+ state.callSequence.push("backfill");
176
+ state.backfillCalls.push({ prefix, kind, allowedSuffixes });
177
+ return state.backfillReturn;
178
+ },
127
179
  }));
128
180
 
129
181
  mock.module("../../../skills/catalog-cache.js", () => ({
@@ -170,6 +222,10 @@ function resetState(): void {
170
222
  state.upsertCalls.length = 0;
171
223
  state.pruneCalls.length = 0;
172
224
  state.upsertThrows = null;
225
+ state.backfillCalls.length = 0;
226
+ state.backfillReturn = 0;
227
+ state.backfillThrows = null;
228
+ state.callSequence.length = 0;
173
229
  _resetSkillStoreForTests();
174
230
  }
175
231
 
@@ -390,6 +446,154 @@ describe("seedV2SkillEntries", () => {
390
446
  expect(getSkillCapability("skills/unknown-skill")).toBeNull();
391
447
  });
392
448
 
449
+ test("skips stale in-flight seed results when a newer refresh is requested", async () => {
450
+ const skillA = makeSummary({
451
+ id: "example-skill-a",
452
+ displayName: "Skill A",
453
+ });
454
+ const skillB = makeSummary({
455
+ id: "example-skill-b",
456
+ displayName: "Skill B",
457
+ });
458
+ state.catalog = [skillA];
459
+ state.resolved = [{ summary: skillA, state: "enabled" }];
460
+ state.embedReturn = [[0.1, 0.2, 0.3]];
461
+
462
+ const firstSeed = seedV2SkillEntries();
463
+ state.catalog = [skillB];
464
+ state.resolved = [{ summary: skillB, state: "enabled" }];
465
+ const secondSeed = seedV2SkillEntries();
466
+
467
+ await Promise.all([firstSeed, secondSeed]);
468
+
469
+ expect(state.upsertCalls.map((call) => call.slug)).toEqual([
470
+ "skills/example-skill-b",
471
+ ]);
472
+ expect(getSkillCapability("example-skill-a")).toBeNull();
473
+ expect(getSkillCapability("example-skill-b")?.content).toContain("Skill B");
474
+ });
475
+
476
+ test("continues draining when waiter continuations enqueue additional generations", async () => {
477
+ const skillA = makeSummary({
478
+ id: "example-skill-a",
479
+ displayName: "Skill A",
480
+ });
481
+ const skillB = makeSummary({
482
+ id: "example-skill-b",
483
+ displayName: "Skill B",
484
+ });
485
+ const skillC = makeSummary({
486
+ id: "example-skill-c",
487
+ displayName: "Skill C",
488
+ });
489
+
490
+ function useSkill(skill: SkillSummary, dense: number[]): void {
491
+ state.catalog = [skill];
492
+ state.resolved = [{ summary: skill, state: "enabled" }];
493
+ state.embedReturn = [dense];
494
+ }
495
+
496
+ useSkill(skillA, [0.1, 0.2, 0.3]);
497
+ const firstSeed = seedV2SkillEntries();
498
+ const secondSeed = firstSeed.then(() => {
499
+ useSkill(skillB, [0.4, 0.5, 0.6]);
500
+ return seedV2SkillEntries();
501
+ });
502
+ const thirdSeed = secondSeed.then(() => {
503
+ useSkill(skillC, [0.7, 0.8, 0.9]);
504
+ return seedV2SkillEntries();
505
+ });
506
+
507
+ let timeout: ReturnType<typeof setTimeout> | undefined;
508
+ try {
509
+ await expect(
510
+ Promise.race([
511
+ Promise.all([firstSeed, secondSeed, thirdSeed]),
512
+ new Promise<never>((_, reject) => {
513
+ timeout = setTimeout(
514
+ () => reject(new Error("seed queue stalled")),
515
+ 500,
516
+ );
517
+ }),
518
+ ]),
519
+ ).resolves.toBeDefined();
520
+ } finally {
521
+ if (timeout) clearTimeout(timeout);
522
+ }
523
+
524
+ expect(state.upsertCalls.map((call) => call.slug)).toEqual([
525
+ "skills/example-skill-a",
526
+ "skills/example-skill-b",
527
+ "skills/example-skill-c",
528
+ ]);
529
+ expect(getSkillCapability("example-skill-a")).toBeNull();
530
+ expect(getSkillCapability("example-skill-b")).toBeNull();
531
+ expect(getSkillCapability("example-skill-c")?.content).toContain("Skill C");
532
+ });
533
+
534
+ test("seeds disk-discovered managed skills omitted from a stale SKILLS.md index", async () => {
535
+ const workspaceDir = mkdtempSync(join(tmpdir(), "skill-store-index-"));
536
+ state.resolved = null;
537
+ state.embedReturn = [[0.7, 0.8, 0.9]];
538
+
539
+ try {
540
+ const skillsDir = join(workspaceDir, "skills");
541
+ const skillDir = join(skillsDir, "geo-article-writer");
542
+ mkdirSync(skillDir, { recursive: true });
543
+ writeFileSync(join(skillsDir, "SKILLS.md"), "- stale-only\n", "utf-8");
544
+ writeFileSync(
545
+ join(skillDir, "SKILL.md"),
546
+ `---
547
+ name: "Geo Article Writer"
548
+ description: "Writes local geo articles"
549
+ metadata:
550
+ vellum:
551
+ activation-hints:
552
+ - user asks for local article drafts
553
+ avoid-when:
554
+ - user only wants citation extraction
555
+ ---
556
+
557
+ Write a local article draft.
558
+ `,
559
+ "utf-8",
560
+ );
561
+
562
+ state.catalog = [
563
+ {
564
+ id: "geo-article-writer",
565
+ name: "Geo Article Writer",
566
+ displayName: "Geo Article Writer",
567
+ description: "Writes local geo articles",
568
+ directoryPath: skillDir,
569
+ skillFilePath: join(skillDir, "SKILL.md"),
570
+ source: "managed",
571
+ activationHints: ["user asks for local article drafts"],
572
+ avoidWhen: ["user only wants citation extraction"],
573
+ },
574
+ ];
575
+
576
+ await seedV2SkillEntries();
577
+ } finally {
578
+ rmSync(workspaceDir, { recursive: true, force: true });
579
+ }
580
+
581
+ expect(state.upsertCalls).toHaveLength(1);
582
+ expect(state.upsertCalls[0].slug).toBe("skills/geo-article-writer");
583
+
584
+ const entry = getSkillCapability("geo-article-writer");
585
+ expect(entry).not.toBeNull();
586
+ expect(entry?.id).toBe("geo-article-writer");
587
+ expect(entry?.content).toContain('The "Geo Article Writer" skill');
588
+ expect(entry?.content).toContain("Writes local geo articles");
589
+ expect(entry?.content).toContain(
590
+ "Use when: user asks for local article drafts.",
591
+ );
592
+ expect(entry?.content).toContain(
593
+ "Avoid when: user only wants citation extraction.",
594
+ );
595
+ });
596
+
393
597
  test("swallows errors from embedWithBackend and leaves prior cache intact", async () => {
394
598
  const skillA = makeSummary({ id: "example-skill-a" });
395
599
  state.catalog = [skillA];
@@ -444,6 +648,118 @@ describe("seedV2SkillEntries", () => {
444
648
  expect([...state.pruneCalls[0].activeSuffixes]).toEqual(["remote-only"]);
445
649
  });
446
650
 
651
+ test("passes kind: 'skill' to upsert and prune so legacy skill rows stay scoped to the skill kind", async () => {
652
+ const skillA = makeSummary({ id: "example-skill-a" });
653
+ state.catalog = [skillA];
654
+ state.resolved = [{ summary: skillA, state: "enabled" }];
655
+ state.fullCatalog = [
656
+ { id: "example-skill-a", name: "example-skill-a", description: "A" },
657
+ ];
658
+ state.embedReturn = [[0.1, 0.2, 0.3]];
659
+
660
+ await seedV2SkillEntries();
661
+
662
+ expect(state.upsertCalls).toHaveLength(1);
663
+ expect(state.upsertCalls[0].kind).toBe("skill");
664
+ expect(state.pruneCalls).toHaveLength(1);
665
+ expect(state.pruneCalls[0].options).toEqual({ kind: "skill" });
666
+ });
667
+
668
+ test("runs the legacy kind backfill before pruning so kindless skill points become prunable", async () => {
669
+ // Simulates an install carrying legacy skill points written before the
670
+ // kind discriminator existed: the backfill must run before prune so the
671
+ // kind-scoped prune can see and delete the orphans.
672
+ const skillA = makeSummary({ id: "example-skill-a" });
673
+ state.catalog = [skillA];
674
+ state.resolved = [{ summary: skillA, state: "enabled" }];
675
+ state.fullCatalog = [
676
+ { id: "example-skill-a", name: "example-skill-a", description: "A" },
677
+ ];
678
+ state.embedReturn = [[0.1, 0.2, 0.3]];
679
+ state.backfillReturn = 3;
680
+
681
+ await seedV2SkillEntries();
682
+
683
+ expect(state.backfillCalls).toHaveLength(1);
684
+ expect(state.backfillCalls[0].prefix).toBe("skills/");
685
+ expect(state.backfillCalls[0].kind).toBe("skill");
686
+ expect([...state.backfillCalls[0].allowedSuffixes].sort()).toEqual([
687
+ "example-skill-a",
688
+ ]);
689
+ expect(state.pruneCalls).toHaveLength(1);
690
+ expect(state.pruneCalls[0].options).toEqual({ kind: "skill" });
691
+ expect(state.callSequence.filter((s) => s !== "upsert")).toEqual([
692
+ "backfill",
693
+ "prune",
694
+ ]);
695
+ });
696
+
697
+ test("backfill only runs once per process across repeated seed runs", async () => {
698
+ const skillA = makeSummary({ id: "example-skill-a" });
699
+ state.catalog = [skillA];
700
+ state.resolved = [{ summary: skillA, state: "enabled" }];
701
+ state.fullCatalog = [
702
+ { id: "example-skill-a", name: "example-skill-a", description: "A" },
703
+ ];
704
+ state.embedReturn = [[0.1, 0.2, 0.3]];
705
+
706
+ await seedV2SkillEntries();
707
+ expect(state.backfillCalls).toHaveLength(1);
708
+
709
+ // A second seed should not re-scan: new upserts already carry kind, so
710
+ // there's nothing for the backfill to do.
711
+ state.embedReturn = [[0.1, 0.2, 0.3]];
712
+ await seedV2SkillEntries();
713
+ expect(state.backfillCalls).toHaveLength(1);
714
+ expect(state.pruneCalls).toHaveLength(2);
715
+ });
716
+
717
+ test("backfill failure is non-fatal — prune still runs and lastSeedError stays clean", async () => {
718
+ const skillA = makeSummary({ id: "example-skill-a" });
719
+ state.catalog = [skillA];
720
+ state.resolved = [{ summary: skillA, state: "enabled" }];
721
+ state.fullCatalog = [
722
+ { id: "example-skill-a", name: "example-skill-a", description: "A" },
723
+ ];
724
+ state.embedReturn = [[0.1, 0.2, 0.3]];
725
+ state.backfillThrows = new Error("qdrant scroll exploded");
726
+
727
+ await expect(
728
+ seedV2SkillEntries({ throwOnError: true }),
729
+ ).resolves.toBeUndefined();
730
+
731
+ // Prune still ran despite the backfill failure — we don't want to block
732
+ // the steady-state prune when the legacy scan trips.
733
+ expect(state.pruneCalls).toHaveLength(1);
734
+ });
735
+
736
+ test("backfill allowlist spans installed + remote catalog ids so user-authored skills/* pages stay untagged", async () => {
737
+ // Regression: backfilling kind on every `skills/*` point would also tag
738
+ // user-authored concept pages slugged like `skills/my-notes` — those
739
+ // would then be pruned as stale skills. The allowlist must contain
740
+ // every legitimate skill id we know about (installed + remote catalog)
741
+ // and nothing else.
742
+ const installed = makeSummary({ id: "installed-skill" });
743
+ state.catalog = [installed];
744
+ state.resolved = [{ summary: installed, state: "enabled" }];
745
+ state.fullCatalog = [
746
+ { id: "installed-skill", name: "installed-skill", description: "X" },
747
+ { id: "remote-only-skill", name: "remote-only-skill", description: "Y" },
748
+ ];
749
+ state.embedReturn = [
750
+ [0.1, 0.2, 0.3],
751
+ [0.4, 0.5, 0.6],
752
+ ];
753
+
754
+ await seedV2SkillEntries();
755
+
756
+ expect(state.backfillCalls).toHaveLength(1);
757
+ expect([...state.backfillCalls[0].allowedSuffixes].sort()).toEqual([
758
+ "installed-skill",
759
+ "remote-only-skill",
760
+ ]);
761
+ });
762
+
447
763
  test("skips pruning when catalog fetch returns empty (network failure guard)", async () => {
448
764
  const skillA = makeSummary({ id: "example-skill-a" });
449
765
  state.catalog = [skillA];
@@ -473,6 +789,38 @@ describe("getSkillCapability", () => {
473
789
 
474
790
  expect(getSkillCapability("does-not-exist")).toBeNull();
475
791
  });
792
+
793
+ test("mutating the returned entry does not corrupt the cache", async () => {
794
+ const skillA = makeSummary({ id: "example-skill-a" });
795
+ state.catalog = [skillA];
796
+ state.resolved = [{ summary: skillA, state: "enabled" }];
797
+ state.embedReturn = [[0.1, 0.2, 0.3]];
798
+
799
+ await seedV2SkillEntries();
800
+
801
+ const first = getSkillCapability("example-skill-a");
802
+ expect(first).not.toBeNull();
803
+ const originalContent = first!.content;
804
+
805
+ // Frozen entries throw in strict mode when mutated; suppress so we can
806
+ // prove cache invariance even if a future refactor swaps freeze for a
807
+ // plain clone.
808
+ try {
809
+ (first as unknown as { id: string }).id = "tampered";
810
+ (first as unknown as { content: string }).content = "tampered";
811
+ } catch {
812
+ // expected under Object.freeze
813
+ }
814
+
815
+ const second = getSkillCapability("example-skill-a");
816
+ expect(second?.id).toBe("example-skill-a");
817
+ expect(second?.content).toBe(originalContent);
818
+
819
+ // listSkillEntries path also unaffected.
820
+ const viaList = listSkillEntries();
821
+ expect(viaList[0].id).toBe("example-skill-a");
822
+ expect(viaList[0].content).toBe(originalContent);
823
+ });
476
824
  });
477
825
 
478
826
  describe("listSkillEntries", () => {
@@ -521,4 +869,35 @@ describe("listSkillEntries", () => {
521
869
  expect(second).toHaveLength(1);
522
870
  expect(second[0].id).toBe("example-skill-a");
523
871
  });
872
+
873
+ test("mutating a returned entry does not corrupt the cache", async () => {
874
+ const skillA = makeSummary({ id: "example-skill-a" });
875
+ state.catalog = [skillA];
876
+ state.resolved = [{ summary: skillA, state: "enabled" }];
877
+ state.embedReturn = [[0.1, 0.2, 0.3]];
878
+
879
+ await seedV2SkillEntries();
880
+
881
+ const first = listSkillEntries();
882
+ expect(first).toHaveLength(1);
883
+ const originalContent = first[0].content;
884
+
885
+ // Frozen entries throw in strict mode (ESM tests are strict) when
886
+ // mutated; suppress so we can prove cache invariance even if a future
887
+ // refactor swaps freeze for a plain clone.
888
+ try {
889
+ (first[0] as { id: string }).id = "tampered";
890
+ (first[0] as { content: string }).content = "tampered";
891
+ } catch {
892
+ // expected under Object.freeze
893
+ }
894
+
895
+ const second = listSkillEntries();
896
+ expect(second[0].id).toBe("example-skill-a");
897
+ expect(second[0].content).toBe(originalContent);
898
+
899
+ // Lookup-by-id path also unaffected.
900
+ const viaLookup = getSkillCapability("example-skill-a");
901
+ expect(viaLookup?.content).toBe(originalContent);
902
+ });
524
903
  });
@@ -32,11 +32,15 @@ mock.module("../../../util/logger.js", () => ({
32
32
  }));
33
33
 
34
34
  let configMemoryV2Enabled = true;
35
+ let configMemoryEnabled = true;
35
36
 
36
37
  mock.module("../../../config/loader.js", () => ({
37
38
  getConfig: () => ({}),
38
39
  loadConfig: () => ({
39
- memory: { v2: { enabled: configMemoryV2Enabled } },
40
+ memory: {
41
+ enabled: configMemoryEnabled,
42
+ v2: { enabled: configMemoryV2Enabled },
43
+ },
40
44
  }),
41
45
  loadRawConfig: () => ({}),
42
46
  saveRawConfig: () => {},
@@ -71,6 +75,7 @@ describe("readMemoryV2StaticContent", () => {
71
75
  beforeEach(() => {
72
76
  mkdirSync(TEST_DIR, { recursive: true });
73
77
  configMemoryV2Enabled = true;
78
+ configMemoryEnabled = true;
74
79
  });
75
80
 
76
81
  afterEach(() => {
@@ -83,6 +88,12 @@ describe("readMemoryV2StaticContent", () => {
83
88
  expect(readMemoryV2StaticContent()).toBeNull();
84
89
  });
85
90
 
91
+ test("returns null when config.memory.enabled is off even with v2 on", () => {
92
+ configMemoryEnabled = false;
93
+ for (const file of MEMORY_FILES) writeMemoryFile(file, `Content ${file}`);
94
+ expect(readMemoryV2StaticContent()).toBeNull();
95
+ });
96
+
86
97
  test("returns headed sections in canonical order when all files have content", () => {
87
98
  writeMemoryFile("essentials.md", "Alice prefers dark mode.");
88
99
  writeMemoryFile("threads.md", "Open thread: ship PR-123 review.");