@vellumai/assistant 0.10.4 → 0.10.5-dev.202607022329.d89bf2f

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 (866) hide show
  1. package/ARCHITECTURE.md +94 -91
  2. package/Dockerfile +5 -16
  3. package/README.md +7 -10
  4. package/bun.lock +0 -4
  5. package/docs/architecture/security.md +20 -20
  6. package/docs/stt-provider-onboarding.md +4 -5
  7. package/docs/trusted-contact-access.md +7 -7
  8. package/eslint-rules/cli-no-daemon-internals.js +4 -0
  9. package/eslint.config.mjs +7 -1
  10. package/knip.json +0 -1
  11. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +233 -0
  13. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +40 -0
  14. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -8
  15. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +33 -0
  17. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +216 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +91 -0
  19. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +39 -3
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
  21. package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
  22. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
  23. package/openapi.yaml +1073 -225
  24. package/package.json +1 -4
  25. package/scripts/test.sh +35 -13
  26. package/src/__tests__/access-request-seed-content-blocks.test.ts +60 -4
  27. package/src/__tests__/acp-session.test.ts +4 -1
  28. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +0 -6
  29. package/src/__tests__/add-message-preassigned-id.test.ts +56 -0
  30. package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
  31. package/src/__tests__/app-conversation-ids-backfill.test.ts +3 -0
  32. package/src/__tests__/app-source-watcher.test.ts +11 -0
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
  34. package/src/__tests__/assistant-attachments.test.ts +62 -4
  35. package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -1
  36. package/src/__tests__/attachments-store-heic-normalize.test.ts +230 -0
  37. package/src/__tests__/attachments-store.test.ts +3 -0
  38. package/src/__tests__/background-shell-bash.test.ts +34 -0
  39. package/src/__tests__/background-shell-host-bash.test.ts +12 -0
  40. package/src/__tests__/background-tool-registry.test.ts +97 -0
  41. package/src/__tests__/background-tool-routes.test.ts +72 -1
  42. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  43. package/src/__tests__/btw-routes.test.ts +57 -3
  44. package/src/__tests__/call-controller.test.ts +474 -84
  45. package/src/__tests__/call-domain.test.ts +48 -1
  46. package/src/__tests__/call-pointer-messages.test.ts +57 -19
  47. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +2 -6
  48. package/src/__tests__/call-routes-http.test.ts +15 -14
  49. package/src/__tests__/call-setup-flow-invite.test.ts +568 -0
  50. package/src/__tests__/call-setup-flow-midcall-trust.test.ts +231 -0
  51. package/src/__tests__/call-setup-flow-name-capture.test.ts +754 -0
  52. package/src/__tests__/call-setup-flow-verification.test.ts +925 -0
  53. package/src/__tests__/call-setup-flow.test.ts +311 -0
  54. package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -1
  55. package/src/__tests__/channel-guardian.test.ts +4 -7
  56. package/src/__tests__/channel-readiness-service.test.ts +0 -6
  57. package/src/__tests__/channel-readiness-slack-remote.test.ts +0 -4
  58. package/src/__tests__/channel-retry-sweep.test.ts +76 -64
  59. package/src/__tests__/channel-setup-panel-ack.test.ts +184 -0
  60. package/src/__tests__/clear-all-lexical.test.ts +90 -0
  61. package/src/__tests__/compactor-media-strip.test.ts +177 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +5 -5
  63. package/src/__tests__/config-schema.test.ts +17 -0
  64. package/src/__tests__/contact-store-interaction-info.test.ts +27 -70
  65. package/src/__tests__/contacts-relay-reads.test.ts +188 -23
  66. package/src/__tests__/contacts-tools.test.ts +42 -1
  67. package/src/__tests__/contacts-write.test.ts +181 -0
  68. package/src/__tests__/context-search-conversations-source.test.ts +220 -81
  69. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  71. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  72. package/src/__tests__/conversation-agent-loop.test.ts +39 -0
  73. package/src/__tests__/conversation-analysis-routes.test.ts +1 -1
  74. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -0
  75. package/src/__tests__/conversation-clear-safety.test.ts +0 -1
  76. package/src/__tests__/conversation-crud-enabled-plugins.test.ts +60 -0
  77. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -1
  78. package/src/__tests__/conversation-disk-view.test.ts +1 -0
  79. package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
  80. package/src/__tests__/conversation-error.test.ts +0 -18
  81. package/src/__tests__/conversation-fork-crud.test.ts +67 -0
  82. package/src/__tests__/conversation-history-web-search.test.ts +7 -137
  83. package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
  84. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
  85. package/src/__tests__/conversation-process-callsite.test.ts +37 -0
  86. package/src/__tests__/conversation-queue.test.ts +149 -0
  87. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +392 -0
  88. package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
  89. package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
  90. package/src/__tests__/conversation-runtime-assembly.test.ts +36 -16
  91. package/src/__tests__/conversation-skill-tools.test.ts +73 -0
  92. package/src/__tests__/conversation-wipe.test.ts +20 -5
  93. package/src/__tests__/credential-prompt-route.test.ts +69 -0
  94. package/src/__tests__/credential-security-invariants.test.ts +1 -3
  95. package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
  96. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -62
  97. package/src/__tests__/edit-propagation.test.ts +56 -2
  98. package/src/__tests__/empty-state-greeting-cache.test.ts +9 -0
  99. package/src/__tests__/gateway-only-enforcement.test.ts +15 -110
  100. package/src/__tests__/guardian-expiry-notifier.test.ts +1 -0
  101. package/src/__tests__/guardian-question-mode.test.ts +57 -0
  102. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -1
  103. package/src/__tests__/guardian-wait-controller.test.ts +619 -0
  104. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +33 -2
  105. package/src/__tests__/heic-fixture.ts +55 -0
  106. package/src/__tests__/helpers/seed-contact-channel.ts +0 -2
  107. package/src/__tests__/http-conversation-lineage.test.ts +6 -1
  108. package/src/__tests__/image-conversion.test.ts +170 -0
  109. package/src/__tests__/inbound-invite-redemption.test.ts +72 -341
  110. package/src/__tests__/introduction-card-resolver.test.ts +354 -0
  111. package/src/__tests__/invite-redeemed-ipc.test.ts +97 -0
  112. package/src/__tests__/invite-routes-http.test.ts +246 -420
  113. package/src/__tests__/job-handler-registration-guard.test.ts +104 -0
  114. package/src/__tests__/lexical-index-dual-write.test.ts +426 -0
  115. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +2 -2
  116. package/src/__tests__/list-messages-attachments.test.ts +159 -0
  117. package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
  118. package/src/__tests__/list-messages-queued.test.ts +25 -0
  119. package/src/__tests__/managed-store.test.ts +129 -1
  120. package/src/__tests__/media-stream-audio-transcode.test.ts +113 -0
  121. package/src/__tests__/media-stream-output.test.ts +350 -19
  122. package/src/__tests__/media-stream-server-integration.test.ts +943 -169
  123. package/src/__tests__/media-stream-stt-session.test.ts +498 -1
  124. package/src/__tests__/memory-jobs-worker-lanes.test.ts +2 -2
  125. package/src/__tests__/model-intents.test.ts +1 -1
  126. package/src/__tests__/mtime-cache.test.ts +19 -0
  127. package/src/__tests__/non-member-access-request.test.ts +349 -3
  128. package/src/__tests__/notification-decision-fallback.test.ts +104 -8
  129. package/src/__tests__/notification-decision-strategy.test.ts +57 -8
  130. package/src/__tests__/onboarding-template-contract.test.ts +27 -43
  131. package/src/__tests__/openai-provider.test.ts +33 -0
  132. package/src/__tests__/openai-responses-provider.test.ts +40 -0
  133. package/src/__tests__/persistence-layering-guard.test.ts +29 -20
  134. package/src/__tests__/persona-resolver.test.ts +56 -2
  135. package/src/__tests__/personal-memory-auth-bypass.test.ts +62 -0
  136. package/src/__tests__/phone.test.ts +1 -15
  137. package/src/__tests__/plugin-bootstrap.test.ts +25 -196
  138. package/src/__tests__/plugin-disabled-state.test.ts +33 -0
  139. package/src/__tests__/plugin-effective-enabled-set.test.ts +227 -0
  140. package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
  141. package/src/__tests__/plugin-import-boundary-guard.test.ts +156 -42
  142. package/src/__tests__/plugin-injector-contribution.test.ts +4 -20
  143. package/src/__tests__/plugin-pipeline.test.ts +63 -1
  144. package/src/__tests__/plugin-route-contribution.test.ts +62 -115
  145. package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
  146. package/src/__tests__/plugin-types.test.ts +0 -1
  147. package/src/__tests__/pointer-conversation-trust.test.ts +38 -6
  148. package/src/__tests__/pre-model-call-sanitize.test.ts +1 -1
  149. package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
  150. package/src/__tests__/public-ingress-urls.test.ts +0 -48
  151. package/src/__tests__/qdrant-collection-migration.test.ts +145 -2
  152. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +0 -1
  153. package/src/__tests__/resolve-trust-class.test.ts +28 -10
  154. package/src/__tests__/scaffold-managed-skill-tool.test.ts +137 -0
  155. package/src/__tests__/schedule-retry.test.ts +38 -14
  156. package/src/__tests__/schedule-routes-workflow-validation.test.ts +3 -3
  157. package/src/__tests__/schedule-routes.test.ts +14 -6
  158. package/src/__tests__/schedule-store.test.ts +3 -1
  159. package/src/__tests__/scheduler-disk-pressure.test.ts +17 -6
  160. package/src/__tests__/scheduler-recurrence.test.ts +57 -107
  161. package/src/__tests__/scheduler-reuse-conversation.test.ts +51 -46
  162. package/src/__tests__/scheduler-wake.test.ts +13 -23
  163. package/src/__tests__/scheduler-worker-standdown.test.ts +220 -0
  164. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  165. package/src/__tests__/shell-observability.test.ts +1 -0
  166. package/src/__tests__/skill-boundary-guard.test.ts +3 -4
  167. package/src/__tests__/skill-load-plugin-scope.test.ts +238 -0
  168. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -0
  169. package/src/__tests__/skill-script-runner-host.test.ts +53 -0
  170. package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
  171. package/src/__tests__/skill-tool-factory.test.ts +73 -1
  172. package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -14
  173. package/src/__tests__/slack-inbound-verification.test.ts +36 -0
  174. package/src/__tests__/slack-notification-approval-card.test.ts +89 -2
  175. package/src/__tests__/slack-reaction-canonical-approval.test.ts +51 -1
  176. package/src/__tests__/stt-hints.test.ts +2 -256
  177. package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
  178. package/src/__tests__/subagent-call-site-routing.test.ts +72 -0
  179. package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
  180. package/src/__tests__/subagent-persistence.test.ts +122 -0
  181. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  182. package/src/__tests__/subagent-terminal-message.test.ts +99 -0
  183. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -1
  184. package/src/__tests__/system-prompt.test.ts +17 -118
  185. package/src/__tests__/task-scheduler.test.ts +68 -89
  186. package/src/__tests__/telephony-credential-preflight.test.ts +312 -0
  187. package/src/__tests__/telephony-tts-guard.test.ts +538 -0
  188. package/src/__tests__/title-generate-hook.test.ts +16 -0
  189. package/src/__tests__/tool-approval-handler.test.ts +87 -1
  190. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +45 -20
  191. package/src/__tests__/tool-grant-request-escalation.test.ts +39 -0
  192. package/src/__tests__/trust-context-guards.test.ts +21 -10
  193. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -0
  194. package/src/__tests__/twilio-provider.test.ts +17 -17
  195. package/src/__tests__/twilio-routes-twiml.test.ts +6 -543
  196. package/src/__tests__/twilio-routes.test.ts +257 -387
  197. package/src/__tests__/twilio-validation.test.ts +1 -1
  198. package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
  199. package/src/__tests__/usage-cache-backfill-migration.test.ts +5 -0
  200. package/src/__tests__/voice-approval-resolver-relay.test.ts +150 -0
  201. package/src/__tests__/voice-ingress-preflight.test.ts +61 -0
  202. package/src/__tests__/voice-quality.test.ts +2 -2
  203. package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -7
  204. package/src/__tests__/voice-session-bridge-processing-wait.test.ts +27 -0
  205. package/src/__tests__/voice-session-bridge.test.ts +1 -10
  206. package/src/__tests__/workspace-git-service.test.ts +1 -1
  207. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -0
  208. package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
  209. package/src/__tests__/workspace-migration-120-revise-onboarding-threads.test.ts +175 -0
  210. package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
  211. package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
  212. package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
  213. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +235 -241
  214. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
  215. package/src/acp/__tests__/session-manager-persistence.test.ts +2 -0
  216. package/src/acp/__tests__/session-manager-resume.test.ts +8 -0
  217. package/src/acp/__tests__/session-manager-usage.test.ts +6 -0
  218. package/src/acp/__tests__/session-manager.test.ts +6 -0
  219. package/src/acp/agent-process.test.ts +116 -0
  220. package/src/acp/agent-process.ts +71 -0
  221. package/src/acp/auto-install.ts +12 -8
  222. package/src/acp/failure-error.test.ts +109 -0
  223. package/src/acp/failure-error.ts +111 -0
  224. package/src/acp/prepare-agent-env.ts +0 -12
  225. package/src/acp/resolve-agent.test.ts +6 -45
  226. package/src/acp/resolve-agent.ts +2 -4
  227. package/src/acp/session-manager.test.ts +312 -0
  228. package/src/acp/session-manager.ts +102 -46
  229. package/src/agent/ax-tree-compaction.test.ts +4 -1
  230. package/src/agent/image-optimize.ts +17 -127
  231. package/src/agent/loop.ts +45 -192
  232. package/src/api/events/background-tool-completed.test.ts +51 -0
  233. package/src/api/events/background-tool-completed.ts +29 -0
  234. package/src/api/events/background-tool-started.test.ts +36 -0
  235. package/src/api/events/background-tool-started.ts +28 -0
  236. package/src/api/events/conversation-error.ts +0 -1
  237. package/src/api/events/open-panel.ts +33 -0
  238. package/src/api/events/subagent-status-changed.ts +4 -1
  239. package/src/api/index.ts +19 -0
  240. package/src/api/responses/conversation-message.ts +34 -0
  241. package/src/approvals/guardian-decision-primitive.ts +74 -24
  242. package/src/approvals/guardian-request-resolvers.ts +450 -122
  243. package/src/apps/app-store.ts +1 -0
  244. package/src/apps/shared-app-links-store.ts +1 -0
  245. package/src/avatar/identity-avatar.ts +3 -7
  246. package/src/background-wake/wake-intent-hooks.test.ts +1 -1
  247. package/src/calls/__tests__/fish-audio-client.test.ts +73 -0
  248. package/src/calls/__tests__/gateway-invite-reader.test.ts +170 -0
  249. package/src/calls/__tests__/relay-setup-router.test.ts +126 -102
  250. package/src/calls/call-controller.ts +134 -48
  251. package/src/calls/call-domain.ts +18 -42
  252. package/src/calls/call-pointer-messages.ts +51 -67
  253. package/src/calls/call-setup-flow-types.ts +76 -0
  254. package/src/calls/call-setup-flow.ts +1743 -0
  255. package/src/calls/call-speech-output.ts +87 -23
  256. package/src/calls/call-store.ts +4 -0
  257. package/src/calls/call-transport.ts +22 -36
  258. package/src/calls/fish-audio-client.ts +3 -0
  259. package/src/calls/gateway-invite-reader.ts +98 -0
  260. package/src/calls/guardian-wait-controller.ts +451 -0
  261. package/src/calls/media-stream-audio-transcode.ts +82 -2
  262. package/src/calls/media-stream-output.ts +142 -36
  263. package/src/calls/media-stream-server.ts +297 -178
  264. package/src/calls/media-stream-stt-session.ts +393 -50
  265. package/src/calls/media-turn-detector.ts +4 -6
  266. package/src/calls/relay-access-wait.ts +3 -4
  267. package/src/calls/relay-setup-router.ts +22 -43
  268. package/src/calls/relay-verification.ts +17 -24
  269. package/src/calls/resolve-call-tts-provider.ts +96 -29
  270. package/src/calls/stt-hints.ts +24 -87
  271. package/src/calls/telephony-credential-preflight.ts +159 -0
  272. package/src/calls/telephony-tts-capability.ts +183 -0
  273. package/src/calls/tts-call-strategy.ts +18 -20
  274. package/src/calls/twilio-provider.ts +3 -2
  275. package/src/calls/twilio-routes.ts +47 -351
  276. package/src/calls/types.ts +5 -3
  277. package/src/calls/voice-ingress-preflight.ts +9 -0
  278. package/src/calls/voice-quality.ts +13 -18
  279. package/src/calls/voice-session-bridge.ts +25 -35
  280. package/src/channels/__tests__/types.test.ts +28 -0
  281. package/src/channels/channel-verification-sessions.ts +7 -2
  282. package/src/channels/config.ts +0 -42
  283. package/src/channels/slack-thread-store.ts +12 -0
  284. package/src/channels/types.ts +23 -0
  285. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
  286. package/src/cli/commands/__tests__/schedules.test.ts +83 -34
  287. package/src/cli/commands/__tests__/status.test.ts +42 -2
  288. package/src/cli/commands/contacts.ts +2 -2
  289. package/src/cli/commands/credentials.ts +17 -0
  290. package/src/cli/commands/monitoring.ts +191 -0
  291. package/src/cli/commands/schedules-worker.ts +175 -0
  292. package/src/cli/commands/schedules.ts +169 -43
  293. package/src/cli/commands/status.ts +12 -1
  294. package/src/cli/commands/tools.ts +5 -1
  295. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
  296. package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
  297. package/src/cli/lib/__tests__/toggle-plugin.test.ts +7 -0
  298. package/src/cli/lib/search-plugins.ts +6 -0
  299. package/src/cli/lib/toggle-plugin.ts +21 -2
  300. package/src/cli/program.ts +3 -1
  301. package/src/config/__tests__/deployment-context-defaults.test.ts +23 -0
  302. package/src/config/acp-defaults.test.ts +0 -10
  303. package/src/config/acp-defaults.ts +0 -6
  304. package/src/config/bundled-skills/acp/SKILL.md +13 -45
  305. package/src/config/bundled-skills/acp/TOOLS.json +3 -3
  306. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  307. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
  308. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +1 -1
  309. package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -1
  310. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  311. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -9
  312. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +10 -7
  313. package/src/config/bundled-skills/skill-management/TOOLS.json +10 -0
  314. package/src/config/call-site-defaults.ts +0 -2
  315. package/src/config/feature-flag-registry.json +16 -8
  316. package/src/config/loader.ts +11 -0
  317. package/src/config/schema.ts +9 -1
  318. package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
  319. package/src/config/schemas/__tests__/memory-v3.test.ts +64 -0
  320. package/src/config/schemas/call-site-catalog.ts +0 -12
  321. package/src/config/schemas/calls.ts +8 -0
  322. package/src/config/schemas/elevenlabs.ts +1 -1
  323. package/src/config/schemas/llm.ts +0 -2
  324. package/src/config/schemas/memory-v2.ts +1 -1
  325. package/src/config/schemas/memory-v3.ts +82 -0
  326. package/src/config/schemas/monitoring.ts +61 -0
  327. package/src/config/schemas/schedules.ts +35 -0
  328. package/src/config/seed-inference-profiles.ts +2 -2
  329. package/src/config/skills-plugin-filter.test.ts +96 -0
  330. package/src/config/skills.ts +26 -0
  331. package/src/contacts/__tests__/member-write-relay.test.ts +156 -4
  332. package/src/contacts/canonical-guardian-store.ts +5 -0
  333. package/src/contacts/contact-store.ts +81 -35
  334. package/src/contacts/contacts-write.ts +50 -25
  335. package/src/contacts/member-write-relay.ts +115 -31
  336. package/src/contacts/types.ts +0 -8
  337. package/src/context/compactor.ts +29 -15
  338. package/src/context/outbound-sanitize.ts +202 -0
  339. package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
  340. package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +122 -0
  341. package/src/daemon/__tests__/embedding-reconcile.test.ts +99 -16
  342. package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
  343. package/src/daemon/abort-watchdog.ts +7 -0
  344. package/src/daemon/app-source-watcher.ts +9 -0
  345. package/src/daemon/assistant-attachments.ts +42 -0
  346. package/src/daemon/conversation-agent-loop-handlers.ts +256 -147
  347. package/src/daemon/conversation-agent-loop.ts +61 -68
  348. package/src/daemon/conversation-error.ts +1 -15
  349. package/src/daemon/conversation-history.ts +32 -236
  350. package/src/daemon/conversation-initial-prompt.ts +77 -0
  351. package/src/daemon/conversation-lifecycle.ts +25 -1
  352. package/src/daemon/conversation-messaging.ts +2 -2
  353. package/src/daemon/conversation-plugin-scope.ts +38 -0
  354. package/src/daemon/conversation-process.ts +50 -14
  355. package/src/daemon/conversation-runtime-assembly.ts +36 -6
  356. package/src/daemon/conversation-skill-tools.ts +19 -3
  357. package/src/daemon/conversation-store.ts +3 -3
  358. package/src/daemon/conversation-surfaces.ts +171 -0
  359. package/src/daemon/conversation-tool-setup.test.ts +95 -0
  360. package/src/daemon/conversation-tool-setup.ts +116 -1
  361. package/src/daemon/conversation-turn-finalize.ts +211 -0
  362. package/src/daemon/conversation.ts +92 -1
  363. package/src/daemon/disk-pressure-policy.ts +2 -1
  364. package/src/daemon/event-loop-watchdog.ts +22 -6
  365. package/src/daemon/external-plugins-bootstrap.ts +47 -134
  366. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +1 -1
  367. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +26 -14
  368. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +27 -14
  369. package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -5
  370. package/src/daemon/handlers/config-a2a.ts +7 -14
  371. package/src/daemon/handlers/config-channels.ts +2 -10
  372. package/src/daemon/handlers/conversation-history.ts +3 -1
  373. package/src/daemon/handlers/conversations.ts +0 -52
  374. package/src/daemon/handlers/shared.ts +4 -24
  375. package/src/daemon/handlers/skills.ts +4 -1
  376. package/src/daemon/host-cu-proxy.ts +1 -1
  377. package/src/daemon/lifecycle.ts +59 -472
  378. package/src/daemon/message-protocol.ts +3 -3
  379. package/src/daemon/message-provenance.ts +12 -14
  380. package/src/daemon/message-types/background-tools.ts +28 -0
  381. package/src/daemon/message-types/contacts.ts +2 -10
  382. package/src/daemon/message-types/conversations.ts +10 -0
  383. package/src/daemon/message-types/integrations.ts +2 -0
  384. package/src/daemon/message-types/surfaces.ts +1 -0
  385. package/src/daemon/message-types/sync.ts +1 -0
  386. package/src/daemon/pointer-conversation-trust.ts +23 -9
  387. package/src/daemon/pointer-turn-runner.ts +174 -0
  388. package/src/daemon/providers-setup.ts +16 -0
  389. package/src/daemon/shutdown-handlers.ts +139 -47
  390. package/src/daemon/skill-memory-refresh.ts +2 -2
  391. package/src/daemon/sqlite-corruption-watchdog.test.ts +217 -0
  392. package/src/daemon/sqlite-corruption-watchdog.ts +155 -0
  393. package/src/daemon/tool-setup-types.ts +8 -0
  394. package/src/daemon/trust-context.ts +21 -6
  395. package/src/daemon/turn-latency-tracker.test.ts +93 -0
  396. package/src/daemon/turn-latency-tracker.ts +38 -0
  397. package/src/daemon/wake-conversation-ops.ts +10 -8
  398. package/src/documents/document-comments-store.test.ts +7 -1
  399. package/src/documents/document-comments-store.ts +8 -0
  400. package/src/documents/document-store.ts +15 -0
  401. package/src/export/__tests__/transcript-formatter.test.ts +11 -0
  402. package/src/export/transcript-formatter.ts +49 -67
  403. package/src/home/__tests__/feed-writer.test.ts +247 -1
  404. package/src/home/conversation-starter-checkpoints.ts +1 -0
  405. package/src/home/feed-writer.ts +61 -0
  406. package/src/home/job-handlers/conversation-starters.ts +1 -0
  407. package/src/hooks/hook-loader.ts +12 -0
  408. package/src/hooks/registry.ts +32 -4
  409. package/src/inbound/public-ingress-urls.ts +0 -17
  410. package/src/ipc/__tests__/contacts-info-ipc.test.ts +285 -0
  411. package/src/ipc/assistant-server.ts +28 -7
  412. package/src/ipc/routes/__tests__/contacts-mirror-apply-atomicity.test.ts +62 -0
  413. package/src/ipc/routes/__tests__/contacts-mirror-apply.test.ts +272 -0
  414. package/src/ipc/routes/__tests__/contacts-mirror-ipc-routes.test.ts +322 -0
  415. package/src/ipc/routes/__tests__/invite-ipc-routes.test.ts +60 -4
  416. package/src/ipc/routes/contacts-info-ipc-routes.ts +239 -0
  417. package/src/ipc/routes/contacts-mirror-ipc-routes.ts +202 -0
  418. package/src/ipc/routes/db-proxy.ts +5 -2
  419. package/src/ipc/routes/invite-ipc-routes.ts +53 -39
  420. package/src/jobs/worker.ts +9 -2
  421. package/src/live-voice/__tests__/live-voice-archive.test.ts +10 -1
  422. package/src/live-voice/live-voice-archive.ts +5 -0
  423. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +22 -6
  424. package/src/messaging/providers/channel-transport.ts +6 -0
  425. package/src/messaging/providers/index.ts +3 -0
  426. package/src/messaging/providers/slack/api.test.ts +49 -1
  427. package/src/messaging/providers/slack/api.ts +110 -0
  428. package/src/messaging/providers/slack/send.test.ts +85 -0
  429. package/src/messaging/providers/slack/send.ts +100 -28
  430. package/src/messaging/providers/slack/transport.ts +7 -13
  431. package/src/messaging/providers/whatsapp/send.ts +29 -9
  432. package/src/monitoring/__tests__/control.test.ts +235 -0
  433. package/src/monitoring/__tests__/sample-ring-buffer.test.ts +96 -0
  434. package/src/monitoring/control.ts +105 -0
  435. package/src/monitoring/resource-sampler.ts +258 -0
  436. package/src/monitoring/sample-ring-buffer.ts +108 -0
  437. package/src/monitoring/worker.ts +94 -0
  438. package/src/notifications/README.md +1 -1
  439. package/src/notifications/access-request-copy.ts +94 -23
  440. package/src/notifications/adapters/slack.ts +89 -21
  441. package/src/notifications/approval-card-builder.ts +33 -18
  442. package/src/notifications/approval-card-data.ts +48 -15
  443. package/src/notifications/broadcaster.ts +25 -16
  444. package/src/notifications/decision-engine.ts +131 -32
  445. package/src/notifications/guardian-question-mode.ts +58 -10
  446. package/src/permissions/approval-policy.ts +8 -1
  447. package/src/permissions/checker.ts +2 -0
  448. package/src/permissions/gateway-threshold-reader.ts +7 -6
  449. package/src/permissions/ipc-risk-types.ts +1 -0
  450. package/src/permissions/secret-prompter.ts +11 -3
  451. package/src/permissions/types.ts +2 -2
  452. package/src/persistence/__tests__/conversation-queries-search.test.ts +397 -0
  453. package/src/persistence/__tests__/conversation-search-lexical.test.ts +103 -0
  454. package/src/persistence/__tests__/jobs-worker-memory-disabled.test.ts +95 -0
  455. package/src/persistence/__tests__/slow-query-log.test.ts +266 -0
  456. package/src/persistence/__tests__/slow-sync-log.test.ts +155 -0
  457. package/src/persistence/a2a-invite-store.ts +205 -0
  458. package/src/persistence/attachments-store.ts +94 -10
  459. package/src/persistence/checkpoints.ts +45 -0
  460. package/src/persistence/conversation-crud.ts +453 -260
  461. package/src/persistence/conversation-display-order-migration.ts +8 -2
  462. package/src/persistence/conversation-group-migration.test.ts +8 -0
  463. package/src/persistence/conversation-group-migration.ts +9 -0
  464. package/src/persistence/conversation-queries.ts +244 -125
  465. package/src/persistence/conversation-row-batch-delete.ts +9 -5
  466. package/src/persistence/conversation-search-lexical.ts +49 -0
  467. package/src/persistence/db-async-query.ts +42 -0
  468. package/src/persistence/db-connection.ts +13 -0
  469. package/src/persistence/delivery-channels.ts +0 -82
  470. package/src/persistence/delivery-crud.ts +27 -6
  471. package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +18 -5
  472. package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +334 -0
  473. package/src/persistence/embeddings/embedding-identity.ts +17 -5
  474. package/src/persistence/embeddings/embedding-runtime-manager.ts +31 -0
  475. package/src/persistence/embeddings/messages-lexical-index.ts +396 -0
  476. package/src/persistence/embeddings/qdrant-client.ts +81 -5
  477. package/src/persistence/group-crud.ts +13 -1
  478. package/src/persistence/job-handlers/__tests__/message-lexical-backfill.test.ts +506 -0
  479. package/src/persistence/job-handlers/__tests__/message-lexical-enqueue.test.ts +164 -0
  480. package/src/persistence/job-handlers/__tests__/message-lexical.test.ts +259 -0
  481. package/src/persistence/job-handlers/cleanup.ts +57 -9
  482. package/src/persistence/job-handlers/message-lexical-backfill.ts +187 -0
  483. package/src/persistence/job-handlers/message-lexical.ts +251 -0
  484. package/src/persistence/jobs-store.ts +102 -8
  485. package/src/persistence/jobs-worker.ts +68 -28
  486. package/src/persistence/llm-request-log-store.ts +35 -19
  487. package/src/persistence/llm-usage-store.ts +11 -0
  488. package/src/persistence/memory-lifecycle-hooks.test.ts +65 -0
  489. package/src/persistence/memory-lifecycle-hooks.ts +55 -1
  490. package/src/persistence/migrations/028-call-session-mode.ts +1 -1
  491. package/src/persistence/migrations/116-messages-fts.ts +8 -1
  492. package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
  493. package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
  494. package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
  495. package/src/persistence/migrations/314-add-conversation-enabled-plugins.ts +25 -0
  496. package/src/persistence/migrations/315-create-a2a-invites.ts +58 -0
  497. package/src/persistence/migrations/316-drop-contact-channels-invite-id.ts +28 -0
  498. package/src/persistence/migrations/317-canonical-guardian-requester-signals.ts +24 -0
  499. package/src/persistence/migrations/318-drop-contact-channel-telemetry.ts +32 -0
  500. package/src/persistence/migrations/__tests__/315-create-a2a-invites.test.ts +161 -0
  501. package/src/persistence/migrations/__tests__/316-drop-contact-channels-invite-id.test.ts +156 -0
  502. package/src/persistence/migrations/__tests__/318-drop-contact-channel-telemetry.test.ts +134 -0
  503. package/src/persistence/migrations/__tests__/migration-prefix-guard.test.ts +99 -0
  504. package/src/persistence/raw-query.ts +50 -11
  505. package/src/persistence/schema/a2a.ts +14 -0
  506. package/src/persistence/schema/contacts.ts +0 -62
  507. package/src/persistence/schema/conversation-groups.ts +12 -0
  508. package/src/persistence/schema/conversations.ts +2 -0
  509. package/src/persistence/schema/documents.ts +65 -0
  510. package/src/persistence/schema/guardian.ts +4 -0
  511. package/src/persistence/schema/index.ts +4 -0
  512. package/src/persistence/schema/memory-injection.ts +62 -0
  513. package/src/persistence/schema/workflows.ts +55 -0
  514. package/src/persistence/slow-query-log.ts +437 -0
  515. package/src/persistence/slow-sync-log.ts +204 -0
  516. package/src/persistence/steps.ts +16 -0
  517. package/src/persistence/subagent-store.ts +125 -0
  518. package/src/persistence/worker-control.ts +45 -238
  519. package/src/plugin-api/event-hub-facade.ts +172 -0
  520. package/src/plugin-api/index.ts +9 -1
  521. package/src/plugin-api/types.ts +40 -9
  522. package/src/plugins/defaults/default-plugin-names.ts +60 -0
  523. package/src/plugins/defaults/index.ts +19 -29
  524. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +46 -67
  525. package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +36 -1
  526. package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +2 -4
  527. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +2 -2
  528. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +9 -0
  529. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -4
  530. package/src/plugins/defaults/memory/config.ts +11 -0
  531. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -1
  532. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +124 -113
  533. package/src/plugins/defaults/memory/embeddings.ts +77 -0
  534. package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
  535. package/src/plugins/defaults/memory/frontmatter.ts +50 -0
  536. package/src/plugins/defaults/memory/graph/bootstrap.test.ts +36 -10
  537. package/src/plugins/defaults/memory/graph/bootstrap.ts +6 -5
  538. package/src/plugins/defaults/memory/graph/consolidation.ts +3 -5
  539. package/src/plugins/defaults/memory/graph/extraction.ts +3 -6
  540. package/src/plugins/defaults/memory/graph/graph-search.ts +12 -23
  541. package/src/plugins/defaults/memory/graph/narrative.ts +3 -5
  542. package/src/plugins/defaults/memory/graph/pattern-scan.ts +3 -5
  543. package/src/plugins/defaults/memory/graph/retriever.test.ts +1 -6
  544. package/src/plugins/defaults/memory/graph/retriever.ts +5 -10
  545. package/src/plugins/defaults/memory/graph/tools.ts +1 -1
  546. package/src/plugins/defaults/memory/hooks/init.ts +31 -5
  547. package/src/plugins/defaults/memory/indexer.ts +2 -3
  548. package/src/plugins/defaults/memory/injectors.ts +2 -2
  549. package/src/plugins/defaults/memory/job-handler-registration.ts +96 -0
  550. package/src/plugins/defaults/memory/job-handlers/backfill.ts +4 -26
  551. package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +6 -19
  552. package/src/plugins/defaults/memory/job-handlers/embedding.ts +9 -22
  553. package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +2 -3
  554. package/src/plugins/defaults/memory/job-handlers.ts +13 -12
  555. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +1 -1
  556. package/src/plugins/defaults/memory/llm-helpers.ts +76 -0
  557. package/src/plugins/defaults/memory/memory-retrospective-job.ts +8 -5
  558. package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +2 -2
  559. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +2 -0
  560. package/src/plugins/defaults/memory/persistence-hooks.ts +46 -1
  561. package/src/plugins/defaults/memory/pkb/pkb-index.ts +1 -5
  562. package/src/plugins/defaults/memory/pkb/pkb-search.ts +2 -2
  563. package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +4 -6
  564. package/src/plugins/defaults/memory/routes/memory-item-routes.ts +1 -1
  565. package/src/plugins/defaults/memory/search/semantic.ts +2 -2
  566. package/src/plugins/defaults/memory/startup.ts +244 -0
  567. package/src/plugins/defaults/memory/task-memory-cleanup.ts +8 -0
  568. package/src/{daemon → plugins/defaults/memory/v2}/__tests__/memory-v2-startup.test.ts +6 -6
  569. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +2 -13
  570. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +4 -5
  571. package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +1 -8
  572. package/src/plugins/defaults/memory/v2/activation.ts +2 -4
  573. package/src/plugins/defaults/memory/v2/cli-command-store.ts +2 -4
  574. package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +1 -1
  575. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.test.ts +8 -8
  576. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.ts +20 -22
  577. package/src/plugins/defaults/memory/v2/migration.ts +2 -5
  578. package/src/plugins/defaults/memory/v2/page-store.ts +1 -1
  579. package/src/plugins/defaults/memory/v2/qdrant.ts +2 -3
  580. package/src/plugins/defaults/memory/v2/router.ts +5 -5
  581. package/src/plugins/defaults/memory/v2/sim.ts +2 -4
  582. package/src/plugins/defaults/memory/v2/skill-store.ts +2 -4
  583. package/src/plugins/defaults/memory/v2/sweep-job.ts +6 -6
  584. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +11 -9
  585. package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +107 -2
  586. package/src/plugins/defaults/memory/v3/__tests__/gate-flag.test.ts +69 -0
  587. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +181 -0
  588. package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +7 -3
  589. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +345 -8
  590. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -4
  591. package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +64 -0
  592. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +18 -13
  593. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +62 -1
  594. package/src/plugins/defaults/memory/v3/card.ts +1 -4
  595. package/src/plugins/defaults/memory/v3/dense.ts +60 -25
  596. package/src/plugins/defaults/memory/v3/edge.ts +1 -1
  597. package/src/plugins/defaults/memory/v3/gate-flag.ts +20 -0
  598. package/src/plugins/defaults/memory/v3/gate.ts +158 -0
  599. package/src/plugins/defaults/memory/v3/maintain-job.ts +2 -4
  600. package/src/plugins/defaults/memory/v3/orchestrate.ts +186 -25
  601. package/src/plugins/defaults/memory/v3/pool-select.test.ts +7 -9
  602. package/src/plugins/defaults/memory/v3/pool-select.ts +6 -6
  603. package/src/plugins/defaults/memory/v3/prune.ts +2 -2
  604. package/src/plugins/defaults/memory/v3/section-dense-store.ts +8 -11
  605. package/src/plugins/defaults/memory/v3/section-needle.ts +27 -7
  606. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -0
  607. package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +1 -4
  608. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
  609. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  610. package/src/plugins/defaults/turn-context/unified-turn-context.ts +12 -0
  611. package/src/plugins/injector-registry.ts +13 -2
  612. package/src/plugins/mtime-cache.ts +36 -47
  613. package/src/plugins/pipeline.ts +15 -1
  614. package/src/plugins/types.ts +14 -23
  615. package/src/prompts/__tests__/system-prompt.test.ts +170 -32
  616. package/src/prompts/persona-resolver.ts +13 -9
  617. package/src/prompts/system-prompt.ts +30 -9
  618. package/src/prompts/templates/BOOTSTRAP.md +16 -119
  619. package/src/prompts/templates/SOUL.md +43 -13
  620. package/src/prompts/templates/system-sections.ts +32 -27
  621. package/src/prompts/templates/users/default.md +19 -0
  622. package/src/providers/__tests__/placeholder-sentinels.test.ts +56 -0
  623. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +96 -1
  624. package/src/providers/model-intents.ts +2 -2
  625. package/src/providers/openai/chat-completions-provider.ts +14 -1
  626. package/src/providers/openai/responses-provider.ts +18 -0
  627. package/src/providers/placeholder-sentinels.ts +18 -8
  628. package/src/providers/retry.ts +1 -0
  629. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +0 -64
  630. package/src/providers/speech-to-text/__tests__/resolve.test.ts +92 -79
  631. package/src/providers/speech-to-text/deepgram-realtime.test.ts +98 -0
  632. package/src/providers/speech-to-text/deepgram-realtime.ts +62 -3
  633. package/src/providers/speech-to-text/provider-catalog.ts +0 -89
  634. package/src/providers/speech-to-text/resolve.ts +55 -0
  635. package/src/providers/tool-progress-events.test.ts +50 -0
  636. package/src/providers/tool-progress-events.ts +99 -0
  637. package/src/providers/types.ts +1 -1
  638. package/src/providers/vellum-model-routing.test.ts +51 -0
  639. package/src/providers/vellum-model-routing.ts +84 -0
  640. package/src/runtime/AGENTS.md +5 -4
  641. package/src/runtime/__tests__/agent-wake.test.ts +158 -1
  642. package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
  643. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +37 -8
  644. package/src/runtime/access-request-helper.ts +154 -3
  645. package/src/runtime/actor-trust-resolver.ts +7 -21
  646. package/src/runtime/agent-wake.ts +41 -1
  647. package/src/runtime/assistant-event.ts +86 -12
  648. package/src/runtime/auth/__tests__/route-policy.test.ts +31 -0
  649. package/src/runtime/channel-approval-types.ts +52 -2
  650. package/src/runtime/channel-approvals.ts +23 -8
  651. package/src/runtime/channel-invite-transport.ts +2 -2
  652. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  653. package/src/runtime/channel-readiness-service.ts +3 -5
  654. package/src/runtime/channel-retry-sweep.ts +18 -24
  655. package/src/runtime/channel-verification-service.ts +9 -11
  656. package/src/runtime/finalize-event-delivery.ts +30 -16
  657. package/src/runtime/guardian-action-service.ts +10 -11
  658. package/src/runtime/guardian-reply-router.ts +122 -21
  659. package/src/runtime/http-server.ts +44 -101
  660. package/src/runtime/introduction-policy.test.ts +214 -0
  661. package/src/runtime/introduction-policy.ts +284 -0
  662. package/src/runtime/invite-service.ts +104 -361
  663. package/src/runtime/local-actor-identity.test.ts +38 -0
  664. package/src/runtime/local-actor-identity.ts +14 -0
  665. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +22 -0
  666. package/src/runtime/middleware/rate-limiter.ts +30 -0
  667. package/src/runtime/middleware/twilio-validation.ts +5 -7
  668. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
  669. package/src/runtime/migrations/origin-mode.ts +6 -6
  670. package/src/runtime/pre-first-message-gate.ts +1 -0
  671. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +6 -35
  672. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +24 -3
  673. package/src/runtime/routes/__tests__/contact-routes.test.ts +80 -15
  674. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +7 -1
  675. package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -3
  676. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +142 -1
  677. package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +119 -30
  678. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
  679. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +194 -0
  680. package/src/runtime/routes/__tests__/plugins-routes.test.ts +755 -29
  681. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +4 -0
  682. package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +70 -0
  683. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +161 -0
  684. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +1 -1
  685. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
  686. package/src/runtime/routes/acp-routes.ts +15 -3
  687. package/src/runtime/routes/app-management-routes.ts +4 -7
  688. package/src/runtime/routes/approval-routes.ts +4 -0
  689. package/src/runtime/routes/attachment-routes.ts +54 -3
  690. package/src/runtime/routes/avatar-routes.ts +0 -4
  691. package/src/runtime/routes/background-tool-routes.ts +34 -5
  692. package/src/runtime/routes/btw-routes.ts +32 -4
  693. package/src/runtime/routes/channel-route-shared.ts +18 -14
  694. package/src/runtime/routes/channel-verification-routes.ts +0 -3
  695. package/src/runtime/routes/contact-routes.ts +298 -117
  696. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  697. package/src/runtime/routes/conversation-management-routes.ts +74 -37
  698. package/src/runtime/routes/conversation-query-routes.ts +5 -2
  699. package/src/runtime/routes/conversation-routes.ts +222 -82
  700. package/src/runtime/routes/conversations-import-routes.ts +8 -1
  701. package/src/runtime/routes/credential-prompt-routes.ts +20 -5
  702. package/src/runtime/routes/debug-routes.ts +1 -0
  703. package/src/runtime/routes/documents-routes.ts +5 -2
  704. package/src/runtime/routes/empty-state-greeting-cache.ts +37 -10
  705. package/src/runtime/routes/global-search-routes.ts +2 -1
  706. package/src/runtime/routes/guardian-approval-interception.ts +51 -0
  707. package/src/runtime/routes/home-feed-routes.ts +60 -1
  708. package/src/runtime/routes/identity-routes.ts +4 -87
  709. package/src/runtime/routes/inbound-message-handler.ts +82 -41
  710. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +430 -57
  711. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +302 -572
  712. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +76 -270
  713. package/src/runtime/routes/inbound-stages/background-dispatch.ts +21 -116
  714. package/src/runtime/routes/inbound-stages/edit-intercept.ts +9 -1
  715. package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
  716. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
  717. package/src/runtime/routes/index.ts +6 -0
  718. package/src/runtime/routes/inference-provider-connection-routes.ts +22 -16
  719. package/src/runtime/routes/internal-twilio-routes.ts +0 -15
  720. package/src/runtime/routes/messages-lexical-routes.ts +75 -0
  721. package/src/runtime/routes/migration-routes.ts +69 -6
  722. package/src/runtime/routes/monitoring-routes.ts +237 -0
  723. package/src/runtime/routes/plugins-routes.ts +329 -8
  724. package/src/runtime/routes/schedule-routes.ts +49 -8
  725. package/src/runtime/routes/schedule-worker-routes.ts +210 -0
  726. package/src/runtime/routes/secret-routes.ts +1 -1
  727. package/src/runtime/routes/surface-conversation-resolver.ts +1 -0
  728. package/src/runtime/services/__tests__/analyze-conversation.test.ts +54 -9
  729. package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +30 -6
  730. package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +43 -8
  731. package/src/runtime/services/__tests__/conversation-serializer.test.ts +20 -0
  732. package/src/runtime/services/analyze-conversation.ts +14 -4
  733. package/src/runtime/services/conversation-serializer.ts +5 -0
  734. package/src/runtime/slack-no-response.ts +47 -0
  735. package/src/runtime/slack-reply-session.test.ts +633 -0
  736. package/src/runtime/slack-reply-session.ts +382 -0
  737. package/src/runtime/slack-task-progress.ts +95 -0
  738. package/src/runtime/sync/resource-sync-events.ts +25 -0
  739. package/src/runtime/tool-grant-request-helper.ts +17 -1
  740. package/src/runtime/trust-class.test.ts +141 -0
  741. package/src/runtime/trust-class.ts +121 -0
  742. package/src/runtime/trust-verdict-consumer.ts +7 -6
  743. package/src/schedule/__tests__/run-script.test.ts +21 -0
  744. package/src/schedule/__tests__/worker-control.test.ts +241 -0
  745. package/src/schedule/run-script.ts +13 -6
  746. package/src/schedule/schedule-usage-store.ts +1 -0
  747. package/src/schedule/scheduler.ts +178 -127
  748. package/src/schedule/worker-control.ts +118 -0
  749. package/src/schedule/worker.ts +131 -0
  750. package/src/skills/__tests__/categories-cache.test.ts +38 -0
  751. package/src/skills/categories-cache.ts +40 -5
  752. package/src/skills/managed-store.ts +41 -6
  753. package/src/skills/validate-input.ts +47 -0
  754. package/src/stt/types.ts +5 -9
  755. package/src/subagent/manager.ts +267 -39
  756. package/src/subagent/types.ts +6 -2
  757. package/src/subagent/validate-allowlists.ts +65 -0
  758. package/src/telemetry/watchdog-direct-emit.test.ts +109 -0
  759. package/src/telemetry/watchdog-direct-emit.ts +89 -0
  760. package/src/tools/acp/list-agents.test.ts +0 -6
  761. package/src/tools/background-tool-registry.ts +61 -0
  762. package/src/tools/document/document-comment-tool.test.ts +4 -0
  763. package/src/tools/host-terminal/host-shell.test.ts +397 -0
  764. package/src/tools/host-terminal/host-shell.ts +203 -14
  765. package/src/tools/skills/find-similar-skills.test.ts +152 -1
  766. package/src/tools/skills/find-similar-skills.ts +30 -4
  767. package/src/tools/skills/load.ts +36 -0
  768. package/src/tools/skills/scaffold-managed.ts +66 -34
  769. package/src/tools/skills/skill-script-runner.ts +17 -0
  770. package/src/tools/skills/skill-tool-factory.ts +5 -1
  771. package/src/tools/terminal/safe-env.ts +3 -0
  772. package/src/tools/terminal/shell.test.ts +177 -0
  773. package/src/tools/terminal/shell.ts +98 -4
  774. package/src/tools/tool-approval-handler.ts +106 -29
  775. package/{node_modules/@vellumai/skill-host-contracts/src → src/tools}/tool-types.ts +16 -17
  776. package/src/tools/types.ts +29 -17
  777. package/src/tools/ui-surface/definitions.ts +3 -1
  778. package/src/tts/__tests__/provider-adapters.test.ts +37 -0
  779. package/src/tts/__tests__/provider-catalog.test.ts +21 -0
  780. package/src/tts/provider-catalog.ts +40 -9
  781. package/src/tts/providers/fish-audio-provider.ts +19 -10
  782. package/src/tts/types.ts +3 -4
  783. package/src/util/__tests__/process-tree.test.ts +10 -2
  784. package/src/util/__tests__/text-spacing.test.ts +53 -0
  785. package/src/util/cgroup-memory.ts +142 -0
  786. package/src/util/image-conversion.ts +281 -0
  787. package/src/util/phone.ts +1 -8
  788. package/src/util/platform.ts +33 -0
  789. package/src/util/process-tree.ts +10 -2
  790. package/src/util/text-spacing.ts +53 -0
  791. package/src/util/worker-process.ts +271 -0
  792. package/src/workflows/journal-store.ts +16 -1
  793. package/src/workflows/library.test.ts +35 -20
  794. package/src/workflows/library.ts +105 -66
  795. package/src/workspace/git-service.ts +1 -0
  796. package/src/workspace/migrations/011-backfill-installation-id.ts +83 -31
  797. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +41 -22
  798. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
  799. package/src/workspace/migrations/118-seed-now-md.ts +64 -0
  800. package/src/workspace/migrations/119-strip-persisted-memory-v3-tuning-defaults.ts +169 -0
  801. package/src/workspace/migrations/120-revise-onboarding-threads.ts +47 -0
  802. package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
  803. package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
  804. package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
  805. package/src/workspace/migrations/__tests__/119-strip-persisted-memory-v3-tuning-defaults.test.ts +181 -0
  806. package/src/workspace/migrations/registry.ts +12 -0
  807. package/drizzle/0000_dizzy_maggott.sql +0 -301
  808. package/drizzle/meta/0000_snapshot.json +0 -1933
  809. package/drizzle/meta/_journal.json +0 -13
  810. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
  811. package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
  812. package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
  813. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
  814. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
  815. package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
  816. package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
  817. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
  818. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
  819. package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
  820. package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
  821. package/src/__tests__/invite-redemption-service.test.ts +0 -1180
  822. package/src/__tests__/invite-service-ipc.test.ts +0 -350
  823. package/src/__tests__/job-handler-registry-guard.test.ts +0 -125
  824. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
  825. package/src/__tests__/relay-server.test.ts +0 -6153
  826. package/src/__tests__/telephony-stt-routing.test.ts +0 -329
  827. package/src/__tests__/voice-invite-redemption.test.ts +0 -702
  828. package/src/calls/relay-server.ts +0 -2463
  829. package/src/calls/telephony-stt-routing.ts +0 -145
  830. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -128
  831. package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
  832. package/src/daemon/daemon-skill-host.ts +0 -262
  833. package/src/daemon/message-types/meet.ts +0 -143
  834. package/src/daemon/shutdown-registry.ts +0 -40
  835. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
  836. package/src/ipc/__tests__/skill-server.test.ts +0 -182
  837. package/src/ipc/skill-ipc-types.ts +0 -54
  838. package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
  839. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
  840. package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
  841. package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
  842. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
  843. package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
  844. package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
  845. package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
  846. package/src/ipc/skill-routes/config.ts +0 -47
  847. package/src/ipc/skill-routes/events.ts +0 -147
  848. package/src/ipc/skill-routes/identity.ts +0 -21
  849. package/src/ipc/skill-routes/index.ts +0 -37
  850. package/src/ipc/skill-routes/log.ts +0 -40
  851. package/src/ipc/skill-routes/memory.ts +0 -92
  852. package/src/ipc/skill-routes/platform.ts +0 -39
  853. package/src/ipc/skill-routes/providers.ts +0 -162
  854. package/src/ipc/skill-routes/registries.ts +0 -338
  855. package/src/ipc/skill-server.ts +0 -831
  856. package/src/ipc/skill-socket-path.ts +0 -20
  857. package/src/jobs/register-job-handlers.ts +0 -74
  858. package/src/persistence/invite-store.ts +0 -481
  859. package/src/plugins/job-handler-registry.ts +0 -94
  860. package/src/providers/cache-control.ts +0 -26
  861. package/src/runtime/__tests__/runtime-mode.test.ts +0 -62
  862. package/src/runtime/invite-redemption-service.ts +0 -828
  863. package/src/runtime/invite-redemption-templates.ts +0 -46
  864. package/src/runtime/runtime-mode.ts +0 -40
  865. package/src/runtime/slack-dm-text-delivery.ts +0 -177
  866. package/src/util/voice-code.ts +0 -31
