@vellumai/assistant 0.8.1 → 0.8.2

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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -30,49 +30,49 @@ For the full plugin authoring guide, see
30
30
 
31
31
  ## Install locally
32
32
 
33
- The assistant scans `~/.vellum/plugins/*` for subdirectories containing a
33
+ The assistant scans `<workspaceDir>/plugins/*` (e.g.
34
+ `~/.vellum/workspace/plugins/`) for subdirectories containing a
34
35
  `register.{ts,js}` file and dynamic-imports each one during assistant
35
36
  startup. Dropping (or symlinking) this directory in place is enough to
36
37
  enable it.
37
38
 
38
- ### Option 1 symlink from the repo (recommended)
39
+ The plugin reads `registerPlugin` from `globalThis.__vellumPluginRuntime`,
40
+ which the daemon attaches before scanning plugins. This works against both
41
+ the `bun --compile`-bundled daemon binary AND a daemon running from
42
+ source — no special install procedure required either way.
43
+
44
+ ### Option 1 — symlink from the repo (simplest in-repo dev)
39
45
 
40
46
  From the repo root:
41
47
 
42
48
  ```bash
43
- mkdir -p ~/.vellum/plugins
44
- ln -s "$(pwd)/assistant/examples/plugins/echo" ~/.vellum/plugins/echo
49
+ mkdir -p ~/.vellum/workspace/plugins
50
+ ln -s "$(pwd)/assistant/examples/plugins/echo" ~/.vellum/workspace/plugins/echo
45
51
  ```
46
52
 
47
53
  Symlinks let you edit the plugin in-place and restart the assistant to
48
- pick up changes. **This is the only zero-edit install path** — the
49
- `register.ts` in this directory uses relative imports
50
- (`../../../src/plugins/registry.js`) that resolve into the in-repo
51
- assistant sources, so the file must stay reachable at that relative
52
- location.
53
-
54
- ### Option 2 standalone copy (requires edits)
55
-
56
- If you want a fully isolated install that does not depend on a local
57
- vellum-assistant checkout, a plain `cp -R` of this directory into
58
- `~/.vellum/plugins/echo/` will **not** work as-is: the relative imports
59
- in `register.ts` resolve to `~/.vellum/src/plugins/...`, which does not
60
- exist. The assistant does not currently publish the plugin API as an npm
61
- package, so to copy-and-adapt this template into a standalone plugin you
62
- must rewrite the imports in `register.ts` to point at an absolute path
63
- inside a vellum-assistant checkout, for example:
54
+ pick up changes.
55
+
56
+ ### Option 2 standalone copy
57
+
58
+ A plain `cp -R` of this directory into `~/.vellum/workspace/plugins/echo/`
59
+ works for the runtime imports (which go through the global bridge), but
60
+ the `import type` lines at the top of `register.ts` still resolve into
61
+ the in-repo assistant source tree. If your standalone copy lives outside
62
+ a vellum-assistant checkout, rewrite those `import type` paths to point
63
+ at an absolute path inside any checkout they're erased at compile time
64
+ and have no module-identity effect at runtime:
64
65
 
65
66
  ```ts
66
67
  // before (repo-local):
67
- import { registerPlugin } from "../../../src/plugins/registry.js";
68
+ import type { VellumPluginRuntime } from "../../../src/plugins/external-api.js";
69
+ import type { Plugin } from "../../../src/plugins/types.js";
68
70
  // after (standalone, edit to your checkout path):
69
- import { registerPlugin } from "/path/to/vellum-assistant/assistant/src/plugins/registry.js";
71
+ import type { VellumPluginRuntime } from "/path/to/vellum-assistant/assistant/src/plugins/external-api.js";
72
+ import type { Plugin } from "/path/to/vellum-assistant/assistant/src/plugins/types.js";
70
73
  ```
71
74
 
72
- Apply the same rewrite to the `import type` line that pulls from
73
- `../../../src/plugins/types.js`. Until a published package exists, the
74
- symlink recipe above is simpler and more portable for day-to-day
75
- development.
75
+ No runtime-import rewriting is needed the bridge already handles that.
76
76
 
77
77
  ### Restart the assistant
78
78
 
@@ -113,7 +113,7 @@ original error still propagates.
113
113
  Remove the symlink (or the copied directory) and restart the assistant:
