@vellumai/assistant 0.7.2 → 0.8.0

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 (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. 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-
@@ -114,8 +114,11 @@ class MockQdrantClient {
114
114
  _name: string,
115
115
  params: { using: string; limit: number; filter?: unknown },
116
116
  ) {
117
- const queue = state.queryResponses[params.using as "dense" | "sparse"];
118
- return queue.shift() ?? { points: [] };
117
+ // The four-channel hybrid query fires body-dense, body-sparse,
118
+ // summary-dense, summary-sparse in order; both dense channels share
119
+ // the dense queue and both sparse channels share the sparse queue.
120
+ const channel = params.using.endsWith("sparse") ? "sparse" : "dense";
121
+ return state.queryResponses[channel].shift() ?? { points: [] };
119
122
  }
120
123
  }
121
124
 
@@ -124,44 +127,32 @@ mock.module("@qdrant/js-client-rest", () => ({
124
127
  }));
125
128
 
126
129
  // ---------------------------------------------------------------------------
127
- // Skill pipeline mocks
130
+ // Skill-store mock
128
131
  // ---------------------------------------------------------------------------
129
132
  //
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.
133
+ // Skills now flow through the unified pipeline under the `skills/<id>` slug
134
+ // prefix — they are scored by `simBatch` against the same Qdrant collection
135
+ // as concept pages, ranked by `selectInjections`, and rendered alongside
136
+ // concept sections. The render path branches on `isSkillSlug(slug)` to fetch
137
+ // content from the in-process cache via `getSkillCapability` instead of
138
+ // reading a page from disk. Tests stage that cache and rely on the regular
139
+ // `stageTurn` plumbing to land skill slugs in the candidate set.
138
140
 
139
141
  const skillState = {
140
- /** Ordered ids `selectSkillInjections.topNow` returns this turn. */
141
- topSkillIds: [] as string[],
142
- /** id → SkillEntry used by `getSkillCapability` and `getAllSkillIds`. */
142
+ /** id SkillEntry consulted by `getSkillCapability`. */
143
143
  entries: new Map<string, SkillEntry>(),
144
144
  };
145
145
 
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
146
  mock.module("../skill-store.js", () => ({
163
- getAllSkillIds: () => [...skillState.entries.keys()],
164
- getSkillCapability: (id: string) => skillState.entries.get(id) ?? null,
147
+ getSkillCapability: (idOrSlug: string) => {
148
+ const id = idOrSlug.startsWith("skills/")
149
+ ? idOrSlug.slice("skills/".length)
150
+ : idOrSlug;
151
+ return skillState.entries.get(id) ?? null;
152
+ },
153
+ isSkillSlug: (slug: string) => slug.startsWith("skills/"),
154
+ SKILL_SLUG_PREFIX: "skills/",
155
+ skillSlugFor: (id: string) => `skills/${id}`,
165
156
  }));
166
157
 
167
158
  // ---------------------------------------------------------------------------
@@ -241,6 +232,18 @@ ref_files:
241
232
  ---
