@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
@@ -0,0 +1,224 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
4
+ import {
5
+ applyRuntimeInjections,
6
+ stripInjectionsForCompaction,
7
+ } from "../daemon/conversation-runtime-assembly.js";
8
+ import {
9
+ DEFAULT_INJECTOR_ORDER,
10
+ defaultInjectorsPlugin,
11
+ DISK_PRESSURE_WARNING_PROMPT,
12
+ } from "../plugins/defaults/injectors.js";
13
+ import {
14
+ registerPlugin,
15
+ resetPluginRegistryForTests,
16
+ } from "../plugins/registry.js";
17
+ import type { Injector, TurnContext } from "../plugins/types.js";
18
+ import type { Message } from "../providers/types.js";
19
+
20
+ function findInjector(name: string): Injector {
21
+ const injector = defaultInjectorsPlugin.injectors?.find(
22
+ (candidate) => candidate.name === name,
23
+ );
24
+ if (!injector) {
25
+ throw new Error(`injector '${name}' not registered`);
26
+ }
27
+ return injector;
28
+ }
29
+
30
+ function makeContext(overrides: Partial<TurnContext> = {}): TurnContext {
31
+ return {
32
+ requestId: "req-test",
33
+ conversationId: "conv-test",
34
+ turnIndex: 0,
35
+ trust: { sourceChannel: "vellum", trustClass: "guardian" },
36
+ ...overrides,
37
+ };
38
+ }
39
+
40
+ function tailTexts(messages: Message[]): string[] {
41
+ const tail = messages[messages.length - 1];
42
+ if (!tail || tail.role !== "user") return [];
43
+ return tail.content
44
+ .filter((block): block is { type: "text"; text: string } => {
45
+ return block.type === "text";
46
+ })
47
+ .map((block) => block.text);
48
+ }
49
+
50
+ const diskPressureInjector = findInjector("disk-pressure-warning");
51
+ const cleanupContext = { cleanupModeActive: true };
52
+
53
+ describe("disk-pressure-warning injector", () => {
54
+ beforeEach(() => {
55
+ resetPluginRegistryForTests();
56
+ registerPlugin(defaultInjectorsPlugin);
57
+ _setOverridesForTesting({ "safe-storage-limits": true });
58
+ });
59
+
60
+ test("emits the exact cleanup prompt while safe storage limits are enabled", async () => {
61
+ const block = await diskPressureInjector.produce(
62
+ makeContext({
63
+ injectionInputs: { diskPressureContext: cleanupContext },
64
+ }),
65
+ );
66
+
67
+ expect(block).toEqual({
68
+ id: "disk-pressure-warning",
69
+ text: DISK_PRESSURE_WARNING_PROMPT,
70
+ placement: "prepend-user-tail",
71
+ });
72
+ expect(diskPressureInjector.order).toBe(
73
+ DEFAULT_INJECTOR_ORDER.diskPressureWarning,
74
+ );
75
+ expect(DISK_PRESSURE_WARNING_PROMPT).toBe(`<disk_pressure_warning>
76
+ Disk usage is critically low: this assistant is in storage cleanup mode because the workspace volume is at least 95% full.
77
+
78
+ In your first paragraph, warn the user that storage is critically low and that normal work is suspended until space is freed.
79
+
80
+ Then help the user clean up storage. Prefer safe inspection steps first, such as checking available space and finding large directories. Ask before deleting files or caches unless the user has already clearly approved the specific cleanup action.
81
+
82
+ Do not work on unrelated tasks until disk usage drops below the critical threshold or the user explicitly overrides the lock. Background processes and messages from trusted contacts are blocked while this cleanup mode is active.
83
+ </disk_pressure_warning>`);
84
+ });
85
+
86
+ test("omits the prompt when cleanup context is null or inactive", async () => {
87
+ await expect(
88
+ diskPressureInjector.produce(
89
+ makeContext({ injectionInputs: { diskPressureContext: null } }),
90
+ ),
91
+ ).resolves.toBeNull();
92
+
93
+ await expect(
94
+ diskPressureInjector.produce(
95
+ makeContext({
96
+ injectionInputs: {
97
+ diskPressureContext: { cleanupModeActive: false },
98
+ },
99
+ }),
100
+ ),
101
+ ).resolves.toBeNull();
102
+ });
103
+
104
+ test("omits the prompt when safe storage limits are disabled", async () => {
105
+ _setOverridesForTesting({ "safe-storage-limits": false });
106
+
107
+ await expect(
108
+ diskPressureInjector.produce(
109
+ makeContext({
110
+ injectionInputs: { diskPressureContext: cleanupContext },
111
+ }),
112
+ ),
113
+ ).resolves.toBeNull();
114
+ });
115
+
116
+ test("prepends ahead of workspace and unified turn context in full mode", async () => {
117
+ const runMessages: Message[] = [
118
+ { role: "user", content: [{ type: "text", text: "clean up space" }] },
119
+ ];
120
+ const workspace = "<workspace>\nRoot: /workspace\n</workspace>";
121
+ const turnContext = "<turn_context>\ninterface: macos\n</turn_context>";
122
+
123
+ const result = await applyRuntimeInjections(runMessages, {
124
+ turnContext: makeContext(),
125
+ diskPressureContext: cleanupContext,
126
+ workspaceTopLevelContext: workspace,
127
+ unifiedTurnContext: turnContext,
128
+ });
129
+
130
+ expect(tailTexts(result.messages).slice(0, 4)).toEqual([
131
+ DISK_PRESSURE_WARNING_PROMPT,
132
+ workspace,
133
+ turnContext,
134
+ "clean up space",
135
+ ]);
136
+ expect(
137
+ result.blocks.injectorChainBlock?.startsWith(
138
+ DISK_PRESSURE_WARNING_PROMPT,
139
+ ),
140
+ ).toBe(true);
141
+ });
142
+
143
+ test("survives minimal mode as safety-critical context", async () => {
144
+ const result = await applyRuntimeInjections(
145
+ [{ role: "user", content: [{ type: "text", text: "status" }] }],
146
+ {
147
+ turnContext: makeContext(),
148
+ mode: "minimal",
149
+ diskPressureContext: cleanupContext,
150
+ workspaceTopLevelContext: "<workspace>...</workspace>",
151
+ unifiedTurnContext: "<turn_context>...</turn_context>",
152
+ },
153
+ );
154
+
155
+ expect(tailTexts(result.messages)).toEqual([
156
+ DISK_PRESSURE_WARNING_PROMPT,
157
+ "<turn_context>...</turn_context>",
158
+ "status",
159
+ ]);
160
+ });
161
+
162
+ test("applies after Slack chronological transcript replacement", async () => {
163
+ const originalRun: Message[] = [
164
+ {
165
+ role: "user",
166
+ content: [{ type: "text", text: "latest raw user text" }],
167
+ },
168
+ ];
169
+ const slackTranscript: Message[] = [
170
+ {
171
+ role: "user",
172
+ content: [{ type: "text", text: "[12:00 user]: earlier" }],
173
+ },
174
+ {
175
+ role: "user",
176
+ content: [{ type: "text", text: "[12:01 @assistant]: cleanup?" }],
177
+ },
178
+ ];
179
+
180
+ const result = await applyRuntimeInjections(originalRun, {
181
+ turnContext: makeContext(),
182
+ diskPressureContext: cleanupContext,
183
+ channelCapabilities: {
184
+ channel: "slack",
185
+ dashboardCapable: false,
186
+ supportsDynamicUi: false,
187
+ supportsVoiceInput: false,
188
+ chatType: "channel",
189
+ },
190
+ slackChronologicalMessages: slackTranscript,
191
+ });
192
+
193
+ expect(result.messages).toHaveLength(2);
194
+ const texts = tailTexts(result.messages);
195
+ expect(texts[0]).toBe(DISK_PRESSURE_WARNING_PROMPT);
196
+ expect(
197
+ texts.some((text) => text.startsWith("<channel_capabilities>")),
198
+ ).toBe(true);
199
+ expect(texts[texts.length - 1]).toBe("[12:01 @assistant]: cleanup?");
200
+ });
201
+
202
+ test("compaction strip plus re-apply does not duplicate the warning", async () => {
203
+ const runMessages: Message[] = [
204
+ { role: "user", content: [{ type: "text", text: "find large files" }] },
205
+ ];
206
+
207
+ const first = await applyRuntimeInjections(runMessages, {
208
+ turnContext: makeContext(),
209
+ diskPressureContext: cleanupContext,
210
+ });
211
+ const stripped = stripInjectionsForCompaction(first.messages);
212
+ expect(tailTexts(stripped)).toEqual(["find large files"]);
213
+
214
+ const second = await applyRuntimeInjections(stripped, {
215
+ turnContext: makeContext(),
216
+ diskPressureContext: cleanupContext,
217
+ });
218
+ expect(
219
+ tailTexts(second.messages).filter(
220
+ (text) => text === DISK_PRESSURE_WARNING_PROMPT,
221
+ ),
222
+ ).toHaveLength(1);
223
+ });
224
+ });
@@ -1,23 +1,26 @@
1
1
  /**
2
2
  * v2 read-side cutover behavior for the PKB-derived default injectors.
3
3
  *
4
- * When `isMemoryV2ReadActive(getConfig())` is true:
4
+ * When `getConfig().memory.v2.enabled` is true:
5
5
  * - `pkb-context` silences itself (concept pages own retrieval).
6
6
  * - `pkb-reminder` still fires (its body is generic recall/remember
7
7
  * guidance) but skips the PKB-search hints — those name PKB paths.
8
8
  * - `now-md` fires unchanged (workspace state, independent of PKB).
9
9
  *
10
- * Mocks `isMemoryV2ReadActive` at the module level so each test can flip the
11
- * effective gate state without standing up a full feature-flag + config
12
- * stack. Mocks the PKB hybrid search so the reminder-with-hints branch can
13
- * resolve deterministically when called.
10
+ * Mocks `getConfig` at the module level so each test can flip the effective
11
+ * gate state without standing up a full config stack. Mocks the PKB hybrid
12
+ * search so the reminder-with-hints branch can resolve deterministically
13
+ * when called.
14
14
  */