114
114
 
115
115
  ```bash
116
- rm ~/.vellum/plugins/echo
116
+ rm ~/.vellum/workspace/plugins/echo
117
117
  vellum restart
118
118
  ```
119
119
 
@@ -9,6 +9,9 @@
9
9
  "engines": {
10
10
  "node": ">=20.12.0"
11
11
  },
12
+ "peerDependencies": {
13
+ "@vellumai/plugin-api": "^0.8.0"
14
+ },
12
15
  "devDependencies": {
13
16
  "@types/bun": "1.3.10",
14
17
  "@types/node": "25.5.0",
@@ -2,34 +2,26 @@
2
2
  * Echo plugin — observes every assistant pipeline and logs one structured
3
3
  * line per invocation to stderr.
4
4
  *
5
- * This plugin is bundled in the repository as an authoring reference. It is
6
- * not shipped with the assistant runtime; to try it locally, symlink this
7
- * directory into `~/.vellum/plugins/echo/` and restart the assistant. See
8
- * `README.md` in this directory for the full install recipe and
9
- * `assistant/docs/plugins.md` for general plugin authoring docs.
5
+ * Bundled in the repository as an authoring reference. To try it locally,
6
+ * symlink (or copy) this directory into `<workspaceDir>/plugins/echo/` and
7
+ * restart the assistant. See `README.md` in this directory for the install
8
+ * recipe and `assistant/docs/plugins.md` for general plugin authoring docs.
10
9
  *
11
- * ## IMPORTANT — imports below are REPO-LOCAL
10
+ * ## Runtime bridge
12
11
  *
13
- * The relative imports from `../../../src/plugins/...` resolve correctly
14
- * only while this file lives inside the vellum-assistant repo at
15
- * `assistant/examples/plugins/echo/`. The path walks back to
16
- * `assistant/src/plugins/` so the example compiles against the assistant's
17
- * in-repo types.
12
+ * The plugin reads `registerPlugin` from `globalThis.__vellumPluginRuntime`,
13
+ * a stable handle the daemon attaches at startup. This lets the same plugin
14
+ * file work whether the daemon is running from source (relative or absolute
15
+ * imports would resolve to the daemon's modules) or as a `bun --compile`
16
+ * binary (where absolute imports would load a disjoint disk copy with a
17
+ * separate registry instance). The bridge is documented in
18
+ * `assistant/src/plugins/external-api.ts`.
18
19
  *
19
- * If you copy (rather than symlink) this directory to
20
- * `~/.vellum/plugins/echo/`, these imports will fail because
21
- * `~/.vellum/src/plugins/...` does not exist. The assistant does not
22
- * currently publish the plugin API as an npm package, so the only
23
- * zero-edit install path is the symlink recipe (Option 1 in README.md).
24
- *
25
- * For a standalone copy that lives outside the repo, you must either:
26
- * - Point the imports at an absolute path into a vellum-assistant checkout
27
- * (`/path/to/vellum-assistant/assistant/src/plugins/registry.js`), or
28
- * - Rewrite the plugin to consume only the public types you need and drop
29
- * the direct registry import in favor of your own entry-point wiring.
30
- *
31
- * See README.md "Option 2 — standalone copy" for the recommended
32
- * standalone-template adaptation steps.
20
+ * Type imports below still come from the in-repo source tree. Types are
21
+ * erased at runtime, so they don't affect module identity — but they only
22
+ * resolve while this file lives inside the vellum-assistant checkout. For a
23
+ * standalone-copy install, rewrite the `import type` paths to absolute paths
24
+ * inside a checkout (or vendor only the types you need).
33
25
  *
34
26
  * ## Design
35
27
  *
@@ -48,7 +40,7 @@
48
40
  * user-plugin-loader contract (see `assistant/src/plugins/user-loader.ts`).
49
41
  */
50
42
 
51
- import { registerPlugin } from "../../../src/plugins/registry.js";
43
+ import type { VellumPluginRuntime } from "../../../src/plugins/external-api.js";
52
44
  import type {
53
45
  CircuitBreakerArgs,
54
46
  CircuitBreakerResult,
@@ -81,6 +73,15 @@ import type {
81
73
  TurnResult,
82
74
  } from "../../../src/plugins/types.js";
83
75
 
76
+ const runtime = (globalThis as { __vellumPluginRuntime?: VellumPluginRuntime })
77
+ .__vellumPluginRuntime;
78
+ if (!runtime || runtime.version !== 1) {
79
+ throw new Error(
80
+ "echo plugin: globalThis.__vellumPluginRuntime is missing or has an unexpected version — install a recent assistant build",
81
+ );
82
+ }
83
+ const { registerPlugin } = runtime;
84
+
84
85
  const PLUGIN_NAME = "echo";
85
86
 
86
87
  /**
@@ -136,9 +137,10 @@ function makeObserver<A, R>(
136
137
  * `PipelineMiddlewareMap` — all thin observers produced by `makeObserver`.
137
138
  *
138
139
  * Manifest:
139
- * - `requires.pluginRuntime: "v1"` satisfies the registry's mandatory
140
- * capability negotiation.
141
- * - `provides: {}` — the plugin exposes no capabilities to other plugins.
140
+ * - Host-compat range lives in `package.json` under
141
+ * `peerDependencies["@vellumai/plugin-api"]`. The external-plugin loader
142
+ * validates it against the running assistant version via
143
+ * `semver.satisfies()` before this file is even imported.
142
144
  * - No `requiresCredential` or `requiresFlag` — the plugin needs no external
143
145
  * state and runs unconditionally.
144
146
  */
@@ -146,8 +148,6 @@ const echoPlugin: Plugin = {
146
148
  manifest: {
147
149
  name: PLUGIN_NAME,
148
150
  version: "0.1.0",
149
- provides: {},
150
- requires: { pluginRuntime: "v1" },
151
151
  },
152
152
  middleware: {
153
153
  turn: makeObserver<TurnArgs, TurnResult>("turn"),
@@ -1,7 +1,9 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
+ buildSlackChannelLabelMap,
4
5
  buildSlackUserLabelMap,
6
+ extractSlackChannelReferenceIds,
5
7
  extractSlackUserMentionIds,
6
8
  renderSlackTextForModel,
7
9
  } from "./index.js";
@@ -9,17 +11,27 @@ import {
9
11
  describe("extractSlackUserMentionIds", () => {
10
12
  test("returns unique user mention IDs in encounter order", () => {
11
13
  expect(
12
- extractSlackUserMentionIds("<@U123> hi <@W456> and <@U123> again")
14
+ extractSlackUserMentionIds("<@U123> hi <@W456> and <@U123> again"),
13
15
  ).toEqual(["U123", "W456"]);
14
16
  });
15
17
  });
16
18
 
19
+ describe("extractSlackChannelReferenceIds", () => {
20
+ test("returns unique channel reference IDs in encounter order", () => {
21
+ expect(
22
+ extractSlackChannelReferenceIds(
23
+ "<#C123> hi <#G456|private> and <#C123> again",
24
+ ),
25
+ ).toEqual(["C123", "G456"]);
26
+ });
27
+ });
28
+
17
29
  describe("renderSlackTextForModel", () => {
18
30
  test("renders resolved user mentions", () => {
19
31
  expect(
20
32
  renderSlackTextForModel("hello <@U123>", {
21
33
  userLabels: { U123: "Alice" },
22
- })
34
+ }),
23
35
  ).toBe("hello @Alice");
24
36
  });
25
37
 
@@ -36,7 +48,7 @@ describe("renderSlackTextForModel", () => {
36
48
  expect(
37
49
  renderSlackTextForModel("hello <@U123>", {
38
50
  userLabels: { U123: "U123" },
39
- })
51
+ }),
40
52
  ).toBe("hello @unknown-user");
41
53
  });
42
54
 
@@ -44,29 +56,61 @@ describe("renderSlackTextForModel", () => {
44
56
  expect(
45
57
  renderSlackTextForModel("<#C123|general> <#C456>", {
46
58
  channelLabels: { C456: "support" },
47
- })
59
+ }),
48
60
  ).toBe("#general #support");
49
61
 
50
62
  expect(renderSlackTextForModel("<#C789>")).toBe("#unknown-channel");
51
63
  });
52
64
 
65
+ test("prefers embedded Slack labels over resolved channel labels", () => {
66
+ expect(
67
+ renderSlackTextForModel("<#C123|old-name>", {
68
+ channelLabels: { C123: "new-name" },
69
+ }),
70
+ ).toBe("#old-name");
71
+ });
72
+
73
+ test("falls back to embedded channel labels when resolved labels are empty", () => {
74
+ expect(
75
+ renderSlackTextForModel("<#C123|general> <#C456|ops>", {
76
+ channelLabels: { C123: "", C456: " " },
77
+ }),
78
+ ).toBe("#general #ops");
79
+ });
80
+
81
+ test("does not emit raw channel IDs from embedded or resolved labels", () => {
82
+ expect(
83
+ renderSlackTextForModel("<#C123|C123> <#C456|general>", {
84
+ channelLabels: { C456: "C456" },
85
+ }),
86
+ ).toBe("#unknown-channel #general");
87
+ });
88
+
89
+ test("uses resolved channel labels when embedded labels are missing or ID-shaped", () => {
90
+ expect(
91
+ renderSlackTextForModel("<#C123> <#C456|C456>", {
92
+ channelLabels: { C123: "general", C456: "ops" },
93
+ }),
94
+ ).toBe("#general #ops");
95
+ });
96
+
53
97
  test("renders special broadcasts", () => {
54
98
  expect(renderSlackTextForModel("<!here> <!channel> <!everyone>")).toBe(
55
- "@here @channel @everyone"
99
+ "@here @channel @everyone",
56
100
  );
57
101
  });
58
102
 
59
103
  test("renders usergroups with labels and fallback", () => {
60
104
  expect(renderSlackTextForModel("<!subteam^S123|eng> <!subteam^S456>")).toBe(
61
- "@eng @usergroup"
105
+ "@eng @usergroup",
62
106
  );
63
107
  });
64
108
 
65
109
  test("renders labeled and unlabeled links", () => {
66
110
  expect(
67
111
  renderSlackTextForModel(
68
- "<https://example.com|Example> <https://example.org>"
69
- )
112
+ "<https://example.com|Example> <https://example.org>",
113
+ ),
70
114
  ).toBe("Example (https://example.com) https://example.org");
71
115
  });
