@vellumai/assistant 0.7.0 → 0.7.1

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 (666) hide show
  1. package/ARCHITECTURE.md +6 -7
  2. package/Dockerfile +1 -0
  3. package/README.md +2 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +79 -139
  5. package/bun.lock +3 -0
  6. package/docs/architecture/security.md +18 -16
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +1 -5
  9. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -5
  10. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -16
  11. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +1 -9
  12. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +12 -12
  13. package/node_modules/@vellumai/slack-text/bun.lock +24 -0
  14. package/node_modules/@vellumai/slack-text/package.json +18 -0
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +153 -0
  16. package/node_modules/@vellumai/slack-text/src/index.ts +235 -0
  17. package/node_modules/@vellumai/slack-text/tsconfig.json +20 -0
  18. package/openapi.yaml +294 -107
  19. package/package.json +4 -2
  20. package/scripts/generate-openapi.ts +16 -111
  21. package/src/__tests__/agent-wake-override-profile.test.ts +23 -1
  22. package/src/__tests__/anthropic-provider.test.ts +56 -13
  23. package/src/__tests__/app-conversation-ids-backfill.test.ts +278 -0
  24. package/src/__tests__/app-conversation-ids.test.ts +151 -0
  25. package/src/__tests__/approval-cascade.test.ts +0 -15
  26. package/src/__tests__/approval-routes-http.test.ts +6 -17
  27. package/src/__tests__/assistant-event-hub.test.ts +126 -77
  28. package/src/__tests__/assistant-event.test.ts +0 -5
  29. package/src/__tests__/assistant-events-sse-hardening.test.ts +37 -15
  30. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -29
  31. package/src/__tests__/background-shell-host-bash.test.ts +34 -43
  32. package/src/__tests__/call-controller.test.ts +1 -1
  33. package/src/__tests__/call-site-routing-provider.test.ts +193 -0
  34. package/src/__tests__/channel-approval-routes.test.ts +10 -296
  35. package/src/__tests__/channel-approvals.test.ts +25 -17
  36. package/src/__tests__/channel-guardian.test.ts +100 -146
  37. package/src/__tests__/checker.test.ts +20 -34
  38. package/src/__tests__/compact-event-conversation-id-guard.test.ts +50 -0
  39. package/src/__tests__/compaction-events.test.ts +2 -0
  40. package/src/__tests__/config-schema.test.ts +6 -48
  41. package/src/__tests__/config-watcher.test.ts +12 -0
  42. package/src/__tests__/connection-policy.test.ts +1 -52
  43. package/src/__tests__/contacts-write.test.ts +2 -64
  44. package/src/__tests__/context-image-dimensions.test.ts +1 -1
  45. package/src/__tests__/context-search-memory-source.test.ts +120 -1
  46. package/src/__tests__/context-search-memory-v2-source.test.ts +383 -0
  47. package/src/__tests__/context-search-pkb-source.test.ts +49 -0
  48. package/src/__tests__/context-search-workspace-source.test.ts +9 -22
  49. package/src/__tests__/context-window-manager.test.ts +46 -0
  50. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  51. package/src/__tests__/conversation-agent-loop-overflow.test.ts +102 -29
  52. package/src/__tests__/conversation-agent-loop.test.ts +980 -13
  53. package/src/__tests__/conversation-analysis-routes.test.ts +12 -10
  54. package/src/__tests__/conversation-attention-telegram.test.ts +11 -3
  55. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -291
  56. package/src/__tests__/conversation-history-web-search.test.ts +4 -3
  57. package/src/__tests__/conversation-inference-profile-route.test.ts +12 -23
  58. package/src/__tests__/conversation-lifecycle.test.ts +4 -4
  59. package/src/__tests__/conversation-process-callsite.test.ts +79 -2
  60. package/src/__tests__/conversation-queue.test.ts +3 -8
  61. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -161
  62. package/src/__tests__/conversation-routes-guardian-reply.test.ts +0 -32
  63. package/src/__tests__/conversation-routes-slash-commands.test.ts +75 -66
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +257 -3
  65. package/src/__tests__/conversation-slash-commands.test.ts +24 -4
  66. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  67. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  68. package/src/__tests__/conversation-starter-routes.test.ts +79 -2
  69. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -5
  70. package/src/__tests__/conversation-surfaces-standalone.test.ts +18 -14
  71. package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -2
  72. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +8 -46
  73. package/src/__tests__/conversation-usage.test.ts +253 -3
  74. package/src/__tests__/credential-execution-shell-lockdown.test.ts +0 -39
  75. package/src/__tests__/credential-health-service.test.ts +68 -0
  76. package/src/__tests__/credential-security-e2e.test.ts +4 -3
  77. package/src/__tests__/credential-security-invariants.test.ts +1 -5
  78. package/src/__tests__/credential-token-resolver.test.ts +180 -0
  79. package/src/__tests__/cu-unified-flow.test.ts +33 -16
  80. package/src/__tests__/daemon-assistant-events.test.ts +34 -21
  81. package/src/__tests__/daemon-credential-client.test.ts +4 -1
  82. package/src/__tests__/db-connection-isolation.test.ts +125 -0
  83. package/src/__tests__/db-migration-rollback.test.ts +101 -0
  84. package/src/__tests__/db-slack-compaction-watermark-migration.test.ts +169 -0
  85. package/src/__tests__/deterministic-verification-control-plane.test.ts +7 -80
  86. package/src/__tests__/document-conversations.test.ts +332 -0
  87. package/src/__tests__/embedding-managed-proxy-selection.test.ts +2 -2
  88. package/src/__tests__/emit-event-signal.test.ts +4 -6
  89. package/src/__tests__/events-client-registration.test.ts +193 -49
  90. package/src/__tests__/filing-service.test.ts +58 -7
  91. package/src/__tests__/first-greeting.test.ts +156 -150
  92. package/src/__tests__/fixtures/mock-chrome-extension.ts +108 -66
  93. package/src/__tests__/get-skill-detail-audit.test.ts +3 -8
  94. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  95. package/src/__tests__/guardian-dispatch.test.ts +1 -1
  96. package/src/__tests__/guardian-grant-minting.test.ts +7 -2
  97. package/src/__tests__/guardian-routing-invariants.test.ts +7 -2
  98. package/src/__tests__/guardian-routing-state.test.ts +1 -1
  99. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +32 -11
  100. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -83
  101. package/src/__tests__/headless-browser-mode.test.ts +4 -9
  102. package/src/__tests__/headless-browser-navigate.test.ts +21 -20
  103. package/src/__tests__/heartbeat-service.test.ts +289 -7
  104. package/src/__tests__/helpers/channel-test-adapter.ts +2 -2
  105. package/src/__tests__/helpers/create-guardian-binding.ts +91 -0
  106. package/src/__tests__/host-bash-proxy.test.ts +46 -122
  107. package/src/__tests__/host-browser-e2e-cloud.test.ts +36 -497
  108. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +26 -96
  109. package/src/__tests__/host-browser-proxy.test.ts +111 -185
  110. package/src/__tests__/host-browser-routes.test.ts +45 -75
  111. package/src/__tests__/host-browser-ws-events-e2e.test.ts +26 -30
  112. package/src/__tests__/host-cu-proxy.test.ts +56 -111
  113. package/src/__tests__/host-file-proxy.test.ts +44 -98
  114. package/src/__tests__/host-file-read-tool.test.ts +42 -21
  115. package/src/__tests__/host-shell-tool.test.ts +33 -68
  116. package/src/__tests__/host-transfer-pending-interactions.test.ts +2 -18
  117. package/src/__tests__/host-transfer-proxy.test.ts +43 -53
  118. package/src/__tests__/http-user-message-parity.test.ts +0 -6
  119. package/src/__tests__/inbound-slack-persistence.test.ts +31 -0
  120. package/src/__tests__/injector-chain.test.ts +10 -5
  121. package/src/__tests__/injector-pkb-v2-silenced.test.ts +124 -0
  122. package/src/__tests__/inline-command-runner.test.ts +0 -66
  123. package/src/__tests__/inline-skill-load-permissions.test.ts +0 -2
  124. package/src/__tests__/install-skill-routing.test.ts +1 -13
  125. package/src/__tests__/llm-callsite-catalog.test.ts +34 -0
  126. package/src/__tests__/llm-catalog-parity.test.ts +90 -0
  127. package/src/__tests__/llm-context-resolution.test.ts +180 -0
  128. package/src/__tests__/llm-resolver.test.ts +80 -12
  129. package/src/__tests__/llm-usage-store.test.ts +269 -4
  130. package/src/__tests__/log-export-routes.test.ts +89 -0
  131. package/src/__tests__/managed-profile-guard.test.ts +225 -0
  132. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -10
  133. package/src/__tests__/manual-token-reconciliation.test.ts +334 -0
  134. package/src/__tests__/memory-v2-static-injector.test.ts +95 -0
  135. package/src/__tests__/migration-cross-version-compatibility.test.ts +197 -291
  136. package/src/__tests__/migration-export-http.test.ts +33 -26
  137. package/src/__tests__/migration-export-streaming.test.ts +18 -10
  138. package/src/__tests__/migration-export-to-gcs.test.ts +49 -9
  139. package/src/__tests__/migration-import-commit-http.test.ts +66 -21
  140. package/src/__tests__/migration-import-from-gcs.test.ts +50 -9
  141. package/src/__tests__/migration-import-from-url.test.ts +20 -6
  142. package/src/__tests__/migration-import-preflight-http.test.ts +95 -95
  143. package/src/__tests__/migration-parity-persistence.test.ts +62 -25
  144. package/src/__tests__/migration-transport.test.ts +115 -23
  145. package/src/__tests__/migration-validate-http.test.ts +105 -80
  146. package/src/__tests__/migration-wizard.test.ts +133 -27
  147. package/src/__tests__/non-member-access-request.test.ts +1 -1
  148. package/src/__tests__/notification-guardian-path.test.ts +1 -1
  149. package/src/__tests__/oauth-store.test.ts +19 -0
  150. package/src/__tests__/platform-bash-auto-approve.test.ts +21 -12
  151. package/src/__tests__/prechat-onboarding-contract.test.ts +31 -7
  152. package/src/__tests__/pricing.test.ts +68 -4
  153. package/src/__tests__/process-message-background-slack.test.ts +331 -0
  154. package/src/__tests__/provider-managed-proxy-integration.test.ts +153 -17
  155. package/src/__tests__/provider-send-message-override-profile.test.ts +50 -0
  156. package/src/__tests__/provider-usage-tracking.test.ts +208 -0
  157. package/src/__tests__/reaction-persistence.test.ts +9 -6
  158. package/src/__tests__/rebind-secrets-screen.test.ts +53 -16
  159. package/src/__tests__/recording-handler.test.ts +64 -81
  160. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +4 -3
  161. package/src/__tests__/relay-server.test.ts +18 -13
  162. package/src/__tests__/require-fresh-approval.test.ts +13 -22
  163. package/src/__tests__/runtime-attachment-metadata.test.ts +1 -1
  164. package/src/__tests__/runtime-events-sse-parity.test.ts +3 -4
  165. package/src/__tests__/runtime-events-sse.test.ts +3 -12
  166. package/src/__tests__/search-skills-unified.test.ts +9 -15
  167. package/src/__tests__/secret-ingress-cli.test.ts +2 -5
  168. package/src/__tests__/secret-ingress-http.test.ts +0 -4
  169. package/src/__tests__/secret-onetime-send.test.ts +4 -2
  170. package/src/__tests__/secret-prompt-log-hygiene.test.ts +24 -7
  171. package/src/__tests__/secret-prompter-channel-fallback.test.ts +42 -47
  172. package/src/__tests__/secret-response-routing.test.ts +29 -15
  173. package/src/__tests__/secret-routes-managed-proxy.test.ts +5 -1
  174. package/src/__tests__/secret-scanner.test.ts +2 -545
  175. package/src/__tests__/send-endpoint-busy.test.ts +9 -24
  176. package/src/__tests__/settings-routes.test.ts +1 -1
  177. package/src/__tests__/shell-credential-ref.test.ts +0 -8
  178. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -56
  179. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -11
  180. package/src/__tests__/skill-tool-factory.test.ts +97 -0
  181. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -30
  182. package/src/__tests__/skills-files-catalog-fallback.test.ts +11 -17
  183. package/src/__tests__/slack-inbound-verification.test.ts +1 -62
  184. package/src/__tests__/subagent-fork-notifications.test.ts +57 -47
  185. package/src/__tests__/subagent-manager-notify.test.ts +70 -70
  186. package/src/__tests__/subagent-notify-parent.test.ts +80 -83
  187. package/src/__tests__/system-prompt.test.ts +115 -13
  188. package/src/__tests__/terminal-tools.test.ts +0 -89
  189. package/src/__tests__/thread-backfill.test.ts +945 -31
  190. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -36
  191. package/src/__tests__/tool-execute-pipeline.test.ts +0 -6
  192. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -16
  193. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +9 -19
  194. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -7
  195. package/src/__tests__/tool-executor.test.ts +12 -19
  196. package/src/__tests__/tool-metrics-listener.test.ts +0 -35
  197. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  198. package/src/__tests__/tool-trace-listener.test.ts +0 -17
  199. package/src/__tests__/transfer-progress-screen.test.ts +63 -26
  200. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -149
  201. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -4
  202. package/src/__tests__/trusted-contact-verification.test.ts +1 -1
  203. package/src/__tests__/tts-catalog-parity.test.ts +16 -5
  204. package/src/__tests__/usage-attribution.test.ts +247 -0
  205. package/src/__tests__/usage-cli.test.ts +143 -0
  206. package/src/__tests__/usage-grouped-buckets.test.ts +155 -0
  207. package/src/__tests__/usage-routes.test.ts +150 -0
  208. package/src/__tests__/validation-results-screen.test.ts +39 -16
  209. package/src/__tests__/vbundle-pax-and-symlink.test.ts +12 -3
  210. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +49 -137
  211. package/src/__tests__/verification-control-plane-policy.test.ts +4 -7
  212. package/src/__tests__/voice-session-bridge.test.ts +5 -5
  213. package/src/__tests__/workspace-migration-062-drop-memory-v2-edges-json.test.ts +103 -0
  214. package/src/__tests__/workspace-migration-063-release-notes-dynamic-model-context.test.ts +77 -0
  215. package/src/__tests__/workspace-migration-064-unwind-main-agent-opus-seed.test.ts +225 -0
  216. package/src/__tests__/workspace-migration-memory-v2-init.test.ts +8 -30
  217. package/src/acp/index.ts +0 -15
  218. package/src/acp/session-manager.ts +37 -34
  219. package/src/agent/loop.ts +16 -1
  220. package/src/approvals/AGENTS.md +4 -0
  221. package/src/approvals/__tests__/guardian-feed-event.test.ts +10 -3
  222. package/src/approvals/guardian-request-resolvers.ts +10 -2
  223. package/src/backup/__tests__/backup-worker.test.ts +36 -8
  224. package/src/backup/__tests__/paths.test.ts +2 -2
  225. package/src/backup/__tests__/restore.test.ts +45 -28
  226. package/src/backup/backup-worker.ts +36 -2
  227. package/src/backup/paths.ts +9 -6
  228. package/src/browser-session/events.ts +0 -9
  229. package/src/calls/call-store.ts +1 -34
  230. package/src/calls/guardian-question-copy.ts +0 -108
  231. package/src/calls/relay-server.ts +0 -24
  232. package/src/calls/twilio-rest.ts +0 -38
  233. package/src/calls/twilio-routes.ts +1 -1
  234. package/src/calls/voice-session-bridge.ts +7 -38
  235. package/src/channels/types.ts +1 -36
  236. package/src/cli/commands/__tests__/cache.test.ts +152 -5
  237. package/src/cli/commands/__tests__/memory-v2.test.ts +14 -28
  238. package/src/cli/commands/__tests__/trust.test.ts +21 -387
  239. package/src/cli/commands/backup.ts +4 -4
  240. package/src/cli/commands/cache-fs.ts +8 -0
  241. package/src/cli/commands/cache.ts +153 -82
  242. package/src/cli/commands/clients.ts +63 -5
  243. package/src/cli/commands/completions.ts +3 -3
  244. package/src/cli/commands/contacts.ts +231 -76
  245. package/src/cli/commands/keys.ts +4 -1
  246. package/src/cli/commands/memory-v2.ts +24 -52
  247. package/src/cli/commands/oauth/shared.ts +2 -29
  248. package/src/cli/commands/pending.ts +102 -0
  249. package/src/cli/commands/skills.ts +77 -35
  250. package/src/cli/commands/trust.ts +70 -430
  251. package/src/cli/commands/usage.ts +25 -16
  252. package/src/cli/lib/daemon-credential-client.ts +14 -0
  253. package/src/cli/program.ts +2 -0
  254. package/src/cli.ts +0 -21
  255. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  256. package/src/config/bundled-skills/messaging/TOOLS.json +14 -4
  257. package/src/config/env-registry.ts +12 -2
  258. package/src/config/env.ts +3 -14
  259. package/src/config/feature-flag-registry.json +30 -30
  260. package/src/config/llm-callsite-catalog.ts +12 -0
  261. package/src/config/llm-context-resolution.ts +80 -0
  262. package/src/config/llm-resolver.ts +58 -22
  263. package/src/config/loader.ts +3 -3
  264. package/src/config/schema.ts +2 -158
  265. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  266. package/src/config/schemas/call-site-catalog.ts +271 -0
  267. package/src/config/schemas/calls.ts +5 -5
  268. package/src/config/schemas/inference.ts +1 -1
  269. package/src/config/schemas/ingress.ts +1 -1
  270. package/src/config/schemas/llm.ts +31 -3
  271. package/src/config/schemas/memory-retrieval.ts +2 -2
  272. package/src/config/schemas/memory-v2.ts +9 -0
  273. package/src/config/schemas/security.ts +1 -42
  274. package/src/config/schemas/services.ts +6 -6
  275. package/src/config/schemas/skills.ts +5 -5
  276. package/src/config/schemas/tts.ts +1 -1
  277. package/src/config/seed-inference-profiles.ts +117 -0
  278. package/src/config/skills.ts +0 -90
  279. package/src/config/types.ts +3 -6
  280. package/src/contacts/contact-store.ts +0 -17
  281. package/src/contacts/contacts-write.ts +1 -105
  282. package/src/context/window-manager.ts +44 -5
  283. package/src/credential-execution/process-manager.ts +34 -10
  284. package/src/credential-health/credential-health-service.ts +21 -16
  285. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +75 -82
  286. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -9
  287. package/src/daemon/connection-policy.ts +1 -26
  288. package/src/daemon/conversation-agent-loop-handlers.ts +53 -4
  289. package/src/daemon/conversation-agent-loop.ts +277 -36
  290. package/src/daemon/conversation-history.ts +8 -8
  291. package/src/daemon/conversation-launch.ts +20 -135
  292. package/src/daemon/conversation-lifecycle.ts +1 -1
  293. package/src/daemon/conversation-messaging.ts +1 -0
  294. package/src/daemon/conversation-process.ts +83 -163
  295. package/src/daemon/conversation-runtime-assembly.ts +219 -76
  296. package/src/daemon/conversation-slash.ts +47 -5
  297. package/src/daemon/conversation-store.ts +7 -31
  298. package/src/daemon/conversation-surfaces.ts +22 -28
  299. package/src/daemon/conversation-tool-setup.ts +3 -33
  300. package/src/daemon/conversation-usage.ts +36 -0
  301. package/src/daemon/conversation.ts +117 -233
  302. package/src/daemon/daemon-control.ts +3 -71
  303. package/src/daemon/daemon-skill-host.ts +8 -11
  304. package/src/daemon/dictation-profile-store.ts +2 -26
  305. package/src/daemon/first-greeting.ts +44 -156
  306. package/src/daemon/handlers/config-channels.ts +12 -12
  307. package/src/daemon/handlers/config-ingress.ts +4 -165
  308. package/src/daemon/handlers/config-model.ts +1 -1
  309. package/src/daemon/handlers/config-voice.ts +0 -42
  310. package/src/daemon/handlers/conversations.ts +11 -190
  311. package/src/daemon/handlers/recording.ts +26 -158
  312. package/src/daemon/handlers/shared.ts +23 -71
  313. package/src/daemon/handlers/skills.ts +42 -93
  314. package/src/daemon/host-bash-proxy.ts +67 -45
  315. package/src/daemon/host-browser-proxy.ts +65 -27
  316. package/src/daemon/host-cu-proxy.ts +40 -39
  317. package/src/daemon/host-file-proxy.ts +58 -37
  318. package/src/daemon/host-transfer-proxy.ts +84 -46
  319. package/src/daemon/lifecycle.ts +49 -15
  320. package/src/daemon/message-types/conversations.ts +7 -0
  321. package/src/daemon/message-types/host-bash.ts +1 -0
  322. package/src/daemon/message-types/host-cu.ts +1 -0
  323. package/src/daemon/message-types/host-file.ts +1 -0
  324. package/src/daemon/message-types/host-transfer.ts +1 -0
  325. package/src/daemon/message-types/messages.ts +10 -9
  326. package/src/daemon/message-types/workspace.ts +1 -1
  327. package/src/daemon/process-message.ts +102 -239
  328. package/src/daemon/server.ts +13 -462
  329. package/src/daemon/shutdown-handlers.ts +2 -2
  330. package/src/daemon/tool-side-effects.ts +125 -107
  331. package/src/daemon/trust-context.ts +13 -0
  332. package/src/daemon/wake-target-adapter.ts +4 -9
  333. package/src/events/domain-events.ts +0 -8
  334. package/src/events/tool-audit-listener.ts +3 -1
  335. package/src/events/tool-domain-event-publisher.ts +0 -10
  336. package/src/events/tool-metrics-listener.ts +0 -17
  337. package/src/events/tool-trace-listener.ts +0 -14
  338. package/src/filing/filing-service.ts +13 -1
  339. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +6 -2
  340. package/src/heartbeat/heartbeat-service.ts +23 -5
  341. package/src/home/__tests__/feed-writer.test.ts +0 -4
  342. package/src/home/__tests__/relationship-state-writer.test.ts +30 -0
  343. package/src/home/feed-writer.ts +1 -2
  344. package/src/home/relationship-state-writer.ts +16 -3
  345. package/src/ipc/__tests__/browser-ipc.test.ts +2 -12
  346. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -1
  347. package/src/ipc/assistant-server.ts +3 -10
  348. package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +39 -20
  349. package/src/ipc/routes/route-adapter.ts +1 -1
  350. package/src/ipc/routes/trust-rules.test.ts +0 -95
  351. package/src/ipc/skill-ipc-types.ts +41 -0
  352. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +13 -27
  353. package/src/ipc/skill-routes/__tests__/identity.test.ts +4 -23
  354. package/src/ipc/skill-routes/events.ts +12 -23
  355. package/src/ipc/skill-routes/identity.ts +4 -17
  356. package/src/ipc/skill-routes/index.ts +1 -1
  357. package/src/ipc/skill-server.ts +6 -39
  358. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +0 -8
  359. package/src/live-voice/protocol.ts +4 -13
  360. package/src/mcp/manager.ts +0 -5
  361. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +55 -0
  362. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +127 -0
  363. package/src/memory/app-git-service.ts +0 -32
  364. package/src/memory/app-store.ts +154 -0
  365. package/src/memory/attachments-store.ts +6 -0
  366. package/src/memory/context-search/sources/memory-v2.ts +578 -0
  367. package/src/memory/context-search/sources/memory.ts +5 -0
  368. package/src/memory/context-search/sources/pkb.ts +10 -1
  369. package/src/memory/context-search/sources/workspace.ts +3 -2
  370. package/src/memory/conversation-crud.ts +29 -4
  371. package/src/memory/conversation-disk-view.ts +1 -5
  372. package/src/memory/conversation-starter-checkpoints.ts +63 -0
  373. package/src/memory/db-connection.ts +62 -0
  374. package/src/memory/db-init.ts +14 -0
  375. package/src/memory/embedding-backend.ts +3 -21
  376. package/src/memory/embedding-gemini.ts +0 -2
  377. package/src/memory/embedding-local.ts +6 -6
  378. package/src/memory/embedding-ollama.ts +6 -6
  379. package/src/memory/embedding-openai.ts +6 -6
  380. package/src/memory/embedding-types.ts +21 -0
  381. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +3 -7
  382. package/src/memory/graph/conversation-graph-memory.ts +35 -13
  383. package/src/memory/graph/injection.test.ts +2 -2
  384. package/src/memory/graph/injection.ts +1 -1
  385. package/src/memory/guardian-action-store.ts +0 -83
  386. package/src/memory/guardian-approvals.ts +0 -48
  387. package/src/memory/indexer.ts +1 -15
  388. package/src/memory/job-handlers/conversation-starters.ts +36 -53
  389. package/src/memory/job-utils.ts +0 -6
  390. package/src/memory/jobs-store.ts +0 -1
  391. package/src/memory/jobs-worker.ts +2 -16
  392. package/src/memory/llm-request-log-store.ts +0 -41
  393. package/src/memory/llm-usage-store.ts +129 -43
  394. package/src/memory/memory-v2-activation-log-store.ts +115 -0
  395. package/src/memory/migrations/233-document-conversations.ts +54 -0
  396. package/src/memory/migrations/234-memory-v2-activation-logs.ts +55 -0
  397. package/src/memory/migrations/235-llm-usage-attribution.ts +31 -0
  398. package/src/memory/migrations/235-slack-compaction-watermark.ts +44 -0
  399. package/src/memory/migrations/236-tool-invocations-matched-rule-id.ts +26 -0
  400. package/src/memory/migrations/__tests__/234-memory-v2-activation-logs.test.ts +182 -0
  401. package/src/memory/migrations/index.ts +14 -0
  402. package/src/memory/migrations/registry.ts +24 -0
  403. package/src/memory/raw-query.ts +2 -68
  404. package/src/memory/schema/conversations.ts +7 -0
  405. package/src/memory/schema/infrastructure.ts +25 -0
  406. package/src/memory/search/semantic.ts +5 -16
  407. package/src/memory/tool-usage-store.ts +2 -0
  408. package/src/memory/usage-buckets.ts +40 -1
  409. package/src/memory/usage-grouped-buckets.ts +127 -0
  410. package/src/memory/v2/__tests__/activation.test.ts +289 -90
  411. package/src/memory/v2/__tests__/backfill-jobs.test.ts +2 -129
  412. package/src/memory/v2/__tests__/consolidation-job.test.ts +28 -11
  413. package/src/memory/v2/__tests__/edge-index.test.ts +278 -0
  414. package/src/memory/v2/__tests__/injection.test.ts +384 -15
  415. package/src/memory/v2/__tests__/migration.test.ts +64 -36
  416. package/src/memory/v2/__tests__/page-store.test.ts +191 -8
  417. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +181 -0
  418. package/src/memory/v2/__tests__/skill-store.test.ts +115 -3
  419. package/src/memory/v2/__tests__/static-context.test.ts +153 -0
  420. package/src/memory/v2/activation.ts +168 -97
  421. package/src/memory/v2/backfill-jobs.ts +15 -100
  422. package/src/memory/v2/consolidation-job.ts +14 -12
  423. package/src/memory/v2/edge-index.ts +191 -0
  424. package/src/memory/v2/injection.ts +182 -58
  425. package/src/memory/v2/migration.ts +57 -64
  426. package/src/memory/v2/now-text.ts +2 -3
  427. package/src/memory/v2/page-store.ts +168 -31
  428. package/src/memory/v2/prompts/consolidation.ts +118 -42
  429. package/src/memory/v2/prompts/sweep.ts +3 -3
  430. package/src/memory/v2/skill-store.ts +55 -7
  431. package/src/memory/v2/static-context.ts +62 -0
  432. package/src/memory/v2/types.ts +10 -20
  433. package/src/memory/validation.ts +0 -11
  434. package/src/messaging/draft-store.ts +0 -6
  435. package/src/messaging/provider-types.ts +8 -0
  436. package/src/messaging/provider.ts +7 -0
  437. package/src/messaging/providers/gmail/client.ts +1 -121
  438. package/src/messaging/providers/outlook/client.ts +0 -73
  439. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +226 -0
  440. package/src/messaging/providers/slack/adapter.ts +122 -21
  441. package/src/messaging/providers/slack/backfill.test.ts +95 -6
  442. package/src/messaging/providers/slack/backfill.ts +89 -11
  443. package/src/messaging/providers/slack/client.ts +10 -124
  444. package/src/messaging/providers/slack/message-metadata.ts +12 -2
  445. package/src/messaging/providers/slack/render-transcript.test.ts +56 -0
  446. package/src/messaging/providers/slack/render-transcript.ts +126 -25
  447. package/src/messaging/providers/slack/types.ts +1 -0
  448. package/src/oauth/connection-resolver.test.ts +8 -0
  449. package/src/oauth/connection-resolver.ts +8 -16
  450. package/src/oauth/credential-token-resolver.ts +97 -0
  451. package/src/oauth/manual-token-connection.ts +30 -34
  452. package/src/oauth/oauth-store.ts +6 -4
  453. package/src/outbound-proxy/certs.ts +0 -7
  454. package/src/outbound-proxy/config.ts +0 -74
  455. package/src/outbound-proxy/health.ts +0 -44
  456. package/src/outbound-proxy/index.ts +0 -22
  457. package/src/permissions/approval-provenance.test.ts +184 -0
  458. package/src/permissions/approval-provenance.ts +70 -0
  459. package/src/permissions/checker.ts +4 -1
  460. package/src/permissions/gateway-threshold-reader.ts +4 -1
  461. package/src/permissions/prompter.ts +9 -2
  462. package/src/permissions/secret-prompter.ts +21 -48
  463. package/src/permissions/types.ts +33 -0
  464. package/src/permissions/workspace-policy.ts +0 -5
  465. package/src/platform/sync-identity.ts +0 -8
  466. package/src/plugins/defaults/injectors.ts +69 -2
  467. package/src/plugins/defaults/overflow-reduce.ts +3 -2
  468. package/src/plugins/types.ts +8 -0
  469. package/src/prompts/system-prompt.ts +34 -70
  470. package/src/prompts/templates/BOOTSTRAP.md +52 -6
  471. package/src/prompts/update-bulletin-job.ts +2 -0
  472. package/src/providers/__tests__/retry-callsite.test.ts +138 -1
  473. package/src/providers/anthropic/client.ts +72 -33
  474. package/src/providers/call-site-routing.ts +42 -3
  475. package/src/providers/gemini/client.ts +18 -2
  476. package/src/providers/managed-proxy/context.ts +0 -5
  477. package/src/providers/model-catalog.ts +105 -19
  478. package/src/providers/openai/chat-completions-provider.ts +6 -0
  479. package/src/providers/openai/responses-provider.ts +7 -1
  480. package/src/providers/provider-send-message.ts +45 -2
  481. package/src/providers/ratelimit.ts +7 -2
  482. package/src/providers/registry.ts +14 -9
  483. package/src/providers/retry.ts +96 -8
  484. package/src/providers/types.ts +13 -0
  485. package/src/providers/usage-tracking.ts +96 -0
  486. package/src/runtime/AGENTS.md +10 -6
  487. package/src/runtime/__tests__/agent-wake.test.ts +89 -0
  488. package/src/runtime/agent-wake.ts +39 -2
  489. package/src/runtime/assistant-event-hub.ts +541 -45
  490. package/src/runtime/assistant-event.ts +1 -6
  491. package/src/runtime/auth/context.ts +0 -9
  492. package/src/runtime/auth/middleware.ts +1 -1
  493. package/src/runtime/auth/route-policy.ts +11 -9
  494. package/src/runtime/auth/token-service.ts +0 -11
  495. package/src/runtime/channel-approvals.ts +6 -2
  496. package/src/runtime/channel-verification-service.ts +3 -5
  497. package/src/runtime/http-errors.ts +0 -34
  498. package/src/runtime/http-router.ts +6 -3
  499. package/src/runtime/http-server.ts +22 -82
  500. package/src/runtime/http-types.ts +5 -0
  501. package/src/runtime/interactive-ui.ts +0 -1
  502. package/src/runtime/middleware/auth.ts +0 -20
  503. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +112 -0
  504. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +11 -4
  505. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +253 -0
  506. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +19 -6
  507. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +71 -27
  508. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +41 -2
  509. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +143 -79
  510. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +143 -23
  511. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +2 -2
  512. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +371 -0
  513. package/src/runtime/migrations/migration-transport.ts +46 -13
  514. package/src/runtime/migrations/migration-wizard.ts +2 -2
  515. package/src/runtime/migrations/origin-mode.ts +40 -0
  516. package/src/runtime/migrations/vbundle-builder.ts +133 -79
  517. package/src/runtime/migrations/vbundle-import-analyzer.ts +9 -7
  518. package/src/runtime/migrations/vbundle-importer.ts +7 -7
  519. package/src/runtime/migrations/vbundle-metadata-merge.ts +1 -1
  520. package/src/runtime/migrations/vbundle-streaming-importer.ts +3 -3
  521. package/src/runtime/migrations/vbundle-streaming-validator.ts +48 -26
  522. package/src/runtime/migrations/vbundle-validator.ts +214 -41
  523. package/src/runtime/pending-interactions.ts +13 -4
  524. package/src/runtime/routes/__tests__/acp-routes.test.ts +0 -1
  525. package/src/runtime/routes/__tests__/backup-routes.test.ts +28 -19
  526. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +235 -0
  527. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +58 -0
  528. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +54 -0
  529. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +19 -6
  530. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +7 -7
  531. package/src/runtime/routes/acp-routes.test.ts +0 -3
  532. package/src/runtime/routes/acp-routes.ts +3 -7
  533. package/src/runtime/routes/app-management-routes.ts +18 -9
  534. package/src/runtime/routes/approval-routes.ts +55 -14
  535. package/src/runtime/routes/avatar-routes.ts +3 -5
  536. package/src/runtime/routes/browser-routes.ts +1 -15
  537. package/src/runtime/routes/channel-guardian-routes.ts +1 -5
  538. package/src/runtime/routes/channel-readiness-routes.ts +3 -7
  539. package/src/runtime/routes/channel-route-shared.ts +2 -28
  540. package/src/runtime/routes/client-routes.ts +45 -12
  541. package/src/runtime/routes/consolidation-routes.ts +115 -0
  542. package/src/runtime/routes/conversation-list-routes.ts +12 -29
  543. package/src/runtime/routes/conversation-management-routes.ts +14 -51
  544. package/src/runtime/routes/conversation-query-routes.ts +120 -8
  545. package/src/runtime/routes/conversation-routes.ts +44 -528
  546. package/src/runtime/routes/conversation-starter-routes.ts +19 -40
  547. package/src/runtime/routes/documents-routes.ts +53 -18
  548. package/src/runtime/routes/events-routes.ts +59 -91
  549. package/src/runtime/routes/filing-routes.ts +18 -1
  550. package/src/runtime/routes/guardian-action-routes.ts +4 -9
  551. package/src/runtime/routes/host-bash-routes.ts +3 -2
  552. package/src/runtime/routes/host-browser-routes.ts +9 -33
  553. package/src/runtime/routes/host-cu-routes.ts +6 -1
  554. package/src/runtime/routes/host-file-routes.ts +3 -2
  555. package/src/runtime/routes/host-transfer-routes.ts +11 -15
  556. package/src/runtime/routes/identity-routes.ts +78 -6
  557. package/src/runtime/routes/inbound-message-handler.ts +580 -137
  558. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -88
  559. package/src/runtime/routes/inbound-stages/background-dispatch.ts +3 -0
  560. package/src/runtime/routes/index.ts +4 -0
  561. package/src/runtime/routes/integrations/slack/channel.ts +0 -24
  562. package/src/runtime/routes/llm-call-sites-routes.ts +22 -0
  563. package/src/runtime/routes/memory-v2-routes.ts +10 -15
  564. package/src/runtime/routes/migration-routes.ts +188 -31
  565. package/src/runtime/routes/playground/guard.ts +1 -1
  566. package/src/runtime/routes/playground/index.ts +0 -2
  567. package/src/runtime/routes/recording-routes.ts +4 -24
  568. package/src/runtime/routes/rename-conversation-routes.ts +2 -6
  569. package/src/runtime/routes/schedule-routes.ts +3 -6
  570. package/src/runtime/routes/secret-routes.ts +87 -18
  571. package/src/runtime/routes/settings-routes.ts +29 -28
  572. package/src/runtime/routes/skills-routes.ts +12 -31
  573. package/src/runtime/routes/suggest-trust-rule-routes.ts +32 -1
  574. package/src/runtime/routes/task-routes.ts +6 -6
  575. package/src/runtime/routes/trust-rules-routes.ts +3 -94
  576. package/src/runtime/routes/types.ts +4 -4
  577. package/src/runtime/routes/upgrade-broadcast-routes.ts +3 -10
  578. package/src/runtime/routes/usage-routes.ts +87 -10
  579. package/src/runtime/routes/user-routes.ts +17 -31
  580. package/src/runtime/routes/work-items-routes.ts +1 -4
  581. package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -2
  582. package/src/runtime/services/analyze-conversation.ts +7 -17
  583. package/src/runtime/services/conversation-serializer.ts +2 -4
  584. package/src/runtime/verification-outbound-actions.ts +1 -1
  585. package/src/runtime/verification-rate-limiter.ts +1 -1
  586. package/src/schedule/schedule-store.ts +0 -16
  587. package/src/security/secret-scanner.ts +14 -547
  588. package/src/security/secure-keys.ts +31 -11
  589. package/src/security/token-manager.ts +7 -3
  590. package/src/signals/cancel.ts +16 -25
  591. package/src/signals/conversation-undo.ts +2 -27
  592. package/src/signals/emit-event.ts +1 -2
  593. package/src/signals/user-message.ts +108 -22
  594. package/src/skills/catalog-install.ts +1 -0
  595. package/src/skills/clawhub.ts +2 -2
  596. package/src/skills/inline-command-runner.ts +1 -7
  597. package/src/subagent/manager.ts +67 -84
  598. package/src/tasks/task-store.ts +1 -28
  599. package/src/telemetry/types.ts +6 -0
  600. package/src/telemetry/usage-telemetry-reporter.test.ts +38 -15
  601. package/src/telemetry/usage-telemetry-reporter.ts +3 -5
  602. package/src/tools/acp/spawn.test.ts +1 -2
  603. package/src/tools/acp/steer.test.ts +1 -2
  604. package/src/tools/browser/__tests__/browser-status.test.ts +44 -127
  605. package/src/tools/browser/browser-execution.ts +31 -147
  606. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +92 -68
  607. package/src/tools/browser/cdp-client/factory.ts +48 -76
  608. package/src/tools/browser/cdp-client/index.ts +1 -14
  609. package/src/tools/executor.ts +44 -31
  610. package/src/tools/host-filesystem/edit.ts +3 -2
  611. package/src/tools/host-filesystem/read.ts +3 -2
  612. package/src/tools/host-filesystem/transfer.test.ts +45 -42
  613. package/src/tools/host-filesystem/transfer.ts +4 -3
  614. package/src/tools/host-filesystem/write.ts +3 -2
  615. package/src/tools/host-terminal/host-shell.ts +4 -3
  616. package/src/tools/network/script-proxy/index.ts +1 -10
  617. package/src/tools/permission-checker.ts +66 -1
  618. package/src/tools/skills/sandbox-runner.ts +1 -6
  619. package/src/tools/skills/skill-tool-factory.ts +32 -0
  620. package/src/tools/terminal/safe-env.ts +1 -0
  621. package/src/tools/terminal/shell.ts +2 -78
  622. package/src/tools/types.ts +12 -39
  623. package/src/tts/__tests__/provider-catalog.test.ts +2 -2
  624. package/src/tts/provider-catalog.ts +1 -1
  625. package/src/usage/actors.ts +2 -1
  626. package/src/usage/attribution.ts +185 -0
  627. package/src/usage/pricing.ts +166 -0
  628. package/src/usage/types.ts +14 -0
  629. package/src/util/json.ts +13 -0
  630. package/src/util/logger.ts +3 -3
  631. package/src/util/pricing.ts +50 -3
  632. package/src/work-items/work-item-runner.ts +15 -42
  633. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +4 -3
  634. package/src/workspace/migrations/052-seed-default-inference-profiles.ts +3 -3
  635. package/src/workspace/migrations/060-memory-v2-init.ts +2 -18
  636. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +59 -0
  637. package/src/workspace/migrations/062-drop-memory-v2-edges-json.ts +27 -0
  638. package/src/workspace/migrations/063-release-notes-dynamic-model-context.ts +70 -0
  639. package/src/workspace/migrations/064-unwind-main-agent-opus-seed.ts +64 -0
  640. package/src/workspace/migrations/registry.ts +8 -0
  641. package/src/workspace/provider-commit-message-generator.ts +3 -3
  642. package/src/__tests__/sandbox-diagnostics.test.ts +0 -138
  643. package/src/__tests__/sandbox-host-parity.test.ts +0 -1024
  644. package/src/__tests__/secret-detection-handler.test.ts +0 -67
  645. package/src/__tests__/secret-scanner-executor.test.ts +0 -450
  646. package/src/__tests__/tcc-sandbox-deny.test.ts +0 -198
  647. package/src/__tests__/terminal-sandbox.test.ts +0 -374
  648. package/src/__tests__/tool-notification-listener.test.ts +0 -65
  649. package/src/context/__tests__/microcompact.test.ts +0 -805
  650. package/src/context/microcompact.ts +0 -443
  651. package/src/daemon/handlers/slack-channel-oauth-install.ts +0 -197
  652. package/src/events/tool-notification-listener.ts +0 -17
  653. package/src/ipc/routes/__tests__/memory-v2-validate.test.ts +0 -219
  654. package/src/memory/v2/__tests__/edges.test.ts +0 -435
  655. package/src/memory/v2/edges.ts +0 -217
  656. package/src/prompts/__tests__/system-prompt-memory-v2.test.ts +0 -197
  657. package/src/runtime/__tests__/chrome-extension-registry.test.ts +0 -518
  658. package/src/runtime/__tests__/client-registry.test.ts +0 -271
  659. package/src/runtime/chrome-extension-registry.ts +0 -368
  660. package/src/runtime/client-registry.ts +0 -254
  661. package/src/runtime/routes/inbound-stages/verification-intercept.ts +0 -329
  662. package/src/tools/secret-detection-handler.ts +0 -269
  663. package/src/tools/terminal/backends/native.ts +0 -327
  664. package/src/tools/terminal/backends/types.ts +0 -37
  665. package/src/tools/terminal/sandbox-diagnostics.ts +0 -87
  666. package/src/tools/terminal/sandbox.ts +0 -40
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * Tests for `assistant/src/memory/v2/page-store.ts`.
3
3
  *