@@ -6,8 +6,12 @@
6
6
  * description, version, path; issues omitted when empty)
7
7
  * - `?q=` substring filter (case-insensitive across id/name/description)
8
8
  * - Trimming + empty-string fallthrough on `?q=`
9
- * - Empty install dir → `{ plugins: [] }`
9
+ * - Empty install dir → `{ plugins: [], categoryCounts: {}, totalCount: 0 }`
10
10
  * - Issues array surfaced when present
11
+ * - `category` resolved from the marketplace catalog (null when unknown)
12
+ * - `categoryCounts` / `totalCount` computed before the `?category=` filter
13
+ * - `?category=` filters the list while counts stay unfiltered
14
+ * - A catalog fetch failure degrades `category` to null without erroring
11
15
  *
12
16
  * GET /v1/plugins/search (catalog search):
13
17
  * - Forwards `?q=` and `?ref=` to the `searchPlugins` lib
@@ -22,10 +26,24 @@
22
26
  * DELETE /v1/plugins/:name (uninstall):
23
27
  * - Forwards `pathParams.name` to the `uninstallPlugin` lib
24
28
  * - Returns `{ name, target }` mirroring the lib's `UninstallPluginResult`
29
+ * - Publishes `sync_changed(plugins:list)` on success (threading the
30
+ * `x-vellum-client-id` origin); no broadcast on an error path
25
31
  * - Maps `InvalidPluginNameError` → BadRequestError (400)
