@vellumai/assistant 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -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;
@@ -126,11 +115,7 @@ class MockQdrantClient {
126
115
  filter: params.filter,
127
116
  });
128
117
  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: [] };
118
+ return state.queryResponses[channel].shift() ?? { points: [] };
134
119
  }
135
120
  }
136
121
 
@@ -138,6 +123,37 @@ mock.module("@qdrant/js-client-rest", () => ({
138
123
  QdrantClient: MockQdrantClient,
139
124
  }));
140
125
 
126
+ // Reranker mock — keeps the activation tests hermetic when rerank.enabled is
127
+ // flipped on by an integration case. Tests stage `rerankState.scores` to
128
+ // program the boost outcome. The activation pipeline now passes both the
129
+ // user-channel and assistant-channel queries into a single rerank call, so
130
+ // `rerankState.calls` records the full `queries` array per invocation.
131
+ const rerankState = {
132
+ scores: null as Map<string, number> | null,
133
+ calls: [] as Array<{ queries: string[]; candidates: string[] }>,
134
+ };
135
+ mock.module("../reranker.js", () => ({
136
+ rerankCandidates: async (
137
+ queries: readonly string[],
138
+ candidates: readonly string[],
139
+ ): Promise<Array<Map<string, number>>> => {
140
+ rerankState.calls.push({
141
+ queries: [...queries],
142
+ candidates: [...candidates],
143
+ });
144
+ return queries.map(() => {
145
+ if (rerankState.scores === null) return new Map();
146
+ const out = new Map<string, number>();
147
+ for (const slug of candidates) {
148
+ const v = rerankState.scores.get(slug);
149
+ if (v !== undefined) out.set(slug, v);
150
+ }
151
+ return out;
152
+ });
153
+ },
154
+ _resetRerankCacheForTests: () => {},
155
+ }));
156
+
141
157
  // Static `import type` is fine — types erase, so they don't run module-init
142
158
  // code that would race the mocks above.
143
159
  import type { EdgeIndex } from "../edge-index.js";
@@ -145,15 +161,11 @@ import type { ActivationState } from "../types.js";
145
161
 
146
162
  const {
147
163
  computeOwnActivation,
148
- computeSkillActivation,
149
164
  selectCandidates,
150
165
  selectInjections,
151
- selectSkillInjections,
152
166
  spreadActivation,
153
167
  } = await import("../activation.js");
154
168
  const { _resetMemoryV2QdrantForTests } = await import("../qdrant.js");
155
- const { _resetMemoryV2SkillQdrantForTests } =
156
- await import("../skill-qdrant.js");
157
169
 
158
170
  // ---------------------------------------------------------------------------
159
171
  // Helpers
@@ -166,16 +178,15 @@ function resetState(): void {
166
178
  state.sparseReturn = { indices: [1, 2, 3], values: [0.5, 0.5, 0.5] };
167
179
  state.queryResponses.dense.length = 0;
168
180
  state.queryResponses.sparse.length = 0;
169
- state.skillQueryResponses.dense.length = 0;
170
- state.skillQueryResponses.sparse.length = 0;
171
181
  state.queryCalls.length = 0;
182
+ rerankState.scores = null;
183
+ rerankState.calls.length = 0;
172
184
  // 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
185
+ // qdrant module's `_client` singleton may already hold a MockQdrantClient
186
+ // instance from a sibling test file (e.g. sim.test.ts). Resetting the
175
187
  // cache AND any latched readiness forces a fresh `new QdrantClient()` —
176
188
  // which under our mock above resolves to *this* file's MockQdrantClient.
177
189
  _resetMemoryV2QdrantForTests();
178
- _resetMemoryV2SkillQdrantForTests();
179
190
  }
180
191
 