4
- * Coverage matrix (from PR 8 acceptance criteria):
4
+ * Coverage matrix:
5
5
  * - slugify: lowercase / kebab-case / ascii / 80-char cap / empty fallback.
6
+ * - validateSlug: accept set, reject set (path-traversal, malformed shapes).
6
7
  * - readPage / writePage round-trip: frontmatter survives, body preserved.
7
8
  * - readPage on missing file: returns null.
8
9
  * - writePage atomicity: a fault between temp-write and rename leaves the
9
10
  * prior file intact (or the new one) — never a half-written page.
10
- * - listPages: excludes non-.md entries, returns slugs only, missing dir → [].
11
- * - deletePage: idempotent on missing file.
11
+ * - writePage creates parent directories for nested slugs.
12
+ * - listPages: walks subdirectories, returns nested slugs in `/`-form,
13
+ * excludes hidden dirs / non-.md / temp files, missing dir → [].
14
+ * - deletePage / pageExists: nested-slug round-trip, idempotent on missing.
12
15
  *
13
16
  * Tests use temp workspaces under `os.tmpdir()` per the cross-cutting safety
14
17
  * rule in the v2 plan; they never touch `~/.vellum/`.
@@ -33,6 +36,7 @@ import {
33
36
  pageExists,
34
37
  readPage,
35
38
  slugify,
39
+ validateSlug,
36
40
  writePage,
37
41
  } from "../page-store.js";