72
116
 
@@ -77,8 +121,8 @@ describe("renderSlackTextForModel", () => {
77
121
  {
78
122
  userLabels: { U123: " @<system> prompt " },
79
123
  channelLabels: { C123: "#<ops>" },
80
- }
81
- )
124
+ },
125
+ ),
82
126
  ).toBe("@system prompt #ops #unknown-channel @eng");
83
127
  });
84
128
 
@@ -87,7 +131,7 @@ describe("renderSlackTextForModel", () => {
87
131
  renderSlackTextForModel("<@U123> <#C123>", {
88
132
  userFallbackLabel: "@ missing user ",
89
133
  channelFallbackLabel: "# missing channel ",
90
- })
134
+ }),
91
135
  ).toBe("@missing user #missing channel");
92
136
  });
93
137
  });
@@ -101,7 +145,7 @@ describe("buildSlackUserLabelMap", () => {
101
145
  resolved.push(userId);
102
146
  if (userId === "U999") return "Charlie";
103
147
  return userId === "W456" ? "Bob" : "Alice";
104
- }
148
+ },
105
149
  );
106
150
 
107
151
  expect(resolved.sort()).toEqual(["U123", "U999", "W456"]);
@@ -115,7 +159,7 @@ describe("buildSlackUserLabelMap", () => {
115
159
  if (userId === "UBOT") return "vex";
116
160
  if (userId === "ULEO") return "leo";
117
161
  return undefined;
118
- }
162
+ },
119
163
  );