26
32
  * - Maps `PluginNotInstalledError` → NotFoundError (404)
27
33
  * - Maps unknown errors → InternalError (500) with message preserved
28
34
  *
35
+ * POST /v1/plugins/:name/enable | disable (toggle):
36
+ * - Forwards `pathParams.name` to the `enablePlugin` / `disablePlugin` lib
37
+ * - Returns `{ ok: true }` and publishes a `sync_changed` carrying the
38
+ * `plugins:list` tag via the canonical resource-sync publisher (enable and
39
+ * disable emit the SAME invalidation)
40
+ * - Threads `x-vellum-client-id` into the published event's `originClientId`
41
+ * - A broadcast failure does not fail a successful toggle (the publisher
42
+ * swallows hub errors)
43
+ * - Maps `InvalidPluginNameError` → BadRequestError (400)
44
+ * - Maps `PluginDirectoryNotFoundError` → NotFoundError (404)
45
+ * - Maps `PluginAlreadyInStateException` → ConflictError (409); no broadcast
46
+ *
29
47
  * The library functions themselves are covered by
30
48
  * `assistant/src/cli/lib/__tests__/list-installed-plugins.test.ts`,
31
49
  * `.../search-plugins.test.ts`, and `.../uninstall-plugin.test.ts`;
@@ -71,6 +89,12 @@ import type {
71
89
  SearchPluginsDeps,
72
90
  } from "../../../cli/lib/search-plugins.js";