38
42
  import type { ConceptPage } from "../types.js";
@@ -90,6 +94,12 @@ describe("slugify", () => {
90
94
  expect(slugify("café résumé")).toMatch(/^[a-z0-9-]+$/);
91
95
  });
92
96
 
97
+ test("collapses '/' to hyphen — slugify produces a single segment", () => {
98
+ // Path-shaped slugs are constructed by writing to nested paths, not by
99
+ // passing slash-bearing input through slugify.
100
+ expect(slugify("People/Alice")).toBe("people-alice");
101
+ });
102
+
93
103
  test("caps slug length at 80 chars and re-trims trailing hyphen", () => {
94
104
  const long = "a".repeat(120);
95
105
  const slug = slugify(long);
@@ -109,6 +119,56 @@ describe("slugify", () => {
109
119
  });
110
120
  });
111
121
 
122
+ // ---------------------------------------------------------------------------
123
+ // validateSlug
124
+ // ---------------------------------------------------------------------------
125
+
126
+ describe("validateSlug", () => {
127
+ test.each([
128
+ ["alice"],
129
+ ["a"],
130
+ ["alice-preferences"],
131
+ ["people/alice"],
132
+ ["procs/git-flow"],
133
+ ["arcs/2025-04-cutover"],
134
+ ["a/b/c/d/e"],
135
+ ["people/colleagues/alice"],
136
+ ])("accepts %p", (slug) => {
137
+ expect(() => validateSlug(slug)).not.toThrow();
138
+ });
139
+
140
+ test.each([
141
+ ["empty string", ""],
142
+ ["leading slash", "/alice"],
143
+ ["trailing slash", "alice/"],
144
+ ["double slash", "people//alice"],
145
+ ["dot-dot segment", "people/../alice"],
146
+ ["pure dot-dot", ".."],
147
+ ["leading dot segment", ".hidden/alice"],
148
+ ["backslash", "people\\alice"],
149
+ ["null byte", "alice\0evil"],
150
+ ["whitespace", "alice bob"],
151
+ ["uppercase", "Alice"],
152
+ ["non-ascii", "café"],
153
+ ["leading hyphen", "-alice"],
154
+ ["non-alphanumeric", "alice!"],
155
+ ])("rejects %s (%p)", (_label, slug) => {
156
+ expect(() => validateSlug(slug)).toThrow(/Invalid concept-page slug/);
157
+ });
158
+
159
+ test("rejects slugs longer than 200 chars", () => {
160
+ expect(() => validateSlug("a".repeat(201))).toThrow(
161
+ /Invalid concept-page slug/,
162
+ );
163
+ });
164
+
165
+ test("rejects segments longer than 80 chars even if total is under 200", () => {
166
+ expect(() => validateSlug("a".repeat(81))).toThrow(
167
+ /Invalid concept-page slug/,
168
+ );
169
+ });
170
+ });
171
+
112
172
  // ---------------------------------------------------------------------------
