@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
@@ -68,17 +68,6 @@ const state = {
68
68
  points: Array<{ score?: number; payload: Record<string, unknown> }>;
69
69
  }>,
70
70
  },
71
- // Separate response queue for the dedicated `memory_v2_skills` collection
72
- // so a test asserting on skill activation does not have to interleave
73
- // responses with concept-page queries.
74
- skillQueryResponses: {
75
- dense: [] as Array<{
76
- points: Array<{ score?: number; payload: Record<string, unknown> }>;
77
- }>,
78
- sparse: [] as Array<{
79
- points: Array<{ score?: number; payload: Record<string, unknown> }>;
80
- }>,
81
- },
82
71
  queryCalls: [] as Array<{
83
72
  collection: string;
84
73
  using: string;
@@ -125,12 +114,11 @@ class MockQdrantClient {
125
114
  limit: params.limit,
126
115
  filter: params.filter,
127
116
  });
128
- const channel = params.using as "dense" | "sparse";
129
- const queue =
130
- name === "memory_v2_skills"
131
- ? state.skillQueryResponses[channel]
132
- : state.queryResponses[channel];
133
- 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: [] };
134
122
  }
135
123
  }
136
124
 
@@ -138,6 +126,37 @@ mock.module("@qdrant/js-client-rest", () => ({
138
126
  QdrantClient: MockQdrantClient,
139
127
  }));
140
128
 
129
+ // Reranker mock — keeps the activation tests hermetic when rerank.enabled is
130
+ // flipped on by an integration case. Tests stage `rerankState.scores` to
131
+ // program the boost outcome. The activation pipeline now passes both the
132
+ // user-channel and assistant-channel queries into a single rerank call, so
133
+ // `rerankState.calls` records the full `queries` array per invocation.
134
+ const rerankState = {
135
+ scores: null as Map<string, number> | null,
136
+ calls: [] as Array<{ queries: string[]; candidates: string[] }>,
137
+ };
138
+ mock.module("../reranker.js", () => ({
139
+ rerankCandidates: async (
140
+ queries: readonly string[],
141
+ candidates: readonly string[],
142
+ ): Promise<Array<Map<string, number>>> => {
143
+ rerankState.calls.push({
144
+ queries: [...queries],
145
+ candidates: [...candidates],
146
+ });
147
+ return queries.map(() => {
148
+ if (rerankState.scores === null) return new Map();
149
+ const out = new Map<string, number>();
150
+ for (const slug of candidates) {
151
+ const v = rerankState.scores.get(slug);
152
+ if (v !== undefined) out.set(slug, v);
153
+ }
154
+ return out;
155
+ });
156
+ },
157
+ _resetRerankCacheForTests: () => {},
158
+ }));
159
+
141
160
  // Static `import type` is fine — types erase, so they don't run module-init
142
161
  // code that would race the mocks above.
143
162
  import type { EdgeIndex } from "../edge-index.js";
@@ -145,15 +164,11 @@ import type { ActivationState } from "../types.js";
145
164
 
146
165
  const {
147
166
  computeOwnActivation,
148
- computeSkillActivation,
149
167
  selectCandidates,
150
168
  selectInjections,
151
- selectSkillInjections,
152
169
  spreadActivation,
153
170
  } = await import("../activation.js");
154
171
  const { _resetMemoryV2QdrantForTests } = await import("../qdrant.js");
155
- const { _resetMemoryV2SkillQdrantForTests } =
156
- await import("../skill-qdrant.js");
157
172
 
158
173
  // ---------------------------------------------------------------------------
159
174
  // Helpers