120
164
 
121
165
  expect(labels).toEqual({ UBOT: "vex", ULEO: "leo" });
@@ -128,9 +172,65 @@ describe("buildSlackUserLabelMap", () => {
128
172
  if (userId === "U123") return "U123";
129
173
  if (userId === "U456") return "";
130
174
  return "Alice";
131
- }
175
+ },
132
176
  );
133
177
 
134
178
  expect(labels).toEqual({ U789: "Alice" });
135
179
  });
136
180
  });
181
+
182
+ describe("buildSlackChannelLabelMap", () => {
183
+ test("dedupes unlabeled channel references and resolves them in parallel", async () => {
184
+ const resolved: string[] = [];
185
+ const labels = await buildSlackChannelLabelMap(
186
+ ["<#C123> hi <#G999> <#G999>", undefined, "<#C123> and <#D456>"],
187
+ async (channelId) => {
188
+ resolved.push(channelId);
189
+ if (channelId === "G999") return "private-team";
190
+ return channelId === "D456" ? "direct-chat" : "general";
191
+ },
192
+ );
193
+
194
+ expect(resolved.sort()).toEqual(["C123", "D456", "G999"]);
195
+ expect(labels).toEqual({
196
+ C123: "general",
197
+ D456: "direct-chat",
198
+ G999: "private-team",
199
+ });
200
+ });
201
+
202
+ test("does not resolve channel references that already have usable embedded labels", async () => {
203
+ const resolved: string[] = [];
204
+ const labels = await buildSlackChannelLabelMap(
205
+ ["<#C123|general> <#C456> <#C789|C789> <#CEMPTY| >"],
206
+ async (channelId) => {
207
+ resolved.push(channelId);
208
+ return channelId === "C456"
209
+ ? "support"
210
+ : channelId === "CEMPTY"
211
+ ? "empty-label"
212
+ : "resolved";
213
+ },
214
+ );
215
+
216
+ expect(resolved.sort()).toEqual(["C456", "C789", "CEMPTY"]);
217
+ expect(labels).toEqual({
218
+ C456: "support",
219
+ C789: "resolved",
220
+ CEMPTY: "empty-label",
221
+ });
222
+ });
223
+
224
+ test("omits unresolved labels and labels equal to the Slack channel ID", async () => {
225
+ const labels = await buildSlackChannelLabelMap(
226
+ ["<#C123> <#C456> <#C789>"],
227
+ async (channelId) => {
228
+ if (channelId === "C123") return "C123";
229
+ if (channelId === "C456") return "";
230
+ return "support";
231
+ },
232
+ );
233
+
234
+ expect(labels).toEqual({ C789: "support" });
235
+ });
236
+ });
@@ -6,6 +6,7 @@ export interface RenderSlackTextOptions {
6
6
  }
