@vellumai/assistant 0.11.4 → 0.11.5-staging.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 (367) 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__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Sustained CPU/memory pressure guard for platform-hosted assistants.
3
+ *
4
+ * Samples container CPU utilization and working-set memory against the
5
+ * container's allocation on a fixed cadence and reports an `elevated` state
6
+ * only when a signal stays over its threshold for nearly the whole sampling
7
+ * window (so a single heavy agent task never trips it). Clearing requires a
8
+ * sustained run of samples below a lower clear threshold (hysteresis), so the
9
+ * state never flaps while usage hovers near a threshold.
10
+ *
11
+ * The guard is platform-gated: off-platform there is no plan allocation to
12
+ * measure against, so the guard stays disabled and samples nothing.
13
+ */
14
+ import {
15
+ type ResourcePressureState,
16
+ type ResourcePressureStatus,
17
+ } from "../api/events/resource-pressure-status-changed.js";
18
+ import { getIsPlatform } from "../config/env-registry.js";
19
+ import { broadcastMessage } from "../runtime/assistant-event-hub.js";
20
+ import { getContainerCpuCores } from "../util/cgroup-cpu.js";
21
+ import {
22
+ getContainerMemoryLimitBytes,
23
+ getContainerMemoryStat,
24
+ getContainerMemoryUsageBytes,
25
+ } from "../util/cgroup-memory.js";
26
+ import { getAverageContainerCpuPercentOrNull } from "../util/container-cpu-sampler.js";
27
+ import { getLogger } from "../util/logger.js";
28
+
29
+ const log = getLogger("resource-pressure-guard");
30
+
31
+ const RESOURCE_PRESSURE_SAMPLE_INTERVAL_MS = 30_000;
32
+ // 20 samples at a 30s cadence: 10 minutes of history.
33
+ export const RESOURCE_PRESSURE_WINDOW_SAMPLES = 20;
34
+ // A signal must exceed its threshold in at least this many of the last
35
+ // RESOURCE_PRESSURE_WINDOW_SAMPLES samples, with the window full, to enter
36
+ // `elevated`.
37
+ export const RESOURCE_PRESSURE_ENTER_SAMPLES = 18;
38
+ // Consecutive samples below the clear threshold (5 minutes) required to leave
39
+ // `elevated`.
40
+ export const RESOURCE_PRESSURE_CLEAR_CONSECUTIVE_SAMPLES = 10;
41
+ const CPU_PRESSURE_THRESHOLD_PERCENT = 85;
42
+ const CPU_PRESSURE_CLEAR_THRESHOLD_PERCENT = 70;
43
+ const MEMORY_PRESSURE_THRESHOLD_PERCENT = 90;
44
+ const MEMORY_PRESSURE_CLEAR_THRESHOLD_PERCENT = 80;
45
+
46
+ export { type ResourcePressureState, type ResourcePressureStatus };
47
+
48
+ interface ResourcePressureMemorySample {
49
+ usageBytes: number;
50
+ limitBytes: number;
51
+ reclaimableBytes: number;
52
+ }
53
+
54
+ interface ResourcePressureSamplers {
55
+ sampleCpuPercent?: () => number | null;
56
+ sampleMemory?: () => ResourcePressureMemorySample | null;
57
+ }
58
+
59
+ interface SignalWindow {
60
+ /** Ring buffer of over-threshold results, oldest first. */
61
+ overThreshold: boolean[];
62
+ consecutiveBelowClear: number;
63
+ elevated: boolean;
64
+ }
65
+
66
+ interface ResourcePressureGuardState {
67
+ timer: ReturnType<typeof setInterval> | null;
68
+ status: ResourcePressureStatus;
69
+ cpuWindow: SignalWindow;
70
+ memoryWindow: SignalWindow;
71
+ }
72
+
73
+ const DISABLED_STATUS: ResourcePressureStatus = {
74
+ enabled: false,
75
+ state: "disabled",
76
+ cpuPercent: null,
77
+ memoryPercent: null,
78
+ cpuElevated: false,
79
+ memoryElevated: false,
80
+ cpuThresholdPercent: CPU_PRESSURE_THRESHOLD_PERCENT,
81
+ memoryThresholdPercent: MEMORY_PRESSURE_THRESHOLD_PERCENT,
82
+ lastCheckedAt: null,
83
+ error: null,
84
+ };
85
+
86
+ const OK_STATUS: ResourcePressureStatus = {
87
+ ...DISABLED_STATUS,
88
+ enabled: true,
89
+ state: "ok",
90
+ };
91
+
92
+ function emptySignalWindow(): SignalWindow {
93
+ return { overThreshold: [], consecutiveBelowClear: 0, elevated: false };
94
+ }
95
+
96
+ const state: ResourcePressureGuardState = {
97
+ timer: null,
98
+ status: cloneStatus(DISABLED_STATUS),
99
+ cpuWindow: emptySignalWindow(),
100
+ memoryWindow: emptySignalWindow(),
101
+ };
102
+
103
+ function cloneStatus(status: ResourcePressureStatus): ResourcePressureStatus {
104
+ return { ...status };
105
+ }
106
+
107
+ // The raw percents and timestamp change on every sample; broadcasting each
108
+ // tick would spam the SSE hub, so only substantive fields participate.
109
+ function statusFingerprint(status: ResourcePressureStatus): string {
110
+ const {
111
+ lastCheckedAt: _lastCheckedAt,
112
+ cpuPercent: _cpuPercent,
113
+ memoryPercent: _memoryPercent,
114
+ ...substantiveStatus
115
+ } = status;
116
+ return JSON.stringify(substantiveStatus);
117
+ }
118
+
119
+ function publishStatusChangedIfNeeded(previous: ResourcePressureStatus): void {
120
+ if (statusFingerprint(previous) === statusFingerprint(state.status)) {
121
+ return;
122
+ }
123
+ const status = cloneStatus(state.status);
124
+ broadcastMessage({
125
+ type: "resource_pressure_status_changed",
126
+ status,
127
+ });
128
+ }
129
+
130
+ function replaceStatus(next: ResourcePressureStatus): ResourcePressureStatus {
131
+ const previous = cloneStatus(state.status);
132
+ state.status = cloneStatus(next);
133
+ publishStatusChangedIfNeeded(previous);
134
+ return cloneStatus(state.status);
135
+ }
136
+
137
+ function ensureEnabledStatus(): void {
138
+ if (!state.status.enabled) {
139
+ state.status = cloneStatus(OK_STATUS);
140
+ }
141
+ }
142
+
143
+ function roundPercent(value: number): number {
144
+ return Math.round(value * 100) / 100;
145
+ }
146
+
147
+ function formatError(error: unknown): string {
148
+ return error instanceof Error ? error.message : String(error);
149
+ }
150
+
151
+ function sampleFailureStatus(errorMessage: string): ResourcePressureStatus {
152
+ return {
153
+ ...OK_STATUS,
154
+ state: "unknown",
155
+ lastCheckedAt: new Date().toISOString(),
156
+ error: errorMessage,
157
+ };
158
+ }
159
+
160
+ function resetSignalWindow(window: SignalWindow): void {
161
+ window.overThreshold = [];
162
+ window.consecutiveBelowClear = 0;
163
+ window.elevated = false;
164
+ }
165
+
166
+ /**
167
+ * Feed one sample into a signal's window and derive its elevated flag.
168
+ *
169
+ * An unavailable sample (null percent) resets the window: the signal cannot
170
+ * hold `elevated` without data, and a fresh full window is required before it
171
+ * can enter again.
172
+ */
173
+ function updateSignal(
174
+ window: SignalWindow,
175
+ percent: number | null,
176
+ thresholdPercent: number,
177
+ clearThresholdPercent: number,
178
+ ): boolean {
179
+ if (percent === null) {
180
+ resetSignalWindow(window);
181
+ return false;
182
+ }
183
+
184
+ window.overThreshold.push(percent >= thresholdPercent);
185
+ if (window.overThreshold.length > RESOURCE_PRESSURE_WINDOW_SAMPLES) {
186
+ window.overThreshold.shift();
187
+ }
188
+ if (percent < clearThresholdPercent) {
189
+ window.consecutiveBelowClear += 1;
190
+ } else {
191
+ window.consecutiveBelowClear = 0;
192
+ }
193
+
194
+ if (window.elevated) {
195
+ // Hysteresis: hold until enough consecutive samples land below the clear
196
+ // threshold.
197
+ if (
198
+ window.consecutiveBelowClear >=
199
+ RESOURCE_PRESSURE_CLEAR_CONSECUTIVE_SAMPLES
200
+ ) {
201
+ window.elevated = false;
202
+ }
203
+ } else {
204
+ const overCount = window.overThreshold.filter(Boolean).length;
205
+ window.elevated =
206
+ window.overThreshold.length >= RESOURCE_PRESSURE_WINDOW_SAMPLES &&
207
+ overCount >= RESOURCE_PRESSURE_ENTER_SAMPLES;
208
+ }
209
+
210
+ return window.elevated;
211
+ }
212
+
213
+ function defaultSampleCpuPercent(): number | null {
214
+ if (getContainerCpuCores() <= 0) {
215
+ return null;
216
+ }
217
+ // Average the rolling sampler's 5s windows across this guard's own 30s
218
+ // cadence: reading one instantaneous value would let a short spike that
219
+ // happens to align with the guard's ticks alias into "sustained" load
220
+ // (and an out-of-phase one be missed). Null while the sampler is still
221
+ // warming up or CPU accounting is unreadable: a placeholder 0% would look
222
+ // like a real measurement and mask the signal being unavailable.
223
+ return getAverageContainerCpuPercentOrNull(
224
+ RESOURCE_PRESSURE_SAMPLE_INTERVAL_MS,
225
+ );
226
+ }
227
+
228
+ function defaultSampleMemory(): ResourcePressureMemorySample | null {
229
+ const limitBytes = getContainerMemoryLimitBytes();
230
+ const usageBytes = getContainerMemoryUsageBytes();
231
+ if (limitBytes === null || limitBytes <= 0 || usageBytes === null) {
232
+ return null;
233
+ }
234
+ // Raw usage includes cache the kernel can drop under pressure; counting
235
+ // it would false-positive, so the working set subtracts the READILY
236
+ // reclaimable part: inactive file pages plus reclaimable slab. The full
237
+ // `file` counter is not used because active file pages (hot mmaps, a hot
238
+ // cache) are not disposable and belong in the working set. When the
239
+ // breakdown is unreadable (cgroups v1, where memory.stat lives
240
+ // elsewhere, or missing v2 counters) the working set is unknowable:
241
+ // report the signal unavailable rather than guessing.
242
+ const stat = getContainerMemoryStat();
243
+ const inactiveFileBytes = stat?.inactiveFileBytes ?? null;
244
+ if (inactiveFileBytes === null) {
245
+ return null;
246
+ }
247
+ return {
248
+ usageBytes,
249
+ limitBytes,
250
+ reclaimableBytes: inactiveFileBytes + (stat?.slabReclaimableBytes ?? 0),
251
+ };
252
+ }
253
+
254
+ export function startResourcePressureGuard(): ResourcePressureStatus {
255
+ if (!getIsPlatform()) {
256
+ return cloneStatus(state.status);
257
+ }
258
+
259
+ ensureEnabledStatus();
260
+
261
+ if (!state.timer) {
262
+ state.timer = setInterval(() => {
263
+ evaluateResourcePressureNow();
264
+ }, RESOURCE_PRESSURE_SAMPLE_INTERVAL_MS);
265
+ (state.timer as { unref?: () => void }).unref?.();
266
+ }
267
+
268
+ return cloneStatus(state.status);
269
+ }
270
+
271
+ export function stopResourcePressureGuard(): void {
272
+ if (!state.timer) {
273
+ return;
274
+ }
275
+ clearInterval(state.timer);
276
+ state.timer = null;
277
+ }
278
+
279
+ export function evaluateResourcePressureNow(
280
+ deps?: ResourcePressureSamplers,
281
+ ): ResourcePressureStatus {
282
+ if (!getIsPlatform()) {
283
+ return cloneStatus(state.status);
284
+ }
285
+
286
+ ensureEnabledStatus();
287
+
288
+ const sampleCpuPercent = deps?.sampleCpuPercent ?? defaultSampleCpuPercent;
289
+ const sampleMemory = deps?.sampleMemory ?? defaultSampleMemory;
290
+
291
+ // A sampler returning null means the signal is legitimately unavailable
292
+ // (e.g. no cgroup limit) and is not an error; only a throwing sampler
293
+ // contributes to sampleError.
294
+ const sampleErrors: string[] = [];
295
+
296
+ let cpuPercent: number | null = null;
297
+ try {
298
+ cpuPercent = sampleCpuPercent();
299
+ } catch (error) {
300
+ sampleErrors.push(`CPU sample failed: ${formatError(error)}`);
301
+ }
302
+
303
+ let memoryPercent: number | null = null;
304
+ try {
305
+ const memorySample = sampleMemory();
306
+ if (memorySample && memorySample.limitBytes > 0) {
307
+ const workingSetBytes = Math.max(
308
+ 0,
309
+ memorySample.usageBytes - memorySample.reclaimableBytes,
310
+ );
311
+ memoryPercent = roundPercent(
312
+ (workingSetBytes / memorySample.limitBytes) * 100,
313
+ );
314
+ }
315
+ } catch (error) {
316
+ sampleErrors.push(`Memory sample failed: ${formatError(error)}`);
317
+ }
318
+
319
+ const sampleError = sampleErrors.length > 0 ? sampleErrors.join("; ") : null;
320
+
321
+ if (cpuPercent === null && memoryPercent === null) {
322
+ resetSignalWindow(state.cpuWindow);
323
+ resetSignalWindow(state.memoryWindow);
324
+ return replaceStatus(
325
+ sampleFailureStatus(
326
+ sampleError ?? "Resource pressure samples unavailable",
327
+ ),
328
+ );
329
+ }
330
+
331
+ // Log on the transition into a failing state, not on every 30s sample. The
332
+ // status fingerprint includes `error`, so the SSE broadcast below reflects
333
+ // the same transition exactly once.
334
+ if (sampleError !== null && sampleError !== state.status.error) {
335
+ log.warn(
336
+ { error: sampleError },
337
+ "Resource pressure sampler failed; the remaining signal still evaluated",
338
+ );
339
+ }
340
+
341
+ const cpuElevated = updateSignal(
342
+ state.cpuWindow,
343
+ cpuPercent,
344
+ CPU_PRESSURE_THRESHOLD_PERCENT,
345
+ CPU_PRESSURE_CLEAR_THRESHOLD_PERCENT,
346
+ );
347
+ const memoryElevated = updateSignal(
348
+ state.memoryWindow,
349
+ memoryPercent,
350
+ MEMORY_PRESSURE_THRESHOLD_PERCENT,
351
+ MEMORY_PRESSURE_CLEAR_THRESHOLD_PERCENT,
352
+ );
353
+
354
+ return replaceStatus({
355
+ ...OK_STATUS,
356
+ state: cpuElevated || memoryElevated ? "elevated" : "ok",
357
+ cpuPercent,
358
+ memoryPercent,
359
+ cpuElevated,
360
+ memoryElevated,
361
+ lastCheckedAt: new Date().toISOString(),
362
+ error: sampleError,
363
+ });
364
+ }
365
+
366
+ export function getResourcePressureStatus(): ResourcePressureStatus {
367
+ if (!getIsPlatform()) {
368
+ return cloneStatus(DISABLED_STATUS);
369
+ }
370
+ if (!state.status.enabled) {
371
+ return cloneStatus(OK_STATUS);
372
+ }
373
+ return cloneStatus(state.status);
374
+ }
375
+
376
+ export function __resetResourcePressureGuardForTests(): void {
377
+ stopResourcePressureGuard();
378
+ state.status = cloneStatus(DISABLED_STATUS);
379
+ resetSignalWindow(state.cpuWindow);
380
+ resetSignalWindow(state.memoryWindow);
381
+ }
382
+
383
+ export function __getResourcePressureGuardTimerForTests(): ReturnType<
384
+ typeof setInterval
385
+ > | null {
386
+ return state.timer;
387
+ }
@@ -38,6 +38,7 @@ import { isStartupComplete } from "./daemon-readiness.js";
38
38
  import { stopDiskPressureGuardForLifecycle } from "./disk-pressure-guard-lifecycle.js";