73
91
  import { PluginCatalogUnavailableError } from "../../../cli/lib/search-plugins.js";
92
+ import {
93
+ InvalidPluginNameError as ToggleInvalidPluginNameError,
94
+ PluginAlreadyInStateException,
95
+ PluginDirectoryNotFoundError,
96
+ type TogglePluginResult,
97
+ } from "../../../cli/lib/toggle-plugin.js";
74
98
  import {
75
99
  PluginNotInstalledError,
76
100
  type UninstallPluginOptions,
@@ -92,6 +116,16 @@ mock.module("../../../cli/lib/list-installed-plugins.js", () => ({
92
116
  listInstalledPlugins: () => installedFixture,
93
117
  }));
94
118
 
119
+ // Set of plugin dir names carrying a `.disabled` sentinel. Tests populate it to
120
+ // mark a plugin disabled; the real check reads the workspace filesystem, so we
121
+ // substitute an in-memory set to keep the route's `enabled` projection
122
+ // deterministic and decoupled from disk.
123
+ const disabledFixture = new Set<string>();
124
+
125
+ mock.module("../../../plugins/disabled-state.js", () => ({
126
+ isPluginDisabled: (name: string) => disabledFixture.has(name),
127
+ }));
128
+
95
129
  // Mock the catalog cache: `getCatalogSpy` records every invocation and
96
130
  // returns the (unfiltered) catalog the route then filters in memory via the
97
131
  // real `filterPluginCatalog`. The real `search-plugins.js` module is left
@@ -228,6 +262,60 @@ mock.module("../../../cli/lib/plugin-pin-history.js", () => ({
228
262
  resolvePinToMarketplaceCommit: resolvePinSpy,
229
263
  }));
230
264
 
265
+ // Mock the toggle-plugin lib. `enablePlugin` / `disablePlugin` flip a
266
+ // `.disabled` sentinel on disk in production; here we spy on them so the
267
+ // route's broadcast + error mapping is the wiring under test. The error
268
+ // classes pass through real so the handler's `instanceof` checks resolve to
269
+ // the same classes the spies throw.
270
+ const enablePluginSpy = mock((_name: string): TogglePluginResult => {
271
+ throw new Error("enablePluginSpy default impl not configured");
272
+ });
273
+ const disablePluginSpy = mock((_name: string): TogglePluginResult => {
274
+ throw new Error("disablePluginSpy default impl not configured");
275
+ });
276
+
277
+ mock.module("../../../cli/lib/toggle-plugin.js", () => ({
278
+ InvalidPluginNameError: ToggleInvalidPluginNameError,
279
+ PluginAlreadyInStateException,
280
+ PluginDirectoryNotFoundError,
281
+ disablePlugin: disablePluginSpy,
282
+ enablePlugin: enablePluginSpy,
283
+ }));
284
+
285
+ // Spy on broadcastMessage so we can assert the sync_changed invalidation the
286
+ // enable/disable handlers emit. The handlers publish through the canonical
287
+ // `publishPluginsChanged` → `publishSyncInvalidation` path (both left real), so
288
+ // the spy receives the actual `{ type: "sync_changed", tags: [...],
289
+ // originClientId? }` payload the publisher builds.
290
+ const broadcastMessageSpy = mock((_msg: unknown): void => {});
291
+
292
+ mock.module("../../assistant-event-hub.js", () => ({
293
+ broadcastMessage: broadcastMessageSpy,
294
+ }));
295
+
296
+ // Make the valid-slug source deterministic: the real `getLocalCategorySlugs`
297
+ // reads the bundled YAML (absent in the test sandbox), so we pin it to the
298
+ // authoritative Skills taxonomy. This decouples the route's category
299
+ // normalization from the filesystem / network.
300
+ const SKILLS_CATEGORY_SLUGS = new Set([
301
+ "email",
302
+ "calendar",
303
+ "messaging",
304
+ "browsing",
305
+ "productivity",
306
+ "development",
307
+ "voice",
308
+ "commerce",
309
+ "content",
310
+ "health",
311
+ "system",
312
+ "integrations",
313
+ ]);
314
+
315
+ mock.module("../../../skills/categories-cache.js", () => ({
316
+ getLocalCategorySlugs: () => SKILLS_CATEGORY_SLUGS,
317
+ }));
318
+
231
319
  import {
232
320
  BadRequestError,
233
321
  ConflictError,
@@ -235,7 +323,11 @@ import {
235
323
  NotFoundError,
236
324
  ServiceUnavailableError,
237
325
  } from "../errors.js";
238
- import { ROUTES as PLUGINS_ROUTES } from "../plugins-routes.js";
326
+ import {
327
+ loadCategoryMapBounded,
328
+ normalizeMarketplaceCategory,
329
+ ROUTES as PLUGINS_ROUTES,
330
+ } from "../plugins-routes.js";
239
331
  import type { RouteDefinition, RouteHandlerArgs } from "../types.js";
240
332
 
241
333
  function findHandler(operationId: string): RouteDefinition["handler"] {
@@ -253,13 +345,22 @@ const inspectHandler = findHandler("plugins_inspect");
253
345
  const versionsHandler = findHandler("plugins_versions");
254
346
  const upgradeHandler = findHandler("plugins_upgrade");
255
347
  const diffHandler = findHandler("plugins_diff");
348
+ const enableHandler = findHandler("plugins_enable");
349
+ const disableHandler = findHandler("plugins_disable");
256
350
 
257
- function invoke(args: RouteHandlerArgs = {}): {
351
+ async function invoke(args: RouteHandlerArgs = {}): Promise<{
258
352
  plugins: Array<Record<string, unknown>>;
259
- } {
260
- return listHandler(args) as { plugins: Array<Record<string, unknown>> };
353
+ categoryCounts: Record<string, number>;
354
+ totalCount: number;
355
+ }> {
356
+ return (await listHandler(args)) as {
357
+ plugins: Array<Record<string, unknown>>;
358
+ categoryCounts: Record<string, number>;
359
+ totalCount: number;
360
+ };
261
361
  }
262
362
 
363
+ // The route wire shape mirrors the lib match — including `category`.
263
364
  async function invokeSearch(args: RouteHandlerArgs = {}): Promise<{
264
365
  query: string;
265
366
  ref: string;
@@ -285,14 +386,29 @@ function pluginEntry(
285
386
 
286
387
  beforeEach(() => {
287
388
  installedFixture = [];
389
+ disabledFixture.clear();
288
390
  });
289
391
 
290
392
  describe("GET /v1/plugins", () => {
291
- test("returns { plugins: [] } when nothing is installed", () => {
292
- expect(invoke()).toEqual({ plugins: [] });
393
+ beforeEach(() => {
394
+ getCatalogSpy.mockClear();
395
+ // Default to an empty catalog so every installed plugin's category resolves
396
+ // to `null` (bucketed under "system"). Tests that exercise real categories
397
+ // override this.
398
+ getCatalogSpy.mockImplementation(async (ref) => catalog(ref, []));
399
+ });
400
+
401
+ test("returns an empty list (with empty counts) when nothing is installed", async () => {
402
+ expect(await invoke()).toEqual({
403
+ plugins: [],
404
+ categoryCounts: {},
405
+ totalCount: 0,
406
+ });
407
+ // Nothing to categorize → the network-bound catalog lookup is skipped.
408
+ expect(getCatalogSpy).not.toHaveBeenCalled();
293
409
  });
294
410
 
295
- test("projects InstalledPluginInfo → response shape with all fields populated", () => {
411
+ test("projects InstalledPluginInfo → response shape with all fields populated", async () => {
296
412
  installedFixture = [
297
413
  pluginEntry({
298
414
  name: "alpha",
@@ -305,20 +421,46 @@ describe("GET /v1/plugins", () => {
305
421
  }),
306
422
  ];
307
423
 
308
- const result = invoke();
424
+ const result = await invoke();
309
425
  expect(result.plugins).toHaveLength(1);
426
+ // No catalog entry for `alpha`, so its category is null.
310
427
  expect(result.plugins[0]).toEqual({
311
428
  id: "alpha",
312
429
  name: "alpha",
430
+ // No `.disabled` sentinel → the plugin is enabled.
431
+ enabled: true,
313
432
  description: "Alpha plugin",
314
433
  version: "1.2.3",
315
434
  path: "/workspace/plugins/alpha",
435
+ category: null,
316
436
  });
317
437
  // `issues` is omitted (not just undefined) when the entry is clean.
318
438
  expect("issues" in result.plugins[0]!).toBe(false);
319
439
  });
320
440
 
321
- test("uses directory name for `id` and `name` even when package.json#name is scoped", () => {
441
+ test("surfaces a marketplace category when the catalog declares one", async () => {
442
+ getCatalogSpy.mockImplementation(async (ref) =>
443
+ catalog(ref, [
444
+ {
445
+ name: "alpha",
446
+ path: "github:acme/alpha@v1",
447
+ category: "productivity",
448
+ source: { kind: "github", repo: "acme/alpha", ref: "v1" },
449
+ },
450
+ ]),
451
+ );
452
+ installedFixture = [
453
+ pluginEntry({
454
+ name: "alpha",
455
+ packageJson: { name: "alpha", version: "1.2.3" },
456
+ }),
457
+ ];
458
+
459
+ const [entry] = (await invoke()).plugins;
460
+ expect(entry?.category).toBe("productivity");
461
+ });
462
+
463
+ test("uses directory name for `id` and `name` even when package.json#name is scoped", async () => {
322
464
  installedFixture = [
323
465
  pluginEntry({
324
466
  name: "fancy-plugin",
@@ -330,12 +472,12 @@ describe("GET /v1/plugins", () => {
330
472
  }),
331
473
  ];
332
474
 
333
- const [entry] = invoke().plugins;
475
+ const [entry] = (await invoke()).plugins;
334
476
  expect(entry?.id).toBe("fancy-plugin");
335
477
  expect(entry?.name).toBe("fancy-plugin");
336
478
  });
337
479
 
338
- test("nulls description and version when package.json is missing or partial", () => {
480
+ test("nulls description and version when package.json is missing or partial", async () => {
339
481
  installedFixture = [
340
482
  pluginEntry({ name: "no-pkg-json", packageJson: null }),
341
483
  pluginEntry({
@@ -344,7 +486,7 @@ describe("GET /v1/plugins", () => {
344
486
  }),
345
487
  ];
346
488
 
347
- const [missing, partial] = invoke().plugins;
489
+ const [missing, partial] = (await invoke()).plugins;
348
490
  expect(missing).toMatchObject({
349
491
  id: "no-pkg-json",
350
492
  description: null,
@@ -357,7 +499,7 @@ describe("GET /v1/plugins", () => {
357
499
  });
358
500
  });
359
501
 
360
- test("surfaces non-fatal issues array when present", () => {
502
+ test("surfaces non-fatal issues array when present", async () => {
361
503
  installedFixture = [
362
504
  pluginEntry({
363
505
  name: "broken",
@@ -366,11 +508,26 @@ describe("GET /v1/plugins", () => {
366
508
  }),
367
509
  ];
368
510
 
369
- const [entry] = invoke().plugins;
511
+ const [entry] = (await invoke()).plugins;
370
512
  expect(entry?.issues).toEqual(["missing package.json"]);
371
513
  });
372
514
 
373
- test("?q= filters case-insensitively on id, name, and description", () => {
515
+ test("reports enabled: false for a plugin with a `.disabled` sentinel, true otherwise", async () => {
516
+ installedFixture = [
517
+ pluginEntry({ name: "off" }),
518
+ pluginEntry({ name: "on" }),
519
+ ];
520
+ // Only `off` carries the sentinel; `on` has none.
521
+ disabledFixture.add("off");
522
+
523
+ const byId = new Map(
524
+ (await invoke()).plugins.map((p) => [p.id, p.enabled]),
525
+ );
526
+ expect(byId.get("off")).toBe(false);
527
+ expect(byId.get("on")).toBe(true);
528
+ });
529
+
530
+ test("?q= filters case-insensitively on id, name, and description", async () => {
374
531
  installedFixture = [
375
532
  pluginEntry({
376
533
  name: "calendar-sync",
@@ -400,49 +557,307 @@ describe("GET /v1/plugins", () => {
400
557
 
401
558
  // id match
402
559
  expect(
403
- invoke({ queryParams: { q: "calendar" } }).plugins.map((p) => p.id),
560
+ (await invoke({ queryParams: { q: "calendar" } })).plugins.map(
561
+ (p) => p.id,
562
+ ),
404
563
  ).toEqual(["calendar-sync"]);
405
564
 
406
565
  // description match (case-insensitive)
407
566
  expect(
408
- invoke({ queryParams: { q: "GOOGLE" } }).plugins.map((p) => p.id),
567
+ (await invoke({ queryParams: { q: "GOOGLE" } })).plugins.map((p) => p.id),
409
568
  ).toEqual(["calendar-sync"]);
410
569
 
411
570
  // matches multiple
412
571
  expect(
413
- invoke({ queryParams: { q: "o" } })
414
- .plugins.map((p) => p.id)
572
+ (await invoke({ queryParams: { q: "o" } })).plugins
573
+ .map((p) => p.id)
415
574
  .sort(),
416
575
  ).toEqual(["calendar-sync", "todo", "weather"].sort());
417
576
 
418
577
  // no match
419
- expect(invoke({ queryParams: { q: "zzz" } }).plugins).toEqual([]);
578
+ expect((await invoke({ queryParams: { q: "zzz" } })).plugins).toEqual([]);
420
579
  });
421
580
 
422
- test("?q= is trimmed; whitespace-only treated as no filter", () => {
581
+ test("?q= is trimmed; whitespace-only treated as no filter", async () => {
423
582
  installedFixture = [
424
583
  pluginEntry({ name: "alpha" }),
425
584
  pluginEntry({ name: "beta" }),
426
585
  ];
427
586
 
428
587
  expect(
429
- invoke({ queryParams: { q: " " } }).plugins.map((p) => p.id),
588
+ (await invoke({ queryParams: { q: " " } })).plugins.map((p) => p.id),
430
589
  ).toEqual(["alpha", "beta"]);
431
590
  });
432
591
 
433
- test("preserves the order returned by listInstalledPlugins", () => {
592
+ test("preserves the order returned by listInstalledPlugins", async () => {
434
593
  installedFixture = [
435
594
  pluginEntry({ name: "alpha" }),
436
595
  pluginEntry({ name: "beta" }),
437
596
  pluginEntry({ name: "zeta" }),
438
597
  ];
439
598
 
440
- expect(invoke().plugins.map((p) => p.id)).toEqual([
599
+ expect((await invoke()).plugins.map((p) => p.id)).toEqual([
441
600
  "alpha",
442
601
  "beta",
443
602
  "zeta",
444
603
  ]);
445
604
  });
605
+
606
+ test('reports categoryCounts + totalCount, bucketing unknown plugins under "system"', async () => {
607
+ getCatalogSpy.mockImplementation(async (ref) =>
608
+ catalog(ref, [
609
+ {
610
+ name: "calendar-sync",
611
+ path: "github:acme/calendar-sync@v1",
612
+ category: "calendar",
613
+ source: { kind: "github", repo: "acme/calendar-sync", ref: "v1" },
614
+ },
615
+ ]),
616
+ );
617
+ installedFixture = [
618
+ pluginEntry({ name: "calendar-sync" }),
619
+ // Not in the catalog → category null → counted under "system".
620
+ pluginEntry({ name: "mystery" }),
621
+ ];
622
+
623
+ const result = await invoke();
624
+ expect(result.totalCount).toBe(2);
625
+ expect(result.categoryCounts).toEqual({ calendar: 1, system: 1 });
626
+ expect(result.plugins.find((p) => p.id === "calendar-sync")?.category).toBe(
627
+ "calendar",
628
+ );
629
+ expect(result.plugins.find((p) => p.id === "mystery")?.category).toBeNull();
630
+ });
631
+
632
+ test("?category= filters the list while categoryCounts stays unfiltered", async () => {
633
+ getCatalogSpy.mockImplementation(async (ref) =>
634
+ catalog(ref, [
635
+ {
636
+ name: "notes",
637
+ path: "github:acme/notes@v1",
638
+ category: "productivity",
639
+ source: { kind: "github", repo: "acme/notes", ref: "v1" },
640
+ },
641
+ {
642
+ name: "inbox",
643
+ path: "github:acme/inbox@v1",
644
+ category: "email",
645
+ source: { kind: "github", repo: "acme/inbox", ref: "v1" },
646
+ },
647
+ ]),
648
+ );
649
+ installedFixture = [
650
+ pluginEntry({ name: "notes" }),
651
+ pluginEntry({ name: "inbox" }),
652
+ // No catalog entry → "system".
653
+ pluginEntry({ name: "loose" }),
654
+ ];
655
+
656
+ const result = await invoke({ queryParams: { category: "productivity" } });
657
+ // The list is filtered to the productivity bucket...
658
+ expect(result.plugins.map((p) => p.id)).toEqual(["notes"]);
659
+ // ...but the badge counts reflect the unfiltered totals.
660
+ expect(result.categoryCounts).toEqual({
661
+ productivity: 1,
662
+ email: 1,
663
+ system: 1,
664
+ });
665
+ expect(result.totalCount).toBe(3);
666
+ });
667
+
668
+ test("degrades to category: null without failing when the catalog fetch throws", async () => {
669
+ getCatalogSpy.mockImplementation(async () => {
670
+ throw new PluginCatalogUnavailableError("HTTP 403", 403);
671
+ });
672
+ installedFixture = [
673
+ pluginEntry({ name: "alpha" }),
674
+ pluginEntry({ name: "beta" }),
675
+ ];
676
+
677
+ const result = await invoke();
678
+ expect(result.plugins.map((p) => p.category)).toEqual([null, null]);
679
+ expect(result.categoryCounts).toEqual({ system: 2 });
680
+ expect(result.totalCount).toBe(2);
681
+ });
682
+
683
+ // The category lookup is bounded so a slow/hanging marketplace fetch (a cold
684
+ // cache stuck on GitHub) can't hold up the installed list — it degrades to an
685
+ // empty map exactly like the rejection path above. `loadCategoryMapBounded`
686
+ // takes an injectable timeout so we can prove the bound without waiting the
687
+ // full 1500ms production budget.
688
+ test("bounds the catalog lookup: a stall past the budget degrades to an empty map", async () => {
689
+ // GIVEN a catalog fetch that resolves only AFTER the (shortened) budget.
690
+ getCatalogSpy.mockImplementation(
691
+ (ref) =>
692
+ new Promise<PluginCatalog>((resolve) => {
693
+ setTimeout(
694
+ () =>
695
+ resolve(
696
+ catalog(ref, [
697
+ {
698
+ name: "alpha",
699
+ path: "github:acme/alpha@v1",
700
+ category: "productivity",
701
+ source: { kind: "github", repo: "acme/alpha", ref: "v1" },
702
+ },
703
+ ]),
704
+ ),
705
+ 80,
706
+ );
707
+ }),
708
+ );
709
+
710
+ // WHEN the bound (10ms) elapses first, the timer wins the race.
711
+ const map = await loadCategoryMapBounded(10);
712
+
713
+ // THEN we fall back to an empty map, so every category resolves to null and
714
+ // the installed list returns immediately instead of blocking on GitHub.
715
+ expect(map.size).toBe(0);
716
+ });
717
+
718
+ test("returns the catalog category map when the lookup resolves within the budget", async () => {
719
+ getCatalogSpy.mockImplementation(async (ref) =>
720
+ catalog(ref, [
721
+ {
722
+ name: "alpha",
723
+ path: "github:acme/alpha@v1",
724
+ category: "productivity",
725
+ source: { kind: "github", repo: "acme/alpha", ref: "v1" },
726
+ },
727
+ ]),
728
+ );
729
+
730
+ const map = await loadCategoryMapBounded();
731
+ expect(map.get("alpha")).toBe("productivity");
732
+ });
733
+
734
+ test("normalizes a `developer` marketplace category to `development`", async () => {
735
+ // The marketplace ships `developer`, which is NOT a Skills slug; without
736
+ // normalization it would be counted in "All" but have no rail row.
737
+ getCatalogSpy.mockImplementation(async (ref) =>
738
+ catalog(ref, [
739
+ {
740
+ name: "dev-tools",
741
+ path: "github:acme/dev-tools@v1",
742
+ category: "developer",
743
+ source: { kind: "github", repo: "acme/dev-tools", ref: "v1" },
744
+ },
745
+ ]),
746
+ );
747
+ installedFixture = [pluginEntry({ name: "dev-tools" })];
748
+
749
+ const result = await invoke();
750
+ expect(result.plugins[0]?.category).toBe("development");
751
+ expect(result.categoryCounts).toEqual({ development: 1 });
752
+ });
753
+
754
+ test("folds an unknown marketplace category (`memory`) to null → system", async () => {
755
+ getCatalogSpy.mockImplementation(async (ref) =>
756
+ catalog(ref, [
757
+ {
758
+ name: "simple-memory",
759
+ path: "github:acme/simple-memory@v1",
760
+ category: "memory",
761
+ source: { kind: "github", repo: "acme/simple-memory", ref: "v1" },
762
+ },
763
+ ]),
764
+ );
765
+ installedFixture = [pluginEntry({ name: "simple-memory" })];
766
+
767
+ const result = await invoke();
768
+ expect(result.plugins[0]?.category).toBeNull();
769
+ expect(result.categoryCounts).toEqual({ system: 1 });
770
+ });
771
+
772
+ test("?category=development selects a `developer`-origin plugin (post-normalization)", async () => {
773
+ getCatalogSpy.mockImplementation(async (ref) =>
774
+ catalog(ref, [
775
+ {
776
+ name: "dev-tools",
777
+ path: "github:acme/dev-tools@v1",
778
+ category: "developer",
779
+ source: { kind: "github", repo: "acme/dev-tools", ref: "v1" },
780
+ },
781
+ ]),
782
+ );
783
+ installedFixture = [pluginEntry({ name: "dev-tools" })];
784
+
785
+ const result = await invoke({ queryParams: { category: "development" } });
786
+ expect(result.plugins.map((p) => p.id)).toEqual(["dev-tools"]);
787
+ });
788
+
789
+ test("?category=memory selects nothing — there is no such Skills slug", async () => {
790
+ getCatalogSpy.mockImplementation(async (ref) =>
791
+ catalog(ref, [
792
+ {
793
+ name: "simple-memory",
794
+ path: "github:acme/simple-memory@v1",
795
+ category: "memory",
796
+ source: { kind: "github", repo: "acme/simple-memory", ref: "v1" },
797
+ },
798
+ ]),
799
+ );
800
+ installedFixture = [pluginEntry({ name: "simple-memory" })];
801
+
802
+ const result = await invoke({ queryParams: { category: "memory" } });
803
+ expect(result.plugins).toEqual([]);
804
+ // The plugin is still counted (under "system"), just not reachable as
805
+ // "memory" — counts match visible rows.
806
+ expect(result.categoryCounts).toEqual({ system: 1 });
807
+ });
808
+ });
809
+
810
+ // ---------------------------------------------------------------------------
811
+ // normalizeMarketplaceCategory (unit)
812
+ // ---------------------------------------------------------------------------
813
+
814
+ describe("normalizeMarketplaceCategory", () => {
815
+ test("passes a valid Skills slug through unchanged", () => {
816
+ expect(
817
+ normalizeMarketplaceCategory("productivity", SKILLS_CATEGORY_SLUGS),
818
+ ).toBe("productivity");
819
+ });
820
+
821
+ test("aliases `developer` → `development`", () => {
822
+ expect(
823
+ normalizeMarketplaceCategory("developer", SKILLS_CATEGORY_SLUGS),
824
+ ).toBe("development");
825
+ });
826
+
827
+ test("folds unknown marketplace slugs to null", () => {
828
+ for (const unknown of ["memory", "interface", "marketing", "hobby"]) {
829
+ expect(
830
+ normalizeMarketplaceCategory(unknown, SKILLS_CATEGORY_SLUGS),
831
+ ).toBeNull();
832
+ }
833
+ });
834
+
835
+ test("treats null / empty / whitespace as null", () => {
836
+ expect(
837
+ normalizeMarketplaceCategory(null, SKILLS_CATEGORY_SLUGS),
838
+ ).toBeNull();
839
+ expect(
840
+ normalizeMarketplaceCategory(undefined, SKILLS_CATEGORY_SLUGS),
841
+ ).toBeNull();
842
+ expect(normalizeMarketplaceCategory("", SKILLS_CATEGORY_SLUGS)).toBeNull();
843
+ expect(
844
+ normalizeMarketplaceCategory(" ", SKILLS_CATEGORY_SLUGS),
845
+ ).toBeNull();
846
+ });
847
+
848
+ test("normalizes case + surrounding whitespace before matching", () => {
849
+ expect(
850
+ normalizeMarketplaceCategory(" Developer ", SKILLS_CATEGORY_SLUGS),
851
+ ).toBe("development");
852
+ expect(
853
+ normalizeMarketplaceCategory("PRODUCTIVITY", SKILLS_CATEGORY_SLUGS),
854
+ ).toBe("productivity");
855
+ });
856
+
857
+ test("an empty valid-slug set folds every category to null", () => {
858
+ expect(normalizeMarketplaceCategory("productivity", new Set())).toBeNull();
859
+ expect(normalizeMarketplaceCategory("developer", new Set())).toBeNull();
860
+ });
446
861
  });
447
862
 
448
863
  // ---------------------------------------------------------------------------
@@ -469,6 +884,7 @@ describe("GET /v1/plugins/search", () => {
469
884
  {
470
885
  name: "simple-memory",
471
886
  path: "github:vellum-ai/simple-memory@ed09a4c01bf18e4ac8859faee94cb65c7cbd1ca3",
887
+ category: "productivity",
472
888
  source: {
473
889
  kind: "github",
474
890
  repo: "vellum-ai/simple-memory",
@@ -479,6 +895,7 @@ describe("GET /v1/plugins/search", () => {
479
895
  name: "caveman",
480
896
  path: "github:JuliusBrussee/caveman@v1.8.2",
481
897
  description: "Ultra-compressed communication mode.",
898
+ category: null,
482
899
  source: {
483
900
  kind: "github",
484
901
  repo: "JuliusBrussee/caveman",
@@ -498,7 +915,8 @@ describe("GET /v1/plugins/search", () => {
498
915
  expect(ref).toBe("my-feature-branch");
499
916
 
500
917
  // The query is applied in-memory by the real filter: `^simple` matches
501
- // only `simple-memory`, and the source discriminator is preserved.
918
+ // only `simple-memory`. The source discriminator and the marketplace
919
+ // `category` both flow through to the wire.
502
920
  expect(result).toEqual({
503
921
  query: "^simple",
504
922
  ref: "my-feature-branch",
@@ -506,6 +924,7 @@ describe("GET /v1/plugins/search", () => {
506
924
  {
507
925
  name: "simple-memory",
508
926
  path: "github:vellum-ai/simple-memory@ed09a4c01bf18e4ac8859faee94cb65c7cbd1ca3",
927
+ category: "productivity",
509
928
  source: {
510
929
  kind: "github",
511
930
  repo: "vellum-ai/simple-memory",
@@ -516,12 +935,39 @@ describe("GET /v1/plugins/search", () => {
516
935
  });
517
936
  });
518
937
 
938
+ test("normalizes match categories to the Skills taxonomy (`developer` → `development`, `hobby` → null)", async () => {
939
+ getCatalogSpy.mockImplementation(async (ref) =>
940
+ catalog(ref, [
941
+ {
942
+ name: "dev-tools",
943
+ path: "github:acme/dev-tools@v1",
944
+ category: "developer",
945
+ source: { kind: "github", repo: "acme/dev-tools", ref: "v1" },
946
+ },
947
+ {
948
+ name: "snake-game",
949
+ path: "github:acme/snake-game@v1",
950
+ category: "hobby",
951
+ source: { kind: "github", repo: "acme/snake-game", ref: "v1" },
952
+ },
953
+ ]),
954
+ );
955
+
956
+ const result = await invokeSearch();
957
+ const byName = new Map(result.matches.map((m) => [m.name, m.category]));
958
+ // `developer` is aliased to the Skills slug; `hobby` has no equivalent and
959
+ // folds to null so "Available" filters against the same taxonomy.
960
+ expect(byName.get("dev-tools")).toBe("development");
961
+ expect(byName.get("snake-game")).toBeNull();
962
+ });
963
+
519
964
  test("missing ?q= matches all (empty-string query) at the default ref", async () => {
520
965
  getCatalogSpy.mockImplementation(async (ref) =>
521
966
  catalog(ref, [
522
967
  {
523
968
  name: "caveman",
524
969
  path: "github:JuliusBrussee/caveman@v1.8.2",
970
+ category: null,
525
971
  source: {
526
972
  kind: "github",
527
973
  repo: "JuliusBrussee/caveman",
@@ -536,6 +982,8 @@ describe("GET /v1/plugins/search", () => {
536
982
  expect(ref).toBe("main");
537
983
  expect(result.query).toBe("");
538
984
  expect(result.matches.map((m) => m.name)).toEqual(["caveman"]);
985
+ // An entry that declares no category surfaces as `category: null`.
986
+ expect(result.matches[0]?.category).toBeNull();
539
987
  });
540
988
 
541
989
  test("whitespace-only ?ref= falls back to the default ref", async () => {
@@ -597,6 +1045,7 @@ describe("GET /v1/plugins/search", () => {
597
1045
  Object.freeze({
598
1046
  name: "a",
599
1047
  path: "github:acme/a@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1048
+ category: null,
600
1049
  source: {
601
1050
  kind: "github",
602
1051
  repo: "acme/a",
@@ -617,6 +1066,7 @@ describe("GET /v1/plugins/search", () => {
617
1066
  result.matches.push({
618
1067
  name: "b",
619
1068
  path: "x",
1069
+ category: null,
620
1070
  source: {
621
1071
  kind: "github",
622
1072
  repo: "acme/b",
@@ -641,6 +1091,7 @@ function invokeUninstall(args: RouteHandlerArgs = {}): {
641
1091
  describe("DELETE /v1/plugins/:name", () => {
642
1092
  beforeEach(() => {
643
1093
  uninstallSpy.mockReset();
1094
+ broadcastMessageSpy.mockReset();
644
1095
  });
645
1096
 
646
1097
  test("forwards pathParams.name to uninstallPlugin and returns its result", () => {
@@ -659,6 +1110,35 @@ describe("DELETE /v1/plugins/:name", () => {
659
1110
  });
660
1111
  });
661
1112
 
1113
+ test("publishes sync_changed(plugins:list) on a successful uninstall", () => {
1114
+ uninstallSpy.mockImplementation((opts) => ({
1115
+ name: opts.name,
1116
+ target: `/workspace/.vellum/plugins/${opts.name}`,
1117
+ }));
1118
+
1119
+ invokeUninstall({ pathParams: { name: "simple-memory" } });
1120
+
1121
+ expectPluginsListBroadcast();
1122
+ });
1123
+
1124
+ test("threads x-vellum-client-id into the published event's originClientId", () => {
1125
+ uninstallSpy.mockImplementation((opts) => ({
1126
+ name: opts.name,
1127
+ target: `/workspace/.vellum/plugins/${opts.name}`,
1128
+ }));
1129
+
1130
+ invokeUninstall({
1131
+ pathParams: { name: "simple-memory" },
1132
+ headers: { "x-vellum-client-id": "client-abc" },
1133
+ });
1134
+
1135
+ const [msg] = broadcastMessageSpy.mock.calls[0]!;
1136
+ expect(msg).toMatchObject({
1137
+ type: "sync_changed",
1138
+ originClientId: "client-abc",
1139
+ });
1140
+ });
1141
+
662
1142
  test("missing pathParams.name passes the empty string through to the lib", () => {
663
1143
  // The lib's `sanitizePluginName` is the validator of last resort —
664
1144
  // the route hands off the raw value without pre-trimming. The lib
@@ -683,7 +1163,7 @@ describe("DELETE /v1/plugins/:name", () => {
683
1163
  ).toThrow(BadRequestError);
684
1164
  });
685
1165
 
686
- test("PluginNotInstalledError → NotFoundError (404)", () => {
1166
+ test("PluginNotInstalledError → NotFoundError (404), no broadcast", () => {
687
1167
  uninstallSpy.mockImplementation((opts) => {
688
1168
  throw new PluginNotInstalledError(
689
1169
  opts.name,
@@ -694,6 +1174,8 @@ describe("DELETE /v1/plugins/:name", () => {
694
1174
  expect(() => invokeUninstall({ pathParams: { name: "ghost" } })).toThrow(
695
1175
  NotFoundError,
696
1176
  );
1177
+ // A failed uninstall must not fan out a spurious invalidation.
1178
+ expect(broadcastMessageSpy).not.toHaveBeenCalled();
697
1179
  });
698
1180
 
699
1181
  test("unknown errors → InternalError with original message preserved", () => {
@@ -856,6 +1338,7 @@ describe("POST /v1/plugins/install", () => {
856
1338
  beforeEach(() => {
857
1339
  installSpy.mockReset();
858
1340
  resolvePinSpy.mockReset();
1341
+ broadcastMessageSpy.mockReset();
859
1342
  });
860
1343
 
861
1344
  test("forwards name/force and shapes the result, pinning ref to the default", async () => {
@@ -886,6 +1369,43 @@ describe("POST /v1/plugins/install", () => {
886
1369
  });
887
1370
  });
888
1371
 
1372
+ test("publishes sync_changed(plugins:list) on a successful install", async () => {
1373
+ installSpy.mockImplementation(async (opts) => ({
1374
+ name: opts.name,
1375
+ target: `/workspace/.vellum/plugins/${opts.name}`,
1376
+ fileCount: 7,
1377
+ ref: opts.ref ?? "main",
1378
+ commit: null,
1379
+ committedAt: null,
1380
+ }));
1381
+
1382
+ await invokeInstall({ body: { name: "caveman" } });
1383
+
1384
+ expectPluginsListBroadcast();
1385
+ });
1386
+
1387
+ test("threads x-vellum-client-id into the published event's originClientId", async () => {
1388
+ installSpy.mockImplementation(async (opts) => ({
1389
+ name: opts.name,
1390
+ target: `/workspace/.vellum/plugins/${opts.name}`,
1391
+ fileCount: 7,
1392
+ ref: opts.ref ?? "main",
1393
+ commit: null,
1394
+ committedAt: null,
1395
+ }));
1396
+
1397
+ await invokeInstall({
1398
+ body: { name: "caveman" },
1399
+ headers: { "x-vellum-client-id": "client-abc" },
1400
+ });
1401
+
1402
+ const [msg] = broadcastMessageSpy.mock.calls[0]!;
1403
+ expect(msg).toMatchObject({
1404
+ type: "sync_changed",
1405
+ originClientId: "client-abc",
1406
+ });
1407
+ });
1408
+
889
1409
  test("ignores a caller-supplied ref and pins to the curated default", async () => {
890
1410
  // Security boundary: installing from an unreviewed ref (a PR branch,
891
1411
  // fork ref, ...) could load attacker-controlled marketplace code, so the
@@ -942,7 +1462,7 @@ describe("POST /v1/plugins/install", () => {
942
1462
  ).rejects.toBeInstanceOf(ConflictError);
943
1463
  });
944
1464
 
945
- test("PluginNotFoundError → NotFoundError (404)", async () => {
1465
+ test("PluginNotFoundError → NotFoundError (404), no broadcast", async () => {
946
1466
  installSpy.mockImplementation(async (opts) => {
947
1467
  throw new PluginNotFoundError(opts.name, "main", "example-org/ghost");
948
1468
  });
@@ -950,6 +1470,8 @@ describe("POST /v1/plugins/install", () => {
950
1470
  await expect(
951
1471
  invokeInstall({ body: { name: "ghost" } }),
952
1472
  ).rejects.toBeInstanceOf(NotFoundError);
1473
+ // A failed install must not fan out a spurious invalidation.
1474
+ expect(broadcastMessageSpy).not.toHaveBeenCalled();
953
1475
  });
954
1476
 
955
1477
  test("PluginSourceUnavailableError → ServiceUnavailableError (503)", async () => {
@@ -1295,6 +1817,30 @@ async function invokeUpgrade(args: RouteHandlerArgs = {}): Promise<{
1295
1817
  describe("POST /v1/plugins/:name/upgrade", () => {
1296
1818
  beforeEach(() => {
1297
1819
  upgradeSpy.mockReset();
1820
+ broadcastMessageSpy.mockReset();
1821
+ });
1822
+
1823
+ test("publishes sync_changed(plugins:list) on a successful upgrade", async () => {
1824
+ upgradeSpy.mockImplementation(async () => upgradeResult());
1825
+
1826
+ await invokeUpgrade({ pathParams: { name: "level-up" } });
1827
+
1828
+ expectPluginsListBroadcast();
1829
+ });
1830
+
1831
+ test("threads x-vellum-client-id into the published event's originClientId", async () => {
1832
+ upgradeSpy.mockImplementation(async () => upgradeResult());
1833
+
1834
+ await invokeUpgrade({
1835
+ pathParams: { name: "level-up" },
1836
+ headers: { "x-vellum-client-id": "client-abc" },
1837
+ });
1838
+
1839
+ const [msg] = broadcastMessageSpy.mock.calls[0]!;
1840
+ expect(msg).toMatchObject({
1841
+ type: "sync_changed",
1842
+ originClientId: "client-abc",
1843
+ });
1298
1844
  });
1299
1845
 
1300
1846
  test("forwards name + dryRun and projects the upgrade result", async () => {
@@ -1430,7 +1976,7 @@ describe("POST /v1/plugins/:name/upgrade", () => {
1430
1976
  ).rejects.toBeInstanceOf(BadRequestError);
1431
1977
  });
1432
1978
 
1433
- test("PluginNotInstalledError → NotFoundError (404)", async () => {
1979
+ test("PluginNotInstalledError → NotFoundError (404), no broadcast", async () => {
1434
1980
  upgradeSpy.mockImplementation(async () => {
1435
1981
  throw new PluginNotInstalledError(
1436
1982
  "ghost",
@@ -1441,6 +1987,8 @@ describe("POST /v1/plugins/:name/upgrade", () => {
1441
1987
  await expect(
1442
1988
  invokeUpgrade({ pathParams: { name: "ghost" } }),
1443
1989
  ).rejects.toBeInstanceOf(NotFoundError);
1990
+ // A failed upgrade must not fan out a spurious invalidation.
1991
+ expect(broadcastMessageSpy).not.toHaveBeenCalled();
1444
1992
  });
1445
1993
 
1446
1994
  test("PluginNotUpgradableError → ConflictError (409)", async () => {
@@ -1628,3 +2176,181 @@ describe("POST /v1/plugins/:name/diff", () => {
1628
2176
  expect((caught as Error).message).toContain("ECONNRESET");
1629
2177
  });
1630
2178
  });
2179
+
2180
+ // ---------------------------------------------------------------------------
2181
+ // POST /v1/plugins/:name/enable | disable (toggle)
2182
+ // ---------------------------------------------------------------------------
2183
+
2184
+ function toggleResult(
2185
+ name: string,
2186
+ action: "enable" | "disable",
2187
+ ): TogglePluginResult {
2188
+ return {
2189
+ name,
2190
+ action,
2191
+ sentinelPath: `/workspace/.vellum/plugins/${name}/.disabled`,
2192
+ };
2193
+ }
2194
+
2195
+ function invokeEnable(args: RouteHandlerArgs = {}): { ok: boolean } {
2196
+ return enableHandler(args) as { ok: boolean };
2197
+ }
2198
+
2199
+ function invokeDisable(args: RouteHandlerArgs = {}): { ok: boolean } {
2200
+ return disableHandler(args) as { ok: boolean };
2201
+ }
2202
+
2203
+ /** Assert the spy received exactly one sync_changed carrying `plugins:list`. */
2204
+ function expectPluginsListBroadcast(): void {
2205
+ expect(broadcastMessageSpy.mock.calls).toHaveLength(1);
2206
+ const [msg] = broadcastMessageSpy.mock.calls[0]!;
2207
+ expect(msg).toMatchObject({ type: "sync_changed" });
2208
+ expect((msg as { tags: string[] }).tags).toContain("plugins:list");
2209
+ }
2210
+
2211
+ describe("POST /v1/plugins/:name/enable", () => {
2212
+ beforeEach(() => {
2213
+ enablePluginSpy.mockReset();
2214
+ broadcastMessageSpy.mockReset();
2215
+ });
2216
+
2217
+ test("enables the plugin and broadcasts sync_changed(plugins:list)", () => {
2218
+ enablePluginSpy.mockImplementation((name) => toggleResult(name, "enable"));
2219
+
2220
+ const result = invokeEnable({ pathParams: { name: "simple-memory" } });
2221
+
2222
+ expect(result).toEqual({ ok: true });
2223
+ expect(enablePluginSpy.mock.calls[0]?.[0]).toBe("simple-memory");
2224
+ expectPluginsListBroadcast();
2225
+ });
2226
+
2227
+ test("threads x-vellum-client-id into the published event's originClientId", () => {
2228
+ enablePluginSpy.mockImplementation((name) => toggleResult(name, "enable"));
2229
+
2230
+ invokeEnable({
2231
+ pathParams: { name: "simple-memory" },
2232
+ headers: { "x-vellum-client-id": "client-abc" },
2233
+ });
2234
+
2235
+ // The initiating client's id flows through the canonical publisher so it
2236
+ // can self-echo-suppress its own invalidation.
2237
+ const [msg] = broadcastMessageSpy.mock.calls[0]!;
2238
+ expect(msg).toMatchObject({
2239
+ type: "sync_changed",
2240
+ originClientId: "client-abc",
2241
+ });
2242
+ });
2243
+
2244
+ test("a broadcast failure does not fail a successful toggle", () => {
2245
+ enablePluginSpy.mockImplementation((name) => toggleResult(name, "enable"));
2246
+ // The sentinel was already flipped; a hub throw AFTER that must not surface
2247
+ // as a 500 — the canonical publisher swallows broadcast errors.
2248
+ broadcastMessageSpy.mockImplementation(() => {
2249
+ throw new Error("hub unavailable");
2250
+ });
2251
+
2252
+ const result = invokeEnable({ pathParams: { name: "simple-memory" } });
2253
+ expect(result).toEqual({ ok: true });
2254
+ });
2255
+
2256
+ test("PluginAlreadyInStateException → ConflictError (409), no broadcast", () => {
2257
+ enablePluginSpy.mockImplementation((name) => {
2258
+ throw new PluginAlreadyInStateException(name, "enable");
2259
+ });
2260
+
2261
+ expect(() =>
2262
+ invokeEnable({ pathParams: { name: "simple-memory" } }),
2263
+ ).toThrow(ConflictError);
2264
+ // A no-op toggle must not fan out a spurious invalidation.
2265
+ expect(broadcastMessageSpy).not.toHaveBeenCalled();
2266
+ });
2267
+
2268
+ test("PluginDirectoryNotFoundError → NotFoundError (404)", () => {
2269
+ enablePluginSpy.mockImplementation((name) => {
2270
+ throw new PluginDirectoryNotFoundError(name);
2271
+ });
2272
+
2273
+ expect(() => invokeEnable({ pathParams: { name: "ghost" } })).toThrow(
2274
+ NotFoundError,
2275
+ );
2276
+ });
2277
+
2278
+ test("InvalidPluginNameError → BadRequestError (400)", () => {
2279
+ enablePluginSpy.mockImplementation(() => {
2280
+ throw new ToggleInvalidPluginNameError("../escape");
2281
+ });
2282
+
2283
+ expect(() => invokeEnable({ pathParams: { name: "../escape" } })).toThrow(
2284
+ BadRequestError,
2285
+ );
2286
+ });
2287
+ });
2288
+
2289
+ describe("POST /v1/plugins/:name/disable", () => {
2290
+ beforeEach(() => {
2291
+ disablePluginSpy.mockReset();
2292
+ broadcastMessageSpy.mockReset();
2293
+ });
2294
+
2295
+ test("disables the plugin and broadcasts sync_changed(plugins:list)", () => {
2296
+ disablePluginSpy.mockImplementation((name) =>
2297
+ toggleResult(name, "disable"),
2298
+ );
2299
+
2300
+ const result = invokeDisable({ pathParams: { name: "simple-memory" } });
2301
+
2302
+ expect(result).toEqual({ ok: true });
2303
+ expect(disablePluginSpy.mock.calls[0]?.[0]).toBe("simple-memory");
2304
+ // Enable and disable emit the SAME invalidation — the tag names the
2305
+ // resource, not the new value.
2306
+ expectPluginsListBroadcast();
2307
+ });
2308
+
2309
+ test("threads x-vellum-client-id into the published event's originClientId", () => {
2310
+ disablePluginSpy.mockImplementation((name) =>
2311
+ toggleResult(name, "disable"),
2312
+ );
2313
+
2314
+ invokeDisable({
2315
+ pathParams: { name: "simple-memory" },
2316
+ headers: { "x-vellum-client-id": "client-xyz" },
2317
+ });
2318
+
2319
+ const [msg] = broadcastMessageSpy.mock.calls[0]!;
2320
+ expect(msg).toMatchObject({
2321
+ type: "sync_changed",
2322
+ originClientId: "client-xyz",
2323
+ });
2324
+ });
2325
+
2326
+ test("PluginAlreadyInStateException → ConflictError (409), no broadcast", () => {
2327
+ disablePluginSpy.mockImplementation((name) => {
2328
+ throw new PluginAlreadyInStateException(name, "disable");
2329
+ });
2330
+
2331
+ expect(() =>
2332
+ invokeDisable({ pathParams: { name: "simple-memory" } }),
2333
+ ).toThrow(ConflictError);
2334
+ expect(broadcastMessageSpy).not.toHaveBeenCalled();
2335
+ });
2336
+
2337
+ test("PluginDirectoryNotFoundError → NotFoundError (404)", () => {
2338
+ disablePluginSpy.mockImplementation((name) => {
2339
+ throw new PluginDirectoryNotFoundError(name);
2340
+ });
2341
+
2342
+ expect(() => invokeDisable({ pathParams: { name: "ghost" } })).toThrow(
2343
+ NotFoundError,
2344
+ );
2345
+ });
2346
+
2347
+ test("InvalidPluginNameError → BadRequestError (400)", () => {
2348
+ disablePluginSpy.mockImplementation(() => {
2349
+ throw new ToggleInvalidPluginNameError("../escape");
2350
+ });
2351
+
2352
+ expect(() => invokeDisable({ pathParams: { name: "../escape" } })).toThrow(
2353
+ BadRequestError,
2354
+ );
2355
+ });
2356
+ });