113
173
  // readPage / writePage round-trip
114
174
  // ---------------------------------------------------------------------------
@@ -188,6 +248,50 @@ describe("writePage + readPage round-trip", () => {
188
248
  const read = await readPage(workspaceDir, page1.slug);
189
249
  expect(read!.body).toBe("second version\n");
190
250
  });
251
+
252
+ test("writePage creates parent directories for nested slugs", async () => {
253
+ const page = makePage({ slug: "people/alice" });
254
+ await writePage(workspaceDir, page);
255
+
256
+ const filePath = join(
257
+ workspaceDir,
258
+ "memory",
259
+ "concepts",
260
+ "people",
261
+ "alice.md",
262
+ );
263
+ expect(existsSync(filePath)).toBe(true);
264
+
265
+ const read = await readPage(workspaceDir, "people/alice");
266
+ expect(read!.slug).toBe("people/alice");
267
+ expect(read!.body).toBe(page.body);
268
+ });
269
+
270
+ test("writePage round-trips deeply nested slugs", async () => {
271
+ const page = makePage({ slug: "people/colleagues/alice" });
272
+ await writePage(workspaceDir, page);
273
+
274
+ const read = await readPage(workspaceDir, "people/colleagues/alice");
275
+ expect(read!.slug).toBe("people/colleagues/alice");
276
+ expect(read!.frontmatter.edges).toEqual(page.frontmatter.edges);
277
+ expect(read!.body).toBe(page.body);
278
+ });
279
+
280
+ test("writePage rejects malicious slugs and writes nothing at the escape target", async () => {
281
+ await expect(
282
+ writePage(workspaceDir, makePage({ slug: "../escape" })),
283
+ ).rejects.toThrow(/Invalid concept-page slug/);
284
+
285
+ // `../escape` would resolve to `<workspace>/memory/escape.md`. Confirm
286
+ // the validation throw fired before any I/O — no file at that target.
287
+ expect(existsSync(join(workspaceDir, "memory", "escape.md"))).toBe(false);
288
+ });
289
+
290
+ test("readPage rejects malicious slugs", async () => {
291
+ await expect(readPage(workspaceDir, "../escape")).rejects.toThrow(
292
+ /Invalid concept-page slug/,
293
+ );
294
+ });
191
295
  });