39
39
  import { stopEventLoopWatchdog } from "./event-loop-watchdog.js";
40
40
  import { stopOrphanReaper } from "./orphan-reaper.js";
41
+ import { stopResourcePressureGuardForLifecycle } from "./resource-pressure-guard-lifecycle.js";
41
42
 
42
43
  const log = getLogger("lifecycle");
43
44
 
@@ -49,6 +50,7 @@ const log = getLogger("lifecycle");
49
50
  function stopBackgroundServicesAndCleanupPidFile(): void {
50
51
  stopGatewayFlagListener();
51
52
  stopDiskPressureGuardForLifecycle();
53
+ stopResourcePressureGuardForLifecycle();
52
54
  stopOrphanReaper();
53
55
  stopEventLoopWatchdog();
54
56
  cleanupPidFile();
@@ -23,6 +23,22 @@ export interface DaemonStartupError {
23
23
 
24
24
  const DAEMON_ERROR_PREFIX = "DAEMON_ERROR:";
25
25
 
26
+ /**
27
+ * Build an `EADDRINUSE`-coded error so callers (and {@link categorizeDaemonError})
28
+ * can branch on `err.code` and surface the structured "already running"
29
+ * guidance instead of a generic UNKNOWN. Used by every transport that detects
30
+ * an occupied address itself rather than receiving a kernel-coded error.
31
+ */
32
+ export function makeAddrInUseError(
33
+ message: string,
34
+ cause?: unknown,
35
+ ): NodeJS.ErrnoException {
36
+ const err = new Error(message, cause === undefined ? undefined : { cause });
37
+ const errno = err as NodeJS.ErrnoException;
38
+ errno.code = "EADDRINUSE";
39
+ return errno;
40
+ }
41
+
26
42
  /**
27
43
  * Inspect an error and return a categorized {@link DaemonStartupError}.
28
44
  */
@@ -7,7 +7,7 @@
7
7
  import type { ChannelConversationType } from "@vellumai/gateway-client";
8
8
 
9
9
  import { isHttpAuthDisabled } from "../config/env.js";
10
- import { shouldExposePersonalMemory } from "../plugins/defaults/memory/substrate/static-context.js";
10
+ import { canSeePersonalMemory } from "../runtime/effective-capabilities.js";
11
11
  import type { TrustClass } from "../runtime/trust-class.js";
12
12
  import type { TrustContext } from "./trust-context-types.js";
13
13
 
@@ -67,21 +67,25 @@ export function resolveTrustClass(
67
67
  * Whether personal-memory content may be surfaced for the actor described by
68
68
  * `trustContext`: the gate admits guardian-class actors and internal/local
69
69
  * flows (including turns with no trust context), and blocks remote untrusted
70
- * actors — see {@link shouldExposePersonalMemory} for the rationale.
70
+ * actors.
71
71
  *
72
72
  * This is THE personal-memory trust gate. Every surface that exposes private
73
73
  * user content — the v2 dynamic/static `<memory>` layers, PKB context, NOW.md,
74
74
  * memory-v3 cards/spotlight, and the `loadFromDb` rehydration of persisted
75
75
  * memory blocks — must call this one helper so the exposure rule cannot drift
76
- * between copies. It folds in {@link resolveTrustClass} so the dev-bypass
77
- * (HTTP auth disabled → guardian) applies uniformly at every call site.
76
+ * between copies.
77
+ *
78
+ * The trust-context binding lives here; the policy itself is
79
+ * {@link canSeePersonalMemory}, alongside the other capability-plus-context
80
+ * compositions. This adapter folds in {@link resolveTrustClass} so the
81
+ * dev-bypass (HTTP auth disabled → guardian) applies uniformly at every call
82
+ * site.
78
83
  */
79
84
  export function isPersonalMemoryAllowed(
80
85
  trustContext: TrustContext | undefined,
81
86
  ): boolean {
82
- return shouldExposePersonalMemory({
83
- sourceChannel: trustContext?.sourceChannel,
84
- isTrustedActor: resolveTrustClass(trustContext) === "guardian",
87
+ return canSeePersonalMemory({
88
+ trustClass: resolveTrustClass(trustContext),
85
89
  });
86
90
  }
87
91
 
@@ -109,9 +113,9 @@ export function mapChatTypeToConversationType(
109
113
  // it for the axis rather than inventing a native-looking word.
110
114
  case "dm":
111
115
  return "dm";
112
- // "mpim" is Slack's multi-party DM. The gateway normalizer currently
113
- // collapses mpim into "channel", so this arm matches nothing from Slack
114
- // today; it pins the correct mapping for the raw Slack vocabulary.
116
+ // "mpim" is Slack's multi-party DM, forwarded under its own name rather
117
+ // than reported as "im": a room with several people in it takes the
118
+ // tighter `private` cell and the group-chat etiquette, not the `dm` one.
115
119
  case "mpim":
116
120
  case "group": // Telegram group
117
121
  case "supergroup": // Telegram supergroup
@@ -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
+ }