@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
@@ -30,7 +30,6 @@ import { applyCorrectionIfCalibrated } from "../anisotropy.js";
30
30
  import { embedWithBackend } from "../embedding-backend.js";
31
31
  import { clampUnitInterval } from "../validation.js";
32
32
  import { hybridQueryConceptPages } from "./qdrant.js";
33
- import { hybridQuerySkills } from "./skill-qdrant.js";
34
33
  import { generateBm25QueryEmbedding } from "./sparse-bm25.js";
35
34
 
36
35
  /**
@@ -121,14 +120,18 @@ export function effectiveWeights(
121
120
  * sparse via the in-process TF-IDF encoder).
122
121
  * 2. Run server-side dense + sparse queries against the v2 concept-page
123
122
  * Qdrant collection, restricted to `candidateSlugs` so we don't waste
124
- * query bandwidth on unrelated pages.
125
- * 3. Fuse: per slug, `score = clamp01(dense_weight · denseCosine +
126
- * sparse_weight · normalizedSparse)`. Sparse scores are normalized by
127
- * the per-batch maximum (so the largest is 1.0); slugs missing from a
128
- * channel contribute 0 from that channel.
123
+ * query bandwidth on unrelated pages. The query hits four channels per
124
+ * page: body dense + body sparse, and (for pages that have a summary
125
+ * embedded) summary dense + summary sparse.
126
+ * 3. Fuse: per slug, score = `max(fused(body), fused(summary))`. Each
127
+ * half is `clamp01(dense_weight · denseCosine + sparse_weight ·
128
+ * normalizedSparse)` with sparse normalized by the per-batch maximum.
129
+ * Pages without a summary embedding fall back to body-only fusion —
130
+ * the summary half is undefined and the max collapses to the body
131
+ * score.
129
132
  *
130
133
  * Returns a `Map<slug, score>` containing only the candidate slugs that hit
131
- * in at least one channel. Slugs in `candidateSlugs` that miss both channels
134
+ * in at least one channel. Slugs in `candidateSlugs` that miss every channel
132
135
  * are absent from the map; callers should treat absence as score = 0 (the
133
136
  * activation pipeline does this implicitly when reading back A_o).
134
137
  *
@@ -147,6 +150,7 @@ export async function simBatch(
147
150
  text: string,
148
151
  candidateSlugs: readonly string[],
149
152
  config: AssistantConfig,
153
+ options?: { signal?: AbortSignal },
150
154
  ): Promise<Map<string, number>> {
151
155
  if (candidateSlugs.length === 0) {
152
156
  return new Map();
@@ -158,12 +162,16 @@ export async function simBatch(
158
162
  // Sparse uses BM25: the query side encodes binary occurrences per token,
159
163
  // and the stored doc vectors carry the IDF · TF-saturated weights — Qdrant
160
164
  // dot product then yields the BM25 score directly.
161
- const denseResult = await embedWithBackend(config, [text]);
165
+ throwIfAborted(options?.signal);
166
+ const denseResult = await embedWithBackend(config, [text], {
167
+ signal: options?.signal,
168
+ });
162
169
  const denseVector = await applyCorrectionIfCalibrated(
163
170
  denseResult.vectors[0],
164
171
  denseResult.provider,
165
172
  denseResult.model,
166
173
  );
174
+ throwIfAborted(options?.signal);
167
175
  const sparseVector = generateBm25QueryEmbedding(text);
168
176
 
169
177
  const hits = await hybridQueryConceptPages(
@@ -177,140 +185,99 @@ export async function simBatch(
177
185
  return new Map();
178
186
  }
179
187
 
180
- const maxSparse = computeMaxSparse(hits);
188
+ // Compute per-batch sparse maxima independently for the body and summary
189
+ // channels so each side normalizes against its own scale. Mixing the two
190
+ // — e.g. dividing every sparse score by the larger of the two maxima —
191
+ // would punish whichever channel happened to have lower-magnitude scores
192
+ // even when its hits were the best matches available.
193
+ const maxBodySparse = computeMaxSparse(hits, (h) => h.sparseScore);
194
+ const maxSummarySparse = computeMaxSparse(hits, (h) => h.summarySparseScore);
181
195
  const { dense_weight: baseDense, sparse_weight: baseSparse } =
182
196
  config.memory.v2;
183
- const { dense: denseWeight, sparse: sparseWeight } = effectiveWeights(
184
- hits,
185
- maxSparse,
197
+ const { dense: bodyDenseWeight, sparse: bodySparseWeight } = effectiveWeights(
198
+ hits.map((h) => ({ sparseScore: h.sparseScore })),
199
+ maxBodySparse,
186
200
  baseDense,
187
201
  baseSparse,
188
202
  config,
189
203
  );
204
+ const { dense: summaryDenseWeight, sparse: summarySparseWeight } =
205
+ effectiveWeights(
206
+ hits.map((h) => ({ sparseScore: h.summarySparseScore })),
207
+ maxSummarySparse,
208
+ baseDense,
209
+ baseSparse,
210
+ config,
211
+ );
190
212
 
191
213
  const scores = new Map<string, number>();
192
214
  for (const hit of hits) {
193
- scores.set(hit.slug, fuseHit(hit, maxSparse, denseWeight, sparseWeight));
215
+ const bodyScore = fuseHalf(
216
+ hit.denseScore,
217
+ hit.sparseScore,
218
+ maxBodySparse,
219
+ bodyDenseWeight,
220
+ bodySparseWeight,
221
+ );
222
+ const summaryScore = fuseHalf(
223
+ hit.summaryDenseScore,
224
+ hit.summarySparseScore,
225
+ maxSummarySparse,
226
+ summaryDenseWeight,
227
+ summarySparseWeight,
228
+ );
229
+ // Pages without a summary embedding return undefined for both summary
230
+ // channels; their `summaryScore` falls back to the body score so the
231
+ // max collapses cleanly to body-only behavior.
232
+ const score = Math.max(bodyScore ?? 0, summaryScore ?? bodyScore ?? 0);
233
+ scores.set(hit.slug, score);
194
234
  }
235
+
195
236
  return scores;
196
237
  }
197
238
 
198
- /**
199
- * Compute hybrid (dense + sparse) similarity scores between a query text and
200
- * a fixed set of candidate skill ids. Mirrors `simBatch` but targets the
201
- * dedicated `memory_v2_skills` Qdrant collection via `hybridQuerySkills`.
202
- *
203
- * Differences from `simBatch`:
204
- * - Keys are skill `id` values (not concept-page slugs).
205
- * - Restricts the query to the caller's candidate ids server-side via
206
- * `hybridQuerySkills`'s `restrictToIds` parameter. Without this, when the
207
- * skills collection has more skills than `ids.length`, Qdrant would
208
- * return its global top-K and candidate ids absent from that top-K would
209
- * silently score 0 — corrupting the activation calculation.
210
- *
211
- * Returns a `Map<id, score>` of fused scores in [0, 1]. Ids that did not hit
212
- * either channel are absent from the map.
213
- *
214
- * Edge cases:
215
- * - Empty `ids` → returns an empty map without touching Qdrant or the
216
- * embedding backend.
217
- * - Empty / whitespace-only `text` → returns an empty map without touching
218
- * Qdrant or the embedding backend. Same rationale as {@link simBatch}:
219
- * Gemini rejects empty content with HTTP 400, so the activation pipeline
220
- * would otherwise fail on turn 1 (where the assistant-text channel is
221
- * `""`). Treating the channel's contribution as 0 matches a no-hit
222
- * query.
223
- */
224
- export async function simSkillBatch(
225
- text: string,
226
- ids: readonly string[],
227
- config: AssistantConfig,
228
- ): Promise<Map<string, number>> {
229
- if (ids.length === 0) {
230
- return new Map();
231
- }
232
- if (text.trim().length === 0) {
233
- return new Map();
239
+ function throwIfAborted(signal: AbortSignal | undefined): void {
240
+ if (signal?.aborted) {
241
+ throw new DOMException("Aborted", "AbortError");
234
242
  }
235
-
236
- const denseResult = await embedWithBackend(config, [text]);
237
- const denseVector = await applyCorrectionIfCalibrated(
238
- denseResult.vectors[0],
239
- denseResult.provider,
240
- denseResult.model,
241
- );
242
- const sparseVector = generateBm25QueryEmbedding(text);
243
-
244
- const hits = await hybridQuerySkills(
245
- denseVector,
246
- sparseVector,
247
- ids.length,
248
- ids,
249
- );
250
-
251
- if (hits.length === 0) {
252
- return new Map();
253
- }
254
-
255
- // Defensive post-filter — `hybridQuerySkills` restricts server-side, so
256
- // every hit should already be in `ids`, but keep this guard so a buggy
257
- // payload (e.g. a missing/typoed id index) can't silently inject
258
- // out-of-set ids into the score map.
259
- const idSet = new Set(ids);
260
- const filtered = hits.filter((h) => idSet.has(h.id));
261
- if (filtered.length === 0) {
262
- return new Map();
263
- }
264
-
265
- const maxSparse = computeMaxSparse(filtered);
266
- const { dense_weight: baseDense, sparse_weight: baseSparse } =
267
- config.memory.v2;
268
- const { dense: denseWeight, sparse: sparseWeight } = effectiveWeights(
269
- filtered,
270
- maxSparse,
271
- baseDense,
272
- baseSparse,
273
- config,
274
- );
275
-
276
- const scores = new Map<string, number>();
277
- for (const hit of filtered) {
278
- scores.set(hit.id, fuseHit(hit, maxSparse, denseWeight, sparseWeight));
279
- }
280
- return scores;
281
243
  }
282
244
 
283
245
  /**
284
- * Per-batch sparse-score maximum used for normalization. Hits missing from
285
- * the sparse channel contribute 0 (handled by the `undefined` guard).
246
+ * Per-batch sparse-score maximum used for normalization. The accessor picks
247
+ * which sparse channel to scan `sparseScore` for the body channel,
248
+ * `summarySparseScore` for the summary channel. Hits missing from the
249
+ * channel contribute 0 (handled by the `undefined` guard).
286
250
  */
287
- function computeMaxSparse(
288
- hits: ReadonlyArray<{ sparseScore?: number }>,
251
+ function computeMaxSparse<T>(
252
+ hits: ReadonlyArray<T>,
253
+ accessor: (hit: T) => number | undefined,
289
254
  ): number {
290
255
  let max = 0;
291
256
  for (const hit of hits) {
292
- if (hit.sparseScore !== undefined && hit.sparseScore > max) {
293
- max = hit.sparseScore;
257
+ const value = accessor(hit);
258
+ if (value !== undefined && value > max) {
259
+ max = value;
294
260
  }
295
261
  }
296
262
  return max;
297
263
  }
298
264
 
299
265
  /**
300
- * Fuse a single hit's dense + sparse scores into a normalized [0, 1] score
266
+ * Fuse one half of a hit (body or summary) into a normalized [0, 1] score
301
267
  * via `clamp01(dense_weight · dense + sparse_weight · sparse/maxSparse)`.
302
- * Missing-channel scores contribute 0.
268
+ * Returns `undefined` when neither channel hit a signal the half had no
269
+ * match at all, so the caller can fall back to the other half cleanly.
303
270
  */
304
- function fuseHit(
305
- hit: { denseScore?: number; sparseScore?: number },
271
+ function fuseHalf(
272
+ denseScore: number | undefined,
273
+ sparseScore: number | undefined,
306
274
  maxSparse: number,
307
275
  denseWeight: number,
308
276
  sparseWeight: number,
309
- ): number {
310
- const dense = hit.denseScore ?? 0;
277
+ ): number | undefined {
278
+ if (denseScore === undefined && sparseScore === undefined) return undefined;
279
+ const dense = denseScore ?? 0;
311
280
  const sparseNormalized =
312
- hit.sparseScore !== undefined && maxSparse > 0
313
- ? hit.sparseScore / maxSparse
314
- : 0;
281
+ sparseScore !== undefined && maxSparse > 0 ? sparseScore / maxSparse : 0;
315
282
  return clamp01(denseWeight * dense + sparseWeight * sparseNormalized);
316
283
  }
@@ -2,9 +2,10 @@ import { getConfig } from "../../config/loader.js";
2
2
  import type { SkillCapabilityInput } from "../../skills/skill-memory.js";
3
3
 
4
4
  /**
5
- * Render the prose-style capability statement embedded into the
6
- * `memory_v2_skills` Qdrant collection and rendered in
7
- * `### Skills You Can Use`. Capped at 500 chars to match v1's behavior.
5
+ * Render the prose-style capability statement embedded into the unified
6
+ * `memory_v2_concept_pages` Qdrant collection (under the `skills/<id>` slug
7
+ * prefix) and rendered in `### Skills You Can Use`. Capped at 500 chars to
8
+ * match v1's behavior.
8
9
  */
9
10
  export function buildSkillContent(input: SkillCapabilityInput): string {
10
11
  let content = `The "${input.displayName}" skill (${input.id}) is available. ${input.description}.`;
@@ -2,18 +2,22 @@
2
2
  // Memory v2 — Skill catalog → embedded skill entries
3
3
  // ---------------------------------------------------------------------------
4
4
  //
5
- // Mirrors v1's `seedSkillGraphNodes` + `seedUninstalledCatalogSkillMemories`
6
- // (capability-seed.ts) for the v2 pipeline: enumerate the enabled-skill
7
- // catalog AND uninstalled catalog skills, render each skill's prose statement
8
- // via `buildSkillContent`, embed dense + sparse, upsert into the dedicated
9
- // `memory_v2_skills` Qdrant collection, and prune stale points from prior
10
- // catalog state. Including uninstalled catalog skills ensures their activation
11
- // hints are discoverable by intent so the model can auto-install them.
5
+ // Enumerate the enabled-skill catalog AND uninstalled catalog skills, render
6
+ // each skill's prose statement via `buildSkillContent`, embed dense + sparse,
7
+ // and upsert into `memory_v2_concept_pages` under the slug `skills/<id>`.
8
+ // Including uninstalled catalog skills ensures their activation hints are
9
+ // discoverable by intent so the model can auto-install them.
12
10
  //
13
- // Unlike v1, skill entries are kept in a small in-process cache so the render
14
- // path can fetch a `SkillEntry` synchronously by id without round-tripping to
15
- // Qdrant. The cache is replaced atomically at the end of a successful seed
16
- // run; on error the prior cache stays intact (skills are best-effort).
11
+ // Skills share the concept-page collection rather than living in a dedicated
12
+ // one so the per-turn activation pipeline scores them against the same
13
+ // candidate ANN as concept pages, with the same decay and spread machinery.
14
+ // The render path branches on the `skills/` slug prefix to surface them as
15
+ // the `### Skills You Can Use` subsection.
16
+ //
17
+ // Skill entries are kept in a small in-process cache so the render path can
18
+ // fetch a `SkillEntry` synchronously by id without round-tripping to Qdrant.
19
+ // The cache is replaced atomically at the end of a successful seed run; on
20
+ // error the prior cache stays intact (skills are best-effort).
17
21
 
18
22
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
19
23
  import { getConfig } from "../../config/loader.js";
@@ -30,15 +34,31 @@ import {
30
34
  embedWithBackend,
31
35
  generateSparseEmbedding,
32
36
  } from "../embedding-backend.js";
37
+ import {
38
+ pruneSlugsWithPrefixExcept,
39
+ upsertConceptPageEmbedding,
40
+ } from "./qdrant.js";
33
41
  import {
34
42
  augmentMcpSetupDescription,
35
43
  buildSkillContent,
36
44
  } from "./skill-content.js";
37
- import { pruneSkillsExcept, upsertSkillEmbedding } from "./skill-qdrant.js";
38
45
  import type { SkillEntry } from "./types.js";
39
46
 
40
47
  const log = getLogger("memory-v2-skill-store");
41
48
 
49
+ /**
50
+ * Slug prefix under which skill embeddings are indexed in
51
+ * `memory_v2_concept_pages`. Concept-page slugs must match
52
+ * `[a-z0-9][a-z0-9-]*(/...)*`, and `skills` matches that pattern, so the
53
+ * prefix coexists with hand-authored concept pages without escape work.
54
+ */
55
+ export const SKILL_SLUG_PREFIX = "skills/";
56
+
57
+ /** Compose the unified-collection slug for a skill id. */
58
+ export function skillSlugFor(id: string): string {
59
+ return `${SKILL_SLUG_PREFIX}${id}`;
60
+ }
61
+
42
62
  /**
43
63
  * Module-level cache of rendered skill entries keyed by skill id. `null` until
44
64
  * the first successful seed run completes; replaced atomically on each
@@ -47,30 +67,27 @@ const log = getLogger("memory-v2-skill-store");
47
67
  let entries: Map<string, SkillEntry> | null = null;
48
68
 
49
69
  /**
50
- * Seed (or re-seed) the v2 skill embedding collection from the live skill
51
- * catalog. Idempotent: safe to call repeatedly. Best-effort: never throws —
52
- * any failure leaves the prior `entries` cache in place and logs a warning.
70
+ * Seed (or re-seed) skill embeddings into the unified concept-page collection.
71
+ * Idempotent: safe to call repeatedly. Best-effort: never throws — any
72
+ * failure leaves the prior `entries` cache in place and logs a warning.
53
73
  *
54
74
  * Steps:
55
- * 1. Enumerate the local skill catalog and resolve each skill's enabled state
56
- * (`resolveSkillStates`).
57
- * 2. Build a `SkillCapabilityInput` per enabled skill, applying the
58
- * mcp-setup augmentation (mirrors v1) and the prose-style content render
59
- * (`buildSkillContent`, capped at 500 chars).
75
+ * 1. Enumerate the local skill catalog and resolve each skill's enabled
76
+ * state (`resolveSkillStates`).
77
+ * 2. Build a `SkillEntry` per enabled skill, applying the mcp-setup
78
+ * augmentation and the prose-style content render (`buildSkillContent`,
79
+ * capped at 500 chars).
60
80
  * 3. Defense-in-depth feature-flag filter: drop any skill whose declared
61
- * `metadata.vellum.feature-flag` is currently disabled. `resolveSkillStates`
62
- * already enforces this, but we mirror v1's enforcement point so the v2
63
- * collection never holds an embedding for a flag-gated skill if the two
64
- * ever drift.
65
- * 3b. Fetch the full remote catalog and seed any uninstalled skills so their
66
- * activation hints are discoverable by semantic search. Best-effort: if
67
- * the catalog fetch fails, only installed skills are seeded.
81
+ * `metadata.vellum.feature-flag` is currently disabled.
82
+ * 3b. Fetch the full remote catalog and seed any uninstalled skills so
83
+ * their activation hints are discoverable by semantic search. Best-effort:
84
+ * if the catalog fetch fails, only installed skills are seeded.
68
85
  * 4. Embed all `content` strings in a single dense `embedWithBackend` call,
69
86
  * and a per-skill synchronous `generateSparseEmbedding`.
70
- * 5. Upsert one Qdrant point per skill via `upsertSkillEmbedding` (keyed
71
- * deterministically on id so re-runs replace in place).
72
- * 6. Call `pruneSkillsExcept` with the active id list to drop any stale
73
- * points from prior catalog state (e.g. uninstalled skills).
87
+ * 5. Upsert one Qdrant point per skill via `upsertConceptPageEmbedding`
88
+ * keyed deterministically on slug `skills/<id>`.
89
+ * 6. Call `pruneSlugsWithPrefixExcept(SKILL_SLUG_PREFIX, ...)` to drop any
90
+ * stale points from prior catalog state (e.g. uninstalled skills).
74
91
  * 7. Replace the module-level `entries` cache with the freshly built map.
75
92
  */
76
93
  export async function seedV2SkillEntries(): Promise<void> {
@@ -83,8 +100,7 @@ export async function seedV2SkillEntries(): Promise<void> {
83
100
  // Track every locally-installed skill id (regardless of enabled/disabled
84
101
  // state) so the catalog-seeding loop below treats them all as "installed"
85
102
  // and never re-seeds a disabled skill from `getCatalog()` as if it were
86
- // uninstalled. Mirrors v1's `seedUninstalledCatalogSkillMemories`, which
87
- // keys off `loadSkillCatalog()` (the installed set) for the same reason.
103
+ // uninstalled.
88
104
  const installedIds = new Set<string>(catalog.map((s) => s.id));
89
105
 
90
106
  // Build the input list, applying the mcp-setup description augmentation
@@ -100,8 +116,8 @@ export async function seedV2SkillEntries(): Promise<void> {
100
116
  }
101
117
 
102
118
  // Seed uninstalled catalog skills so their activation hints are
103
- // discoverable by intent (mirrors v1's seedUninstalledCatalogSkillMemories).
104
- // Track whether the catalog was available so we can guard pruning below.
119
+ // discoverable by intent. Track whether the catalog was available so we
120
+ // can guard pruning below.
105
121
  let catalogAvailable = false;
106
122
  try {
107
123
  const fullCatalog = await getCatalog();
@@ -135,24 +151,29 @@ export async function seedV2SkillEntries(): Promise<void> {
135
151
 
136
152
  const now = Date.now();
137
153
  const nextEntries = new Map<string, SkillEntry>();
138
- for (let i = 0; i < seeds.length; i++) {
139
- const seed = seeds[i];
140
- await upsertSkillEmbedding({
141
- ...seed,
142
- dense: denseVectors[i],
143
- sparse: generateSparseEmbedding(seed.content),
144
- updatedAt: now,
145
- });
154
+ await Promise.all(
155
+ seeds.map((seed, i) =>
156
+ upsertConceptPageEmbedding({
157
+ slug: skillSlugFor(seed.id),
158
+ dense: denseVectors[i],
159
+ sparse: generateSparseEmbedding(seed.content),
160
+ updatedAt: now,
161
+ }),
162
+ ),
163
+ );
164
+ for (const seed of seeds) {
146
165
  nextEntries.set(seed.id, seed);
147
166
  }
148
167
 
149
- // Prune stale points. When the catalog is unavailable (empty array from
150
- // network failure or cold cache), we cannot enumerate which uninstalled
151
- // catalog skills should exist, so skip pruning entirely to avoid
152
- // aggressively removing previously-seeded catalog skill embeddings.
153
- // Mirrors v1's safeguard in capability-seed.ts (lines 124–143).
168
+ // Prune stale skill slugs. When the catalog is unavailable (empty array
169
+ // from network failure or cold cache), we cannot enumerate which
170
+ // uninstalled catalog skills should exist, so skip pruning entirely to
171
+ // avoid aggressively removing previously-seeded catalog skill embeddings.
154
172
  if (catalogAvailable) {
155
- await pruneSkillsExcept(seeds.map((s) => s.id));
173
+ await pruneSlugsWithPrefixExcept(
174
+ SKILL_SLUG_PREFIX,
175
+ seeds.map((s) => s.id),
176
+ );
156
177
  } else {
157
178
  log.info(
158
179
  "Catalog unavailable — skipping skill pruning to preserve prior catalog embeddings",
@@ -169,20 +190,22 @@ export async function seedV2SkillEntries(): Promise<void> {
169
190
  /**
170
191
  * Synchronous lookup of a previously-seeded `SkillEntry` by skill id. Returns
171
192
  * `null` when the cache has not yet been populated, when the id is unknown,
172
- * or when a prior seed run dropped the id (e.g. the skill was disabled). Used
173
- * by the render path to attach skill-related content to outgoing prompts.
193
+ * or when a prior seed run dropped the id (e.g. the skill was disabled).
194
+ *
195
+ * Accepts either a bare skill id (`example-skill`) or its unified-collection
196
+ * slug (`skills/example-skill`) so render-side callers can pass through what
197
+ * they have without a manual prefix strip.
174
198
  */
175
- export function getSkillCapability(id: string): SkillEntry | null {
199
+ export function getSkillCapability(idOrSlug: string): SkillEntry | null {
200
+ const id = idOrSlug.startsWith(SKILL_SLUG_PREFIX)
201
+ ? idOrSlug.slice(SKILL_SLUG_PREFIX.length)
202
+ : idOrSlug;
176
203
  return entries?.get(id) ?? null;
177
204
  }
178
205
 
179
- /**
180
- * Every skill id in the cache — both installed-and-enabled skills and
181
- * uninstalled-catalog skills. Empty before the first `seedV2SkillEntries`
182
- * run completes.
183
- */
184
- export function getAllSkillIds(): string[] {
185
- return entries ? [...entries.keys()] : [];
206
+ /** True iff the slug refers to a skill entry in the unified collection. */
207
+ export function isSkillSlug(slug: string): boolean {
208
+ return slug.startsWith(SKILL_SLUG_PREFIX);
186
209
  }
187
210
 
188
211
  /** @internal Test-only: clear the module-level cache. */
@@ -17,7 +17,7 @@
17
17
  // content through when `mode === "full"` (first turn / post-compaction),
18
18
  // matching the existing PKB auto-inject pattern.
19
19
 
20
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
20
+ import type { ChannelId } from "../../channels/types.js";
21
21
  import { loadConfig } from "../../config/loader.js";
22
22
  import { readPromptFile } from "../../prompts/system-prompt.js";
23
23
  import { getWorkspacePromptPath } from "../../util/platform.js";
@@ -35,9 +35,9 @@ const MEMORY_V2_STATIC_BLOCKS: readonly MemoryV2StaticBlock[] = [
35
35
  ];
36
36
 
37
37
  /**
38
- * Build the v2 static memory block, gated on `memory-v2-enabled` +
39
- * `config.memory.v2.enabled`. Empty/missing files are skipped; returns
40
- * `null` when the gate is off or every file is empty.
38
+ * Build the v2 static memory block, gated on `config.memory.v2.enabled`.
39
+ * Empty/missing files are skipped; returns `null` when the gate is off or
40
+ * every file is empty.
41
41
  */
42
42
  export function readMemoryV2StaticContent(): string | null {
43
43
  let config;
@@ -46,10 +46,7 @@ export function readMemoryV2StaticContent(): string | null {
46
46
  } catch {
47
47
  return null;
48
48
  }
49
- if (
50
- !isAssistantFeatureFlagEnabled("memory-v2-enabled", config) ||
51
- !config.memory.v2.enabled
52
- ) {
49
+ if (!config.memory.v2.enabled) {
53
50
  return null;
54
51
  }
55
52
 
@@ -61,3 +58,24 @@ export function readMemoryV2StaticContent(): string | null {
61
58
  }
62
59
  return sections.length > 0 ? sections.join("\n\n") : null;
63
60
  }
61
+
62
+ /**
63
+ * Static memory holds the user's aggregate personal pages
64
+ * (essentials/threads/recent/buffer). Block injection when a non-guardian
65
+ * actor reaches the assistant over a remote channel — otherwise the model
66
+ * can be prompt-injected into reciting private memory. Internal flows
67
+ * (`sourceChannel: "vellum"`) and turns with no trust context pass through
68
+ * unchanged; this gate exists only to keep remote untrusted actors out.
69
+ */
70
+ export function shouldLoadMemoryV2Static(args: {
71
+ shouldInjectNowAndPkb: boolean;
72
+ sourceChannel: ChannelId | undefined;
73
+ isTrustedActor: boolean;
74
+ }): boolean {
75
+ if (!args.shouldInjectNowAndPkb) return false;
76
+ const isRemoteUntrustedActor =
77
+ args.sourceChannel !== undefined &&
78
+ args.sourceChannel !== "vellum" &&
79
+ !args.isTrustedActor;
80
+ return !isRemoteUntrustedActor;
81
+ }
@@ -13,10 +13,10 @@
13
13
  * extraction-trigger path. Until then this handler is invoked only by
14
14
  * `memory_v2_sweep` rows enqueued explicitly (tests, future CLI).
15
15
  *
16
- * Skipped entirely when the `memory-v2-enabled` feature flag is off, or when
16
+ * Skipped entirely when `config.memory.v2.enabled` is false, or when
17
17
  * `config.memory.v2.sweep_enabled` is false — keeps the sweep dormant in
18
18
  * v1-only workspaces and in v2 workspaces that haven't opted in, even if a
19
- * stale row sits in the queue at flag-flip time.
19
+ * stale row sits in the queue when v2 is disabled.
20
20
  */
21
21
 
22
22
  import { readFileSync } from "node:fs";
@@ -25,7 +25,6 @@ import { join } from "node:path";
25
25
  import { desc, gt } from "drizzle-orm";
26
26
  import { z } from "zod";
27
27
 
28
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
29
28
  import type { AssistantConfig } from "../../config/types.js";
30
29
  import { getAssistantName } from "../../daemon/identity-helpers.js";
31
30
  import {
@@ -104,12 +103,12 @@ export async function memoryV2SweepJob(
104
103
  _job: MemoryJob,
105
104
  config: AssistantConfig,
106
105
  ): Promise<number> {
107
- if (!isAssistantFeatureFlagEnabled("memory-v2-enabled", config)) {
108
- log.debug("memory-v2-enabled flag off; sweep skipped");
106
+ if (!config.memory?.v2?.enabled) {
107
+ log.debug("memory.v2.enabled is false; sweep skipped");
109
108
  return 0;
110
109
  }
111
110
 
112
- if (!config.memory?.v2?.sweep_enabled) {
111
+ if (!config.memory.v2.sweep_enabled) {
113
112
  log.debug("memory.v2.sweep_enabled is false; sweep skipped");
114
113
  return 0;
115
114
  }
@@ -26,10 +26,17 @@ import { z } from "zod";
26
26
  * B → A. The full graph is the union of every page's `edges:` list — there
27
27
  * is no separate edges-index file. `ref_files` lists paths to attached media
28
28
  * (images, audio, etc.).
29
+ *
30
+ * `summary` is a 1-4 sentence prose description of the page. When present,
31
+ * retrieval injects the path + summary instead of the full page so the agent
32
+ * can decide whether to read the file. Optional because legacy pages predating
33
+ * the summary field still parse — those fall back to full-page injection and
34
+ * full-page-only similarity.
29
35
  */
30
36
  export const ConceptPageFrontmatterSchema = z.object({
31
37
  edges: z.array(z.string()).default([]),
32
38
  ref_files: z.array(z.string()).default([]),
39
+ summary: z.string().optional(),
33
40
  });
34
41
 
35
42
  export type ConceptPageFrontmatter = z.infer<
@@ -85,20 +92,20 @@ export const ActivationStateSchema = z.object({
85
92
  export type ActivationState = z.infer<typeof ActivationStateSchema>;
86
93
 
87
94
  // ---------------------------------------------------------------------------
88
- // Skill autoinjection (synthetic in-memory entries, not on-disk pages)
95
+ // Skill entries (synthetic concept-collection rows, not on-disk pages)
89
96
  // ---------------------------------------------------------------------------
90
97
 
91
98
  /**
92
- * Per-skill capability snapshot held in-process and embedded into the
93
- * `memory_v2_skills` Qdrant collection. `content` is the rendered
94
- * `buildSkillContent` string — already capped at 500 chars upstream and
95
- * already containing the skill's display name — and is what we embed and
96
- * what we render verbatim in `### Skills You Can Use`.
99
+ * Per-skill capability snapshot held in-process and embedded into the unified
100
+ * `memory_v2_concept_pages` Qdrant collection under the slug `skills/<id>`.
101
+ * `content` is the rendered `buildSkillContent` string — already capped at
102
+ * 500 chars upstream and already containing the skill's display name — and
103
+ * is what we embed and what we render verbatim in `### Skills You Can Use`.
97
104
  *
98
- * Plain interface (no Zod) because skill data does not cross a
99
- * serialization boundary: it is built in-process by `seedV2SkillEntries`
100
- * and read in-process by `renderInjectionBlock`. The Qdrant payload is
101
- * not parsed back through this type.
105
+ * Plain interface (no Zod) because skill data does not cross a serialization
106
+ * boundary: it is built in-process by `seedV2SkillEntries` and read in-process
107
+ * by `renderInjectionBlock`. The Qdrant payload is not parsed back through
108
+ * this type.
102
109
  */
103
110
  export interface SkillEntry {
104
111
  id: string;