@vellumai/assistant 0.7.2 → 0.7.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 (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -8,18 +8,18 @@
8
8
  * - A new topic appearing on a later turn injects only the new slug.
9
9
  * - `evictCompactedTurns` re-enables a previously-injected slug —
10
10
  * after eviction the same slug appears again in `toInject`.
11
- * - Skill pipeline: skill-only block, mixed concept-page+skill block,
12
- * both-empty null, no skill dedup across turns, `top_k_skills: 0`
13
- * short-circuit.
11
+ * - Unified-pool skills: a `skills/<id>` slug ranked into the top-K is
12
+ * rendered under `### Skills You Can Use`, mixed concept-page+skill
13
+ * blocks render concept sections first then the skills suffix, both
14
+ * empty → null block, skills participate in `everInjected` so they
15
+ * deduplicate across turns just like concepts.
14
16
  *
15
17
  * Hermetic by design: the embedding backend, qdrant client, and `getConfig`
16
18
  * are mocked at the module level so the suite never reaches a real backend.
17
- * The skill activation pipeline (`computeSkillActivation`,
18
- * `selectSkillInjections`) and the skill-store helpers (`getAllSkillIds`,
19
- * `getSkillCapability`) are also mocked at the module level so each test can
20
- * stage its skill slate without touching the dedicated skills Qdrant
21
- * collection. The activation-store uses an in-memory SQLite database so
22
- * writes are real but contained.
19
+ * The skill-store cache (`getSkillCapability`, `isSkillSlug`) is mocked so
20
+ * each test can stage skill content without touching the real catalog.
21
+ * The activation-store uses an in-memory SQLite database so writes are
22
+ * real but contained.
23
23
  *
24
24
  * Tests use a temp workspace (mkdtemp) and never touch `~/.vellum/`. Sample
25
25
  * page content uses generic placeholders (Alice, Bob, etc.) per the cross-
@@ -124,44 +124,32 @@ mock.module("@qdrant/js-client-rest", () => ({
124
124
  }));
125
125
 
126
126
  // ---------------------------------------------------------------------------
127
- // Skill pipeline mocks
127
+ // Skill-store mock
128
128
  // ---------------------------------------------------------------------------
129
129
  //
130
- // The skill side of the per-turn pipeline (`computeSkillActivation`,
131
- // `selectSkillInjections`) has its own dedicated Qdrant collection and
132
- // embedding round-trips. Rather than threading staged hits through that whole
133
- // pipeline for every test, we mock the two activation helpers and the two
134
- // skill-store helpers (`getAllSkillIds` for the candidate pool,
135
- // `getSkillCapability` for content lookup) at the module level and let each
136
- // test stage a `topNow` ordering and the matching `SkillEntry` content
137
- // directly.
130
+ // Skills now flow through the unified pipeline under the `skills/<id>` slug
131
+ // prefix — they are scored by `simBatch` against the same Qdrant collection
132
+ // as concept pages, ranked by `selectInjections`, and rendered alongside
133
+ // concept sections. The render path branches on `isSkillSlug(slug)` to fetch
134
+ // content from the in-process cache via `getSkillCapability` instead of
135
+ // reading a page from disk. Tests stage that cache and rely on the regular
136
+ // `stageTurn` plumbing to land skill slugs in the candidate set.
138
137
 
139
138
  const skillState = {
140
- /** Ordered ids `selectSkillInjections.topNow` returns this turn. */
141
- topSkillIds: [] as string[],
142
- /** id → SkillEntry used by `getSkillCapability` and `getAllSkillIds`. */
139
+ /** id SkillEntry consulted by `getSkillCapability`. */
143
140
  entries: new Map<string, SkillEntry>(),
144
141
  };
145
142
 
146
- const realActivation = await import("../activation.js");
147
- mock.module("../activation.js", () => ({
148
- ...realActivation,
149
- // The injection wiring only consumes `topNow` — the candidate set and
150
- // activation map are inputs to `selectSkillInjections`, not anything the
151
- // injection logic introspects. Stub them to empty so the test stays focused
152
- // on the wiring, not the pipeline internals (covered in activation.test.ts).
153
- computeSkillActivation: async () => ({
154
- activation: new Map<string, number>(),
155
- breakdown: new Map(),
156
- }),
157
- selectSkillInjections: ({ topK }: { topK: number }) => ({
158
- topNow: skillState.topSkillIds.slice(0, topK),
159
- }),
160
- }));
161
-
162
143
  mock.module("../skill-store.js", () => ({
163
- getAllSkillIds: () => [...skillState.entries.keys()],
164
- getSkillCapability: (id: string) => skillState.entries.get(id) ?? null,
144
+ getSkillCapability: (idOrSlug: string) => {
145
+ const id = idOrSlug.startsWith("skills/")
146
+ ? idOrSlug.slice("skills/".length)
147
+ : idOrSlug;
148
+ return skillState.entries.get(id) ?? null;
149
+ },
150
+ isSkillSlug: (slug: string) => slug.startsWith("skills/"),
151
+ SKILL_SLUG_PREFIX: "skills/",
152
+ skillSlugFor: (id: string) => `skills/${id}`,
165
153
  }));
166
154
 
167
155
  // ---------------------------------------------------------------------------
@@ -293,7 +281,6 @@ function makeConfig(
293
281
  k: number;
294
282
  hops: number;
295
283
  top_k: number;
296
- top_k_skills: number;
297
284
  epsilon: number;
298
285
  dense_weight: number;
299
286
  sparse_weight: number;
@@ -308,8 +295,7 @@ function makeConfig(
308
295
  c_now: 0.2,
309
296
  k: 0.5,
310
297
  hops: 2,
311
- top_k: 20,
312
- top_k_skills: 5,
298
+ top_k: 25,
313
299
  epsilon: 0.01,
314
300
  dense_weight: 1.0,
315
301
  sparse_weight: 0.0,
@@ -358,7 +344,6 @@ function resetState(): void {
358
344
  state.sparseReturn = { indices: [1, 2, 3], values: [0.5, 0.5, 0.5] };
359
345
  state.queryResponses.dense.length = 0;
360
346
  state.queryResponses.sparse.length = 0;
361
- skillState.topSkillIds.length = 0;
362
347
  skillState.entries.clear();
363
348
  telemetryState.recordCalls.length = 0;
364
349
  telemetryState.recordShouldThrow = false;
@@ -368,10 +353,8 @@ function resetState(): void {
368
353
  _resetMemoryV2QdrantForTests();
369
354
  }
370
355
 
371
- /** Stage the next turn's skill slate and the entries the renderer will look up. */
372
- function stageSkills(ids: string[], entries: SkillEntry[] = []): void {
373
- skillState.topSkillIds.length = 0;
374
- skillState.topSkillIds.push(...ids);
356
+ /** Stage skill-store cache entries for the upcoming render. */
357
+ function stageSkills(entries: SkillEntry[]): void {
375
358
  for (const entry of entries) {
376
359
  skillState.entries.set(entry.id, entry);
377
360
  }
@@ -676,24 +659,22 @@ describe("injectMemoryV2Block", () => {
676
659
  });
677
660
 
678
661
  // ---------------------------------------------------------------------------
679
- // Skill subsection rendering
662
+ // Unified pool — skills as `skills/<id>` slugs
680
663
  // ---------------------------------------------------------------------------
681
664
 
682
- test("renders a skill-only block alongside concept-page-only blocks", async () => {
683
- // No concept-page candidates this turn — the candidate query and the three
684
- // simBatch queries all return empty. The skill pipeline is mocked to
685
- // surface a single skill.
686
- stageTurn([]);
687
- stageSkills(
688
- ["example-skill-a"],
689
- [
690
- {
691
- id: "example-skill-a",
692
- content:
693
- 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
694
- },
695
- ],
696
- );
665
+ test("renders a skill-only block via the skills/ slug prefix", async () => {
666
+ // No concept-page candidates this turn — the only ANN hit is a skill
667
+ // slug. The render path branches on `skills/` prefix: it pulls the
668
+ // entry from the skill-store cache (mocked) and emits the bullet under
669
+ // the `### Skills You Can Use` subsection.
670
+ stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
671
+ stageSkills([
672
+ {
673
+ id: "example-skill-a",
674
+ content:
675
+ 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
676
+ },
677
+ ]);
697
678
 
698
679
  const result = await injectMemoryV2Block({
699
680
  database: db,
@@ -706,15 +687,11 @@ describe("injectMemoryV2Block", () => {
706
687
  config: makeConfig(),
707
688
  });
708
689
 
709
- expect(result.toInject).toEqual([]);
690
+ expect(result.toInject).toEqual(["skills/example-skill-a"]);
710
691
  expect(result.block).not.toBeNull();
711
- // `block` is the unwrapped inner content; the caller adds the
712
- // `<memory>...</memory>` wrapper exactly once at injection time.
713
692
  expect(result.block).not.toContain("<memory>");
714
693
  expect(result.block).not.toContain("</memory>");
715
694
  expect(result.block).not.toContain("## What I Remember Right Now");
716
- // No concept-page sections; skills subsection present with the right
717
- // bullet shape and the unconditional `→ use skill_load to activate` suffix.
718
695
  expect(result.block).not.toContain("### alice-vscode");
719
696
  expect(result.block).toContain("### Skills You Can Use");
720
697
  expect(result.block).toContain(
@@ -723,19 +700,19 @@ describe("injectMemoryV2Block", () => {
723
700
  });
724
701
 
725
702
  test("renders concept-page sections before the skills subsection in mixed blocks", async () => {
726
- // Concept page hits AND a skill — concept-page sections come first, then
703
+ // Concept page hit AND a skill — concept-page sections come first, then
727
704
  // the skills subsection.
728
- stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
729
- stageSkills(
730
- ["example-skill-a"],
731
- [
732
- {
733
- id: "example-skill-a",
734
- content:
735
- 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
736
- },
737
- ],
738
- );
705
+ stageTurn([
706
+ { slug: "alice-vscode", denseScore: 0.9 },
707
+ { slug: "skills/example-skill-a", denseScore: 0.7 },
708
+ ]);
709
+ stageSkills([
710
+ {
711
+ id: "example-skill-a",
712
+ content:
713
+ 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
714
+ },
715
+ ]);
739
716
 
740
717
  const result = await injectMemoryV2Block({
741
718
  database: db,
@@ -748,7 +725,10 @@ describe("injectMemoryV2Block", () => {
748
725
  config: makeConfig(),
749
726
  });
750
727
 
751
- expect(result.toInject).toEqual(["alice-vscode"]);
728
+ // Both slugs ranked into top-K and got freshly attached.
729
+ expect(new Set(result.toInject)).toEqual(
730
+ new Set(["alice-vscode", "skills/example-skill-a"]),
731
+ );
752
732
  expect(result.block).not.toBeNull();
753
733
 
754
734
  const aliceIdx = result.block!.indexOf("### alice-vscode");
@@ -757,46 +737,20 @@ describe("injectMemoryV2Block", () => {
757
737
  expect(skillsIdx).toBeGreaterThan(-1);
758
738
  expect(aliceIdx).toBeLessThan(skillsIdx);
759
739
 
760
- // The activation suffix is always appended for skills.
761
740
  expect(result.block).toContain(
762
741
  '- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
763
742
  );
764
743
  });
765
744
 
766
- test("returns null when both concept pages and skills are empty", async () => {
767
- // Empty concept-page candidate set (all simBatch + ANN responses empty)
768
- // AND no skill ids.
769
- stageTurn([]);
770
- stageSkills([]);
771
-
772
- const result = await injectMemoryV2Block({
773
- database: db,
774
- conversationId: "conv-1",
775
- currentTurn: 1,
776
- userMessage: "anything",
777
- assistantMessage: "",
778
- nowText: "",
779
- messageId: "msg-1",
780
- config: makeConfig(),
781
- });
782
-
783
- expect(result.toInject).toEqual([]);
784
- expect(result.block).toBeNull();
785
- });
786
-
787
- test("re-renders the same top-ranked skill on consecutive turns (no dedup)", async () => {
788
- // Skills are stateless: the same id can appear on back-to-back turns.
789
- // Stage no concept-page candidates so the block content is purely the
790
- // skills subsection.
745
+ test("skills participate in everInjected an attached skill is not re-attached on the next turn", async () => {
746
+ // Turn 1: skill ranks high, gets attached.
791
747
  const skillEntry = {
792
748
  id: "example-skill-a",
793
749
  content:
794
750
  'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
795
751
  };
796
-
797
- // Turn 1 — only the skill.
798
- stageTurn([]);
799
- stageSkills(["example-skill-a"], [skillEntry]);
752
+ stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
753
+ stageSkills([skillEntry]);
800
754
  const result1 = await injectMemoryV2Block({
801
755
  database: db,
802
756
  conversationId: "conv-1",
@@ -807,15 +761,14 @@ describe("injectMemoryV2Block", () => {
807
761
  messageId: "msg-1",
808
762
  config: makeConfig(),
809
763
  });
810
- expect(result1.block).not.toBeNull();
764
+ expect(result1.toInject).toEqual(["skills/example-skill-a"]);
811
765
  expect(result1.block).toContain("### Skills You Can Use");
812
- expect(result1.block).toContain("example-skill-a");
813
766
 
814
- // Turn 2 same skill ranks top again. Persisted state has advanced (the
815
- // first call wrote a fresh activation_state row), and `everInjected` was
816
- // not touched by the skill pipeline. The skill must still appear.
817
- stageTurn([]);
818
- stageSkills(["example-skill-a"], [skillEntry]);
767
+ // Turn 2: same skill ranks top again. It is already in `everInjected`, so
768
+ // `toInject` is empty and the block is null the attachment from turn 1
769
+ // remains visible to the agent via the cached prior user message.
770
+ stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
771
+ stageSkills([skillEntry]);
819
772
  const result2 = await injectMemoryV2Block({
820
773
  database: db,
821
774
  conversationId: "conv-1",
@@ -826,21 +779,57 @@ describe("injectMemoryV2Block", () => {
826
779
  messageId: "msg-2",
827
780
  config: makeConfig(),
828
781
  });
829
- expect(result2.block).not.toBeNull();
830
- expect(result2.block).toContain("### Skills You Can Use");
831
- expect(result2.block).toContain("example-skill-a");
832
-
833
- // The skill content line is identical across the two turns — the renderer
834
- // is deterministic in `id → entry` lookup and the entry is unchanged.
835
- const skillLine =
836
- '- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate';
837
- expect(result1.block).toContain(skillLine);
838
- expect(result2.block).toContain(skillLine);
839
-
840
- // `everInjected` is untouched by the skill pipeline — both turns left it
841
- // empty (no concept pages were injected).
782
+ expect(result2.toInject).toEqual([]);
783
+ expect(result2.block).toBeNull();
784
+
842
785
  const persisted = await hydrate(db, "conv-1");
843
- expect(persisted!.everInjected).toEqual([]);
786
+ expect(persisted!.everInjected).toEqual([
787
+ { slug: "skills/example-skill-a", turn: 1 },
788
+ ]);
789
+ });
790
+
791
+ test("skill slugs whose entry is missing from the cache are dropped silently", async () => {
792
+ // The skill ranks into top-K but the in-process cache no longer knows
793
+ // its content (skill uninstalled mid-run). The render path drops it
794
+ // without surfacing it as a `missingSlugs` page-missing event — that
795
+ // status is reserved for on-disk concept pages, not catalog-derived
796
+ // skill entries.
797
+ stageTurn([{ slug: "skills/missing-skill", denseScore: 0.9 }]);
798
+ // No `stageSkills` call — cache stays empty.
799
+
800
+ const result = await injectMemoryV2Block({
801
+ database: db,
802
+ conversationId: "conv-1",
803
+ currentTurn: 1,
804
+ userMessage: "anything",
805
+ assistantMessage: "",
806
+ nowText: "Now",
807
+ messageId: "msg-1",
808
+ config: makeConfig(),
809
+ });
810
+
811
+ // `toInject` still records the slug (it ranked into top-K) but the
812
+ // block collapses to null because the only entry was a cache miss.
813
+ expect(result.toInject).toEqual(["skills/missing-skill"]);
814
+ expect(result.block).toBeNull();
815
+ });
816
+
817
+ test("returns null when both concept pages and skills are empty", async () => {
818
+ stageTurn([]);
819
+
820
+ const result = await injectMemoryV2Block({
821
+ database: db,
822
+ conversationId: "conv-1",
823
+ currentTurn: 1,
824
+ userMessage: "anything",
825
+ assistantMessage: "",
826
+ nowText: "",
827
+ messageId: "msg-1",
828
+ config: makeConfig(),
829
+ });
830
+
831
+ expect(result.toInject).toEqual([]);
832
+ expect(result.block).toBeNull();
844
833
  });
845
834
 
846
835
  test("context-load mode renders topNow even when every slug was previously injected", async () => {
@@ -932,39 +921,6 @@ describe("injectMemoryV2Block", () => {
932
921
  expect(persisted!.everInjected).toHaveLength(3);
933
922
  });
934
923
 
935
- test("`top_k_skills: 0` short-circuits to no skills subsection", async () => {
936
- // Even when the underlying mock would surface skills, the cap at 0 must
937
- // drop them via `selectSkillInjections.topK = 0` → empty `topNow`.
938
- stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
939
- stageSkills(
940
- ["example-skill-a"],
941
- [
942
- {
943
- id: "example-skill-a",
944
- content:
945
- 'The "Example Skill A" skill (example-skill-a) is available.',
946
- },
947
- ],
948
- );
949
-
950
- const result = await injectMemoryV2Block({
951
- database: db,
952
- conversationId: "conv-1",
953
- currentTurn: 1,
954
- userMessage: "Alice's editor",
955
- assistantMessage: "",
956
- nowText: "Now",
957
- messageId: "msg-1",
958
- config: makeConfig({ top_k_skills: 0 }),
959
- });
960
-
961
- expect(result.toInject).toEqual(["alice-vscode"]);
962
- expect(result.block).not.toBeNull();
963
- expect(result.block).toContain("### alice-vscode");
964
- expect(result.block).not.toContain("### Skills You Can Use");
965
- expect(result.block).not.toContain("example-skill-a");
966
- });
967
-
968
924
  // ---------------------------------------------------------------------------
969
925
  // Activation-log telemetry
970
926
  // ---------------------------------------------------------------------------
@@ -1013,13 +969,12 @@ describe("injectMemoryV2Block", () => {
1013
969
  status: string;
1014
970
  source: string;
1015
971
  }>;
1016
- skills: unknown[];
1017
972
  config: { top_k: number };
1018
973
  };
1019
974
  expect(row.conversationId).toBe("conv-1");
1020
975
  expect(row.turn).toBe(2);
1021
976
  expect(row.mode).toBe("per-turn");
1022
- expect(row.config.top_k).toBe(20);
977
+ expect(row.config.top_k).toBe(25);
1023
978
 
1024
979
  // The candidate set is the union of fromPrior (alice) and fromAnn
1025
980
  // (alice + carol) → two concept rows.
@@ -1041,6 +996,41 @@ describe("injectMemoryV2Block", () => {
1041
996
  expect(byslug.get("carol-jazz")!.status).toBe("injected");
1042
997
  });
1043
998
 
999
+ test("activation-log concepts include skill rows under the skills/ prefix", async () => {
1000
+ // Skills participate in the unified telemetry list — they live in the
1001
+ // same `concepts` array, identifiable by the `skills/` slug prefix.
1002
+ stageTurn([
1003
+ { slug: "alice-vscode", denseScore: 0.9 },
1004
+ { slug: "skills/example-skill-a", denseScore: 0.7 },
1005
+ ]);
1006
+ stageSkills([
1007
+ {
1008
+ id: "example-skill-a",
1009
+ content: "skill content",
1010
+ },
1011
+ ]);
1012
+
1013
+ await injectMemoryV2Block({
1014
+ database: db,
1015
+ conversationId: "conv-1",
1016
+ currentTurn: 1,
1017
+ userMessage: "Alice's editor",
1018
+ assistantMessage: "",
1019
+ nowText: "Now",
1020
+ messageId: "msg-1",
1021
+ config: makeConfig(),
1022
+ });
1023
+
1024
+ expect(telemetryState.recordCalls.length).toBe(1);
1025
+ const row = telemetryState.recordCalls[0] as {
1026
+ concepts: Array<{ slug: string; status: string }>;
1027
+ };
1028
+ const slugs = row.concepts.map((c) => c.slug);
1029
+ expect(new Set(slugs)).toEqual(
1030
+ new Set(["alice-vscode", "skills/example-skill-a"]),
1031
+ );
1032
+ });
1033
+
1044
1034
  test("context-load mode marks every rendered slug as `injected`, never `in_context`", async () => {
1045
1035
  // Turn 1 (per-turn): seed alice as injected so the next turn's prior
1046
1036
  // `everInjected` includes her — the same setup the per-turn telemetry
@@ -4,7 +4,14 @@
4
4
  * file-based override and falls back to the bundled prompt when the
5
5
  * override is missing/empty/unreadable.
6
6
  */
7
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
+ import { execFileSync } from "node:child_process";
8
+ import {
9
+ mkdirSync,
10
+ mkdtempSync,
11
+ rmSync,
12
+ symlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
8
15
  import { homedir, tmpdir } from "node:os";
9
16
  import { join } from "node:path";
10
17
  import {
@@ -67,7 +74,14 @@ beforeEach(() => {
67
74
  });
68
75
 
69
76
  afterEach(() => {
70
- for (const entry of ["custom-prompt.md", "empty.md", "no-placeholder.md"]) {
77
+ for (const entry of [
78
+ "custom-prompt.md",
79
+ "empty.md",
80
+ "no-placeholder.md",
81
+ "huge.md",
82
+ "link.md",
83
+ "fifo",
84
+ ]) {
71
85
  rmSync(join(tmpWorkspace, entry), { force: true });
72
86
  }
73
87
  });
@@ -178,4 +192,49 @@ describe("resolveConsolidationPrompt — failure modes", () => {
178
192
  const data = warnCalls[0].data as Record<string, unknown>;
179
193
  expect(data.reason).toBe("empty_override");
180
194
  });
195
+
196
+ test("falls back to bundled prompt when the override exceeds the size limit", () => {
197
+ const path = join(tmpWorkspace, "huge.md");
198
+ // 1 MiB + 1 byte — just over the cap so we don't waste test memory.
199
+ writeFileSync(path, Buffer.alloc(1 * 1024 * 1024 + 1, 0x61));
200
+
201
+ const result = resolveConsolidationPrompt(path, CUTOFF);
202
+
203
+ expect(result).toBe(bundledPrompt());
204
+ expect(warnCalls).toHaveLength(1);
205
+ const data = warnCalls[0].data as Record<string, unknown>;
206
+ expect(data.reason).toBe("oversized_override");
207
+ expect(data.size).toBe(1 * 1024 * 1024 + 1);
208
+ });
209
+
210
+ test("falls back to bundled prompt when the override is a symlink", () => {
211
+ const target = join(tmpWorkspace, "custom-prompt.md");
212
+ writeFileSync(target, "real prompt body\n");
213
+ const link = join(tmpWorkspace, "link.md");
214
+ symlinkSync(target, link);
215
+
216
+ const result = resolveConsolidationPrompt(link, CUTOFF);
217
+
218
+ expect(result).toBe(bundledPrompt());
219
+ expect(warnCalls).toHaveLength(1);
220
+ const data = warnCalls[0].data as Record<string, unknown>;
221
+ expect(data.reason).toBe("not_regular_file");
222
+ });
223
+
224
+ test("falls back to bundled prompt when the override is a FIFO", () => {
225
+ const fifoPath = join(tmpWorkspace, "fifo");
226
+ try {
227
+ execFileSync("mkfifo", [fifoPath]);
228
+ } catch {
229
+ // mkfifo unavailable on this platform — skip without failing.
230
+ return;
231
+ }
232
+
233
+ const result = resolveConsolidationPrompt(fifoPath, CUTOFF);
234
+
235
+ expect(result).toBe(bundledPrompt());
236
+ expect(warnCalls).toHaveLength(1);
237
+ const data = warnCalls[0].data as Record<string, unknown>;
238
+ expect(data.reason).toBe("not_regular_file");
239
+ });
181
240
  });
@@ -190,6 +190,22 @@ describe("memory v2 qdrant — collection lifecycle", () => {
190
190
  expect(state.collectionExistsCalls).toBe(1);
191
191
  });
192
192
 
193
+ test("deduplicates concurrent collection creation", async () => {
194
+ state.collectionExistsBeforeCreate = false;
195
+
196
+ await Promise.all([
197
+ ensureConceptPageCollection(),
198
+ ensureConceptPageCollection(),
199
+ ensureConceptPageCollection(),
200
+ ]);
201
+
202
+ expect(state.collectionExistsCalls).toBe(1);
203
+ expect(state.createCollectionCalls).toBe(1);
204
+ expect(state.createIndexCalls).toEqual([
205
+ { field_name: "slug", field_schema: "keyword" },
206
+ ]);
207
+ });
208
+
193
209
  test("treats 409-on-create as success (concurrent creation race)", async () => {
194
210
  state.collectionExistsBeforeCreate = false;
195
211
  const conflict = Object.assign(new Error("Conflict"), { status: 409 });