@@ -166,16 +181,15 @@ function resetState(): void {
166
181
  state.sparseReturn = { indices: [1, 2, 3], values: [0.5, 0.5, 0.5] };
167
182
  state.queryResponses.dense.length = 0;
168
183
  state.queryResponses.sparse.length = 0;
169
- state.skillQueryResponses.dense.length = 0;
170
- state.skillQueryResponses.sparse.length = 0;
171
184
  state.queryCalls.length = 0;
185
+ rerankState.scores = null;
186
+ rerankState.calls.length = 0;
172
187
  // Bun's `mock.module` persists across files in the same process, so the
173
- // qdrant modules' `_client` singletons may already hold a MockQdrantClient
174
- // instance from a sibling test file (e.g. sim.test.ts). Resetting both the
188
+ // qdrant module's `_client` singleton may already hold a MockQdrantClient
189
+ // instance from a sibling test file (e.g. sim.test.ts). Resetting the
175
190
  // cache AND any latched readiness forces a fresh `new QdrantClient()` —
176
191
  // which under our mock above resolves to *this* file's MockQdrantClient.
177
192
  _resetMemoryV2QdrantForTests();
178
- _resetMemoryV2SkillQdrantForTests();
179
193
  }
180
194
 
181
195
  /**
@@ -212,9 +226,20 @@ function makeConfig(
212
226
  } as unknown as AssistantConfig;
213
227
  }
214
228
 
215
- /** Stage a single dense + sparse pair on the response queues. */
229
+ /**
230
+ * Stage a single hybrid-query response — body channels first, then summary
231
+ * channels (which default to empty). The four-channel hybrid query fires
232
+ * body-dense, body-sparse, summary-dense, summary-sparse in that order, so
233
+ * each logical call consumes 2 dense + 2 sparse queue entries.
234
+ */
216
235
  function stageHybridResponse(
217
- hits: Array<{ slug: string; denseScore?: number; sparseScore?: number }>,
236
+ hits: Array<{
237
+ slug: string;
238
+ denseScore?: number;
239
+ sparseScore?: number;
240
+ summaryDenseScore?: number;
241
+ summarySparseScore?: number;
242
+ }>,
218
243
  ): void {
219
244
  state.queryResponses.dense.push({
220
245
  points: hits
@@ -226,6 +251,22 @@ function stageHybridResponse(
226
251
  .filter((h) => h.sparseScore !== undefined)
227
252
  .map((h) => ({ score: h.sparseScore, payload: { slug: h.slug } })),
228
253
  });
254
+ state.queryResponses.dense.push({
255
+ points: hits
256
+ .filter((h) => h.summaryDenseScore !== undefined)
257
+ .map((h) => ({
258
+ score: h.summaryDenseScore,
259
+ payload: { slug: h.slug },
260
+ })),
261
+ });
262
+ state.queryResponses.sparse.push({
263
+ points: hits
264
+ .filter((h) => h.summarySparseScore !== undefined)
265
+ .map((h) => ({
266
+ score: h.summarySparseScore,
267
+ payload: { slug: h.slug },
268
+ })),
269
+ });
229
270
  }
230
271
 
231
272
  beforeEach(resetState);
@@ -358,7 +399,7 @@ describe("selectCandidates", () => {
358
399
  nowText: "",
359
400
  config: makeConfig(),
360
401
  });
361
- expect(state.queryCalls).toHaveLength(2);
402
+ expect(state.queryCalls).toHaveLength(4);
362
403
  for (const call of state.queryCalls) {
363
404
  expect(call.limit).toBe(1_000_000);
364
405
  expect(call.filter).toBeUndefined();
@@ -374,7 +415,7 @@ describe("selectCandidates", () => {
374
415
  nowText: "",
375
416
  config: makeConfig({ ann_candidate_limit: 25 }),
376
417
  });
377
- expect(state.queryCalls).toHaveLength(2);
418
+ expect(state.queryCalls).toHaveLength(4);
378
419
  for (const call of state.queryCalls) {
379
420
  expect(call.limit).toBe(25);
380
421
  expect(call.filter).toBeUndefined();
@@ -554,6 +595,257 @@ describe("computeOwnActivation", () => {
554
595
  // No prior state → prev=0 → priorContribution=0 regardless of `d`.
555
596
  expect(out.breakdown.get("fresh")?.priorContribution).toBe(0);
556
597
  });
598
+
599
+ test("rerank boost on user/assistant flips top-1 when fused had it second", async () => {
600
+ // Three Qdrant queries fire in parallel inside computeOwnActivation:
601
+ // user, assistant, now. Stage identical hits for each so the only signal
602
+ // separating slugs is the rerank boost on the user + assistant channels.
603
+ const stagedHits = [
604
+ { slug: "lexical", denseScore: 0.6, sparseScore: 0 },
605
+ { slug: "semantic", denseScore: 0.5, sparseScore: 0 },
606
+ ];
607
+ stageHybridResponse(stagedHits); // user channel
608
+ stageHybridResponse(stagedHits); // assistant channel
609
+ stageHybridResponse(stagedHits); // now channel
610
+ rerankState.scores = new Map([
611
+ ["lexical", 0.05],
612
+ ["semantic", 0.95],
613
+ ]);
614
+
615
+ const config = {
616
+ memory: {
617
+ v2: {
618
+ d: 0.0,
619
+ c_user: 0.5,
620
+ c_assistant: 0.5,
621
+ c_now: 0.0,
622
+ dense_weight: 1.0,
623
+ sparse_weight: 0.0,
624
+ rerank: {
625
+ enabled: true,
626
+ top_k: 50,
627
+ alpha: 0.5,
628
+ model: "test-model",
629
+ },
630
+ },
631
+ },
632
+ } as unknown as AssistantConfig;
633
+
634
+ const out = await computeOwnActivation({
635
+ candidates: new Set(["lexical", "semantic"]),
636
+ priorState: null,
637
+ userText: "u",
638
+ assistantText: "a",
639
+ nowText: "n",
640
+ config,
641
+ });
642
+
643
+ // Without rerank: lexical (0.6) would beat semantic (0.5) on both
644
+ // user and assistant channels.
645
+ // With rerank (alpha=0.5):
646
+ // lexical: 0.6 + 0.5 · (0.05/0.95) ≈ 0.626
647
+ // semantic: 0.5 + 0.5 · 1.0 = 1.0
648
+ // The semantic candidate now wins on both rerank-boosted channels.
649
+ expect(out.activation.get("semantic")!).toBeGreaterThan(
650
+ out.activation.get("lexical")!,
651
+ );
652
+ // Both rerank-enabled channels ride in a single batched rerank call.
653
+ expect(rerankState.calls).toHaveLength(1);
654
+ expect(rerankState.calls[0].queries).toEqual(["u", "a"]);
655
+ });
656
+
657
+ test("rerank pool is the unified top-K by pre-rerank A_o, not per-channel fused", async () => {
658
+ // Three candidates. The per-channel fused-sim top-2s would have picked
659
+ // different sets:
660
+ // user channel: a=0.9, b=0.5, c=0.4 → per-channel top-2 = [a, b]
661
+ // assistant channel: a=0.5, b=0.4, c=0.9 → per-channel top-2 = [c, a]
662
+ // But pre-rerank A_o (c_user=c_assistant=0.5) is:
663
+ // a = 0.5·0.9 + 0.5·0.5 = 0.70
664
+ // b = 0.5·0.5 + 0.5·0.4 = 0.45
665
+ // c = 0.5·0.4 + 0.5·0.9 = 0.65
666
+ // → unified top-2 = [a, c]. b drops out, even though it would have made
667
+ // the user-channel pool under the old per-channel selection.
668
+ stageHybridResponse([
669
+ { slug: "a", denseScore: 0.9 },
670
+ { slug: "b", denseScore: 0.5 },
671
+ { slug: "c", denseScore: 0.4 },
672
+ ]); // user
673
+ stageHybridResponse([
674
+ { slug: "a", denseScore: 0.5 },
675
+ { slug: "b", denseScore: 0.4 },
676
+ { slug: "c", denseScore: 0.9 },
677
+ ]); // assistant
678
+ stageHybridResponse([]); // now (no signal)
679
+ rerankState.scores = new Map([
680
+ ["a", 0.5],
681
+ ["b", 0.5],
682
+ ["c", 0.5],
683
+ ]);
684
+
685
+ const config = {
686
+ memory: {
687
+ v2: {
688
+ d: 0.0,
689
+ c_user: 0.5,
690
+ c_assistant: 0.5,
691
+ c_now: 0.0,
692
+ dense_weight: 1.0,
693
+ sparse_weight: 0.0,
694
+ rerank: {
695
+ enabled: true,
696
+ top_k: 2,
697
+ alpha: 0.3,
698
+ model: "test-model",
699
+ },
700
+ },
701
+ },
702
+ } as unknown as AssistantConfig;
703
+
704
+ await computeOwnActivation({
705
+ candidates: new Set(["a", "b", "c"]),
706
+ priorState: null,
707
+ userText: "u",
708
+ assistantText: "a",
709
+ nowText: "",
710
+ config,
711
+ });
712
+
713
+ // Single batched rerank call carrying both channel queries against the
714
+ // unified slug set, sorted by pre-rerank A_o descending.
715
+ expect(rerankState.calls).toHaveLength(1);
716
+ expect(rerankState.calls[0].queries).toEqual(["u", "a"]);
717
+ expect(rerankState.calls[0].candidates).toEqual(["a", "c"]);
718
+ });
719
+
720
+ test("rerank-disabled candidates outside the unified pool get zero boost", async () => {
721
+ // Two candidates, top_k=1. The lower pre-rerank A_o slug must end up
722
+ // with simUserRerankBoost=0 / simAssistantRerankBoost=0 in the breakdown.
723
+ stageHybridResponse([
724
+ { slug: "winner", denseScore: 0.9 },
725
+ { slug: "loser", denseScore: 0.2 },
726
+ ]); // user
727
+ stageHybridResponse([
728
+ { slug: "winner", denseScore: 0.9 },
729
+ { slug: "loser", denseScore: 0.2 },
730
+ ]); // assistant
731
+ stageHybridResponse([]); // now
732
+ // The mocked reranker hands back scores for whatever slugs it's
733
+ // called with. Stage scores for both; the assertion below is that
734
+ // the loser still receives 0 because it's never sent to the
735
+ // reranker — top_k=1 cuts it off.
736
+ rerankState.scores = new Map([
737
+ ["winner", 0.5],
738
+ ["loser", 0.5],
739
+ ]);
740
+
741
+ const config = {
742
+ memory: {
743
+ v2: {
744
+ d: 0.0,
745
+ c_user: 0.5,
746
+ c_assistant: 0.5,
747
+ c_now: 0.0,
748
+ dense_weight: 1.0,
749
+ sparse_weight: 0.0,
750
+ rerank: {
751
+ enabled: true,
752
+ top_k: 1,
753
+ alpha: 0.3,
754
+ model: "test-model",
755
+ },
756
+ },
757
+ },
758
+ } as unknown as AssistantConfig;
759
+
760
+ const out = await computeOwnActivation({
761
+ candidates: new Set(["winner", "loser"]),
762
+ priorState: null,
763
+ userText: "u",
764
+ assistantText: "a",
765
+ nowText: "",
766
+ config,
767
+ });
768
+
769
+ expect(out.breakdown.get("loser")?.simUserRerankBoost).toBe(0);
770
+ expect(out.breakdown.get("loser")?.simAssistantRerankBoost).toBe(0);
771
+ expect(out.breakdown.get("winner")?.simUserRerankBoost).toBeGreaterThan(0);
772
+ expect(
773
+ out.breakdown.get("winner")?.simAssistantRerankBoost,
774
+ ).toBeGreaterThan(0);
775
+ // inRerankPool tags pool membership independently of the boost value, so
776
+ // the inspector can keep the rerank rows visible even when the channel
777
+ // max happened to normalise to 0.
778
+ expect(out.breakdown.get("winner")?.inRerankPool).toBe(true);
779
+ expect(out.breakdown.get("loser")?.inRerankPool).toBe(false);
780
+ });
781
+
782
+ test("inRerankPool is false for every slug when rerank is disabled", async () => {
783
+ stageHybridResponse([{ slug: "alice", denseScore: 0.5 }]);
784
+ stageHybridResponse([{ slug: "alice", denseScore: 0.4 }]);
785
+ stageHybridResponse([{ slug: "alice", denseScore: 0.2 }]);
786
+
787
+ // No `rerank` block at all → rerankCfg is undefined and the rerank
788
+ // branch never runs, so no slug is in the pool.
789
+ const out = await computeOwnActivation({
790
+ candidates: new Set(["alice"]),
791
+ priorState: null,
792
+ userText: "u",
793
+ assistantText: "a",
794
+ nowText: "n",
795
+ config: makeConfig(),
796
+ });
797
+
798
+ expect(out.breakdown.get("alice")?.inRerankPool).toBe(false);
799
+ expect(out.breakdown.get("alice")?.simUserRerankBoost).toBe(0);
800
+ expect(out.breakdown.get("alice")?.simAssistantRerankBoost).toBe(0);
801
+ });
802
+
803
+ test("rerank boost is additive on A_o and leaves raw simUser / simAssistant untouched", async () => {
804
+ stageHybridResponse([{ slug: "a", denseScore: 0.5 }]); // user
805
+ stageHybridResponse([{ slug: "a", denseScore: 0.4 }]); // assistant
806
+ stageHybridResponse([]); // now
807
+ rerankState.scores = new Map([["a", 0.8]]);
808
+
809
+ const config = {
810
+ memory: {
811
+ v2: {
812
+ d: 0.0,
813
+ c_user: 0.5,
814
+ c_assistant: 0.5,
815
+ c_now: 0.0,
816
+ dense_weight: 1.0,
817
+ sparse_weight: 0.0,
818
+ rerank: {
819
+ enabled: true,
820
+ top_k: 50,
821
+ alpha: 0.4,
822
+ model: "test-model",
823
+ },
824
+ },
825
+ },
826
+ } as unknown as AssistantConfig;
827
+
828
+ const out = await computeOwnActivation({
829
+ candidates: new Set(["a"]),
830
+ priorState: null,
831
+ userText: "u",
832
+ assistantText: "a",
833
+ nowText: "",
834
+ config,
835
+ });
836
+
837
+ const breakdown = out.breakdown.get("a");
838
+ // Raw fused similarities are reported untouched by rerank.
839
+ expect(breakdown?.simUser).toBeCloseTo(0.5, 6);
840
+ expect(breakdown?.simAssistant).toBeCloseTo(0.4, 6);
841
+ // Both rerank deltas are alpha · r_norm = 0.4 · 1.0 = 0.4 (single
842
+ // candidate normalises to 1.0 in each channel).
843
+ expect(breakdown?.simUserRerankBoost).toBeCloseTo(0.4, 6);
844
+ expect(breakdown?.simAssistantRerankBoost).toBeCloseTo(0.4, 6);
845
+ // Final A_o = c_user·simU + c_assistant·simA + c_user·boostU + c_assistant·boostA
846
+ // = 0.5·0.5 + 0.5·0.4 + 0.5·0.4 + 0.5·0.4 = 0.25+0.20+0.20+0.20 = 0.85
847
+ expect(out.activation.get("a")).toBeCloseTo(0.85, 6);
848
+ });
557
849
  });
558
850
 
559
851
  // ---------------------------------------------------------------------------
@@ -895,48 +1187,26 @@ describe("selectInjections", () => {
895
1187
  });
896
1188
 
897
1189
  // ---------------------------------------------------------------------------
898
- // computeSkillActivation
1190
+ // Skills as concept slugs — the unified pool
899
1191
  // ---------------------------------------------------------------------------
1192
+ //
1193
+ // Skills participate in the concept-page pipeline under the slug prefix
1194
+ // `skills/<id>`. There is no longer a dedicated skill activation function;
1195
+ // the only post-unification behavioral assertion worth preserving here is
1196
+ // that a `skills/<id>` slug flows through `computeOwnActivation` exactly
1197
+ // like a concept slug — same formula, same clamp, same breakdown shape.
1198
+
1199
+ describe("skills participate in the unified pipeline", () => {
1200
+ test("computeOwnActivation scores a `skills/<id>` slug like any concept slug", async () => {
1201
+ // Three simBatch responses, one per channel (user/assistant/now), with
1202
+ // a single skill-prefixed slug as the only candidate.
1203
+ stageHybridResponse([{ slug: "skills/example-skill-a", denseScore: 0.5 }]);
1204
+ stageHybridResponse([{ slug: "skills/example-skill-a", denseScore: 0.4 }]);
1205
+ stageHybridResponse([{ slug: "skills/example-skill-a", denseScore: 0.2 }]);
900
1206
 
901
- /** Stage a single hybrid response on the skills queues (payload key = `id`). */
902
- function stageSkillHybridResponse(
903
- hits: Array<{ id: string; denseScore?: number; sparseScore?: number }>,
904
- ): void {
905
- state.skillQueryResponses.dense.push({
906
- points: hits
907
- .filter((h) => h.denseScore !== undefined)
908
- .map((h) => ({ score: h.denseScore, payload: { id: h.id } })),
909
- });
910
- state.skillQueryResponses.sparse.push({
911
- points: hits
912
- .filter((h) => h.sparseScore !== undefined)
913
- .map((h) => ({ score: h.sparseScore, payload: { id: h.id } })),
914
- });
915
- }
916
-
917
- describe("computeSkillActivation", () => {
918
- test("empty candidates short-circuits without backend calls", async () => {
919
- const out = await computeSkillActivation({
920
- candidates: new Set(),
921
- userText: "u",
922
- assistantText: "a",
923
- nowText: "n",
924
- config: makeConfig(),
925
- });
926
- expect(out.activation.size).toBe(0);
927
- expect(out.breakdown.size).toBe(0);
928
- expect(state.embedCalls).toHaveLength(0);
929
- expect(state.queryCalls).toHaveLength(0);
930
- });
931
-
932
- test("applies similarity-only formula with no decay term", async () => {
933
- // Stage three skill responses — one per `simSkillBatch` call.
934
- stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]); // simU
935
- stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.4 }]); // simA
936
- stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.2 }]); // simN
937
-
938
- const out = await computeSkillActivation({
939
- candidates: new Set(["example-skill-a"]),
1207
+ const out = await computeOwnActivation({
1208
+ candidates: new Set(["skills/example-skill-a"]),
1209
+ priorState: null,
940
1210
  userText: "u",
941
1211
  assistantText: "a",
942
1212
  nowText: "n",
@@ -947,191 +1217,12 @@ describe("computeSkillActivation", () => {
947
1217
  c_now: 0.2,
948
1218
  }),
949
1219
  });
950
- // No `d · prev` term: 0.3*0.5 + 0.2*0.4 + 0.2*0.2 = 0.15 + 0.08 + 0.04 = 0.27
951
- expect(out.activation.get("example-skill-a")).toBeCloseTo(0.27, 6);
952
- });
953
-
954
- test("output excludes any decay term — d coefficient is unused", async () => {
955
- // The skill activation formula is `c_user·simU + c_assistant·simA +
956
- // c_now·simN`. Run with d=0.9 and d=0.0 — if the implementation
957
- // accidentally included a `d · prev` term, the two would diverge. The
958
- // function has no priorState parameter, so prev=0; both runs must equal
959
- // the d-free formula exactly. Stage three sim responses per run.
960
- const stage = () => {
961
- stageSkillHybridResponse([{ id: "alpha", denseScore: 0.4 }]);
962
- stageSkillHybridResponse([{ id: "alpha", denseScore: 0.4 }]);
963
- stageSkillHybridResponse([{ id: "alpha", denseScore: 0.4 }]);
964
- };
965
- const baseConfig = { c_user: 0.3, c_assistant: 0.2, c_now: 0.2 };
966
-
967
- stage();
968
- const withHighD = await computeSkillActivation({
969
- candidates: new Set(["alpha"]),
970
- userText: "u",
971
- assistantText: "a",
972
- nowText: "n",
973
- config: makeConfig({ ...baseConfig, d: 0.9 }),
974
- });
975
- stage();
976
- const withZeroD = await computeSkillActivation({
977
- candidates: new Set(["alpha"]),
978
- userText: "u",
979
- assistantText: "a",
980
- nowText: "n",
981
- config: makeConfig({ ...baseConfig, d: 0.0 }),
982
- });
983
-
984
- // Both equal `0.3*0.4 + 0.2*0.4 + 0.2*0.4 = 0.28` — d is ignored.
985
- expect(withHighD.activation.get("alpha")).toBeCloseTo(0.28, 6);
986
- expect(withZeroD.activation.get("alpha")).toBeCloseTo(0.28, 6);
987
- });
988
-
989
- test("clamps over-1.0 results down to [0, 1]", async () => {
990
- stageSkillHybridResponse([{ id: "loud-skill", denseScore: 1.0 }]); // simU
991
- stageSkillHybridResponse([{ id: "loud-skill", denseScore: 1.0 }]); // simA
992
- stageSkillHybridResponse([{ id: "loud-skill", denseScore: 1.0 }]); // simN
993
-
994
- // Coefficients intentionally sum to > 1 so the unclamped result
995
- // overshoots — the implementation must still produce <= 1.0.
996
- const out = await computeSkillActivation({
997
- candidates: new Set(["loud-skill"]),
998
- userText: "u",
999
- assistantText: "a",
1000
- nowText: "n",
1001
- config: makeConfig({
1002
- c_user: 0.5,
1003
- c_assistant: 0.5,
1004
- c_now: 0.5,
1005
- }),
1006
- });
1007
- expect(out.activation.get("loud-skill")).toBe(1);
1008
- });
1009
-
1010
- test("candidate with no sim hits resolves to 0", async () => {
1011
- stageSkillHybridResponse([]);
1012
- stageSkillHybridResponse([]);
1013
- stageSkillHybridResponse([]);
1014
-
1015
- const out = await computeSkillActivation({
1016
- candidates: new Set(["ghost-skill"]),
1017
- userText: "u",
1018
- assistantText: "a",
1019
- nowText: "n",
1020
- config: makeConfig(),
1021
- });
1022
- expect(out.activation.get("ghost-skill")).toBe(0);
1023
- });
1024
-
1025
- test("breakdown captures the raw sims for each candidate", async () => {
1026
- stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]); // simU
1027
- stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.4 }]); // simA
1028
- stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.2 }]); // simN
1029
-
1030
- const out = await computeSkillActivation({
1031
- candidates: new Set(["example-skill-a"]),
1032
- userText: "u",
1033
- assistantText: "a",
1034
- nowText: "n",
1035
- config: makeConfig({
1036
- c_user: 0.3,
1037
- c_assistant: 0.2,
1038
- c_now: 0.2,
1039
- }),
1040
- });
1041
- const breakdown = out.breakdown.get("example-skill-a");
1042
- expect(breakdown).toBeDefined();
1043
- expect(breakdown?.simUser).toBeCloseTo(0.5, 6);
1044
- expect(breakdown?.simAssistant).toBeCloseTo(0.4, 6);
1045
- expect(breakdown?.simNow).toBeCloseTo(0.2, 6);
1046
- });
1047
-
1048
- test("uses the dedicated skills collection and never queries concept pages", async () => {
1049
- stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]);
1050
- stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]);
1051
- stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]);
1052
-
1053
- await computeSkillActivation({
1054
- candidates: new Set(["example-skill-a"]),
1055
- userText: "u",
1056
- assistantText: "a",
1057
- nowText: "n",
1058
- config: makeConfig(),
1059
- });
1060
-
1061
- // Three simSkillBatch calls × 2 channels = 6 total queries, all against
1062
- // the skills collection. No spread → no extra calls beyond these.
1063
- expect(state.queryCalls).toHaveLength(6);
1064
- for (const call of state.queryCalls) {
1065
- expect(call.collection).toBe("memory_v2_skills");
1066
- }
1067
- });
1068
- });
1069
-
1070
- // ---------------------------------------------------------------------------
1071
- // selectSkillInjections
1072
- // ---------------------------------------------------------------------------
1073
-
1074
- describe("selectSkillInjections", () => {
1075
- test("returns empty when activation is empty", () => {
1076
- const out = selectSkillInjections({ A: new Map(), topK: 5 });
1077
- expect(out).toEqual({ topNow: [] });
1078
- });
1079
-
1080
- test("returns empty when topK is 0", () => {
1081
- const out = selectSkillInjections({
1082
- A: new Map([
1083
- ["example-skill-a", 0.5],
1084
- ["example-skill-b", 0.4],
1085
- ]),
1086
- topK: 0,
1087
- });
1088
- expect(out).toEqual({ topNow: [] });
1089
- });
1090
-
1091
- test("ranks by activation descending and trims to topK", () => {
1092
- const out = selectSkillInjections({
1093
- A: new Map([
1094
- ["example-skill-a", 0.1],
1095
- ["example-skill-b", 0.9],
1096
- ["example-skill-c", 0.5],
1097
- ["example-skill-d", 0.3],
1098
- ]),
1099
- topK: 2,
1100
- });
1101
- expect(out.topNow).toEqual(["example-skill-b", "example-skill-c"]);
1102
- });
1103
1220
 
1104
- test("skills are stateless: the same id may be returned on consecutive turns", () => {
1105
- // No `everInjected` parameter exists selectSkillInjections takes only
1106
- // the activation map and topK. So calling it twice with the same A map
1107
- // returns the same result; there is no dedup against prior turns.
1108
- const A = new Map([
1109
- ["example-skill-a", 0.9],
1110
- ["example-skill-b", 0.5],
1111
- ]);
1112
- const turn1 = selectSkillInjections({ A, topK: 5 });
1113
- const turn2 = selectSkillInjections({ A, topK: 5 });
1114
- expect(turn1.topNow).toEqual(["example-skill-a", "example-skill-b"]);
1115
- expect(turn2.topNow).toEqual(turn1.topNow);
1116
- });
1117
-
1118
- test("breaks ties by id ascending for deterministic output", () => {
1119
- const out = selectSkillInjections({
1120
- A: new Map([
1121
- ["zeta-skill", 0.5],
1122
- ["example-skill-a", 0.5],
1123
- ["mike-skill", 0.5],
1124
- ]),
1125
- topK: 5,
1126
- });
1127
- expect(out.topNow).toEqual(["example-skill-a", "mike-skill", "zeta-skill"]);
1128
- });
1129
-
1130
- test("topK clamps to the available activation entries", () => {
1131
- const out = selectSkillInjections({
1132
- A: new Map([["only-skill", 0.7]]),
1133
- topK: 100,
1134
- });
1135
- expect(out.topNow).toEqual(["only-skill"]);
1221
+ // No prior state priorContribution = 0.
1222
+ // 0.3*0.5 + 0.2*0.4 + 0.2*0.2 = 0.15 + 0.08 + 0.04 = 0.27
1223
+ expect(out.activation.get("skills/example-skill-a")).toBeCloseTo(0.27, 6);
1224
+ expect(out.breakdown.get("skills/example-skill-a")?.priorContribution).toBe(
1225
+ 0,
1226
+ );
1136
1227
  });
1137
1228
  });