7
7
 
8
8
  const SLACK_USER_MENTION_RE = /<@([UW][A-Z0-9]+)>/g;
9
+ const SLACK_CHANNEL_REFERENCE_RE = /<#([CDG][A-Z0-9]+)(?:\|[^>]*)?>/g;
9
10
 
10
11
  export function extractSlackUserMentionIds(text: string): string[] {
11
12
  const seen = new Set<string>();
@@ -22,9 +23,24 @@ export function extractSlackUserMentionIds(text: string): string[] {
22
23
  return ids;
23
24
  }
24
25
 
26
+ export function extractSlackChannelReferenceIds(text: string): string[] {
27
+ const seen = new Set<string>();
28
+ const ids: string[] = [];
29
+
30
+ for (const match of text.matchAll(SLACK_CHANNEL_REFERENCE_RE)) {
31
+ const id = match[1];
32
+ if (!seen.has(id)) {
33
+ seen.add(id);
34
+ ids.push(id);
35
+ }
36
+ }
37
+
38
+ return ids;
39
+ }
40
+
25
41
  export function renderSlackTextForModel(
26
42
  text: string,
27
- options: RenderSlackTextOptions = {}
43
+ options: RenderSlackTextOptions = {},
28
44
  ): string {
29
45
  return text.replace(/<([^<>\s][^<>]*)>/g, (token, content: string) => {
30
46
  if (content.startsWith("@")) {
@@ -49,7 +65,7 @@ export function renderSlackTextForModel(
49
65
 
50
66
  export async function buildSlackUserLabelMap(
51
67
  texts: Iterable<string | undefined>,
52
- resolveLabel: (userId: string) => Promise<string | undefined | null>
68
+ resolveLabel: (userId: string) => Promise<string | undefined | null>,
53
69
  ): Promise<Record<string, string>> {
54
70
  const ids: string[] = [];
55
71
  const seen = new Set<string>();
@@ -66,18 +82,55 @@ export async function buildSlackUserLabelMap(
66
82
  if (ids.length === 0) return {};
67
83
 
68
84
  const entries = await Promise.all(
69
- ids.map(
70
- async (id): Promise<[string, string] | undefined> => {
71
- try {
72
- const label = await resolveLabel(id);
73
- const sanitized = sanitizeOptionalLabel(label ?? undefined);
74
- if (!sanitized || sanitized === id) return undefined;
75
- return [id, sanitized];
76
- } catch {
77
- return undefined;
78
- }
85
+ ids.map(async (id): Promise<[string, string] | undefined> => {
86
+ try {
87
+ const label = await resolveLabel(id);
88
+ const sanitized = sanitizeOptionalLabel(label ?? undefined);
89
+ if (!sanitized || sanitized === id) return undefined;
90
+ return [id, sanitized];
91
+ } catch {
92
+ return undefined;
93
+ }
94
+ }),
95
+ );
96
+
97
+ return Object.fromEntries(entries.filter((entry) => entry !== undefined));
98
+ }
99
+
100
+ export async function buildSlackChannelLabelMap(
101
+ texts: Iterable<string | undefined>,
102
+ resolveLabel: (channelId: string) => Promise<string | undefined | null>,
103
+ ): Promise<Record<string, string>> {
104
+ const ids: string[] = [];
105
+ const seen = new Set<string>();
106
+
107
+ for (const text of texts) {
108
+ if (!text) continue;
109
+ for (const match of text.matchAll(SLACK_CHANNEL_REFERENCE_RE)) {
110
+ const id = match[1];
111
+ const [, embeddedLabel] = splitSlackLabel(match[0].slice(1, -1));
112
+ const sanitizedEmbeddedLabel = sanitizeOptionalLabel(embeddedLabel);
113
+ if (sanitizedEmbeddedLabel && sanitizedEmbeddedLabel !== id) continue;
114
+ if (!seen.has(id)) {
115
+ seen.add(id);
116
+ ids.push(id);
117
+ }
118
+ }
119
+ }
120
+
121
+ if (ids.length === 0) return {};
122
+
123
+ const entries = await Promise.all(
124
+ ids.map(async (id): Promise<[string, string] | undefined> => {
125
+ try {
126
+ const label = await resolveLabel(id);
127
+ const sanitized = sanitizeOptionalLabel(label ?? undefined);
128
+ if (!sanitized || sanitized === id) return undefined;
129
+ return [id, sanitized];
130
+ } catch {
131
+ return undefined;
79
132
  }
80
- )
133
+ }),
81
134
  );
82
135
 
83
136
  return Object.fromEntries(entries.filter((entry) => entry !== undefined));
@@ -85,7 +138,7 @@ export async function buildSlackUserLabelMap(
85
138
 
86
139
  function renderUserMention(
87
140
  content: string,
88
- options: RenderSlackTextOptions
141
+ options: RenderSlackTextOptions,
89
142
  ): string {
90
143
  const id = content.slice(1);
91
144
  if (!isSlackUserId(id)) {
@@ -102,16 +155,27 @@ function renderUserMention(
102
155
 
103
156
  function renderChannelReference(
104
157
  content: string,
105
- options: RenderSlackTextOptions
158
+ options: RenderSlackTextOptions,
106
159
  ): string {
107
160
  const [idWithPrefix, label] = splitSlackLabel(content);
108
161
  const channelId = idWithPrefix.slice(1);
109
162
  const fallback = sanitizeLabel(
110
163
  options.channelFallbackLabel,
111
- "unknown-channel"
164
+ "unknown-channel",
112
165
  );
113
- const resolvedLabel = label ?? options.channelLabels?.[channelId];
114
- return `#${sanitizeLabel(resolvedLabel, fallback)}`;
166
+ const embeddedLabel = sanitizeOptionalLabel(label);
167
+ if (embeddedLabel && embeddedLabel !== channelId) {
168
+ return `#${embeddedLabel}`;
169
+ }
170
+
171
+ const resolvedLabel = sanitizeOptionalLabel(
172
+ options.channelLabels?.[channelId],
173
+ );
174
+ if (resolvedLabel && resolvedLabel !== channelId) {
175
+ return `#${resolvedLabel}`;
176
+ }
177
+
178
+ return `#${fallback}`;
115
179
  }
116
180
 
117
181
  function renderSpecialReference(content: string): string {