15
15
  import { beforeEach, describe, expect, mock, test } from "bun:test";
16
16
 
17
17
  let v2Active = false;
18
18
 
19
- mock.module("../memory/context-search/sources/memory-v2.js", () => ({
20
- isMemoryV2ReadActive: () => v2Active,
19
+ const realLoader = await import("../config/loader.js");
20
+
21
+ mock.module("../config/loader.js", () => ({
22
+ ...realLoader,
23
+ getConfig: () => ({ memory: { v2: { enabled: v2Active } } }),
21
24
  }));
22
25
 
23
26
  mock.module("../memory/pkb/pkb-search.js", () => ({
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Tests for the memory-v2 skill seed gate invoked from the daemon startup
3
- * path (`assistant/src/daemon/memory-v2-startup.ts`).
2
+ * Tests for the memory-v2 skill seed gate and the v2 concept-page schema
3
+ * rebuild gate, both invoked from the daemon startup path
4
+ * (`assistant/src/daemon/memory-v2-startup.ts`).
4
5
  *
5
- * The gate is exercised in isolation rather than mounting the full lifecycle
6
- * import graph. Coverage matrix from PR 8 acceptance criteria:
7
- * - Case 1: feature flag on + `config.memory.v2.enabled` on seed runs.
8
- * - Case 2: feature flag off → seed does not run.
9
- * - Case 3: `config.memory.v2.enabled` off (flag on) seed does not run.
10
- * - Case 4: `seedV2SkillEntries` rejects → gate does not throw and the
11
- * warning is logged.
6
+ * The gates are exercised in isolation rather than mounting the full
7
+ * lifecycle import graph. Coverage matrix:
8
+ * - Skill seed (`maybeSeedMemoryV2Skills`): config gating, rejection
9
+ * swallowing.
10
+ * - Schema rebuild (`maybeRebuildMemoryV2Concepts`): config gating,
11
+ * drift-triggered reembed enqueue, empty-after-create reembed enqueue,
12
+ * no enqueue when collection is healthy, error swallowing.
12
13
  *
13
- * The seed call itself is fire-and-forget (`void` + `.catch`); the gate must
14
- * never block startup or surface an exception.
14
+ * Both gates must never block startup or surface an exception.
15
15
  */
16
16
  import { beforeEach, describe, expect, mock, test } from "bun:test";
17
17
 
@@ -22,31 +22,36 @@ import type { AssistantConfig } from "../config/schema.js";
22
22
  // ---------------------------------------------------------------------------
23
23
 
24
24
  interface TestState {
25
- flagOverrides: Record<string, boolean>;
26
25
  seedCallCount: number;
27
26
  seedShouldReject: Error | null;
28
27
  warnCalls: Array<{ obj: unknown; msg: unknown }>;
28
+ infoCalls: Array<{ obj: unknown; msg: unknown }>;
29
+ // Rebuild-gate mocks (drive maybeRebuildMemoryV2Concepts).
30
+ ensureCollectionCallCount: number;
31
+ ensureCollectionResult: { migrated: boolean };
32
+ ensureCollectionThrows: Error | null;
33
+ countResult: number;
34
+ listPagesResult: string[];
35
+ enqueueCalls: Array<{ type: string; payload: Record<string, unknown> }>;
29
36
  }
30
37
 
31
38
  const state: TestState = {
32
- flagOverrides: {},
33
39
  seedCallCount: 0,
34
40
  seedShouldReject: null,
35
41
  warnCalls: [],
42
+ infoCalls: [],
43
+ ensureCollectionCallCount: 0,
44
+ ensureCollectionResult: { migrated: false },
45
+ ensureCollectionThrows: null,
46
+ countResult: 0,
47
+ listPagesResult: [],
48
+ enqueueCalls: [],
36
49
  };
37
50
 
38
51
  // ---------------------------------------------------------------------------
39
52
  // Mocks — installed before the module under test is loaded.
40
53
  // ---------------------------------------------------------------------------
41
54
 
42
- mock.module("../config/assistant-feature-flags.js", () => ({
43
- isAssistantFeatureFlagEnabled: (key: string, _config: unknown): boolean => {
44
- const explicit = state.flagOverrides[key];
45
- if (typeof explicit === "boolean") return explicit;
46
- return true; // undeclared flags default to enabled
47
- },
48
- }));
49
-
50
55
  mock.module("../memory/v2/skill-store.js", () => ({
51
56
  seedV2SkillEntries: async (): Promise<void> => {
52
57
  state.seedCallCount += 1;
@@ -54,18 +59,51 @@ mock.module("../memory/v2/skill-store.js", () => ({
54
59
  },
55
60
  }));
56
61
 
62
+ mock.module("../memory/v2/qdrant.js", () => ({
63
+ ensureConceptPageCollection: async (): Promise<{ migrated: boolean }> => {
64
+ state.ensureCollectionCallCount += 1;
65
+ if (state.ensureCollectionThrows) throw state.ensureCollectionThrows;
66
+ return state.ensureCollectionResult;
67
+ },
68
+ countConceptPagePoints: async (): Promise<number> => state.countResult,
69
+ // The rebuild gate does not call this, but the seed gate's fire-and-forget
70
+ // chain imports it; provide a no-op so the dynamic import resolves.
71
+ dropLegacySkillsCollection: async (): Promise<void> => {},
72
+ }));
73
+
74
+ mock.module("../memory/v2/page-store.js", () => ({
75
+ hasConceptPages: async (): Promise<boolean> =>
76
+ state.listPagesResult.length > 0,
77
+ }));
78
+
79
+ mock.module("../memory/jobs-store.js", () => ({
80
+ enqueueMemoryJob: (
81
+ type: string,
82
+ payload: Record<string, unknown>,
83
+ ): string => {
84
+ state.enqueueCalls.push({ type, payload });
85
+ return "test-job-id";
86
+ },
87
+ }));
88
+
89
+ mock.module("../util/platform.js", () => ({
90
+ getWorkspaceDir: () => "/tmp/test-workspace",
91
+ }));
92
+
57
93
  mock.module("../util/logger.js", () => ({
58
94
  getLogger: () => ({
59
95
  warn: (obj: unknown, msg: unknown) => {
60
96
  state.warnCalls.push({ obj, msg });
61
97
  },
62
- info: () => {},
98
+ info: (obj: unknown, msg: unknown) => {
99
+ state.infoCalls.push({ obj, msg });
100
+ },
63
101
  error: () => {},
64
102
  debug: () => {},
65
103
  }),
66
104
  }));
67
105
 
68
- const { maybeSeedMemoryV2Skills } =
106
+ const { maybeSeedMemoryV2Skills, maybeRebuildMemoryV2Concepts } =
69
107
  await import("../daemon/memory-v2-startup.js");
70
108
 
71
109
  // ---------------------------------------------------------------------------
@@ -99,65 +137,37 @@ async function flushMicrotasks(): Promise<void> {
99
137
  // Tests
100
138
  // ---------------------------------------------------------------------------
101
139
 
140
+ function resetState(): void {
141
+ state.seedCallCount = 0;
142
+ state.seedShouldReject = null;
143
+ state.warnCalls = [];
144
+ state.infoCalls = [];
145
+ state.ensureCollectionCallCount = 0;
146
+ state.ensureCollectionResult = { migrated: false };
147
+ state.ensureCollectionThrows = null;
148
+ state.countResult = 0;
149
+ state.listPagesResult = [];
150
+ state.enqueueCalls = [];
151
+ }
152
+
102
153
  describe("maybeSeedMemoryV2Skills (daemon startup gate)", () => {
103
- beforeEach(() => {
104
- state.flagOverrides = {};
105
- state.seedCallCount = 0;
106
- state.seedShouldReject = null;
107
- state.warnCalls = [];
108
- });
154
+ beforeEach(resetState);
109
155
 
110
- test("invokes seedV2SkillEntries when flag and config are both enabled", async () => {
111
- state.flagOverrides = { "memory-v2-enabled": true };
156
+ test("invokes seedV2SkillEntries when memory.v2.enabled is true", async () => {
112
157
  maybeSeedMemoryV2Skills(makeConfig(true));
113
158
  await flushMicrotasks();
114
159
  expect(state.seedCallCount).toBe(1);
115
160
  expect(state.warnCalls).toHaveLength(0);
116
161
  });
117
162
 
118
- test("does not invoke seedV2SkillEntries when feature flag is off", async () => {
119
- state.flagOverrides = { "memory-v2-enabled": false };
120
- maybeSeedMemoryV2Skills(makeConfig(true));
121
- await flushMicrotasks();
122
- expect(state.seedCallCount).toBe(0);
123
- expect(state.warnCalls).toHaveLength(0);
124
- });
125
-
126
- test("does not invoke seedV2SkillEntries when config.memory.v2.enabled is off", async () => {
127
- state.flagOverrides = { "memory-v2-enabled": true };
128
- maybeSeedMemoryV2Skills(makeConfig(false));
129
- await flushMicrotasks();
130
- expect(state.seedCallCount).toBe(0);
131
- expect(state.warnCalls).toHaveLength(0);
132
- });
133
-
134
- test("does not invoke seedV2SkillEntries when both gates are off", async () => {
135
- state.flagOverrides = { "memory-v2-enabled": false };
163
+ test("does not invoke seedV2SkillEntries when memory.v2.enabled is false", async () => {
136
164
  maybeSeedMemoryV2Skills(makeConfig(false));
137
165
  await flushMicrotasks();
138
166
  expect(state.seedCallCount).toBe(0);
139
167
  expect(state.warnCalls).toHaveLength(0);
140
168
  });
141
169
 
142
- test("re-invocation seeds after flag flips on (deferred-init race recovery)", async () => {
143
- // Models the lifecycle-startup race: the synchronous seed call evaluates
144
- // the flag while the gateway IPC override fetch is still in flight, falls
145
- // through to the registry default (`false`), and skips. Once
146
- // `initFeatureFlagOverrides()` resolves, the chained `.then` re-invokes
147
- // the seed with the now-populated cache and the flag flips to `true`.
148
- state.flagOverrides = { "memory-v2-enabled": false };
149
- maybeSeedMemoryV2Skills(makeConfig(true));
150
- await flushMicrotasks();
151
- expect(state.seedCallCount).toBe(0);
152
-
153
- state.flagOverrides = { "memory-v2-enabled": true };
154
- maybeSeedMemoryV2Skills(makeConfig(true));
155
- await flushMicrotasks();
156
- expect(state.seedCallCount).toBe(1);
157
- });
158
-
159
170
  test("swallows seedV2SkillEntries rejections and logs a warning", async () => {
160
- state.flagOverrides = { "memory-v2-enabled": true };
161
171
  state.seedShouldReject = new Error("seed failed");
162
172
 
163
173
  // The gate must not throw — startup must not block on this.
@@ -172,3 +182,80 @@ describe("maybeSeedMemoryV2Skills (daemon startup gate)", () => {
172
182
  expect(msg).toBe("Failed to seed v2 skill entries");
173
183
  });
174
184
  });
185
+
186
+ describe("maybeRebuildMemoryV2Concepts (daemon startup gate)", () => {
187
+ beforeEach(resetState);
188
+
189
+ test("does nothing when memory.v2.enabled is false", async () => {
190
+ await maybeRebuildMemoryV2Concepts(makeConfig(false));
191
+
192
+ expect(state.ensureCollectionCallCount).toBe(0);
193
+ expect(state.enqueueCalls).toEqual([]);
194
+ });
195
+
196
+ test("enqueues memory_v2_reembed when the collection was migrated", async () => {
197
+ state.ensureCollectionResult = { migrated: true };
198
+
199
+ await maybeRebuildMemoryV2Concepts(makeConfig(true));
200
+
201
+ expect(state.ensureCollectionCallCount).toBe(1);
202
+ expect(state.enqueueCalls).toEqual([
203
+ { type: "memory_v2_reembed", payload: {} },
204
+ ]);
205
+ // Migrated path skips the count probe — drift detection is the trigger.
206
+ // (The mock's countConceptPagePoints would silently return 0 either way,
207
+ // but keeping the path conditional keeps the lifecycle hook predictable.)
208
+ });
209
+
210
+ test("enqueues reembed when the collection is empty but pages exist on disk (crash-mid-rebuild recovery)", async () => {
211
+ state.ensureCollectionResult = { migrated: false };
212
+ state.countResult = 0;
213
+ state.listPagesResult = ["people/alice", "topics/zsh"];
214
+
215
+ await maybeRebuildMemoryV2Concepts(makeConfig(true));
216
+
217
+ expect(state.enqueueCalls).toEqual([
218
+ { type: "memory_v2_reembed", payload: {} },
219
+ ]);
220
+ });
221
+
222
+ test("does not enqueue when the collection is healthy and populated", async () => {
223
+ state.ensureCollectionResult = { migrated: false };
224
+ state.countResult = 1185;
225
+ state.listPagesResult = ["people/alice"];
226
+
227
+ await maybeRebuildMemoryV2Concepts(makeConfig(true));
228
+
229
+ expect(state.enqueueCalls).toEqual([]);
230
+ });
231
+
232
+ test("does not enqueue when the collection is empty AND no pages exist on disk (fresh workspace)", async () => {
233
+ state.ensureCollectionResult = { migrated: false };
234
+ state.countResult = 0;
235
+ state.listPagesResult = [];
236
+
237
+ await maybeRebuildMemoryV2Concepts(makeConfig(true));
238
+
239
+ expect(state.enqueueCalls).toEqual([]);
240
+ });
241
+
242
+ test("swallows ensureConceptPageCollection failures and logs a warning", async () => {
243
+ state.ensureCollectionThrows = new Error("Qdrant unreachable");
244
+
245
+ // Must not throw — startup never blocks on this gate.
246
+ let thrown: unknown = null;
247
+ try {
248
+ await maybeRebuildMemoryV2Concepts(makeConfig(true));
249
+ } catch (err) {
250
+ thrown = err;
251
+ }
252
+ expect(thrown).toBeNull();
253
+
254
+ expect(state.enqueueCalls).toEqual([]);
255
+ expect(state.warnCalls.length).toBeGreaterThan(0);
256
+ const lastWarn = state.warnCalls[state.warnCalls.length - 1];
257
+ expect((lastWarn.obj as { err: Error }).err.message).toBe(
258
+ "Qdrant unreachable",
259
+ );
260
+ });
261
+ });
@@ -125,6 +125,16 @@ describe("PUT /v1/config/llm/profiles/:name — managed profile guard", () => {
125
125
  ).toThrow(BadRequestError);
126
126
  });
127
127
 
128
+ test("allows edits to custom-balanced (user-owned)", () => {
129
+ savedRaw = null;
130
+ const result = replaceRoute.handler({
131
+ pathParams: { name: "custom-balanced" },
132
+ body: { provider: "openai", model: "gpt-4o" },
133
+ });
134
+ expect(result).toEqual({ ok: true });
135
+ expect(savedRaw).not.toBeNull();
136
+ });
137
+
128
138
  test("allows edits to a user-defined profile", () => {
129
139
  savedRaw = null;
130
140
  const result = replaceRoute.handler({
@@ -165,6 +175,14 @@ describe("PATCH /v1/config — managed profile deletion guard", () => {
165
175
  ).rejects.toThrow(BadRequestError);
166
176
  });
167
177
 
178
+ test("allows deletion of custom-balanced via null (user-owned)", async () => {
179
+ savedRaw = null;
180
+ const result = await patchRoute.handler({
181
+ body: { llm: { profiles: { "custom-balanced": null } } },
182
+ });
183
+ expect(result).toEqual({ ok: true });
184
+ });
185
+
168
186
  test("allows deletion of a user-defined profile via null", async () => {
169
187
  savedRaw = null;
170
188
  const result = await patchRoute.handler({