@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
@@ -201,6 +201,22 @@ describe("client OS per-row evidence marker", () => {
201
201
  expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(true);
202
202
  });
203
203
 
204
+ // Mirrors the Mac-originated case above: a plain browser tab reports
205
+ // `client.os: "web"`, while the Electron desktop renderer resolves its own
206
+ // host OS first and persists `macos`/`windows` instead.
207
+ test("marks an OS this row's own transport reported (web)", async () => {
208
+ const ctx = createWebTurnContext("web");
209
+ await persistQueuedMessageBody(ctx, {
210
+ content: "hello",
211
+ requestId: "req-transport-os-web",
212
+ requestClientOs: "web",
213
+ });
214
+
215
+ expect(lastUserMetadata().client).toEqual({ os: "web" });
216
+ expect(lastUserMetadata().clientOsFromRequest).toBe(true);
217
+ expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(false);
218
+ });
219
+
204
220
  test("marks an OS this row's own request headers reported", async () => {
205
221
  const ctx = createWebTurnContext(undefined);
206
222
  await persistQueuedMessageBody(ctx, {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Compaction must never bake invalid image bytes into the rebuilt history.
3
+ *
4
+ * Retained images are re-hydrated from the attachment store and re-optimized
5
+ * at compaction time. If that pipeline yields bytes that no longer parse as an
6
+ * image (e.g. a corrupt conversion-cache entry), embedding them would make the
7
+ * provider reject EVERY subsequent turn with a 400: the conversation wedges
8
+ * until the block is manually removed. `buildRetainedImageBlocks` drops such
9
+ * payloads instead.
10
+ */
11
+ import { beforeEach, describe, expect, test } from "bun:test";
12
+
13
+ import {
14
+ buildRetainedImageBlocks,
15
+ collectImageManifest,
16
+ } from "../context/compactor.js";
17
+ import { attachInlineAttachmentToMessage } from "../persistence/attachments-store.js";
18
+ import {
19
+ addMessage,
20
+ createConversation,
21
+ } from "../persistence/conversation-crud.js";
22
+ import { getDb } from "../persistence/db-connection.js";
23
+ import { initializeDb } from "../persistence/db-init.js";
24
+
25
+ await initializeDb();
26
+
27
+ // 1x1 transparent PNG.
28
+ const PNG_1X1_BASE64 =
29
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
30
+
31
+ // Declared image/png, but the bytes are not any known image format:
32
+ // the stand-in for a corrupted payload surviving in the pipeline.
33
+ const GARBAGE_BASE64 = Buffer.from("definitely not an image").toString(
34
+ "base64",
35
+ );
36
+
37
+ function resetTables(): void {
38
+ const db = getDb();
39
+ db.run("DELETE FROM message_attachments");
40
+ db.run("DELETE FROM attachments");
41
+ db.run("DELETE FROM messages");
42
+ db.run("DELETE FROM conversations");
43
+ }
44
+
45
+ async function addImageMessage(
46
+ conversationId: string,
47
+ filename: string,
48
+ dataBase64: string,
49
+ ): Promise<void> {
50
+ const inserted = await addMessage(
51
+ conversationId,
52
+ "user",
53
+ JSON.stringify([{ type: "text", text: filename }]),
54
+ {
55
+ metadata: { provenanceTrustClass: "guardian" },
56
+ skipIndexing: true,
57
+ },
58
+ );
59
+ await attachInlineAttachmentToMessage(
60
+ inserted.id,
61
+ 0,
62
+ filename,
63
+ "image/png",
64
+ dataBase64,
65
+ );
66
+ }
67
+
68
+ describe("buildRetainedImageBlocks corrupt-bytes gate", () => {
69
+ beforeEach(resetTables);
70
+
71
+ test("valid image bytes are retained", async () => {
72
+ const conv = createConversation();
73
+ await addImageMessage(conv.id, "good.png", PNG_1X1_BASE64);
74
+ const manifest = collectImageManifest(conv.id, "guardian");
75
+
76
+ const { blocks, resolved, missing } = await buildRetainedImageBlocks(
77
+ ["good.png"],
78
+ manifest,
79
+ );
80
+
81
+ expect(resolved).toEqual(["good.png"]);
82
+ expect(missing).toEqual([]);
83
+ expect(blocks).toHaveLength(1);
84
+ expect(blocks[0].source.type).toBe("base64");
85
+ });
86
+
87
+ test("bytes that do not parse as an image are dropped, not embedded", async () => {
88
+ const conv = createConversation();
89
+ await addImageMessage(conv.id, "corrupt.png", GARBAGE_BASE64);
90
+ await addImageMessage(conv.id, "good.png", PNG_1X1_BASE64);
91
+ const manifest = collectImageManifest(conv.id, "guardian");
92
+
93
+ const { blocks, resolved, missing } = await buildRetainedImageBlocks(
94
+ ["corrupt.png", "good.png"],
95
+ manifest,
96
+ );
97
+
98
+ // The corrupt payload is dropped (not listed as manifest-missing, since
99
+ // it resolved fine; its bytes were the problem) and the valid one
100
+ // survives.
101
+ expect(resolved).toEqual(["good.png"]);
102
+ expect(missing).toEqual([]);
103
+ expect(blocks).toHaveLength(1);
104
+ });
105
+
106
+ test("a truncated JPEG (valid SOI header, no EOI) is dropped", async () => {
107
+ // A JPEG torn mid-write keeps its SOI header, so a format sniff alone
108
+ // accepts it; the gate must also require an EOI marker.
109
+ const tornJpeg = Buffer.concat([
110
+ Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]),
111
+ Buffer.from("JFIF\0"),
112
+ Buffer.alloc(64),
113
+ ]).toString("base64");
114
+ const conv = createConversation();
115
+ await addImageMessage(conv.id, "torn.jpg", tornJpeg);
116
+ const manifest = collectImageManifest(conv.id, "guardian");
117
+
118
+ const { blocks, resolved, missing } = await buildRetainedImageBlocks(
119
+ ["torn.jpg"],
120
+ manifest,
121
+ );
122
+
123
+ expect(resolved).toEqual([]);
124
+ expect(missing).toEqual([]);
125
+ expect(blocks).toHaveLength(0);
126
+ });
127
+
128
+ test("unresolvable filenames still land in missing", async () => {
129
+ const conv = createConversation();
130
+ await addImageMessage(conv.id, "good.png", PNG_1X1_BASE64);
131
+ const manifest = collectImageManifest(conv.id, "guardian");
132
+
133
+ const { blocks, resolved, missing } = await buildRetainedImageBlocks(
134
+ ["never-uploaded.png", "good.png"],
135
+ manifest,
136
+ );
137
+
138
+ expect(missing).toEqual(["never-uploaded.png"]);
139
+ expect(resolved).toEqual(["good.png"]);
140
+ expect(blocks).toHaveLength(1);
141
+ });
142
+ });
@@ -49,6 +49,7 @@ afterAll(() => {
49
49
  });
50
50
 
51
51
  import {
52
+ CODE_DEFAULT_PROFILE_ENTRIES,
52
53
  getEffectiveProfile,
53
54
  getEffectiveProfiles,
54
55
  getEffectiveProfilesForProvider,
@@ -164,6 +165,14 @@ const LEGACY_HATCH_PROFILE_NAMES = [
164
165
  "custom-cost-optimized",
165
166
  ] as const;
166
167
 
168
+ /**
169
+ * The managed Balanced model. These tests assert that resolution serves the
170
+ * code catalog rather than a workspace body, so they follow the catalog
171
+ * instead of restating its pin.
172
+ */
173
+ const MANAGED_BALANCED_MODEL = CODE_DEFAULT_PROFILE_ENTRIES.balanced
174
+ .model as string;
175
+
167
176
  function createProviderConnectionsDb(): DrizzleDb {
168
177
  const sqlite = new Database(":memory:");
169
178
  sqlite.exec("PRAGMA journal_mode=WAL");
@@ -669,7 +678,7 @@ describe("loadConfig startup behavior", () => {
669
678
  expect(raw.llm.profiles).toEqual({});
670
679
  // Default content resolves from the code catalog via the effective view.
671
680
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
672
- expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
681
+ expect(effectiveBalanced?.model).toBe(MANAGED_BALANCED_MODEL);
673
682
  expect(effectiveBalanced?.provider).toBe("vellum");
674
683
  expect(effectiveBalanced?.provider_connection).toBeUndefined();
675
684
  });
@@ -997,7 +1006,7 @@ describe("loadConfig startup behavior", () => {
997
1006
  // Resolution ignores the drifted body: a managed-source entry contributes
998
1007
  // only label/status/topP, everything else comes from the catalog.
999
1008
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
1000
- expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1009
+ expect(effectiveBalanced?.model).toBe(MANAGED_BALANCED_MODEL);
1001
1010
  expect(effectiveBalanced?.provider_connection).toBeUndefined();
1002
1011
  });
1003
1012
 
@@ -1077,7 +1086,7 @@ describe("loadConfig startup behavior", () => {
1077
1086
  expect(raw.llm.profiles.balanced).toEqual(drifted);
1078
1087
  expect(raw.llm.activeProfile).toBe("balanced");
1079
1088
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
1080
- expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1089
+ expect(effectiveBalanced?.model).toBe(MANAGED_BALANCED_MODEL);
1081
1090
  expect(effectiveBalanced?.maxTokens).toBe(32000);
1082
1091
  expect(effectiveBalanced?.provider_connection).toBeUndefined();
1083
1092
  // The catalog body carries no topP and the entry has none, so the
@@ -1109,7 +1118,7 @@ describe("loadConfig startup behavior", () => {
1109
1118
  expect(raw.llm.profiles.balanced).toEqual(edited);
1110
1119
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
1111
1120
  // Content is served from the catalog...
1112
- expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1121
+ expect(effectiveBalanced?.model).toBe(MANAGED_BALANCED_MODEL);
1113
1122
  // ...with the user's label and status overlaid.
1114
1123
  expect(effectiveBalanced?.label).toBe("My Default");
1115
1124
  expect(effectiveBalanced?.status).toBe("disabled");
@@ -1134,7 +1143,7 @@ describe("loadConfig startup behavior", () => {
1134
1143
  expect(raw.llm.profiles.balanced).toEqual(stub);
1135
1144
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
1136
1145
  expect(effectiveBalanced?.label).toBe("My Default");
1137
- expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1146
+ expect(effectiveBalanced?.model).toBe(MANAGED_BALANCED_MODEL);
1138
1147
  });
1139
1148
 
1140
1149
  test("off-platform boot preserves user-toggled status on a managed stub", () => {
@@ -1156,7 +1165,7 @@ describe("loadConfig startup behavior", () => {
1156
1165
  expect(effectiveBalanced?.status).toBe("disabled");
1157
1166
  // Content still comes from the catalog — only label/status/topP are
1158
1167
  // workspace-owned.
1159
- expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1168
+ expect(effectiveBalanced?.model).toBe(MANAGED_BALANCED_MODEL);
1160
1169
  });
1161
1170
 
1162
1171
  test("boot preserves a user-edited topP override on a managed stub", () => {
@@ -1180,7 +1189,7 @@ describe("loadConfig startup behavior", () => {
1180
1189
  expect(effectiveBalanced?.topP).toBe(0.5);
1181
1190
  // Content still comes from the catalog — topP is workspace-owned, the
1182
1191
  // rest is code-owned.
1183
- expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1192
+ expect(effectiveBalanced?.model).toBe(MANAGED_BALANCED_MODEL);
1184
1193
  });
1185
1194
 
1186
1195
  test("effective balanced profile carries no topP override by default", () => {
@@ -1235,7 +1244,7 @@ describe("loadConfig startup behavior", () => {
1235
1244
  expect(raw.llm.profiles.balanced).toBeUndefined();
1236
1245
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
1237
1246
  expect(effectiveBalanced?.label).toBe("Balanced");
1238
- expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1247
+ expect(effectiveBalanced?.model).toBe(MANAGED_BALANCED_MODEL);
1239
1248
  // Status is unset — the default resolves active.
1240
1249
  expect("status" in (effectiveBalanced ?? {})).toBe(false);
1241
1250
  });
@@ -1314,7 +1323,7 @@ describe("loadConfig startup behavior", () => {
1314
1323
  // overlay boot. The overlay-set label is what shows through the
1315
1324
  // effective view.
1316
1325
  expect(mainAgentConfig.provider).toBe("vellum");
1317
- expect(mainAgentConfig.model).toBe("gpt-5.6-luna");
1326
+ expect(mainAgentConfig.model).toBe(MANAGED_BALANCED_MODEL);
1318
1327
 
1319
1328
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
1320
1329
  expect(raw.llm.profiles.balanced).toEqual({
@@ -1345,7 +1354,7 @@ describe("loadConfig startup behavior", () => {
1345
1354
  );
1346
1355
  expect(effectiveBalanced?.provider).toBe("vellum");
1347
1356
  expect(effectiveBalanced?.provider_connection).toBeUndefined();
1348
- expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1357
+ expect(effectiveBalanced?.model).toBe(MANAGED_BALANCED_MODEL);
1349
1358
  expect(effectiveBalanced?.maxTokens).toBe(32000);
1350
1359
  expect(effectiveBalanced?.thinking).toEqual({
1351
1360
  enabled: true,
@@ -0,0 +1,153 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ __resetContainerCpuSamplerForTests,
5
+ __runContainerCpuSamplerTickForTests,
6
+ computeCpuPercent,
7
+ computeDurationWeightedMeanPercent,
8
+ getAverageContainerCpuPercentOrNull,
9
+ getCachedContainerCpuPercent,
10
+ getCachedContainerCpuPercentOrNull,
11
+ } from "../util/container-cpu-sampler.js";
12
+
13
+ describe("computeCpuPercent", () => {
14
+ test("computes percent of the full allocation from a CPU-time delta", () => {
15
+ // 2.5s of CPU over a 5s window on 1 core is 50% of the allocation.
16
+ expect(computeCpuPercent(2_500_000, 5_000, 1)).toBe(50);
17
+ // The same delta on 2 cores is half the utilization.
18
+ expect(computeCpuPercent(2_500_000, 5_000, 2)).toBe(25);
19
+ // Fully saturating 4 cores for the whole window is 100%.
20
+ expect(computeCpuPercent(20_000_000, 5_000, 4)).toBe(100);
21
+ });
22
+
23
+ test("rounds to 2 decimal places", () => {
24
+ expect(computeCpuPercent(123_456, 5_000, 2)).toBe(1.23);
25
+ expect(computeCpuPercent(123_999, 5_000, 2)).toBe(1.24);
26
+ expect(computeCpuPercent(0, 5_000, 1)).toBe(0);
27
+ });
28
+
29
+ test("yields a non-finite result for zero cores, which callers must guard", () => {
30
+ expect(Number.isFinite(computeCpuPercent(1_000_000, 5_000, 0))).toBe(false);
31
+ expect(Number.isFinite(computeCpuPercent(0, 5_000, 0))).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe("getCachedContainerCpuPercent", () => {
36
+ test("returns a finite number", () => {
37
+ expect(Number.isFinite(getCachedContainerCpuPercent())).toBe(true);
38
+ });
39
+ });
40
+
41
+ describe("getCachedContainerCpuPercentOrNull", () => {
42
+ test("returns null until the sampler computes its first delta", () => {
43
+ __resetContainerCpuSamplerForTests();
44
+
45
+ expect(getCachedContainerCpuPercentOrNull()).toBeNull();
46
+ // The plain accessor keeps its 0 placeholder for /v1/health.
47
+ expect(getCachedContainerCpuPercent()).toBe(0);
48
+
49
+ // Drive one sampler tick a full interval ahead of the reset baseline so
50
+ // it computes a real delta from process.cpuUsage().
51
+ __runContainerCpuSamplerTickForTests(Date.now() + 5_000);
52
+
53
+ const sampled = getCachedContainerCpuPercentOrNull();
54
+ expect(sampled).not.toBeNull();
55
+ expect(Number.isFinite(sampled!)).toBe(true);
56
+ expect(sampled).toBe(getCachedContainerCpuPercent());
57
+ });
58
+ });
59
+
60
+ describe("computeDurationWeightedMeanPercent", () => {
61
+ test("weights each percent by the wall time its delta spanned", () => {
62
+ // 25s at 80% then 5s at 100% is 83.33% of the half minute, not the 90%
63
+ // an equal-weight mean would report.
64
+ expect(
65
+ computeDurationWeightedMeanPercent(
66
+ [
67
+ { at: 25_000, percent: 80, elapsedMs: 25_000 },
68
+ { at: 30_000, percent: 100, elapsedMs: 5_000 },
69
+ ],
70
+ 0,
71
+ ),
72
+ ).toBe(83.33);
73
+ // Equal durations degrade to the plain mean.
74
+ expect(
75
+ computeDurationWeightedMeanPercent(
76
+ [
77
+ { at: 5_000, percent: 40, elapsedMs: 5_000 },
78
+ { at: 10_000, percent: 60, elapsedMs: 5_000 },
79
+ ],
80
+ 0,
81
+ ),
82
+ ).toBe(50);
83
+ });
84
+
85
+ test("clips a delayed tick's contribution to its overlap with the window", () => {
86
+ // A 25s CPU-bound stall at 100% ends 1s into the window; only that 1s
87
+ // may count. With 29s of idle at 10% after it, the window reads 13%,
88
+ // not the 52.8% an unclipped duration weighting would report.
89
+ expect(
90
+ computeDurationWeightedMeanPercent(
91
+ [
92
+ { at: 1_000, percent: 100, elapsedMs: 25_000 },
93
+ { at: 30_000, percent: 10, elapsedMs: 29_000 },
94
+ ],
95
+ 0,
96
+ ),
97
+ ).toBe(13);
98
+ // A tick entirely before the window contributes nothing.
99
+ expect(
100
+ computeDurationWeightedMeanPercent(
101
+ [
102
+ { at: -1_000, percent: 100, elapsedMs: 5_000 },
103
+ { at: 10_000, percent: 20, elapsedMs: 10_000 },
104
+ ],
105
+ 0,
106
+ ),
107
+ ).toBe(20);
108
+ });
109
+
110
+ test("returns null without any overlapping samples", () => {
111
+ expect(computeDurationWeightedMeanPercent([], 0)).toBeNull();
112
+ expect(
113
+ computeDurationWeightedMeanPercent(
114
+ [{ at: 5_000, percent: 50, elapsedMs: 0 }],
115
+ 0,
116
+ ),
117
+ ).toBeNull();
118
+ expect(
119
+ computeDurationWeightedMeanPercent(
120
+ [{ at: -1_000, percent: 50, elapsedMs: 5_000 }],
121
+ 0,
122
+ ),
123
+ ).toBeNull();
124
+ });
125
+ });
126
+
127
+ describe("getAverageContainerCpuPercentOrNull", () => {
128
+ test("returns null until a tick lands inside the window", () => {
129
+ __resetContainerCpuSamplerForTests();
130
+
131
+ expect(getAverageContainerCpuPercentOrNull(60_000)).toBeNull();
132
+ });
133
+
134
+ test("averages the ticks recorded within the trailing window", () => {
135
+ __resetContainerCpuSamplerForTests();
136
+
137
+ // A single recorded tick makes the average equal the last cached value.
138
+ __runContainerCpuSamplerTickForTests(Date.now() + 5_000);
139
+ const single = getAverageContainerCpuPercentOrNull(60_000);
140
+ expect(single).not.toBeNull();
141
+ expect(single).toBe(getCachedContainerCpuPercentOrNull());
142
+
143
+ // More ticks keep the average finite and within the recorded range; the
144
+ // exact value depends on real process CPU deltas, so only the invariant
145
+ // is asserted.
146
+ __runContainerCpuSamplerTickForTests(Date.now() + 10_000);
147
+ __runContainerCpuSamplerTickForTests(Date.now() + 15_000);
148
+ const averaged = getAverageContainerCpuPercentOrNull(60_000);
149
+ expect(averaged).not.toBeNull();
150
+ expect(Number.isFinite(averaged!)).toBe(true);
151
+ expect(averaged!).toBeGreaterThanOrEqual(0);
152
+ });
153
+ });
@@ -71,7 +71,6 @@ mock.module("../plugins/defaults/memory/substrate/edge-index.js", () => ({
71
71
  getEdgeIndex: async (): Promise<EdgeIndex> => edgeIndex,
72
72
  invalidateEdgeIndex: () => {},
73
73
  getReachable: () => new Set<string>(),
74
- validateEdgeTargets: () => ({ ok: true, missing: [] }),
75
74
  totalEdgeCount: () => 0,
76
75
  }));
77
76
 
@@ -15,7 +15,6 @@ mock.module("../permissions/checker.js", () => ({
15
15
  isDynamicSkillLoadInvocation: () => false,
16
16
  check: checkSpy,
17
17
  classifyRisk: () => Promise.resolve({ level: "low" }),
18
- generateAllowlistOptions: () => Promise.resolve([]),
19
18
  generateScopeOptions: () => [],
20
19
  }));
21
20
 
@@ -521,6 +521,23 @@ describe("classifyConversationError", () => {
521
521
  expect(result.userMessage.toLowerCase()).toContain("image");
522
522
  });
523
523
 
524
+ it("classifies an OpenAI-compatible 400 for an unreadable image as image_unsupported_format (non-retryable)", () => {
525
+ // The rejection an OpenAI-compatible endpoint returns when any image in
526
+ // the request is not one of the formats it decodes (a HEIC photo renamed
527
+ // to .png, say). It names no index, so the whole turn dies unless it is
528
+ // classified for the image-recovery path with actionable copy.
529
+ const err = new ProviderError(
530
+ "The image data you provided does not represent a valid image. Please check your input and try again with one of the supported image formats: [image/jpeg, image/png, image/gif, image/webp]",
531
+ "openai",
532
+ 400,
533
+ );
534
+ const result = classifyConversationError(err, baseCtx);
535
+ expect(result.code).toBe("IMAGE_TOO_LARGE");
536
+ expect(result.errorCategory).toBe("image_unsupported_format");
537
+ expect(result.retryable).toBe(false);
538
+ expect(result.userMessage.toLowerCase()).toContain("image");
539
+ });
540
+
524
541
  it("does not steal generic 400s that happen to mention 'image'", () => {
525
542
  const err = new ProviderError(
526
543
  "invalid request: image source is missing",
@@ -652,14 +652,12 @@ describe("loadFromDb metadata injection rehydration", () => {
652
652
  expect(messages[2].content).toEqual([{ type: "text", text: "Tail" }]);
653
653
  });
654
654
 
655
- test("internal-channel trusted_contact view still rehydrates memoryV2StaticBlock", async () => {
656
- // Rehydration keys on `sourceChannel`, not `trustClass`: injection uses
657
- // `shouldExposePersonalMemory`, which exposes personal memory whenever
658
- // `sourceChannel === "vellum"` regardless of actor trust class. So a
659
- // trusted_contact view arriving over the internal `"vellum"` channel
660
- // rehydrates `memoryV2StaticBlock`. The rehydrate gate must match
661
- // injection so a daemon-restart reload of the same conversation produces
662
- // an identical prefix.
655
+ test("internal-channel trusted_contact view does not rehydrate memoryV2StaticBlock", async () => {
656
+ // Rehydration keys on the actor, not the surface: a trusted_contact is
657
+ // denied personal memory on the internal `"vellum"` channel exactly as it
658
+ // is on a remote one. Injection and rehydration share
659
+ // `canSeePersonalMemory`, so a daemon-restart reload of the same
660
+ // conversation produces an identical prefix.
663
661
  mockConversation = defaultConv();
664
662
  mockDbMessages = [
665
663
  {
@@ -697,13 +695,8 @@ describe("loadFromDb metadata injection rehydration", () => {
697
695
  const messages = conversation.getMessages();
698
696
 
699
697
  expect(messages).toHaveLength(3);
700
- expect(messages[0].content).toEqual([
701
- {
702
- type: "text",
703
- text: "<info>\n## Essentials\n\nAlice prefers VS Code.\n</info>",
704
- },
705
- { type: "text", text: "First" },
706
- ]);
698
+ // The persisted block stays out: the console is a surface, not an actor.
699
+ expect(messages[0].content).toEqual([{ type: "text", text: "First" }]);
707
700
  });
708
701
 
709
702
  test("rehydration order matches injection-time order for the full personal-memory set", async () => {
@@ -1047,12 +1040,14 @@ describe("loadFromDb metadata injection rehydration", () => {
1047
1040
  expect(messages[2].content).toEqual([{ type: "text", text: "Tail" }]);
1048
1041
  });
1049
1042
 
1050
- test("ensureActorScopedHistory reloads when sourceChannel changes within the same trust class", async () => {
1051
- // Regression: cache invalidation previously keyed only on trust class.
1052
- // `loadFromDb` gates `memoryV2StaticBlock` rehydration on `sourceChannel`
1053
- // via `shouldExposePersonalMemory`, so a same-trust-class reuse from a
1054
- // different channel (e.g. internal `vellum` → remote channel) must
1055
- // re-run `loadFromDb` or stale personal-memory exposure persists.
1043
+ test("ensureActorScopedHistory reloads when the actor's memory visibility changes", async () => {
1044
+ // Reuse of a resident conversation by a different actor must re-run
1045
+ // `loadFromDb`, or the incoming actor inherits the previous actor's
1046
+ // transcript with its personal-memory blocks still spliced in.
1047
+ //
1048
+ // Driven by a trust-class change rather than a channel change: personal
1049
+ // memory is keyed on the actor, so two channels at the same class are the
1050
+ // same view and reloading between them would prove nothing.
1056
1051
  mockConversation = defaultConv();
1057
1052
  mockDbMessages = [
1058
1053
  {
@@ -1080,10 +1075,9 @@ describe("loadFromDb metadata injection rehydration", () => {
1080
1075
  ];
1081
1076
 
1082
1077
  const conversation = makeConversation();
1083
- // First load: internal channel, trusted_contact actor → personal memory
1084
- // exposed via `shouldExposePersonalMemory({sourceChannel: "vellum", ...})`.
1078
+ // First load: guardian actor → personal memory exposed.
1085
1079
  conversation.setTrustContext({
1086
- trustClass: "trusted_contact",
1080
+ trustClass: "guardian",
1087
1081
  sourceChannel: "vellum",
1088
1082
  });
1089
1083
  await conversation.ensureActorScopedHistory();
@@ -1095,8 +1089,8 @@ describe("loadFromDb metadata injection rehydration", () => {
1095
1089
  { type: "text", text: "First" },
1096
1090
  ]);
1097
1091
 
1098
- // Reuse with the same trust class but a remote channel. The cache must
1099
- // invalidate and trigger a reload that strips the personal-memory block.
1092
+ // Reuse by a contact. The cache must invalidate and trigger a reload that
1093
+ // strips the personal-memory block.
1100
1094
  conversation.setTrustContext({
1101
1095
  trustClass: "trusted_contact",
1102
1096
  sourceChannel: "telegram",