@vellumai/assistant 0.11.4 → 0.11.5-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/conversation-attachments.test.ts +0 -1
  38. package/src/__tests__/conversation-error.test.ts +17 -0
  39. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  40. package/src/__tests__/conversation-pairing.test.ts +186 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  42. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  43. package/src/__tests__/credential-routes.test.ts +22 -3
  44. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  45. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  46. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  47. package/src/__tests__/dm-backfill.test.ts +63 -0
  48. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  49. package/src/__tests__/edit-propagation.test.ts +107 -4
  50. package/src/__tests__/events-client-registration.test.ts +28 -0
  51. package/src/__tests__/file-write-tool.test.ts +4 -2
  52. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  53. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  54. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  55. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  56. package/src/__tests__/host-shell-tool.test.ts +23 -4
  57. package/src/__tests__/image-conversion.test.ts +143 -1
  58. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  59. package/src/__tests__/input-repairs.test.ts +207 -0
  60. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  61. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  62. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  63. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  64. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  65. package/src/__tests__/mtime-cache.test.ts +2 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  67. package/src/__tests__/openai-provider.test.ts +22 -0
  68. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  69. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  70. package/src/__tests__/platform.test.ts +16 -1
  71. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  72. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  73. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  74. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  75. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  76. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  77. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  78. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  79. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  80. package/src/__tests__/reaction-persistence.test.ts +200 -7
  81. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  82. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  83. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  84. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  85. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  86. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  87. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  88. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  89. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  90. package/src/__tests__/tool-executor.test.ts +63 -38
  91. package/src/__tests__/tool-policy.test.ts +97 -1
  92. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  93. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  94. package/src/__tests__/validate-input.test.ts +243 -7
  95. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  97. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  98. package/src/acp/acp-claude-oauth.ts +38 -21
  99. package/src/acp/prepare-agent-env.ts +146 -55
  100. package/src/agent/loop-tool-dedup.test.ts +161 -0
  101. package/src/agent/loop.ts +68 -22
  102. package/src/api/constants/app-tools.ts +34 -0
  103. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  104. package/src/api/index.ts +19 -0
  105. package/src/api/responses/resource-pressure-status.ts +25 -0
  106. package/src/approvals/guardian-channel-delivery.ts +70 -6
  107. package/src/approvals/guardian-request-resolvers.ts +32 -66
  108. package/src/channels/__tests__/message-audience.test.ts +49 -0
  109. package/src/channels/__tests__/types.test.ts +17 -3
  110. package/src/channels/message-audience.ts +40 -0
  111. package/src/channels/types.ts +16 -12
  112. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  113. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  114. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  115. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  116. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  117. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  118. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  119. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  120. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  121. package/src/cli/commands/conversations.help.ts +34 -0
  122. package/src/cli/commands/conversations.ts +125 -0
  123. package/src/cli/commands/inference-providers.ts +9 -4
  124. package/src/cli/commands/inference.help.ts +9 -4
  125. package/src/cli/commands/keys.help.ts +13 -1
  126. package/src/cli/commands/keys.ts +26 -15
  127. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  128. package/src/cli/commands/memory/index.help.ts +41 -27
  129. package/src/cli/commands/memory/index.ts +2 -0
  130. package/src/cli/commands/memory/memory-v2.ts +58 -54
  131. package/src/cli/commands/memory/memory-validate.ts +18 -0
  132. package/src/cli/commands/monitoring.ts +1 -0
  133. package/src/cli/commands/plugins.help.ts +28 -31
  134. package/src/cli/commands/plugins.ts +10 -0
  135. package/src/cli/commands/skills.help.ts +13 -16
  136. package/src/cli/commands/trust.ts +3 -14
  137. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  138. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  139. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  140. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  141. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  142. package/src/cli/lib/bundled-marketplace.json +13 -0
  143. package/src/cli/lib/daemon-credential-client.ts +25 -3
  144. package/src/cli/lib/install-from-github.ts +35 -24
  145. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  146. package/src/cli/lib/plugin-surfaces.ts +27 -0
  147. package/src/config/feature-flag-registry.json +35 -19
  148. package/src/config/webhook-routing.ts +8 -0
  149. package/src/context/compactor.ts +31 -2
  150. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  151. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  152. package/src/daemon/conversation-error.ts +13 -0
  153. package/src/daemon/conversation-messaging.ts +22 -2
  154. package/src/daemon/conversation-process.ts +11 -0
  155. package/src/daemon/conversation-store.ts +10 -0
  156. package/src/daemon/conversation-surfaces.ts +21 -8
  157. package/src/daemon/conversation.ts +12 -10
  158. package/src/daemon/lifecycle.ts +6 -2
  159. package/src/daemon/message-types/conversations.ts +5 -7
  160. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  161. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  162. package/src/daemon/resource-pressure-guard.ts +387 -0
  163. package/src/daemon/shutdown-handlers.ts +2 -0
  164. package/src/daemon/startup-error.ts +16 -0
  165. package/src/daemon/trust-context.ts +14 -10
  166. package/src/daemon/unsendable-image-notice.ts +76 -0
  167. package/src/hooks/registry.ts +65 -51
  168. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  169. package/src/ipc/gateway-client.test.ts +1 -1
  170. package/src/ipc/gateway-client.ts +14 -21
  171. package/src/ipc/socket-cleanup.ts +2 -11
  172. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  173. package/src/messaging/provider-message-metadata.ts +128 -0
  174. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  175. package/src/messaging/providers/channel-transport.ts +90 -13
  176. package/src/messaging/providers/discord/send.ts +16 -0
  177. package/src/messaging/providers/discord/transport.ts +11 -1
  178. package/src/messaging/providers/index.ts +109 -16
  179. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  180. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  181. package/src/messaging/providers/slack/send.test.ts +6 -9
  182. package/src/messaging/providers/slack/send.ts +40 -28
  183. package/src/messaging/providers/slack/transport.ts +22 -26
  184. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  185. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  186. package/src/messaging/read-provider-metadata.test.ts +157 -0
  187. package/src/messaging/read-provider-metadata.ts +51 -0
  188. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  189. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  190. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  191. package/src/notifications/adapters/macos.ts +55 -0
  192. package/src/notifications/adapters/slack.ts +10 -5
  193. package/src/notifications/assistant-reply-producer.ts +37 -4
  194. package/src/notifications/conversation-pairing.ts +111 -21
  195. package/src/notifications/edit-notification.ts +38 -8
  196. package/src/notifications/emit-signal.ts +12 -14
  197. package/src/notifications/home-feed-side-effect.ts +218 -15
  198. package/src/notifications/types.ts +5 -0
  199. package/src/permissions/AGENTS.md +16 -0
  200. package/src/permissions/checker.test.ts +75 -122
  201. package/src/permissions/checker.ts +44 -560
  202. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  203. package/src/permissions/confirmation-guardian-request.ts +5 -1
  204. package/src/persistence/conversation-crud.ts +23 -20
  205. package/src/persistence/conversation-queries.ts +14 -1
  206. package/src/persistence/conversation-types.ts +22 -0
  207. package/src/persistence/delivery-crud.ts +122 -15
  208. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  209. package/src/plugin-api/conversation-turn.ts +27 -0
  210. package/src/plugin-api/credential-scope.test.ts +15 -0
  211. package/src/plugin-api/credential-scope.ts +14 -0
  212. package/src/plugin-api/index.ts +19 -1
  213. package/src/plugin-api/resolve-credential.ts +15 -10
  214. package/src/plugin-api/store-credential.ts +148 -0
  215. package/src/plugin-api/system-card.ts +36 -0
  216. package/src/plugin-api/vision-support.test.ts +82 -0
  217. package/src/plugin-api/vision-support.ts +14 -2
  218. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  219. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  220. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  221. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  222. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  223. package/src/plugins/defaults/main.ts +15 -0
  224. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  225. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  226. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  227. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  228. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  229. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  230. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  231. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  232. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  233. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  234. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  238. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  239. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  240. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  241. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  242. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  243. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  244. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  245. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  246. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  247. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  248. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  249. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  250. package/src/plugins/mtime-cache.ts +1 -1
  251. package/src/plugins/pipeline.ts +14 -7
  252. package/src/plugins/plugin-execution-context.ts +35 -11
  253. package/src/plugins/plugin-tree-walk.ts +63 -4
  254. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  255. package/src/providers/anthropic/client.ts +332 -241
  256. package/src/providers/connection-resolution.ts +2 -1
  257. package/src/providers/content-blocks.ts +22 -0
  258. package/src/providers/gemini/client.ts +5 -2
  259. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  260. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  261. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  262. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  263. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  264. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  265. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  266. package/src/providers/inference/adapter-factory.ts +33 -2
  267. package/src/providers/inference/auth.ts +13 -2
  268. package/src/providers/inference/credential-usage.ts +37 -0
  269. package/src/providers/inference/missing-credential-guard.ts +110 -0
  270. package/src/providers/inference/resolve-auth.ts +7 -7
  271. package/src/providers/media-resolve.ts +176 -15
  272. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  273. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  274. package/src/providers/openai/chat-completions-provider.ts +322 -42
  275. package/src/routes/route-host-protocol.ts +7 -0
  276. package/src/routes/worker.ts +31 -10
  277. package/src/runtime/AGENTS.md +35 -0
  278. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  279. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  280. package/src/runtime/assistant-event-hub.ts +38 -0
  281. package/src/runtime/channel-reply-delivery.ts +54 -31
  282. package/src/runtime/channel-retry-sweep.ts +6 -4
  283. package/src/runtime/effective-capabilities.test.ts +19 -0
  284. package/src/runtime/effective-capabilities.ts +25 -0
  285. package/src/runtime/guardian-reply-router.ts +6 -2
  286. package/src/runtime/http-errors.ts +1 -0
  287. package/src/runtime/http-server.ts +20 -5
  288. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  289. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  290. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  291. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  292. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  293. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  294. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  295. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  296. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  297. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  298. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  299. package/src/runtime/routes/approval-routes.ts +37 -4
  300. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  301. package/src/runtime/routes/canned-message-complete.ts +68 -29
  302. package/src/runtime/routes/client-routes.ts +113 -0
  303. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  304. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  305. package/src/runtime/routes/conversation-routes.ts +36 -22
  306. package/src/runtime/routes/credential-in-use.ts +85 -0
  307. package/src/runtime/routes/credential-routes.ts +55 -85
  308. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  309. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  310. package/src/runtime/routes/identity-routes.ts +5 -121
  311. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  312. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  313. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  314. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  315. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  316. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  317. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  318. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  319. package/src/runtime/routes/index.ts +2 -0
  320. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  321. package/src/runtime/routes/monitoring-routes.ts +9 -1
  322. package/src/runtime/routes/plugins-routes.ts +17 -5
  323. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  324. package/src/runtime/routes/secret-routes.ts +39 -8
  325. package/src/runtime/routes/settings-routes.ts +6 -6
  326. package/src/runtime/routes/surface-action-routes.ts +20 -19
  327. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  328. package/src/runtime/routes/user-route-resolution.ts +18 -1
  329. package/src/runtime/slack-reply-session.test.ts +23 -13
  330. package/src/runtime/slack-reply-session.ts +38 -55
  331. package/src/runtime/web-presence.ts +90 -0
  332. package/src/skills/validate-input.ts +177 -40
  333. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  334. package/src/subagent/consult-context.ts +7 -23
  335. package/src/tools/credentials/broker.ts +24 -79
  336. package/src/tools/credentials/ref-parse.ts +35 -0
  337. package/src/tools/credentials/resolve.ts +4 -10
  338. package/src/tools/credentials/store.ts +168 -0
  339. package/src/tools/credentials/tool-policy.ts +67 -0
  340. package/src/tools/executor.ts +53 -15
  341. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  342. package/src/tools/network/url-safety.ts +9 -16
  343. package/src/tools/network/web-search.ts +21 -3
  344. package/src/tools/permission-checker.ts +43 -52
  345. package/src/tools/schema-transforms.ts +40 -5
  346. package/src/tools/shared/input-repairs.ts +160 -0
  347. package/src/tools/skills/skill-tool-factory.ts +18 -4
  348. package/src/tools/subagent/spawn.ts +0 -1
  349. package/src/tools/tool-approval-handler.ts +16 -10
  350. package/src/tools/tool-types.ts +26 -13
  351. package/src/tools/types.ts +6 -6
  352. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  353. package/src/util/cgroup-memory.ts +3 -0
  354. package/src/util/container-cpu-sampler.ts +250 -0
  355. package/src/util/image-conversion.ts +178 -14
  356. package/src/util/platform.ts +93 -10
  357. package/src/permissions/ipc-risk-types.ts +0 -143
  358. package/src/permissions/risk-types.ts +0 -76
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Transcript notice for images a turn cannot show the model.
3
+ *
4
+ * Providers accept exactly four image formats (PNG, JPEG, GIF, WebP), so an
5
+ * attachment whose bytes are anything else (a corrupt payload, or a HEIC/AVIF
6
+ * file carrying a `.png` name) is dropped at the provider send boundary by
7
+ * `resolveMediaReferences`. Dropping is what keeps the rest of the turn alive,
8
+ * but on its own it is silent: the user sees their image in the transcript and
9
+ * a reply that ignores it. The card names the files, so the removal is visible
10
+ * and actionable (convert and re-attach) instead of a mystery.
11
+ */
12
+
13
+ import { isUnsendableImageSource } from "../providers/media-resolve.js";
14
+ import type { MediaSource } from "../providers/types.js";
15
+ import { persistSystemCard } from "../runtime/routes/canned-message-complete.js";
16
+ import { getLogger } from "../util/logger.js";
17
+
18
+ const log = getLogger("unsendable-image-notice");
19
+
20
+ /** An image attachment of a persisted message, as the card check sees it. */
21
+ export interface AttachedImage {
22
+ filename: string;
23
+ source: MediaSource;
24
+ }
25
+
26
+ /** The attachments the send boundary will drop, by filename. */
27
+ export function unsendableImageFilenames(images: AttachedImage[]): string[] {
28
+ return images
29
+ .filter((image) => isUnsendableImageSource(image.source))
30
+ .map((image) => image.filename);
31
+ }
32
+
33
+ /** Transcript copy for the images this message cannot send to the model. */
34
+ export function unsendableImageCardText(filenames: string[]): string {
35
+ const names = filenames.join(", ");
36
+ return filenames.length === 1
37
+ ? `${names} was not sent to the model: the file is not a PNG, JPEG, GIF, or WebP image. Convert it and attach it again to include it.`
38
+ : `${names} were not sent to the model: the files are not PNG, JPEG, GIF, or WebP images. Convert them and attach them again to include them.`;
39
+ }
40
+
41
+ /**
42
+ * Post a card naming the attachments of a just-persisted message that the send
43
+ * boundary will drop, or do nothing when every attachment is readable.
44
+ *
45
+ * The card is non-terminal: it rides the messages-changed invalidation without
46
+ * a `message_complete`, so posting it while the turn it describes is in flight
47
+ * leaves that turn's streaming state untouched. Failures are logged and
48
+ * swallowed: the turn is already usable without the notice, and losing the
49
+ * user's whole message to a card write is a worse outcome than losing the card.
50
+ */
51
+ export async function postUnsendableImageNotice(
52
+ conversationId: string,
53
+ images: AttachedImage[],
54
+ ): Promise<void> {
55
+ const filenames = unsendableImageFilenames(images);
56
+ if (filenames.length === 0) {
57
+ return;
58
+ }
59
+ log.warn(
60
+ { conversationId, filenames },
61
+ "User attachments are not a provider-readable image format; posting notice",
62
+ );
63
+ try {
64
+ await persistSystemCard({
65
+ conversationId,
66
+ text: unsendableImageCardText(filenames),
67
+ metadata: { unsendableImageFilenames: filenames },
68
+ endsTurn: false,
69
+ });
70
+ } catch (err) {
71
+ log.error(
72
+ { conversationId, err },
73
+ "Failed to post unsendable-image notice",
74
+ );
75
+ }
76
+ }
@@ -11,13 +11,16 @@
11
11
  * boot, plus the user-land lookup that walks discovered plugin names from