242
233
  Demo body content.`,
243
234
  );
235
+ // A page WITH a `summary` in its frontmatter — exercises the summary-only
236
+ // injection path. Body is intentionally longer than the summary so tests
237
+ // can assert that the body is *not* injected when the summary is present.
238
+ writeFileSync(
239
+ join(tmpWorkspace, "memory", "concepts", "summarized-page.md"),
240
+ `---
241
+ edges: []
242
+ ref_files: []
243
+ summary: A short prose description of the summarized page that retrieval injects in place of the full body.
244
+ ---
245
+ Long-form body content that should NOT appear in the injection block when the page has a summary in frontmatter — the agent reads the file on demand instead.`,
246
+ );
244
247
  });
245
248
 
246
249
  afterAll(() => {
@@ -293,7 +296,6 @@ function makeConfig(
293
296
  k: number;
294
297
  hops: number;
295
298
  top_k: number;
296
- top_k_skills: number;
297
299
  epsilon: number;
298
300
  dense_weight: number;
299
301
  sparse_weight: number;
@@ -308,8 +310,7 @@ function makeConfig(
308
310
  c_now: 0.2,
309
311
  k: 0.5,
310
312
  hops: 2,
311
- top_k: 20,
312
- top_k_skills: 5,
313
+ top_k: 25,
313
314
  epsilon: 0.01,
314
315
  dense_weight: 1.0,
315
316
  sparse_weight: 0.0,
@@ -322,14 +323,26 @@ function makeConfig(
322
323
  /**
323
324
  * Stage one set of dense/sparse hits, used uniformly by every `simBatch`
324
325
  * channel call (user/assistant/now) AND by the un-restricted ANN candidate
325
- * query. The candidate query runs first, then three simBatch calls, so we
326
- * push 4 dense + 4 sparse responses per turn.
326
+ * query. The candidate query runs first, then three simBatch calls that's
327
+ * `channels` (= 4) logical hybrid queries. Each logical hybrid query now
328
+ * fires a four-channel fan-out (body dense, body sparse, summary dense,
329
+ * summary sparse), so we push 2 dense + 2 sparse responses per logical
330
+ * call to match the post-summary-vector wire pattern.
327
331
  *
328
332
  * Each entry is mapped to a hit per channel; pass `denseScore`/`sparseScore`
329
- * undefined to omit a slug from that channel.
333
+ * undefined to omit a slug from that channel. `summaryDenseScore` /
334
+ * `summarySparseScore` route to the summary-side channels — tests that
335
+ * don't care about summary scoring leave them undefined and the summary
336
+ * channel falls back to body-only behavior.
330
337
  */
331
338
  function stageTurn(
332
- hits: Array<{ slug: string; denseScore?: number; sparseScore?: number }>,
339
+ hits: Array<{
340
+ slug: string;
341
+ denseScore?: number;
342
+ sparseScore?: number;
343
+ summaryDenseScore?: number;
344
+ summarySparseScore?: number;
345
+ }>,
333
346
  channels = 4,
334
347
  ): void {
335
348
  // Clear any leftovers from a prior turn before staging this one so unused
@@ -350,6 +363,22 @@ function stageTurn(
350
363
  .filter((h) => h.sparseScore !== undefined)
351
364
  .map((h) => ({ score: h.sparseScore, payload: { slug: h.slug } })),
352
365
  });
366
+ state.queryResponses.dense.push({
367
+ points: hits
368
+ .filter((h) => h.summaryDenseScore !== undefined)
369
+ .map((h) => ({
370
+ score: h.summaryDenseScore,
371
+ payload: { slug: h.slug },
372
+ })),
373
+ });
374
+ state.queryResponses.sparse.push({
375
+ points: hits
376
+ .filter((h) => h.summarySparseScore !== undefined)
377
+ .map((h) => ({
378
+ score: h.summarySparseScore,
379
+ payload: { slug: h.slug },
380
+ })),
381
+ });
353
382
  }
354
383
  }
355
384
 
@@ -358,7 +387,6 @@ function resetState(): void {
358
387
  state.sparseReturn = { indices: [1, 2, 3], values: [0.5, 0.5, 0.5] };
359
388
  state.queryResponses.dense.length = 0;
360
389
  state.queryResponses.sparse.length = 0;
361
- skillState.topSkillIds.length = 0;
362
390
  skillState.entries.clear();
363
391
  telemetryState.recordCalls.length = 0;
364
392
  telemetryState.recordShouldThrow = false;
@@ -368,10 +396,8 @@ function resetState(): void {
368
396
  _resetMemoryV2QdrantForTests();
369
397
  }
370
398
 
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);
399
+ /** Stage skill-store cache entries for the upcoming render. */
400
+ function stageSkills(entries: SkillEntry[]): void {
375
401
  for (const entry of entries) {
376
402
  skillState.entries.set(entry.id, entry);
377
403
  }
@@ -412,7 +438,7 @@ describe("injectMemoryV2Block", () => {
412
438
  expect(result.block).not.toContain("<memory>");
413
439
  expect(result.block).not.toContain("</memory>");
414
440
  expect(result.block).not.toContain("## What I Remember Right Now");
415
- expect(result.block).toContain("### alice-vscode");
441
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
416
442
  expect(result.block).toContain("VS Code");
417
443
 
418
444
  // State persisted: alice's activation is above epsilon and recorded;
@@ -501,10 +527,10 @@ describe("injectMemoryV2Block", () => {
501
527
  });
502
528
 
503
529
  expect(result.toInject).toEqual(["carol-jazz"]);
504
- expect(result.block).toContain("### carol-jazz");
530
+ expect(result.block).toContain("# memory/concepts/carol-jazz.md");
505
531
  // The block only shows the new slug — alice's attachment lives on the
506
532
  // previous turn's user message.
507
- expect(result.block).not.toContain("### alice-vscode");
533
+ expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
508
534
 
509
535
  const persisted = await hydrate(db, "conv-1");
510
536
  expect(persisted!.everInjected).toEqual([
@@ -549,7 +575,7 @@ describe("injectMemoryV2Block", () => {
549
575
  });
550
576
 
551
577
  expect(result.toInject).toEqual(["alice-vscode"]);
552
- expect(result.block).toContain("### alice-vscode");
578
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
553
579
 
554
580
  const persisted = await hydrate(db, "conv-1");
555
581
  expect(persisted!.everInjected).toEqual([
@@ -557,6 +583,74 @@ describe("injectMemoryV2Block", () => {
557
583
  ]);
558
584
  });
559
585
 
586
+ test("page with summary renders as path + summary, no body, with the CRITICAL header", async () => {
587
+ // Pages whose frontmatter carries a `summary` should inject only the
588
+ // summary text behind the path header — the agent reads the full file
589
+ // on demand. The leading `**CRITICAL:**` line tells the agent how to
590
+ // read the block.
591
+ stageTurn([{ slug: "summarized-page", denseScore: 0.9 }]);
592
+
593
+ const result = await injectMemoryV2Block({
594
+ database: db,
595
+ conversationId: "conv-1",
596
+ currentTurn: 1,
597
+ userMessage: "tell me about the summarized page",
598
+ assistantMessage: "",
599
+ nowText: "Now",
600
+ messageId: "msg-1",
601
+ config: makeConfig(),
602
+ });
603
+
604
+ expect(result.block).not.toBeNull();
605
+ expect(result.block).toContain(
606
+ "**CRITICAL:** These are page summaries. Read the page file if it looks relevant.",
607
+ );
608
+ expect(result.block).toContain(
609
+ "# memory/concepts/summarized-page.md\nA short prose description",
610
+ );
611
+ // Body is NOT in the block — the agent must follow up with a read tool.
612
+ expect(result.block).not.toContain("Long-form body content");
613
+ // Frontmatter is also omitted; the path header carries the identifying
614
+ // information by itself, and edges flow through the activation graph.
615
+ expect(result.block).not.toContain("---\nedges:");
616
+ });
617
+
618
+ test("mixed batch — summary page renders short, fallback page renders full", async () => {
619
+ // Both pages rank into top-K. summarized-page has a summary → short
620
+ // form. frontmatter-demo has no summary → full-page fallback. The
621
+ // single CRITICAL header sits at the top regardless.
622
+ stageTurn([
623
+ { slug: "summarized-page", denseScore: 0.95 },
624
+ { slug: "frontmatter-demo", denseScore: 0.85 },
625
+ ]);
626
+
627
+ const result = await injectMemoryV2Block({
628
+ database: db,
629
+ conversationId: "conv-1",
630
+ currentTurn: 1,
631
+ userMessage: "show me everything",
632
+ assistantMessage: "",
633
+ nowText: "Now",
634
+ messageId: "msg-1",
635
+ config: makeConfig(),
636
+ });
637
+
638
+ expect(result.block).not.toBeNull();
639
+ // CRITICAL header appears exactly once.
640
+ const criticalCount = (
641
+ result.block!.match(/\*\*CRITICAL:\*\* These are page summaries\./g) ?? []
642
+ ).length;
643
+ expect(criticalCount).toBe(1);
644
+ // summarized-page → short form (path + summary, no body, no frontmatter).
645
+ expect(result.block).toContain("# memory/concepts/summarized-page.md\nA");
646
+ expect(result.block).not.toContain("Long-form body content");
647
+ // frontmatter-demo → full-page fallback (path + frontmatter + body).
648
+ expect(result.block).toContain(
649
+ "# memory/concepts/frontmatter-demo.md\n---\n",
650
+ );
651
+ expect(result.block).toContain("Demo body content.");
652
+ });
653
+
560
654
  test("includes the page frontmatter (edges, ref_files) in each rendered section", async () => {
561
655
  // The frontmatter (`edges`, `ref_files`) lives on disk above the page
562
656
  // body and is part of the page's content. Injection must reproduce both
@@ -577,8 +671,12 @@ describe("injectMemoryV2Block", () => {
577
671
  });
578
672
 
579
673
  expect(result.block).not.toBeNull();
580
- // Slug header is immediately followed by the frontmatter open delimiter.
581
- expect(result.block).toContain("### frontmatter-demo\n---\n");
674
+ // Path header is immediately followed by the frontmatter open delimiter.
675
+ // The fallback path renders the full page (frontmatter + body) when the
676
+ // page has no `summary` field — `frontmatter-demo` predates the field.
677
+ expect(result.block).toContain(
678
+ "# memory/concepts/frontmatter-demo.md\n---\n",
679
+ );
582
680
  // Both fields render in YAML block style with their populated values.
583
681
  expect(result.block).toContain("edges:\n - alice-vscode");
584
682
  expect(result.block).toContain("ref_files:\n - images/demo.jpg");
@@ -606,8 +704,8 @@ describe("injectMemoryV2Block", () => {
606
704
  });
607
705
 
608
706
  expect(result.toInject).toEqual(["carol-jazz", "alice-vscode"]);
609
- const carolIdx = result.block!.indexOf("### carol-jazz");
610
- const aliceIdx = result.block!.indexOf("### alice-vscode");
707
+ const carolIdx = result.block!.indexOf("# memory/concepts/carol-jazz.md");
708
+ const aliceIdx = result.block!.indexOf("# memory/concepts/alice-vscode.md");
611
709
  expect(carolIdx).toBeGreaterThan(-1);
612
710
  expect(aliceIdx).toBeGreaterThan(-1);
613
711
  expect(carolIdx).toBeLessThan(aliceIdx);
@@ -676,24 +774,22 @@ describe("injectMemoryV2Block", () => {
676
774
  });
677
775
 
678
776
  // ---------------------------------------------------------------------------
679
- // Skill subsection rendering
777
+ // Unified pool — skills as `skills/<id>` slugs
680
778
  // ---------------------------------------------------------------------------
681
779
 
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
- );
780
+ test("renders a skill-only block via the skills/ slug prefix", async () => {
781
+ // No concept-page candidates this turn — the only ANN hit is a skill
782
+ // slug. The render path branches on `skills/` prefix: it pulls the
783
+ // entry from the skill-store cache (mocked) and emits the bullet under
784
+ // the `### Skills You Can Use` subsection.
785
+ stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
786
+ stageSkills([
787
+ {
788
+ id: "example-skill-a",
789
+ content:
790
+ 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
791
+ },
792
+ ]);
697
793
 