192
296
 
193
297
  // ---------------------------------------------------------------------------
@@ -239,6 +343,40 @@ describe("writePage atomicity", () => {
239
343
  expect(orphanTmps).toEqual([]);
240
344
  });
241
345
 
346
+ test("nested-slug write that fails leaves no orphan tmp in nested folder", async () => {
347
+ // Seed a nested page so the parent dir exists.
348
+ const original = makePage({
349
+ slug: "people/alice",
350
+ body: "original body\n",
351
+ });
352
+ await writePage(workspaceDir, original);
353
+
354
+ const targetPath = join(
355
+ workspaceDir,
356
+ "memory",
357
+ "concepts",
358
+ "people",
359
+ "alice.md",
360
+ );
361
+ rmSync(targetPath);
362
+ mkdirSync(targetPath);
363
+ writeFileSync(join(targetPath, "blocker"), "x", "utf-8");
364
+
365
+ await expect(
366
+ writePage(
367
+ workspaceDir,
368
+ makePage({ slug: "people/alice", body: "interrupted\n" }),
369
+ ),
370
+ ).rejects.toThrow();
371
+
372
+ rmSync(targetPath, { recursive: true, force: true });
373
+
374
+ const peopleDir = join(workspaceDir, "memory", "concepts", "people");
375
+ const remaining = readdirSync(peopleDir);
376
+ const orphanTmps = remaining.filter((name) => name.includes(".tmp."));
377
+ expect(orphanTmps).toEqual([]);
378
+ });
379
+
242
380
  test("successful write produces no orphan tmp files", async () => {
243
381
  await writePage(workspaceDir, makePage());
244
382
 
@@ -274,15 +412,51 @@ describe("listPages", () => {
274
412
  expect(slugs).toEqual(["alice"]);
275
413
  });
276
414
 
277
- test("excludes subdirectories (only files count)", async () => {
415
+ test("walks subdirectories and returns nested slugs in '/'-form", async () => {
278
416
  await writePage(workspaceDir, makePage({ slug: "alice" }));
417
+ await writePage(workspaceDir, makePage({ slug: "people/bob" }));
418
+ await writePage(workspaceDir, makePage({ slug: "people/carol" }));
419
+ await writePage(workspaceDir, makePage({ slug: "arcs/2025-04/cutover" }));
279
420
 
280
- mkdirSync(join(workspaceDir, "memory", "concepts", "subdir"), {
281
- recursive: true,
282
- });
421
+ const slugs = await listPages(workspaceDir);
422
+ expect(slugs).toEqual([
423
+ "alice",
424
+ "arcs/2025-04/cutover",
425
+ "people/bob",
426
+ "people/carol",
427
+ ]);
428
+ });
429
+
430
+ test("skips hidden subdirectories and non-.md files inside nested dirs", async () => {
431
+ await writePage(workspaceDir, makePage({ slug: "people/alice" }));
432
+
433
+ const conceptsDir = join(workspaceDir, "memory", "concepts");
434
+ mkdirSync(join(conceptsDir, ".git"), { recursive: true });
435
+ writeFileSync(join(conceptsDir, ".git", "config.md"), "fake", "utf-8");
436
+ writeFileSync(join(conceptsDir, "people", "notes.txt"), "ignore", "utf-8");
283
437
 
284
438
  const slugs = await listPages(workspaceDir);
285
- expect(slugs).toEqual(["alice"]);
439
+ expect(slugs).toEqual(["people/alice"]);
440
+ });
441
+
442
+ test("skips orphaned .tmp.* files at any depth", async () => {
443
+ const conceptsDir = join(workspaceDir, "memory", "concepts");
444
+ await writePage(workspaceDir, makePage({ slug: "people/alice" }));
445
+
446
+ // Synthesize an orphan tmp file at root and inside the people/ subdir.
447
+ writeFileSync(
448
+ join(conceptsDir, "alice.md.tmp.123.abc-def"),
449
+ "stranded",
450
+ "utf-8",
451
+ );
452
+ writeFileSync(
453
+ join(conceptsDir, "people", "bob.md.tmp.123.abc-def"),
454
+ "stranded",
455
+ "utf-8",
456
+ );
457
+
458
+ const slugs = await listPages(workspaceDir);
459
+ expect(slugs).toEqual(["people/alice"]);
286
460
  });
287
461
 
288
462
  test("returns [] when the concepts directory does not exist", async () => {
@@ -316,6 +490,15 @@ describe("deletePage", () => {
316
490
  expect(await readPage(workspaceDir, page.slug)).toBeNull();
317
491
  });
318
492
 
493
+ test("removes nested pages", async () => {
494
+ const page = makePage({ slug: "people/alice" });
495
+ await writePage(workspaceDir, page);
496
+ expect(await pageExists(workspaceDir, "people/alice")).toBe(true);
497
+
498
+ await deletePage(workspaceDir, "people/alice");
499
+ expect(await pageExists(workspaceDir, "people/alice")).toBe(false);
500
+ });
501
+
319
502
  test("is idempotent — deleting a missing page does not throw", async () => {
320
503
  await deletePage(workspaceDir, "never-existed");
321
504
  // Second call still does not throw.
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Tests for `assistant/src/memory/v2/prompts/consolidation.ts` —
3
+ * specifically `resolveConsolidationPrompt` which loads an optional
4
+ * file-based override and falls back to the bundled prompt when the
5
+ * override is missing/empty/unreadable.
6
+ */
7
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
+ import { homedir, tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import {
11
+ afterAll,
12
+ afterEach,
13
+ beforeAll,
14
+ beforeEach,
15
+ describe,
16
+ expect,
17
+ mock,
18
+ test,
19
+ } from "bun:test";
20
+
21
+ const warnCalls: Array<{ data: unknown; msg: string }> = [];
22
+ const recordingLogger = {
23
+ warn: (data: unknown, msg: string) => {
24
+ warnCalls.push({ data, msg });
25
+ },
26
+ info: () => {},
27
+ debug: () => {},
28
+ error: () => {},
29
+ trace: () => {},
30
+ fatal: () => {},
31
+ child: () => recordingLogger,
32
+ };
33
+
34
+ mock.module("../../../util/logger.js", () => ({
35
+ getLogger: () => recordingLogger,
36
+ }));
37
+
38
+ let tmpWorkspace: string;
39
+ let previousWorkspaceEnv: string | undefined;
40
+
41
+ beforeAll(() => {
42
+ tmpWorkspace = mkdtempSync(join(tmpdir(), "memory-v2-prompt-test-"));
43
+ previousWorkspaceEnv = process.env.VELLUM_WORKSPACE_DIR;
44
+ process.env.VELLUM_WORKSPACE_DIR = tmpWorkspace;
45
+ });
46
+
47
+ afterAll(() => {
48
+ if (previousWorkspaceEnv === undefined) {
49
+ delete process.env.VELLUM_WORKSPACE_DIR;
50
+ } else {
51
+ process.env.VELLUM_WORKSPACE_DIR = previousWorkspaceEnv;
52
+ }
53
+ rmSync(tmpWorkspace, { recursive: true, force: true });
54
+ });
55
+
56
+ const { CONSOLIDATION_PROMPT, CUTOFF_PLACEHOLDER, resolveConsolidationPrompt } =
57
+ await import("../prompts/consolidation.js");
58
+
59
+ const CUTOFF = "2026-05-01T12:00:00.000Z";
60
+
61
+ const bundledPrompt = (): string =>
62
+ (CONSOLIDATION_PROMPT as string).replaceAll(CUTOFF_PLACEHOLDER, CUTOFF);
63
+
64
+ beforeEach(() => {
65
+ warnCalls.length = 0;
66
+ mkdirSync(tmpWorkspace, { recursive: true });
67
+ });
68
+
69
+ afterEach(() => {
70
+ for (const entry of ["custom-prompt.md", "empty.md", "no-placeholder.md"]) {
71
+ rmSync(join(tmpWorkspace, entry), { force: true });
72
+ }
73
+ });
74
+
75
+ describe("resolveConsolidationPrompt — no override", () => {
76
+ test("returns the bundled prompt with {{CUTOFF}} substituted when overridePath is null", () => {
77
+ const result = resolveConsolidationPrompt(null, CUTOFF);
78
+ expect(result).toContain("You are running memory consolidation");
79
+ expect(result).toContain(CUTOFF);
80
+ expect(result).not.toContain(CUTOFF_PLACEHOLDER);
81
+ expect(warnCalls).toHaveLength(0);
82
+ });
83
+ });
84
+
85
+ describe("resolveConsolidationPrompt — with override", () => {
86
+ test("loads an absolute path verbatim and substitutes {{CUTOFF}}", () => {
87
+ const path = join(tmpWorkspace, "custom-prompt.md");
88
+ writeFileSync(path, "Custom prompt at {{CUTOFF}}\n");
89
+
90
+ const result = resolveConsolidationPrompt(path, CUTOFF);
91
+
92
+ expect(result).toBe(`Custom prompt at ${CUTOFF}\n`);
93
+ expect(warnCalls).toHaveLength(0);
94
+ });
95
+
96
+ test("resolves a relative path against the workspace dir", () => {
97
+ writeFileSync(
98
+ join(tmpWorkspace, "custom-prompt.md"),
99
+ "Workspace-relative {{CUTOFF}}\n",
100
+ );
101
+
102
+ const result = resolveConsolidationPrompt("custom-prompt.md", CUTOFF);
103
+
104
+ expect(result).toBe(`Workspace-relative ${CUTOFF}\n`);
105
+ expect(warnCalls).toHaveLength(0);
106
+ });
107
+
108
+ test("expands a leading ~/ to the home directory", () => {
109
+ const filename = `.vellum-prompt-test-${process.pid}.md`;
110
+ const path = join(homedir(), filename);
111
+ writeFileSync(path, "Home dir {{CUTOFF}}\n");
112
+ try {
113
+ const result = resolveConsolidationPrompt(`~/${filename}`, CUTOFF);
114
+ expect(result).toBe(`Home dir ${CUTOFF}\n`);
115
+ expect(warnCalls).toHaveLength(0);
116
+ } finally {
117
+ rmSync(path, { force: true });
118
+ }
119
+ });
120
+
121
+ test("returns the file body verbatim when {{CUTOFF}} is absent", () => {
122
+ const body = "No placeholder here. Just a plain prompt.\n";
123
+ writeFileSync(join(tmpWorkspace, "no-placeholder.md"), body);
124
+
125
+ const result = resolveConsolidationPrompt("no-placeholder.md", CUTOFF);
126
+
127
+ expect(result).toBe(body);
128
+ expect(warnCalls).toHaveLength(0);
129
+ });
130
+
131
+ test("substitutes every {{CUTOFF}} occurrence (replaceAll, not replace)", () => {
132
+ writeFileSync(
133
+ join(tmpWorkspace, "custom-prompt.md"),
134
+ "{{CUTOFF}} ... {{CUTOFF}} ... {{CUTOFF}}",
135
+ );
136
+
137
+ const result = resolveConsolidationPrompt("custom-prompt.md", CUTOFF);
138
+
139
+ expect(result).toBe(`${CUTOFF} ... ${CUTOFF} ... ${CUTOFF}`);
140
+ expect(result).not.toContain(CUTOFF_PLACEHOLDER);
141
+ });
142
+ });
143
+
144
+ describe("resolveConsolidationPrompt — failure modes", () => {
145
+ test("falls back to bundled prompt and logs a warning when the file is missing", () => {
146
+ const result = resolveConsolidationPrompt(
147
+ "/this/path/does/not/exist.md",
148
+ CUTOFF,
149
+ );
150
+
151
+ expect(result).toBe(bundledPrompt());
152
+ expect(warnCalls).toHaveLength(1);
153
+ const data = warnCalls[0].data as Record<string, unknown>;
154
+ expect(data.code).toBe("ENOENT");
155
+ expect(data.fallback).toBe("bundled");
156
+ });
157
+
158
+ test("falls back to bundled prompt when the file is empty", () => {
159
+ const path = join(tmpWorkspace, "empty.md");
160
+ writeFileSync(path, "");
161
+
162
+ const result = resolveConsolidationPrompt(path, CUTOFF);
163
+
164
+ expect(result).toBe(bundledPrompt());
165
+ expect(warnCalls).toHaveLength(1);
166
+ const data = warnCalls[0].data as Record<string, unknown>;
167
+ expect(data.reason).toBe("empty_override");
168
+ });
169
+
170
+ test("falls back to bundled prompt when the file is whitespace-only", () => {
171
+ const path = join(tmpWorkspace, "empty.md");
172
+ writeFileSync(path, " \n\n\t\n");
173
+
174
+ const result = resolveConsolidationPrompt(path, CUTOFF);
175
+
176
+ expect(result).toBe(bundledPrompt());
177
+ expect(warnCalls).toHaveLength(1);
178
+ const data = warnCalls[0].data as Record<string, unknown>;
179
+ expect(data.reason).toBe("empty_override");
180
+ });
181
+ });
@@ -19,6 +19,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
19
19
  import { makeMockLogger } from "../../../__tests__/helpers/mock-logger.js";
20
20
  import type { ResolvedSkill } from "../../../config/skill-state.js";
21
21
  import type { SkillSummary } from "../../../config/skills.js";
22
+ import type { CatalogSkill } from "../../../skills/catalog-install.js";
22
23
 
23
24
  mock.module("../../../util/logger.js", () => ({
24
25
  getLogger: () => makeMockLogger(),
@@ -31,6 +32,8 @@ mock.module("../../../util/logger.js", () => ({
31
32
  interface TestState {
32
33
  catalog: SkillSummary[];
33
34
  resolved: ResolvedSkill[];
35
+ fullCatalog: CatalogSkill[];
36
+ fullCatalogThrows: Error | null;
34
37
  flagsEnabled: Record<string, boolean>;
35
38
  embedThrows: Error | null;
36
39
  embedReturn: number[][];
@@ -49,6 +52,8 @@ interface TestState {
49
52
  const state: TestState = {
50
53
  catalog: [],
51
54
  resolved: [],
55
+ fullCatalog: [],
56
+ fullCatalogThrows: null,
52
57
  flagsEnabled: {},
53
58
  embedThrows: null,
54
59
  embedReturn: [],
@@ -104,6 +109,13 @@ mock.module("../skill-qdrant.js", () => ({
104
109
  },
105
110
  }));
106
111
 
112
+ mock.module("../../../skills/catalog-cache.js", () => ({
113
+ getCatalog: async () => {
114
+ if (state.fullCatalogThrows) throw state.fullCatalogThrows;
115
+ return state.fullCatalog;
116
+ },
117
+ }));
118
+
107
119
  // Imported AFTER all mocks are wired so the module under test sees the stubs.
108
120
  const { seedV2SkillEntries, getSkillCapability, _resetSkillStoreForTests } =
109
121
  await import("../skill-store.js");
@@ -128,6 +140,8 @@ function makeSummary(overrides: Partial<SkillSummary> = {}): SkillSummary {
128
140
  function resetState(): void {
129
141
  state.catalog = [];
130
142
  state.resolved = [];
143
+ state.fullCatalog = [];
144
+ state.fullCatalogThrows = null;
131
145
  state.flagsEnabled = {};
132
146
  state.embedThrows = null;
133
147
  state.embedReturn = [];
@@ -196,6 +210,63 @@ describe("seedV2SkillEntries", () => {
196
210
  expect(state.upsertCalls[0].id).toBe("example-skill-a");
197
211
  });
198
212
 
213
+ test("does not re-seed an installed-but-disabled skill from the remote catalog", async () => {
214
+ // Regression for https://github.com/vellum-ai/vellum-assistant/pull/28635
215
+ // (Codex P1): if `seenIds` is built only from enabled skills, a locally
216
+ // installed-but-disabled skill falls through to the catalog loop and gets
217
+ // embedded as if it were a discoverable uninstalled skill — contradicting
218
+ // the user's explicit disablement.
219
+ const enabledSkill = makeSummary({ id: "example-skill-a" });
220
+ const disabledSkill = makeSummary({ id: "example-skill-b" });
221
+ state.catalog = [enabledSkill, disabledSkill];
222
+ state.resolved = [
223
+ { summary: enabledSkill, state: "enabled" },
224
+ { summary: disabledSkill, state: "disabled" },
225
+ ];
226
+ // The remote catalog also contains the disabled skill (same id) — the
227
+ // seed function must NOT pull it back in via `getCatalog()`.
228
+ state.fullCatalog = [
229
+ {
230
+ id: "example-skill-b",
231
+ name: "example-skill-b",
232
+ description: "Disabled skill that also lives in the remote catalog",
233
+ },
234
+ ];
235
+ state.embedReturn = [[0.1, 0.2, 0.3]];
236
+
237
+ await seedV2SkillEntries();
238
+
239
+ expect(state.upsertCalls).toHaveLength(1);
240
+ expect(state.upsertCalls[0].id).toBe("example-skill-a");
241
+ });
242
+
243
+ test("seeds genuinely uninstalled catalog skills alongside enabled installed skills", async () => {
244
+ const installed = makeSummary({ id: "example-skill-a" });
245
+ state.catalog = [installed];
246
+ state.resolved = [{ summary: installed, state: "enabled" }];
247
+ state.fullCatalog = [
248
+ {
249
+ id: "example-skill-a",
250
+ name: "example-skill-a",
251
+ description: "Installed (must not duplicate)",
252
+ },
253
+ {
254
+ id: "uninstalled-skill",
255
+ name: "uninstalled-skill",
256
+ description: "Discoverable from the catalog",
257
+ },
258
+ ];
259
+ state.embedReturn = [
260
+ [0.1, 0.2, 0.3],
261
+ [0.4, 0.5, 0.6],
262
+ ];
263
+
264
+ await seedV2SkillEntries();
265
+
266
+ const ids = state.upsertCalls.map((c) => c.id).sort();
267
+ expect(ids).toEqual(["example-skill-a", "uninstalled-skill"]);
268
+ });
269
+
199
270
  test("skips skills whose declared feature flag is disabled", async () => {
200
271
  const flagged = makeSummary({
201
272
  id: "example-skill-a",
@@ -224,6 +295,12 @@ describe("seedV2SkillEntries", () => {
224
295
  { summary: skillA, state: "enabled" },
225
296
  { summary: skillB, state: "enabled" },
226
297
  ];
298
+ // Remote catalog must be non-empty so catalogAvailable is true and
299
+ // pruning is not skipped.
300
+ state.fullCatalog = [
301
+ { id: "example-skill-a", name: "example-skill-a", description: "A" },
302
+ { id: "example-skill-b", name: "example-skill-b", description: "B" },
303
+ ];
227
304
  state.embedReturn = [
228
305
  [0.1, 0.2, 0.3],
229
306
  [0.4, 0.5, 0.6],
@@ -250,6 +327,12 @@ describe("seedV2SkillEntries", () => {
250
327
  { summary: unflagged, state: "enabled" },
251
328
  ];
252
329
  state.flagsEnabled = { "off-flag": false };
330
+ // Remote catalog must be non-empty so catalogAvailable is true and
331
+ // pruning is not skipped.
332
+ state.fullCatalog = [
333
+ { id: "example-skill-a", name: "example-skill-a", description: "A" },
334
+ { id: "example-skill-b", name: "example-skill-b", description: "B" },
335
+ ];
253
336
  state.embedReturn = [[0.4, 0.5, 0.6]];
254
337
 
255
338
  await seedV2SkillEntries();
@@ -320,17 +403,46 @@ describe("seedV2SkillEntries", () => {
320
403
  expect(after).toEqual(before);
321
404
  });
322
405
 
323
- test("no enabled skills yields empty cache and a single empty prune call", async () => {
406
+ test("no enabled skills yields empty cache and no prune when catalog is empty", async () => {
324
407
  state.catalog = [];
325
408
  state.resolved = [];
409
+ // fullCatalog defaults to [] — catalog unavailable, so pruning is skipped.
326
410
 
327
411
  await seedV2SkillEntries();
328
412
 
329
413
  expect(state.upsertCalls).toHaveLength(0);
330
- expect(state.pruneCalls).toHaveLength(1);
331
- expect([...state.pruneCalls[0]]).toEqual([]);
414
+ expect(state.pruneCalls).toHaveLength(0);
332
415
  expect(getSkillCapability("anything")).toBeNull();
333
416
  });
417
+
418
+ test("no enabled skills prunes when catalog is available", async () => {
419
+ state.catalog = [];
420
+ state.resolved = [];
421
+ state.fullCatalog = [
422
+ { id: "remote-only", name: "remote-only", description: "Remote skill" },
423
+ ];
424
+ state.embedReturn = [[0.1, 0.2, 0.3]];
425
+
426
+ await seedV2SkillEntries();
427
+
428
+ expect(state.upsertCalls).toHaveLength(1);
429
+ expect(state.upsertCalls[0].id).toBe("remote-only");
430
+ expect(state.pruneCalls).toHaveLength(1);
431
+ expect([...state.pruneCalls[0]]).toEqual(["remote-only"]);
432
+ });
433
+
434
+ test("skips pruning when catalog fetch returns empty (network failure guard)", async () => {
435
+ const skillA = makeSummary({ id: "example-skill-a" });
436
+ state.catalog = [skillA];
437
+ state.resolved = [{ summary: skillA, state: "enabled" }];
438
+ state.fullCatalog = []; // Simulates cold cache / network failure
439
+ state.embedReturn = [[0.1, 0.2, 0.3]];
440
+
441
+ await seedV2SkillEntries();
442
+
443
+ expect(state.upsertCalls).toHaveLength(1);
444
+ expect(state.pruneCalls).toHaveLength(0);
445
+ });
334
446
  });
335
447
 
336
448
  describe("getSkillCapability", () => {