12
12
  * the plugin cache.
13
13
  *
14
- * {@link getHooksFor} combines both sources: in-process hooks from this
15
- * registry (filtered by `isPluginDisabled` at read time) and user-land hooks
16
- * from the plugin cache. The read-time filtering is what makes `assistant
17
- * plugins disable default-*` take effect immediately in a running assistant:
18
- * the hooks stay registered but are filtered out on the next turn.
14
+ * {@link getHooksFor} and {@link getUserHooksFor} both resolve through
15
+ * {@link getUserHookEntriesFor}, which prepends in-process default-plugin
16
+ * hooks from this registry (filtered by `isPluginDisabled` at read time)
17
+ * ahead of user-land hooks from the plugin cache. The read-time filtering
18
+ * is what makes `assistant plugins disable default-*` take effect immediately
19
+ * in a running assistant: the hooks stay registered but are filtered out on
20
+ * the next turn.
19
21
  */
20
22
 
23
+ import { isFirstPartyDefaultPlugin } from "../plugins/defaults/main.js";
21
24
  import { isPluginDisabled } from "../plugins/disabled-state.js";
22
25
  import { getDiscoveredUserPluginNames } from "../plugins/mtime-cache.js";
23
26
  import type { HookEntry, HookFunction } from "../plugins/types.js";