698
794
  const result = await injectMemoryV2Block({
699
795
  database: db,
@@ -706,16 +802,12 @@ describe("injectMemoryV2Block", () => {
706
802
  config: makeConfig(),
707
803
  });
708
804
 
709
- expect(result.toInject).toEqual([]);
805
+ expect(result.toInject).toEqual(["skills/example-skill-a"]);
710
806
  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
807
  expect(result.block).not.toContain("<memory>");
714
808
  expect(result.block).not.toContain("</memory>");
715
809
  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
- expect(result.block).not.toContain("### alice-vscode");
810
+ expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
719
811
  expect(result.block).toContain("### Skills You Can Use");
720
812
  expect(result.block).toContain(
721
813
  '- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
@@ -723,19 +815,19 @@ describe("injectMemoryV2Block", () => {
723
815
  });
724
816
 
725
817
  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
818
+ // Concept page hit AND a skill — concept-page sections come first, then
727
819
  // 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
- );
820
+ stageTurn([
821
+ { slug: "alice-vscode", denseScore: 0.9 },
822
+ { slug: "skills/example-skill-a", denseScore: 0.7 },
823
+ ]);
824
+ stageSkills([
825
+ {
826
+ id: "example-skill-a",
827
+ content:
828
+ 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
829
+ },
830
+ ]);
739
831
 
740
832
  const result = await injectMemoryV2Block({
741
833
  database: db,
@@ -748,55 +840,34 @@ describe("injectMemoryV2Block", () => {
748
840
  config: makeConfig(),
749
841
  });
750
842
 
751
- expect(result.toInject).toEqual(["alice-vscode"]);
843
+ // Both slugs ranked into top-K and got freshly attached.
844
+ expect(new Set(result.toInject)).toEqual(
845
+ new Set(["alice-vscode", "skills/example-skill-a"]),
846
+ );
752
847
  expect(result.block).not.toBeNull();
753
848
 
754
- const aliceIdx = result.block!.indexOf("### alice-vscode");
849
+ const aliceHeaderIdx = result.block!.indexOf(
850
+ "# memory/concepts/alice-vscode.md",
851
+ );
755
852
  const skillsIdx = result.block!.indexOf("### Skills You Can Use");
756
- expect(aliceIdx).toBeGreaterThan(-1);
853
+ expect(aliceHeaderIdx).toBeGreaterThan(-1);
757
854
  expect(skillsIdx).toBeGreaterThan(-1);
758
- expect(aliceIdx).toBeLessThan(skillsIdx);
855
+ expect(aliceHeaderIdx).toBeLessThan(skillsIdx);
759
856
 
760
- // The activation suffix is always appended for skills.
761
857
  expect(result.block).toContain(
762
858
  '- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
763
859
  );
764
860
  });
765
861
 
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.
862
+ test("skills participate in everInjected an attached skill is not re-attached on the next turn", async () => {
863
+ // Turn 1: skill ranks high, gets attached.
791
864
  const skillEntry = {
792
865
  id: "example-skill-a",
793
866
  content:
794
867
  'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
795
868
  };
796
-
797
- // Turn 1 — only the skill.
798
- stageTurn([]);
799
- stageSkills(["example-skill-a"], [skillEntry]);
869
+ stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
870
+ stageSkills([skillEntry]);
800
871
  const result1 = await injectMemoryV2Block({
801
872
  database: db,
802
873
  conversationId: "conv-1",
@@ -807,15 +878,14 @@ describe("injectMemoryV2Block", () => {
807
878
  messageId: "msg-1",
808
879
  config: makeConfig(),
809
880
  });
810
- expect(result1.block).not.toBeNull();
881
+ expect(result1.toInject).toEqual(["skills/example-skill-a"]);
811
882
  expect(result1.block).toContain("### Skills You Can Use");
812
- expect(result1.block).toContain("example-skill-a");
813
883
 
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]);
884
+ // Turn 2: same skill ranks top again. It is already in `everInjected`, so
885
+ // `toInject` is empty and the block is null the attachment from turn 1
886
+ // remains visible to the agent via the cached prior user message.
887
+ stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
888
+ stageSkills([skillEntry]);
819
889
  const result2 = await injectMemoryV2Block({
820
890
  database: db,
821
891
  conversationId: "conv-1",
@@ -826,21 +896,57 @@ describe("injectMemoryV2Block", () => {
826
896
  messageId: "msg-2",
827
897
  config: makeConfig(),
828
898
  });
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).
899
+ expect(result2.toInject).toEqual([]);
900
+ expect(result2.block).toBeNull();
901
+
842
902
  const persisted = await hydrate(db, "conv-1");
843
- expect(persisted!.everInjected).toEqual([]);
903
+ expect(persisted!.everInjected).toEqual([
904
+ { slug: "skills/example-skill-a", turn: 1 },
905
+ ]);
906
+ });
907
+
908
+ test("skill slugs whose entry is missing from the cache are dropped silently", async () => {
909
+ // The skill ranks into top-K but the in-process cache no longer knows
910
+ // its content (skill uninstalled mid-run). The render path drops it
911
+ // without surfacing it as a `missingSlugs` page-missing event — that
912
+ // status is reserved for on-disk concept pages, not catalog-derived
913
+ // skill entries.
914
+ stageTurn([{ slug: "skills/missing-skill", denseScore: 0.9 }]);
915
+ // No `stageSkills` call — cache stays empty.
916
+
917
+ const result = await injectMemoryV2Block({
918
+ database: db,
919
+ conversationId: "conv-1",
920
+ currentTurn: 1,
921
+ userMessage: "anything",
922
+ assistantMessage: "",
923
+ nowText: "Now",
924
+ messageId: "msg-1",
925
+ config: makeConfig(),
926
+ });
927
+
928
+ // `toInject` still records the slug (it ranked into top-K) but the
929
+ // block collapses to null because the only entry was a cache miss.
930
+ expect(result.toInject).toEqual(["skills/missing-skill"]);
931
+ expect(result.block).toBeNull();
932
+ });
933
+
934
+ test("returns null when both concept pages and skills are empty", async () => {
935
+ stageTurn([]);
936
+
937
+ const result = await injectMemoryV2Block({
938
+ database: db,
939
+ conversationId: "conv-1",
940
+ currentTurn: 1,
941
+ userMessage: "anything",
942
+ assistantMessage: "",
943
+ nowText: "",
944
+ messageId: "msg-1",
945
+ config: makeConfig(),
946
+ });
947
+
948
+ expect(result.toInject).toEqual([]);
949
+ expect(result.block).toBeNull();
844
950
  });
845
951
 
846
952
  test("context-load mode renders topNow even when every slug was previously injected", async () => {
@@ -875,7 +981,7 @@ describe("injectMemoryV2Block", () => {
875
981
  });
876
982
 
877
983
  expect(result.block).not.toBeNull();
878
- expect(result.block).toContain("### alice-vscode");
984
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
879
985
  // No newly-injected slug — alice was already in everInjected.
880
986
  expect(result.toInject).toEqual([]);
881
987
 
@@ -911,9 +1017,9 @@ describe("injectMemoryV2Block", () => {
911
1017
  });
912
1018
 
913
1019
  expect(result.block).not.toBeNull();
914
- expect(result.block).toContain("### alice-vscode");
915
- expect(result.block).toContain("### bob-coffee");
916
- expect(result.block).toContain("### carol-jazz");
1020
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
1021
+ expect(result.block).toContain("# memory/concepts/bob-coffee.md");
1022
+ expect(result.block).toContain("# memory/concepts/carol-jazz.md");
917
1023
  // The seeded directed edges (alice→bob, bob→alice, frontmatter-demo→alice)
918
1024
  // mean alice has two incoming predecessors and bob has one, so directed
919
1025
  // spread normalizes alice's activation more aggressively than bob's. The
@@ -932,39 +1038,6 @@ describe("injectMemoryV2Block", () => {
932
1038
  expect(persisted!.everInjected).toHaveLength(3);
933
1039
  });
934
1040
 
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
1041
  // ---------------------------------------------------------------------------
969
1042
  // Activation-log telemetry
970
1043
  // ---------------------------------------------------------------------------
@@ -1013,13 +1086,12 @@ describe("injectMemoryV2Block", () => {
1013
1086
  status: string;
1014
1087
  source: string;
1015
1088
  }>;
1016
- skills: unknown[];
1017
1089
  config: { top_k: number };
1018
1090
  };
1019
1091
  expect(row.conversationId).toBe("conv-1");
1020
1092
  expect(row.turn).toBe(2);
1021
1093
  expect(row.mode).toBe("per-turn");
1022
- expect(row.config.top_k).toBe(20);
1094
+ expect(row.config.top_k).toBe(25);
1023
1095
 
1024
1096
  // The candidate set is the union of fromPrior (alice) and fromAnn
1025
1097
  // (alice + carol) → two concept rows.
@@ -1041,6 +1113,41 @@ describe("injectMemoryV2Block", () => {
1041
1113
  expect(byslug.get("carol-jazz")!.status).toBe("injected");
1042
1114
  });
1043
1115
 
1116
+ test("activation-log concepts include skill rows under the skills/ prefix", async () => {
1117
+ // Skills participate in the unified telemetry list — they live in the
1118
+ // same `concepts` array, identifiable by the `skills/` slug prefix.
1119
+ stageTurn([
1120
+ { slug: "alice-vscode", denseScore: 0.9 },
1121
+ { slug: "skills/example-skill-a", denseScore: 0.7 },
1122
+ ]);
1123
+ stageSkills([
1124
+ {
1125
+ id: "example-skill-a",
1126
+ content: "skill content",
1127
+ },
1128
+ ]);
1129
+
1130
+ await injectMemoryV2Block({
1131
+ database: db,
1132
+ conversationId: "conv-1",
1133
+ currentTurn: 1,
1134
+ userMessage: "Alice's editor",
1135
+ assistantMessage: "",
1136
+ nowText: "Now",
1137
+ messageId: "msg-1",
1138
+ config: makeConfig(),
1139
+ });
1140
+
1141
+ expect(telemetryState.recordCalls.length).toBe(1);
1142
+ const row = telemetryState.recordCalls[0] as {
1143
+ concepts: Array<{ slug: string; status: string }>;
1144
+ };
1145
+ const slugs = row.concepts.map((c) => c.slug);
1146
+ expect(new Set(slugs)).toEqual(
1147
+ new Set(["alice-vscode", "skills/example-skill-a"]),
1148
+ );
1149
+ });
1150
+
1044
1151
  test("context-load mode marks every rendered slug as `injected`, never `in_context`", async () => {
1045
1152
  // Turn 1 (per-turn): seed alice as injected so the next turn's prior
1046
1153
  // `everInjected` includes her — the same setup the per-turn telemetry
@@ -1173,7 +1280,7 @@ describe("injectMemoryV2Block", () => {
1173
1280
  expect(telemetryState.recordCalls.length).toBe(0);
1174
1281
  expect(result.toInject).toEqual(["alice-vscode"]);
1175
1282
  expect(result.block).not.toBeNull();
1176
- expect(result.block).toContain("### alice-vscode");
1283
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
1177
1284
 
1178
1285
  const persisted = await hydrate(db, "conv-1");
1179
1286
  expect(persisted!.everInjected).toEqual([