181
192
  /**
@@ -554,6 +565,257 @@ describe("computeOwnActivation", () => {
554
565
  // No prior state → prev=0 → priorContribution=0 regardless of `d`.
555
566
  expect(out.breakdown.get("fresh")?.priorContribution).toBe(0);
556
567
  });
568
+
569
+ test("rerank boost on user/assistant flips top-1 when fused had it second", async () => {
570
+ // Three Qdrant queries fire in parallel inside computeOwnActivation:
571
+ // user, assistant, now. Stage identical hits for each so the only signal
572
+ // separating slugs is the rerank boost on the user + assistant channels.
573
+ const stagedHits = [
574
+ { slug: "lexical", denseScore: 0.6, sparseScore: 0 },
575
+ { slug: "semantic", denseScore: 0.5, sparseScore: 0 },
576
+ ];
577
+ stageHybridResponse(stagedHits); // user channel
578
+ stageHybridResponse(stagedHits); // assistant channel
579
+ stageHybridResponse(stagedHits); // now channel
580
+ rerankState.scores = new Map([
581
+ ["lexical", 0.05],
582
+ ["semantic", 0.95],
583
+ ]);
584
+
585
+ const config = {
586
+ memory: {
587
+ v2: {
588
+ d: 0.0,
589
+ c_user: 0.5,
590
+ c_assistant: 0.5,
591
+ c_now: 0.0,
592
+ dense_weight: 1.0,
593
+ sparse_weight: 0.0,
594
+ rerank: {
595
+ enabled: true,
596
+ top_k: 50,
597
+ alpha: 0.5,
598
+ model: "test-model",
599
+ },
600
+ },
601
+ },
602
+ } as unknown as AssistantConfig;
603
+
604
+ const out = await computeOwnActivation({
605
+ candidates: new Set(["lexical", "semantic"]),
606
+ priorState: null,
607
+ userText: "u",
608
+ assistantText: "a",
609
+ nowText: "n",
610
+ config,
611
+ });
612
+
613
+ // Without rerank: lexical (0.6) would beat semantic (0.5) on both
614
+ // user and assistant channels.
615
+ // With rerank (alpha=0.5):
616
+ // lexical: 0.6 + 0.5 · (0.05/0.95) ≈ 0.626
617
+ // semantic: 0.5 + 0.5 · 1.0 = 1.0
618
+ // The semantic candidate now wins on both rerank-boosted channels.
619
+ expect(out.activation.get("semantic")!).toBeGreaterThan(
620
+ out.activation.get("lexical")!,
621
+ );
622
+ // Both rerank-enabled channels ride in a single batched rerank call.
623
+ expect(rerankState.calls).toHaveLength(1);
624
+ expect(rerankState.calls[0].queries).toEqual(["u", "a"]);
625
+ });
626
+
627
+ test("rerank pool is the unified top-K by pre-rerank A_o, not per-channel fused", async () => {
628
+ // Three candidates. The per-channel fused-sim top-2s would have picked
629
+ // different sets:
630
+ // user channel: a=0.9, b=0.5, c=0.4 → per-channel top-2 = [a, b]
631
+ // assistant channel: a=0.5, b=0.4, c=0.9 → per-channel top-2 = [c, a]
632
+ // But pre-rerank A_o (c_user=c_assistant=0.5) is:
633
+ // a = 0.5·0.9 + 0.5·0.5 = 0.70
634
+ // b = 0.5·0.5 + 0.5·0.4 = 0.45
635
+ // c = 0.5·0.4 + 0.5·0.9 = 0.65
636
+ // → unified top-2 = [a, c]. b drops out, even though it would have made
637
+ // the user-channel pool under the old per-channel selection.
638
+ stageHybridResponse([
639
+ { slug: "a", denseScore: 0.9 },
640
+ { slug: "b", denseScore: 0.5 },
641
+ { slug: "c", denseScore: 0.4 },
642
+ ]); // user
643
+ stageHybridResponse([
644
+ { slug: "a", denseScore: 0.5 },
645
+ { slug: "b", denseScore: 0.4 },
646
+ { slug: "c", denseScore: 0.9 },
647
+ ]); // assistant
648
+ stageHybridResponse([]); // now (no signal)
649
+ rerankState.scores = new Map([
650
+ ["a", 0.5],
651
+ ["b", 0.5],
652
+ ["c", 0.5],
653
+ ]);
654
+
655
+ const config = {
656
+ memory: {
657
+ v2: {
658
+ d: 0.0,
659
+ c_user: 0.5,
660
+ c_assistant: 0.5,
661
+ c_now: 0.0,
662
+ dense_weight: 1.0,
663
+ sparse_weight: 0.0,
664
+ rerank: {
665
+ enabled: true,
666
+ top_k: 2,
667
+ alpha: 0.3,
668
+ model: "test-model",
669
+ },
670
+ },
671
+ },
672
+ } as unknown as AssistantConfig;
673
+
674
+ await computeOwnActivation({
675
+ candidates: new Set(["a", "b", "c"]),
676
+ priorState: null,
677
+ userText: "u",
678
+ assistantText: "a",
679
+ nowText: "",
680
+ config,
681
+ });
682
+
683
+ // Single batched rerank call carrying both channel queries against the
684
+ // unified slug set, sorted by pre-rerank A_o descending.
685
+ expect(rerankState.calls).toHaveLength(1);
686
+ expect(rerankState.calls[0].queries).toEqual(["u", "a"]);
687
+ expect(rerankState.calls[0].candidates).toEqual(["a", "c"]);
688
+ });
689
+
690
+ test("rerank-disabled candidates outside the unified pool get zero boost", async () => {
691
+ // Two candidates, top_k=1. The lower pre-rerank A_o slug must end up
692
+ // with simUserRerankBoost=0 / simAssistantRerankBoost=0 in the breakdown.
693
+ stageHybridResponse([
694
+ { slug: "winner", denseScore: 0.9 },
695
+ { slug: "loser", denseScore: 0.2 },
696
+ ]); // user
697
+ stageHybridResponse([
698
+ { slug: "winner", denseScore: 0.9 },
699
+ { slug: "loser", denseScore: 0.2 },
700
+ ]); // assistant
701
+ stageHybridResponse([]); // now
702
+ // The mocked reranker hands back scores for whatever slugs it's
703
+ // called with. Stage scores for both; the assertion below is that
704
+ // the loser still receives 0 because it's never sent to the
705
+ // reranker — top_k=1 cuts it off.
706
+ rerankState.scores = new Map([
707
+ ["winner", 0.5],
708
+ ["loser", 0.5],
709
+ ]);
710
+
711
+ const config = {
712
+ memory: {
713
+ v2: {
714
+ d: 0.0,
715
+ c_user: 0.5,
716
+ c_assistant: 0.5,
717
+ c_now: 0.0,
718
+ dense_weight: 1.0,
719
+ sparse_weight: 0.0,
720
+ rerank: {
721
+ enabled: true,
722
+ top_k: 1,
723
+ alpha: 0.3,
724
+ model: "test-model",
725
+ },
726
+ },
727
+ },
728
+ } as unknown as AssistantConfig;
729
+
730
+ const out = await computeOwnActivation({
731
+ candidates: new Set(["winner", "loser"]),
732
+ priorState: null,
733
+ userText: "u",
734
+ assistantText: "a",
735
+ nowText: "",
736
+ config,
737
+ });
738
+
739
+ expect(out.breakdown.get("loser")?.simUserRerankBoost).toBe(0);
740
+ expect(out.breakdown.get("loser")?.simAssistantRerankBoost).toBe(0);
741
+ expect(out.breakdown.get("winner")?.simUserRerankBoost).toBeGreaterThan(0);
742
+ expect(
743
+ out.breakdown.get("winner")?.simAssistantRerankBoost,
744
+ ).toBeGreaterThan(0);
745
+ // inRerankPool tags pool membership independently of the boost value, so
746
+ // the inspector can keep the rerank rows visible even when the channel
747
+ // max happened to normalise to 0.
748
+ expect(out.breakdown.get("winner")?.inRerankPool).toBe(true);
749
+ expect(out.breakdown.get("loser")?.inRerankPool).toBe(false);
750
+ });
751
+
752
+ test("inRerankPool is false for every slug when rerank is disabled", async () => {
753
+ stageHybridResponse([{ slug: "alice", denseScore: 0.5 }]);
754
+ stageHybridResponse([{ slug: "alice", denseScore: 0.4 }]);
755
+ stageHybridResponse([{ slug: "alice", denseScore: 0.2 }]);
756
+
757
+ // No `rerank` block at all → rerankCfg is undefined and the rerank
758
+ // branch never runs, so no slug is in the pool.
759
+ const out = await computeOwnActivation({
760
+ candidates: new Set(["alice"]),
761
+ priorState: null,
762
+ userText: "u",
763
+ assistantText: "a",
764
+ nowText: "n",
765
+ config: makeConfig(),
766
+ });
767
+
768
+ expect(out.breakdown.get("alice")?.inRerankPool).toBe(false);
769
+ expect(out.breakdown.get("alice")?.simUserRerankBoost).toBe(0);
770
+ expect(out.breakdown.get("alice")?.simAssistantRerankBoost).toBe(0);
771
+ });
772
+
773
+ test("rerank boost is additive on A_o and leaves raw simUser / simAssistant untouched", async () => {
774
+ stageHybridResponse([{ slug: "a", denseScore: 0.5 }]); // user
775
+ stageHybridResponse([{ slug: "a", denseScore: 0.4 }]); // assistant
776
+ stageHybridResponse([]); // now
777
+ rerankState.scores = new Map([["a", 0.8]]);
778
+
779
+ const config = {
780
+ memory: {
781
+ v2: {
782
+ d: 0.0,
783
+ c_user: 0.5,
784
+ c_assistant: 0.5,
785
+ c_now: 0.0,
786
+ dense_weight: 1.0,
787
+ sparse_weight: 0.0,
788
+ rerank: {
789
+ enabled: true,
790
+ top_k: 50,
791
+ alpha: 0.4,
792
+ model: "test-model",
793
+ },
794
+ },
795
+ },
796
+ } as unknown as AssistantConfig;
797
+
798
+ const out = await computeOwnActivation({
799
+ candidates: new Set(["a"]),
800
+ priorState: null,
801
+ userText: "u",
802
+ assistantText: "a",
803
+ nowText: "",
804
+ config,
805
+ });
806
+
807
+ const breakdown = out.breakdown.get("a");
808
+ // Raw fused similarities are reported untouched by rerank.
809
+ expect(breakdown?.simUser).toBeCloseTo(0.5, 6);
810
+ expect(breakdown?.simAssistant).toBeCloseTo(0.4, 6);
811
+ // Both rerank deltas are alpha · r_norm = 0.4 · 1.0 = 0.4 (single
812
+ // candidate normalises to 1.0 in each channel).
813
+ expect(breakdown?.simUserRerankBoost).toBeCloseTo(0.4, 6);
814
+ expect(breakdown?.simAssistantRerankBoost).toBeCloseTo(0.4, 6);
815
+ // Final A_o = c_user·simU + c_assistant·simA + c_user·boostU + c_assistant·boostA
816
+ // = 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
817
+ expect(out.activation.get("a")).toBeCloseTo(0.85, 6);
818
+ });
557
819
  });
558
820
 
559
821
  // ---------------------------------------------------------------------------
@@ -895,48 +1157,26 @@ describe("selectInjections", () => {
895
1157
  });
896
1158
 
897
1159
  // ---------------------------------------------------------------------------
898
- // computeSkillActivation
1160
+ // Skills as concept slugs — the unified pool
899
1161
  // ---------------------------------------------------------------------------
1162
+ //
1163
+ // Skills participate in the concept-page pipeline under the slug prefix
1164
+ // `skills/<id>`. There is no longer a dedicated skill activation function;
1165
+ // the only post-unification behavioral assertion worth preserving here is
1166
+ // that a `skills/<id>` slug flows through `computeOwnActivation` exactly
1167
+ // like a concept slug — same formula, same clamp, same breakdown shape.
1168
+
1169
+ describe("skills participate in the unified pipeline", () => {
1170
+ test("computeOwnActivation scores a `skills/<id>` slug like any concept slug", async () => {
1171
+ // Three simBatch responses, one per channel (user/assistant/now), with
1172
+ // a single skill-prefixed slug as the only candidate.
1173
+ stageHybridResponse([{ slug: "skills/example-skill-a", denseScore: 0.5 }]);
1174
+ stageHybridResponse([{ slug: "skills/example-skill-a", denseScore: 0.4 }]);
1175
+ stageHybridResponse([{ slug: "skills/example-skill-a", denseScore: 0.2 }]);
900
1176
 
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"]),
1177
+ const out = await computeOwnActivation({
1178
+ candidates: new Set(["skills/example-skill-a"]),
1179
+ priorState: null,
940
1180
  userText: "u",
941
1181
  assistantText: "a",
942
1182
  nowText: "n",
@@ -947,191 +1187,12 @@ describe("computeSkillActivation", () => {
947
1187
  c_now: 0.2,
948
1188
  }),
949
1189
  });
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
1190
 
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
-
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"]);
1191
+ // No prior state priorContribution = 0.
1192
+ // 0.3*0.5 + 0.2*0.4 + 0.2*0.2 = 0.15 + 0.08 + 0.04 = 0.27
1193
+ expect(out.activation.get("skills/example-skill-a")).toBeCloseTo(0.27, 6);
1194
+ expect(out.breakdown.get("skills/example-skill-a")?.priorContribution).toBe(
1195
+ 0,
1196
+ );
1136
1197
  });
1137
1198
  });
@@ -372,26 +372,58 @@ describe("memoryV2ConsolidateJob — concurrent invocations", () => {
372
372
  writeFileSync(bufferPath(), "- [Apr 27, 9:00 AM] Alice prefers VS Code.\n");
373
373
  });
374
374
 
375
- test("a stale lock file blocks a second concurrent invocation", async () => {
376
- // Pre-seed a lock file as if a prior run was still in flight. The
377
- // simple wx-based lock has no liveness probe, so this also covers
378
- // stale-lock-on-disk behavior operators clear stale locks manually.
375
+ test("a live lock holder blocks a second concurrent invocation", async () => {
376
+ // Pre-seed a lock file with the current process's PID so the liveness
377
+ // probe sees a running holder and the second invocation correctly
378
+ // reports `locked` rather than taking over.
379
379
  mkdirSync(join(memoryDir(), ".v2-state"), { recursive: true });
380
- writeFileSync(lockPath(), "9999 1700000000000\n");
380
+ writeFileSync(lockPath(), `${process.pid} 1700000000000\n`);
381
381
 
382
382
  const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
383
383
 
384
384
  expect(result.kind).toBe("locked");
385
385
  if (result.kind === "locked") {
386
- expect(result.holder).toContain("9999");
386
+ expect(result.holder).toContain(`${process.pid}`);
387
387
  }
388
388
  expect(bootstrapCalls).toBe(0);
389
389
  expect(wakeCalls).toBe(0);
390
390
  expect(enqueuedJobs).toHaveLength(0);
391
- // The pre-seeded lock must NOT be removed by a contender — only the
392
- // owner releases it.
391
+ // The live holder's lock must NOT be removed by a contender.
393
392
  expect(existsSync(lockPath())).toBe(true);
394
393
  });
394
+
395
+ test("a stale lock from a non-running PID is taken over and consolidation proceeds", async () => {
396
+ // PID 999999 is well outside the typical kernel max_pid range on macOS
397
+ // and Linux, so kill(pid, 0) reliably returns ESRCH. The takeover path
398
+ // must unlink the stale file, retry the wx create, and bootstrap the
399
+ // background conversation as if the lock had been free all along.
400
+ mkdirSync(join(memoryDir(), ".v2-state"), { recursive: true });
401
+ writeFileSync(lockPath(), "999999 1700000000000\n");
402
+
403
+ const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
404
+
405
+ expect(result.kind).toBe("invoked");
406
+ expect(bootstrapCalls).toBe(1);
407
+ expect(wakeCalls).toBe(1);
408
+ // Lock is released in the finally block after a successful run.
409
+ expect(existsSync(lockPath())).toBe(false);
410
+ });
411
+
412
+ test("an empty / corrupted lock file is treated as stale and taken over", async () => {
413
+ // A zero-byte file simulates a prior holder that crashed between the
414
+ // O_EXCL create and the PID write. With only one writer ever, an
415
+ // unparseable payload is unambiguously corruption, not a live
416
+ // mid-write — take it over.
417
+ mkdirSync(join(memoryDir(), ".v2-state"), { recursive: true });
418
+ writeFileSync(lockPath(), "");
419
+
420
+ const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
421
+
422
+ expect(result.kind).toBe("invoked");
423
+ expect(bootstrapCalls).toBe(1);
424
+ expect(wakeCalls).toBe(1);
425
+ expect(existsSync(lockPath())).toBe(false);
426
+ });
395
427
  });
396
428
 
397
429
  describe("CONSOLIDATION_PROMPT", () => {