@@ -78,35 +81,74 @@ export function unregisterPluginHooks(pluginName: string): void {
78
81
  // ─── Queries ─────────────────────────────────────────────────────────────────
79
82
 
80
83
  /**
81
- * Get all hooks for a given event name from user plugins and standalone
82
- * workspace hooks, from the hook loader's in-memory cache. Plugin hooks run
84
+ * In-process (default plugin) hook entries for `name`, filtered by the
85
+ * `.disabled` sentinel at read time. First-party default plugins always pass
86
+ * the per-chat allowlist; other in-process registrations (test fixtures) honor
87
+ * it. Used by {@link getUserHookEntriesFor} so `getUserHooksFor` returns the
88
+ * same default-plugin hooks {@link getHooksFor} does.
89
+ */
90
+ function collectInProcessHookEntries<TCtx = unknown>(
91
+ name: string,
92
+ effectiveEnabledPlugins?: Set<string> | null,
93
+ ): HookEntry<TCtx>[] {
94
+ const defaultEntries: HookEntry<TCtx>[] = [];
95
+ for (const entry of hookRegistry.get(name) ?? []) {
96
+ if (isPluginDisabled(entry.pluginName)) {
97
+ continue;
98
+ }
99
+ if (
100
+ effectiveEnabledPlugins != null &&
101
+ !effectiveEnabledPlugins.has(entry.pluginName) &&
102
+ !isFirstPartyDefaultPlugin(entry.pluginName)
103
+ ) {
104
+ continue;
105
+ }
106
+ defaultEntries.push({
107
+ fn: entry.fn as HookFunction<TCtx>,
108
+ owner: { kind: "plugin", id: entry.pluginName },
109
+ });
110
+ }
111
+ return defaultEntries;
112
+ }
113
+
114
+ /**
115
+ * Get all hooks for a given event name: in-process default-plugin hooks
116
+ * first, then user plugins and standalone workspace hooks. Plugin hooks run
83
117
  * in install-date order, the workspace hook runs last.
84
118
  *
85
- * This is a pure cache read: it never scans disk, activates a plugin, or
86
- * runs `init`. Activation happens only at boot (`populateCacheAtBoot`)
87
- * and through the imperative install/uninstall poke
88
- * (`reconcilePluginSourcesNow`), both main-daemon paths, so a
119
+ * Default-plugin hooks come from the in-process registry (registration at
120
+ * boot). User-land hooks are a pure cache read: this never scans disk,
121
+ * activates a plugin, or runs `init`. Activation happens only at boot
122
+ * (`populateCacheAtBoot`) and through the imperative install/uninstall
123
+ * poke (`reconcilePluginSourcesNow`), both main-daemon paths, so a
89
124
  * sidecar process that dispatches hooks (a worker running conversation
90
125
  * turns) can never bring a plugin up in its own process.
91
126
  *
92
127
  * `effectiveEnabledPlugins` carries the per-chat plugin scope: when non-null,
93
- * user plugins outside the set are skipped (standalone workspace hooks always
94
- * run). `null`/omitted means no per-chat restriction.
128
+ * user plugins outside the set are skipped. First-party default plugins
129
+ * always pass that allowlist (a workspace `.disabled` sentinel still
130
+ * excludes them). Standalone workspace hooks always run. `null`/omitted
131
+ * means no per-chat restriction.
95
132
  */
96
133
  export async function getUserHookEntriesFor<TCtx = unknown>(
97
134
  hookName: string,
98
135
  effectiveEnabledPlugins?: Set<string> | null,
99
136
  ): Promise<HookEntry<TCtx>[]> {
100
- return collectUserHookEntries<TCtx>(
137
+ const defaultEntries = collectInProcessHookEntries<TCtx>(
138
+ hookName,
139
+ effectiveEnabledPlugins,
140
+ );
141
+ const userEntries = await collectUserHookEntries<TCtx>(
101
142
  hookName,
102
143
  getDiscoveredUserPluginNames(),
103
144
  effectiveEnabledPlugins,
104
145
  );
146
+ return [...defaultEntries, ...userEntries];
105
147
  }
106
148
 
107
149
  /**
108
150
  * {@link getUserHookEntriesFor} without owner attribution. Returns just the
109
- * hook functions in the same order.
151
+ * hook functions in the same order, including in-process default-plugin hooks.
110
152
  */
111
153
  export async function getUserHooksFor<TCtx = unknown>(
112
154
  hookName: string,
@@ -131,17 +173,19 @@ export async function getUserHooksFor<TCtx = unknown>(
131
173
  * (async, pure cache read). Default hooks are prepended so they compose
132
174
  * innermost, ahead of any user plugins.
133
175
  *
134
- * The `TCtx` generic mirrors {@link HookFunction}'s — callers parameterize
176
+ * The `TCtx` generic mirrors {@link HookFunction}'s: callers parameterize
135
177
  * over the concrete context type their hook receives. Hooks that mutate the
136
178
  * context in place return `void`; hooks that return a new context replace
137
179
  * the threaded value for the next hook in the chain.
138
180
  *
139
181
  * When `conversationId` is given, the conversation's effective plugin scope is
140
182
  * resolved from it (memory, then DB) and layered on top of the global disabled
141
- * check: a hook's contributing plugin must also be a member of that set or the
142
- * hook is excluded for this turn (applies to both in-process default plugins
143
- * and user-land plugins). Omit it (or pass a conversation with no per-chat
144
- * restriction) and every globally-enabled plugin's hooks run, unchanged.
183
+ * check: a user plugin must also be a member of that set or its hook is
184
+ * excluded for this turn. First-party default plugins are not subject to the
185
+ * per-chat allowlist (they are core runtime infrastructure; the chat pills
186
+ * only list user-installed plugins), but a workspace `.disabled` sentinel
187
+ * still excludes them. Omit `conversationId` (or pass a conversation with no
188
+ * per-chat restriction) and every globally-enabled plugin's hooks run.
145
189
  */
146
190
  export async function getHookEntriesFor<TCtx = unknown>(
147
191
  name: string,
@@ -160,37 +204,7 @@ export async function getHookEntriesFor<TCtx = unknown>(
160
204
  options.conversationId,
161
205
  );
162
206
  }
163
- // First-party defaults from the hook registry, filtered by the `.disabled`
164
- // sentinel at read time. This is what makes `assistant plugins disable
165
- // default-*` take effect immediately in a running assistant: the hooks stay
166
- // registered but are filtered out on the next turn.
167
- const defaultEntries: HookEntry<TCtx>[] = [];
168
- for (const entry of hookRegistry.get(name) ?? []) {
169
- if (isPluginDisabled(entry.pluginName)) {
170
- continue;
171
- }
172
- if (
173
- effectiveEnabledPlugins != null &&
174
- !effectiveEnabledPlugins.has(entry.pluginName)
175
- ) {
176
- continue;
177
- }
178
- defaultEntries.push({
179
- fn: entry.fn as HookFunction<TCtx>,
180
- owner: { kind: "plugin", id: entry.pluginName },
181
- });
182
- }
183
-
184
- // User-land hooks from the plugin cache (async, pure cache read; dispatch
185
- // never activates plugins). The per-chat
186
- // scope is threaded through so a deselected user plugin's hooks are excluded
187
- // too — standalone workspace hooks (not owned by a plugin) always run.
188
- const userEntries = await getUserHookEntriesFor<TCtx>(
189
- name,
190
- effectiveEnabledPlugins,
191
- );
192
-
193
- return [...defaultEntries, ...userEntries];
207
+ return getUserHookEntriesFor<TCtx>(name, effectiveEnabledPlugins);
194
208
  }
195
209
 
196
210
  /**
@@ -1,10 +1,14 @@
1
1
  import { createServer } from "node:net";
2
2
  import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
3
4
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
5
 
5
6
  import { ensureSocketPathFree } from "../socket-cleanup.js";
6
7
  import { resolveIpcSocketPath } from "../socket-path.js";
7
8
 
9
+ // Fake workspace under tmpdir so the live-workspace guard accepts it.
10
+ const WS_DIR = join(tmpdir(), "vellum-workspace-test");
11
+
8
12
  let savedWorkspaceDir: string | undefined;
9
13
  let savedGatewayIpcSocketDir: string | undefined;
10
14
 
@@ -29,7 +33,7 @@ afterEach(() => {
29
33
  describe("resolveIpcSocketPath", () => {
30
34
  test("uses env var override when set", () => {
31
35
  process.env.GATEWAY_IPC_SOCKET_DIR = "/run/gateway-ipc";
32
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/vellum-workspace-test";
36
+ process.env.VELLUM_WORKSPACE_DIR = WS_DIR;
33
37
 
34
38
  const resolved = resolveIpcSocketPath("gateway");
35
39
 
@@ -39,28 +43,32 @@ describe("resolveIpcSocketPath", () => {
39
43
 
40
44
  test("ignores empty env var override", () => {
41
45
  process.env.GATEWAY_IPC_SOCKET_DIR = " ";
42
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/vellum-workspace-test";
46
+ process.env.VELLUM_WORKSPACE_DIR = WS_DIR;
43
47
 
44
48
  const resolved = resolveIpcSocketPath("gateway");
45
49
 
46
50
  expect(resolved.source).toBe("workspace");
47
- expect(resolved.path).toBe("/tmp/vellum-workspace-test/gateway.sock");
51
+ expect(resolved.path).toBe(join(WS_DIR, "gateway.sock"));
48
52
  });
49
53
 
50
54
  test("uses workspace path by default", () => {
51
55
  delete process.env.GATEWAY_IPC_SOCKET_DIR;
52
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/vellum-workspace-test";
56
+ process.env.VELLUM_WORKSPACE_DIR = WS_DIR;
53
57
 
54
58
  const resolved = resolveIpcSocketPath("gateway");
55
59
 
56
60
  expect(resolved.source).toBe("workspace");
57
- expect(resolved.path).toBe("/tmp/vellum-workspace-test/gateway.sock");
61
+ expect(resolved.path).toBe(join(WS_DIR, "gateway.sock"));
58
62
  });
59
63
 
60
64
  test("falls back to tmpdir when workspace path exceeds AF_UNIX limit", () => {
61
65
  delete process.env.GATEWAY_IPC_SOCKET_DIR;
62
66
  // 90-char workspace dir + /gateway.sock = well over 103 bytes
63
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/" + "a".repeat(85) + "/workspace";
67
+ process.env.VELLUM_WORKSPACE_DIR = join(
68
+ tmpdir(),
69
+ "a".repeat(90),
70
+ "workspace",
71
+ );
64
72
 
65
73
  const resolved = resolveIpcSocketPath("gateway");
66
74
 
@@ -70,7 +78,7 @@ describe("resolveIpcSocketPath", () => {
70
78
 
71
79
  test("derives a hyphenated socket name's env var and filename", () => {
72
80
  process.env.EXAMPLE_SERVICE_IPC_SOCKET_DIR = "/run/example-ipc";
73
- process.env.VELLUM_WORKSPACE_DIR = "/tmp/vellum-workspace-test";
81
+ process.env.VELLUM_WORKSPACE_DIR = WS_DIR;
74
82
 
75
83
  const resolved = resolveIpcSocketPath("example-service");
76
84
 
@@ -143,7 +143,7 @@ describe("ipcClassifyRisk retry", () => {
143
143
  const CLASSIFY_OK = {
144
144
  risk: "low",
145
145
  reason: "safe",
146
- matchType: "none",
146
+ matchType: "registry",
147
147
  scopeOptions: [],
148
148
  };
149
149
  const emptyParams = {} as unknown as Parameters<typeof ipcClassifyRisk>[0];
@@ -10,16 +10,17 @@
10
10
  * deterministic fallback for long AF_UNIX paths.
11
11
  */
12
12
 
13
+ import {
14
+ type ClassifyRiskIpcParams,
15
+ type ClassifyRiskIpcResponse,
16
+ ClassifyRiskIpcResponseSchema,
17
+ } from "@vellumai/gateway-client";
13
18
  import {
14
19
  ipcCall as packageIpcCall,
15
20
  IpcCallError,
16
21
  PersistentIpcClient as PackagePersistentIpcClient,
17
22
  } from "@vellumai/gateway-client/ipc-client";
18
23
 
19
- import type {
20
- ClassificationResult,
21
- ClassifyRiskParams,
22
- } from "../permissions/ipc-risk-types.js";
23
24
  import { getLogger } from "../util/logger.js";
24
25
  import { abortableSleep, computeRetryDelay } from "../util/retry.js";
25
26
  import { resolveIpcSocketPath } from "./socket-path.js";
@@ -173,37 +174,29 @@ const CLASSIFY_RISK_ATTEMPT_TIMEOUT_MS = 1_500; // per-attempt cap (< 5s default
173
174
  * When a `signal` is supplied, retries stop as soon as it aborts.
174
175
  */
175
176
  export async function ipcClassifyRisk(
176
- params: ClassifyRiskParams,
177
+ params: ClassifyRiskIpcParams,
177
178
  signal?: AbortSignal,
178
- ): Promise<ClassificationResult | undefined> {
179
+ ): Promise<ClassifyRiskIpcResponse | undefined> {
179
180
  for (let attempt = 0; ; attempt++) {
180
181
  try {
181
182
  const result = await ipcCallPersistent(
182
183
  "classify_risk",
183
- params as unknown as Record<string, unknown>,
184
+ params,
184
185
  CLASSIFY_RISK_ATTEMPT_TIMEOUT_MS,
185
186
  );
186
187
 
187
- // Returned-but-malformed responses are deterministic, not transient:
188
+ // A returned-but-malformed response is deterministic, not transient:
188
189
  // fail closed immediately (no retry).
189
- if (!result || typeof result !== "object" || Array.isArray(result)) {
190
- log.warn(
191
- { result },
192
- "ipcClassifyRisk: gateway returned non-object response",
193
- );
194
- return undefined;
195
- }
196
-
197
- const obj = result as Record<string, unknown>;
198
- if (typeof obj.risk !== "string") {
190
+ const parsed = ClassifyRiskIpcResponseSchema.safeParse(result);
191
+ if (!parsed.success) {
199
192
  log.warn(
200
- { result },
201
- "ipcClassifyRisk: gateway response missing 'risk' field",
193
+ { result, issues: parsed.error.issues },
194
+ "ipcClassifyRisk: gateway response does not match the classify_risk contract",
202
195
  );
203
196
  return undefined;
204
197
  }
205
198
 
206
- return result as ClassificationResult;
199
+ return parsed.data;
207
200
  } catch (err) {
208
201
  // A structured gateway error means the gateway was reachable and
209
202
  // deterministically rejected the request — not a transient blip.
@@ -15,6 +15,8 @@ import {
15
15
  removeIpcEndpointFile,
16
16
  } from "@vellumai/ipc-server-utils";
17
17
 
18
+ import { makeAddrInUseError } from "../daemon/startup-error.js";
19
+
18
20
  /**
19
21
  * Maximum time to wait for the probe `connect()` to settle before declaring
20
22
  * the path occupied. Without a bound, a hung process holding the socket would
@@ -25,17 +27,6 @@ import {
25
27
  */
26
28
  const PROBE_CONNECT_TIMEOUT_MS = 2000;
27
29
 
28
- /**
29
- * Build an `EADDRINUSE`-coded error so callers (and `categorizeDaemonError`)
30
- * can branch on `err.code` and surface the structured "already running"
31
- * guidance instead of a generic UNKNOWN.
32
- */
33
- function makeAddrInUseError(message: string): NodeJS.ErrnoException {
34
- const err = new Error(message) as NodeJS.ErrnoException;
35
- err.code = "EADDRINUSE";
36
- return err;
37
- }
38
-
39
30
  /**
40
31
  * Probe-connect to `socketPath`. Behavior:
41
32
  * - Path doesn't exist → return.
@@ -67,7 +67,7 @@ mock.module("../../daemon/mcp-reload-service.js", () => ({
67
67
 
68
68
  mock.module("../../config/env-registry.js", () => ({
69
69
  getIsContainerized: () => false,
70
- getWorkspaceDirOverride: () => undefined,
70
+ getWorkspaceDirOverride: () => process.env.VELLUM_WORKSPACE_DIR,
71
71
  // Imported by the real util/logger.js; ESM named-import validation
72
72
  // requires it even though the silent test logger never calls it.
73
73
  getDebugStdoutLogs: () => false,
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+
3
+ import { CHANNEL_IDS } from "../channels/types.js";
4
+
5
+ /**
6
+ * What a stored channel message row knows about itself, in terms no single
7
+ * provider owns.
8
+ *
9
+ * "Provider" here is the channel sense, the one `messaging/providers/*` uses:
10
+ * Slack, Telegram, Discord. It is not the inference sense in `providers/*`,
11
+ * whose `providerMetadata` rides on a content block and describes a model.
12
+ *
13
+ * Named for the provider rather than the channel because `channel metadata`
14
+ * is already taken: `buildChannelMetadata` builds the whole envelope a
15
+ * message row carries (provenance, `userMessageChannel`, interfaces), and
16
+ * this is one key inside that envelope. This is the neutral counterpart of
17
+ * `slackMeta`, so it reads as the generalization of what `providers/slack`
18
+ * stores.
19
+ *
20
+ * Every channel needs the same facts to reconstruct a transcript: which chat
21
+ * the row belongs to, the provider's id for the row itself, the thread it sits
22
+ * in, whether it is a message or a reaction, and, when it is a reaction, which
23
+ * message it was attached to.
24
+ *
25
+ * History assembly is structural, not Slack logic: it groups a reaction with
26
+ * the message it targets, inside a thread. That rule holds for any
27
+ * thread-scoped channel.
28
+ *
29
+ * The schema passes through what it does not name, so a provider carries its
30
+ * own fields (Slack's file markers and timezone labels) on this same object
31
+ * and validates them with its own schema. This is how `SourceMetadataSchema`
32
+ * already carries `slackBotMentioned` and the email fields on the wire: one
33
+ * object per row, no per-provider sub-envelope, and no second copy of
34
+ * anything named here.
35
+ */
36
+
37
+ const providerReactionMetadataSchema = z.object({
38
+ /**
39
+ * Provider id of the message this reaction was attached to, in the same
40
+ * namespace as `messageId`. Resolution is keyed on it, so it is required.
41
+ */
42
+ targetMessageId: z.string(),
43
+ emoji: z.string(),
44
+ op: z.enum(["added", "removed"]),
45
+ actorDisplayName: z.string().optional(),
46
+ });
47
+
48
+ export const providerMessageMetadataSchema = z
49
+ .object({
50
+ source: z.enum(CHANNEL_IDS),
51
+ /**
52
+ * Provider id of the chat, channel or room this row belongs to. Same name
53
+ * and meaning as on the inbound wire, where "conversation" is the canonical
54
+ * word for a delivery address and "channel" names the provider.
55
+ */
56
+ conversationExternalId: z.string(),
57
+ /**
58
+ * Provider id of this row itself. Absent on a reaction, which no channel
59
+ * gives an id of its own: a reaction event names the chat, the message it
60
+ * was attached to, the actor and the emoji, and nothing identifies the
61
+ * reaction. The message it acts on is `reaction.targetMessageId`.
62
+ */
63
+ messageId: z.string().optional(),
64
+ /**
65
+ * Provider id of the thread this row sits in, absent when it is not in one.
66
+ * Never synthesized from `messageId`: a value here asserts that a thread
67
+ * exists, and inventing one keys conversations on threads that never do.
68
+ */
69
+ threadId: z.string().optional(),
70
+ /**
71
+ * Provider id of whoever authored the row or performed the reaction. Trust
72
+ * is keyed on this everywhere else, and it is what lets a channel say who
73
+ * acted; a display name is a label, not an identity. Optional because a
74
+ * provider envelope need not carry one.
75
+ */
76
+ actorExternalId: z.string().optional(),
77
+ displayName: z.string().optional(),
78
+ /**
79
+ * Only reactions get a row of their own. An edit rewrites the row it
80
+ * targets and a delete stamps `deletedAt` on it, so neither appears here.
81
+ */
82
+ eventKind: z.enum(["message", "reaction"]),
83
+ reaction: providerReactionMetadataSchema.optional(),
84
+ editedAt: z.number().optional(),
85
+ deletedAt: z.number().optional(),
86
+ })
87
+ .passthrough();
88
+
89
+ export type ProviderReactionMetadata = z.infer<
90
+ typeof providerReactionMetadataSchema
91
+ >;
92
+ export type ProviderMessageMetadata = z.infer<
93
+ typeof providerMessageMetadataSchema
94
+ >;
95
+
96
+ /**
97
+ * Parse and validate a serialized `ProviderMessageMetadata`, the counterpart of
98
+ * `readSlackMetadata` for the neutral shape. Anything that does not parse or
99
+ * does not validate reads as null.
100
+ */
101
+ export function readProviderMessageMetadata(
102
+ raw: unknown,
103
+ ): ProviderMessageMetadata | null {
104
+ if (typeof raw !== "string") {
105
+ return null;
106
+ }
107
+ let parsed: unknown;
108
+ try {
109
+ parsed = JSON.parse(raw);
110
+ } catch {
111
+ return null;
112
+ }
113
+ const result = providerMessageMetadataSchema.safeParse(parsed);
114
+ return result.success ? result.data : null;
115
+ }
116
+
117
+ /**
118
+ * The id a row is grouped by when assembling a thread: its own for a message,
119
+ * its target's for a reaction, so a reaction lands beside the message it was
120
+ * attached to rather than in a block of its own.
121
+ *
122
+ * Undefined for a row that identifies neither, which cannot be placed.
123
+ */
124
+ export function groupingMessageId(
125
+ meta: ProviderMessageMetadata,
126
+ ): string | undefined {
127
+ return meta.reaction?.targetMessageId ?? meta.messageId;
128
+ }