@vellumai/assistant 0.11.7 → 0.11.8-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -98,7 +98,12 @@ mock.module("../schedule/schedule-store.js", () => ({
98
98
  deleteSchedule: async () => {},
99
99
  }));
100
100
 
101
+ // Spread the actual module so transitive importers of names this
102
+ // factory does not stub keep resolving (a partial factory breaks at
103
+ // import time when the graph gains a new named import).
104
+ const actualFeedWriter = await import("../home/feed-writer.js");
101
105
  mock.module("../home/feed-writer.js", () => ({
106
+ ...actualFeedWriter,
102
107
  stripConversationIds: async () => {},
103
108
  }));
104
109
 
@@ -101,6 +101,9 @@ mock.module("../persistence/conversation-crud.js", () => ({
101
101
  getConversationOriginInterface: () => null,
102
102
  getConversationOriginChannel: () => null,
103
103
  getMessages: () => mockDbMessages,
104
+ // Read by camera-frame retention on the compaction pipeline's summarizer
105
+ // input. Empty unless a test seeds tagged frames.
106
+ selectSightFrameCaptureTimes: () => sightFrameCaptureTimes,
104
107
  getConversation: () => mockConversation,
105
108
  createConversation: () => ({ id: "conv-1" }),
106
109
  addMessage: () => ({ id: `msg-${Date.now()}` }),
@@ -114,6 +117,9 @@ mock.module("../persistence/conversation-queries.js", () => ({
114
117
  listConversations: () => [],
115
118
  }));
116
119
 
120
+ /** Tagged camera frames for the retention pass; seeded per test. */
121
+ let sightFrameCaptureTimes = new Map<string, number>();
122
+
117
123
  // Captured defaultCompact invocations + per-test canned result.
118
124
  let compactCalls: CompactionContext[] = [];
119
125
  let mockCompactResult: ContextWindowResult = makeNoopResult();
@@ -366,6 +372,7 @@ describe("Conversation.summarizeUpToMessage", () => {
366
372
  mockCompactResult = makeNoopResult();
367
373
  mockDbMessages = threeTurnRows();
368
374
  mockConversation = baseConversationRow();
375
+ sightFrameCaptureTimes = new Map();
369
376
  });
370
377
 
371
378
  test("maps the boundary row straight through when there is no context summary", async () => {
@@ -939,3 +946,98 @@ describe("formatSummarizeUpToResult", () => {
939
946
  expect(card).toContain("4 recent messages kept in full.");
940
947
  });
941
948
  });
949
+
950
+ /**
951
+ * `forceCompact`, `maybeCompact`, and `summarizeUpToMessage` all funnel through
952
+ * the private `runCompaction`, so the retention applied to its summarizer input
953
+ * covers all three. `forceCompact` is the cheapest of them to drive.
954
+ */
955
+ describe("direct compaction trims the summarizer's own input", () => {
956
+ beforeEach(() => {
957
+ compactCalls = [];
958
+ slackWatermarkCalls = [];
959
+ mockCompactResult = makeNoopResult();
960
+ mockDbMessages = threeTurnRows();
961
+ mockConversation = baseConversationRow();
962
+ sightFrameCaptureTimes = new Map();
963
+ });
964
+
965
+ /** A frame as the live history carries it: inline bytes plus its row id. */
966
+ function frame(attachmentId: string): Message {
967
+ return {
968
+ role: "user",
969
+ content: [
970
+ {
971
+ type: "image",
972
+ source: { type: "base64", media_type: "image/png", data: "AAAAAAAA" },
973
+ _attachmentId: attachmentId,
974
+ },
975
+ ],
976
+ };
977
+ }
978
+
979
+ function framesInCompactCall(): number {
980
+ return (compactCalls[0]?.messages ?? []).flatMap((message) =>
981
+ message.content.filter((block) => block.type === "image"),
982
+ ).length;
983
+ }
984
+
985
+ function frameStubsInCompactCall(): number {
986
+ return (compactCalls[0]?.messages ?? []).flatMap((message) =>
987
+ message.content.filter(
988
+ (block) =>
989
+ block.type === "text" &&
990
+ block.text.startsWith("[Camera frame omitted from context:"),
991
+ ),
992
+ ).length;
993
+ }
994
+
995
+ test("bounds an over-full camera history before the summary call", async () => {
996
+ sightFrameCaptureTimes = new Map([
997
+ ["f1", 1000],
998
+ ["f2", 2000],
999
+ ["f3", 3000],
1000
+ ["f4", 4000],
1001
+ ]);
1002
+ const conversation = makeConversation();
1003
+ conversation.messages = [
1004
+ frame("f1"),
1005
+ frame("f2"),
1006
+ frame("f3"),
1007
+ frame("f4"),
1008
+ ];
1009
+
1010
+ await conversation.forceCompact();
1011
+
1012
+ // The summary call is a provider request like any other, so it carries the
1013
+ // bound rather than every frame the session captured.
1014
+ expect(compactCalls).toHaveLength(1);
1015
+ expect(framesInCompactCall()).toBe(2);
1016
+ expect(frameStubsInCompactCall()).toBe(2);
1017
+ });
1018
+
1019
+ test("leaves an untagged history untouched", async () => {
1020
+ const conversation = makeConversation();
1021
+ conversation.messages = [frame("f1"), frame("f2"), frame("f3")];
1022
+
1023
+ await conversation.forceCompact();
1024
+
1025
+ expect(framesInCompactCall()).toBe(3);
1026
+ expect(frameStubsInCompactCall()).toBe(0);
1027
+ });
1028
+
1029
+ test("summarizeUpToMessage reaches the same trim through the shared seam", async () => {
1030
+ sightFrameCaptureTimes = new Map([
1031
+ ["f1", 1000],
1032
+ ["f2", 2000],
1033
+ ["f3", 3000],
1034
+ ["f4", 4000],
1035
+ ]);
1036
+ const conversation = makeConversation();
1037
+ await conversation.summarizeUpToMessage("m4");
1038
+ // The boundary machinery reloads history from the mocked rows, so this
1039
+ // asserts the seam ran, not the frame maths (covered above).
1040
+ expect(compactCalls).toHaveLength(1);
1041
+ expect(compactCalls[0].fixedTailStartIndex).toBeDefined();
1042
+ });
1043
+ });
@@ -99,18 +99,6 @@ mock.module("../persistence/attachments-store.js", () => ({
99
99
  linkAttachmentToMessage: () => {},
100
100
  }));
101
101
 
102
- mock.module("../memory/retriever.js", () => ({
103
- buildMemoryRecall: async () => ({
104
- enabled: false,
105
- degraded: false,
106
- injectedText: "",
107
- semanticHits: 0,
108
- injectedTokens: 0,
109
- latencyMs: 0,
110
- }),
111
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
112
- }));
113
-
114
102
  mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
115
103
  ContextWindowManager: class {
116
104
  estimateInputTokens() {
@@ -81,11 +81,6 @@ mock.module("../persistence/attachments-store.js", () => ({
81
81
  linkAttachmentToMessage: () => {},
82
82
  }));
83
83
 
84
- mock.module("../memory/retriever.js", () => ({
85
- buildMemoryRecall: async () => null,
86
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
87
- }));
88
-
89
84
  mock.module("../memory/query-builder.js", () => ({
90
85
  buildMemoryQuery: () => "",
91
86
  }));
@@ -86,23 +86,6 @@ mock.module("../persistence/attachments-store.js", () => ({
86
86
  uploadAttachment: () => ({ id: "att-1" }),
87
87
  linkAttachmentToMessage: () => {},
88
88
  }));
89
- mock.module("../memory/retriever.js", () => ({
90
- buildMemoryRecall: async () => ({
91
- enabled: false,
92
- degraded: false,
93
- reason: null,
94
- provider: "mock",
95
- model: "mock",
96
- injectedText: "",
97
- semanticHits: 0,
98
- mergedCount: 0,
99
- selectedCount: 0,
100
- injectedTokens: 0,
101
- latencyMs: 0,
102
- topCandidates: [],
103
- }),
104
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
105
- }));
106
89
  mock.module("../memory/query-builder.js", () => ({
107
90
  buildMemoryQuery: () => "",
108
91
  }));
@@ -83,23 +83,6 @@ mock.module("../persistence/attachments-store.js", () => ({
83
83
  uploadAttachment: () => ({ id: "att-1" }),
84
84
  linkAttachmentToMessage: () => {},
85
85
  }));
86
- mock.module("../memory/retriever.js", () => ({
87
- buildMemoryRecall: async () => ({
88
- enabled: false,
89
- degraded: false,
90
- reason: null,
91
- provider: "mock",
92
- model: "mock",
93
- injectedText: "",
94
- semanticHits: 0,
95
- mergedCount: 0,
96
- selectedCount: 0,
97
- injectedTokens: 0,
98
- latencyMs: 0,
99
- topCandidates: [],
100
- }),
101
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
102
- }));
103
86
  mock.module("../memory/query-builder.js", () => ({
104
87
  buildMemoryQuery: () => "",
105
88
  }));
@@ -12,6 +12,8 @@ let mockProviders: Array<{
12
12
  pingMethod: string | null;
13
13
  pingHeaders: string | null;
14
14
  pingBody: string | null;
15
+ authorizeParams: string | null;
16
+ scopeSeparator: string | null;
15
17
  managedServiceConfigKey?: string;
16
18
  }> = [];
17
19
 
@@ -191,11 +193,16 @@ function addProvider(
191
193
  pingUrl?: string | null;
192
194
  pingMethod?: string | null;
193
195
  managedServiceConfigKey?: string;
196
+ authorizeParams?: Record<string, string>;
194
197
  },
195
198
  ) {
196
199
  mockProviders.push({
197
200
  provider,
198
201
  defaultScopes: JSON.stringify(opts?.defaultScopes ?? []),
202
+ authorizeParams: opts?.authorizeParams
203
+ ? JSON.stringify(opts.authorizeParams)
204
+ : null,
205
+ scopeSeparator: " ",
199
206
  pingUrl: opts?.pingUrl ?? null,
200
207
  pingMethod: opts?.pingMethod ?? null,
201
208
  pingHeaders: null,
@@ -443,6 +450,44 @@ describe("credential-health-service", () => {
443
450
  expect(report.results[0]!.canAutoRecover).toBe(false);
444
451
  });
445
452
 
453
+ test("measures a user-token grant against user_scope, not the bot scopes", async () => {
454
+ // Slack issues two grants from one authorization and the stored token is
455
+ // the user one, so its grant is compared against `user_scope`. Measuring
456
+ // it against the bot `scope` parameter reports scopes that grant can never
457
+ // hold: `channels:join` is bot-only and is never requested for the user.
458
+ addProvider("slack", {
459
+ defaultScopes: ["channels:join", "chat:write", "search:read"],
460
+ authorizeParams: { user_scope: "chat:write,search:read" },
461
+ });
462
+ addConnection("slack", "conn-1", {
463
+ grantedScopes: ["chat:write", "search:read"],
464
+ expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
465
+ });
466
+ setToken("conn-1");
467
+
468
+ const report = await checkAllCredentials();
469
+ expect(report.results[0]!.status).not.toBe("missing_scopes");
470
+ expect(report.results[0]!.missingScopes).toEqual([]);
471
+ });
472
+
473
+ test("still reports a user scope the installer did not grant", async () => {
474
+ // Sensitivity check for the test above: comparing against `user_scope`
475
+ // must keep catching a genuinely declined user scope, not pass everything.
476
+ addProvider("slack", {
477
+ defaultScopes: ["channels:join", "chat:write", "search:read"],
478
+ authorizeParams: { user_scope: "chat:write,search:read" },
479
+ });
480
+ addConnection("slack", "conn-1", {
481
+ grantedScopes: ["chat:write"],
482
+ expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
483
+ });
484
+ setToken("conn-1");
485
+
486
+ const report = await checkAllCredentials();
487
+ expect(report.results[0]!.status).toBe("missing_scopes");
488
+ expect(report.results[0]!.missingScopes).toEqual(["search:read"]);
489
+ });
490
+
446
491
  test("returns revoked when ping returns 401", async () => {
447
492
  mockFetchResponse = { ok: false, status: 401 };
448
493
  addProvider("google", {
@@ -0,0 +1,256 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+
5
+ import { credentialKey } from "@vellumai/credential-storage";
6
+ import type { CredentialRecord } from "@vellumai/service-contracts/credential-rpc";
7
+
8
+ import type { CredentialRecordBackend } from "../security/ces-rpc-record-backend.js";
9
+ import {
10
+ _ensureCesRecordsLoaded,
11
+ _setMetadataPath,
12
+ getCredentialMetadata,
13
+ setCredentialRecordBackend,
14
+ upsertCredentialMetadata,
15
+ } from "../tools/credentials/metadata-store.js";
16
+ import { getDataDir } from "../util/platform.js";
17
+
18
+ function leftoverPath(): string {
19
+ return join(getDataDir(), "credentials", "metadata.json");
20
+ }
21
+
22
+ function writeLeftover(record: CredentialRecord): void {
23
+ const path = leftoverPath();
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ writeFileSync(
26
+ path,
27
+ JSON.stringify({
28
+ version: 5,
29
+ credentials: [record],
30
+ }),
31
+ );
32
+ }
33
+
34
+ function leftoverContents(): string {
35
+ return readFileSync(leftoverPath(), "utf-8");
36
+ }
37
+
38
+ function makeBackend(): CredentialRecordBackend & {
39
+ store: Map<string, CredentialRecord>;
40
+ bulkSetCalls: number;
41
+ listCalls: number;
42
+ } {
43
+ const store = new Map<string, CredentialRecord>();
44
+ const backend: CredentialRecordBackend & {
45
+ store: Map<string, CredentialRecord>;
46
+ bulkSetCalls: number;
47
+ listCalls: number;
48
+ } = {
49
+ store,
50
+ bulkSetCalls: 0,
51
+ listCalls: 0,
52
+ isAvailable: () => true,
53
+ get: async (account) => store.get(account),
54
+ set: async (account, record) => {
55
+ store.set(account, record);
56
+ return true;
57
+ },
58
+ delete: async (account) => {
59
+ if (!store.has(account)) {
60
+ return "not-found";
61
+ }
62
+ store.delete(account);
63
+ return "deleted";
64
+ },
65
+ list: async () => {
66
+ backend.listCalls += 1;
67
+ return [...store.entries()].map(([account, record]) => ({
68
+ account,
69
+ record,
70
+ }));
71
+ },
72
+ bulkSet: async (records) => {
73
+ backend.bulkSetCalls += 1;
74
+ for (const { account, record } of records) {
75
+ store.set(account, record);
76
+ }
77
+ return records.map((entry) => ({ account: entry.account, ok: true }));
78
+ },
79
+ };
80
+ return backend;
81
+ }
82
+
83
+ describe("CES credential record cache", () => {
84
+ beforeEach(() => {
85
+ setCredentialRecordBackend(undefined);
86
+ _setMetadataPath(null);
87
+ const leftover = leftoverPath();
88
+ if (existsSync(leftover)) {
89
+ rmSync(leftover, { force: true });
90
+ }
91
+ });
92
+
93
+ afterEach(() => {
94
+ setCredentialRecordBackend(undefined);
95
+ _setMetadataPath(null);
96
+ const leftover = leftoverPath();
97
+ if (existsSync(leftover)) {
98
+ rmSync(leftover, { force: true });
99
+ }
100
+ });
101
+
102
+ test("first catalog read lazy-loads CES records into the in-process cache", async () => {
103
+ const backend = makeBackend();
104
+ const record: CredentialRecord = {
105
+ credentialId: "cred-1",
106
+ service: "github",
107
+ field: "token",
108
+ allowedTools: ["bash"],
109
+ allowedDomains: [],
110
+ createdAt: 1,
111
+ updatedAt: 2,
112
+ };
113
+ backend.store.set(credentialKey("github", "token"), record);
114
+ setCredentialRecordBackend(backend);
115
+ expect(backend.listCalls).toBe(0);
116
+
117
+ getCredentialMetadata("github", "token");
118
+ await _ensureCesRecordsLoaded();
119
+ expect(backend.listCalls).toBe(1);
120
+
121
+ expect(getCredentialMetadata("github", "token")?.credentialId).toBe(
122
+ "cred-1",
123
+ );
124
+ expect(getCredentialMetadata("github", "token")?.allowedTools).toEqual([
125
+ "bash",
126
+ ]);
127
+ getCredentialMetadata("github", "token");
128
+ await _ensureCesRecordsLoaded();
129
+ expect(backend.listCalls).toBe(1);
130
+ });
131
+
132
+ test("lazy load leaves leftover metadata.json in place and ignores it", async () => {
133
+ const leftoverRecord: CredentialRecord = {
134
+ credentialId: "cred-leftover",
135
+ service: "github",
136
+ field: "token",
137
+ allowedTools: ["bash"],
138
+ allowedDomains: ["github.com"],
139
+ createdAt: 1,
140
+ updatedAt: 2,
141
+ };
142
+ writeLeftover(leftoverRecord);
143
+ const before = leftoverContents();
144
+
145
+ const backend = makeBackend();
146
+ backend.store.set(credentialKey("github", "token"), leftoverRecord);
147
+ setCredentialRecordBackend(backend);
148
+
149
+ getCredentialMetadata("github", "token");
150
+ await _ensureCesRecordsLoaded();
151
+
152
+ expect(existsSync(leftoverPath())).toBe(true);
153
+ expect(leftoverContents()).toBe(before);
154
+ expect(backend.bulkSetCalls).toBe(0);
155
+ expect(getCredentialMetadata("github", "token")?.allowedTools).toEqual([
156
+ "bash",
157
+ ]);
158
+ });
159
+
160
+ test("lazy load ignores leftover metadata.json when CES list fails", async () => {
161
+ const leftoverRecord: CredentialRecord = {
162
+ credentialId: "cred-keep",
163
+ service: "slack_channel",
164
+ field: "bot_token",
165
+ allowedTools: ["bash"],
166
+ allowedDomains: [],
167
+ createdAt: 1,
168
+ updatedAt: 2,
169
+ };
170
+ writeLeftover(leftoverRecord);
171
+
172
+ const backend = makeBackend();
173
+ backend.list = async () => null;
174
+ setCredentialRecordBackend(backend);
175
+
176
+ getCredentialMetadata("slack_channel", "bot_token");
177
+ await _ensureCesRecordsLoaded();
178
+
179
+ expect(existsSync(leftoverPath())).toBe(true);
180
+ expect(backend.bulkSetCalls).toBe(0);
181
+ expect(
182
+ getCredentialMetadata("slack_channel", "bot_token"),
183
+ ).toBeUndefined();
184
+ });
185
+
186
+ test("lazy load ignores leftover metadata.json when CES has no leftover accounts", async () => {
187
+ const leftoverRecord: CredentialRecord = {
188
+ credentialId: "cred-missing",
189
+ service: "github",
190
+ field: "token",
191
+ allowedTools: ["bash"],
192
+ allowedDomains: [],
193
+ createdAt: 1,
194
+ updatedAt: 2,
195
+ };
196
+ writeLeftover(leftoverRecord);
197
+
198
+ const backend = makeBackend();
199
+ setCredentialRecordBackend(backend);
200
+
201
+ getCredentialMetadata("github", "token");
202
+ await _ensureCesRecordsLoaded();
203
+
204
+ expect(existsSync(leftoverPath())).toBe(true);
205
+ expect(backend.store.size).toBe(0);
206
+ expect(backend.bulkSetCalls).toBe(0);
207
+ expect(getCredentialMetadata("github", "token")).toBeUndefined();
208
+ });
209
+
210
+ test("lazy load does not throw when CES isAvailable throws", async () => {
211
+ const backend = makeBackend();
212
+ backend.isAvailable = () => {
213
+ throw new Error("CES boom");
214
+ };
215
+ setCredentialRecordBackend(backend);
216
+
217
+ getCredentialMetadata("github", "token");
218
+ await _ensureCesRecordsLoaded();
219
+ });
220
+
221
+ test("upsert write-throughs to CES without updating leftover metadata.json", async () => {
222
+ const leftoverRecord: CredentialRecord = {
223
+ credentialId: "cred-leftover",
224
+ service: "github",
225
+ field: "token",
226
+ allowedTools: ["bash"],
227
+ allowedDomains: [],
228
+ createdAt: 1,
229
+ updatedAt: 2,
230
+ };
231
+ writeLeftover(leftoverRecord);
232
+ const leftoverBefore = leftoverContents();
233
+
234
+ const backend = makeBackend();
235
+ backend.store.set(credentialKey("github", "token"), leftoverRecord);
236
+ setCredentialRecordBackend(backend);
237
+ getCredentialMetadata("github", "token");
238
+ await _ensureCesRecordsLoaded();
239
+ expect(existsSync(leftoverPath())).toBe(true);
240
+
241
+ const created = upsertCredentialMetadata("slack_channel", "bot_token", {
242
+ allowedTools: ["bash"],
243
+ });
244
+ await Promise.resolve();
245
+
246
+ expect(existsSync(leftoverPath())).toBe(true);
247
+ expect(leftoverContents()).toBe(leftoverBefore);
248
+ expect(
249
+ backend.store.get(credentialKey("slack_channel", "bot_token"))
250
+ ?.credentialId,
251
+ ).toBe(created.credentialId);
252
+ expect(getCredentialMetadata("slack_channel", "bot_token")?.allowedTools).toEqual(
253
+ ["bash"],
254
+ );
255
+ });
256
+ });
@@ -1,3 +1,5 @@
1
+ import { existsSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { afterEach, describe, expect, test } from "bun:test";
2
4
 
3
5
  import { credentialKey } from "@vellumai/credential-storage";
@@ -6,10 +8,10 @@ import type { CredentialRecord } from "@vellumai/service-contracts/credential-rp
6
8
  import type { CredentialRecordBackend } from "../security/ces-rpc-record-backend.js";
7
9
  import {
8
10
  _setMetadataPath,
9
- persistCredentialMetadata,
10
11
  setCredentialRecordBackend,
11
12
  upsertCredentialMetadata,
12
13
  } from "../tools/credentials/metadata-store.js";
14
+ import { getDataDir } from "../util/platform.js";
13
15
 
14
16
  function makeBackend(): CredentialRecordBackend & {
15
17
  store: Map<string, CredentialRecord>;
@@ -45,6 +47,10 @@ describe("CES credential record write-through", () => {
45
47
  afterEach(() => {
46
48
  setCredentialRecordBackend(undefined);
47
49
  _setMetadataPath(null);
50
+ const leftover = join(getDataDir(), "credentials", "metadata.json");
51
+ if (existsSync(leftover)) {
52
+ rmSync(leftover, { force: true });
53
+ }
48
54
  });
49
55
 
50
56
  test("upsert write-through updates the CES backend", async () => {
@@ -54,7 +60,6 @@ describe("CES credential record write-through", () => {
54
60
  const created = upsertCredentialMetadata("slack_channel", "bot_token", {
55
61
  allowedTools: ["bash"],
56
62
  });
57
- await persistCredentialMetadata(created);
58
63
 
59
64
  const stored = backend.store.get(
60
65
  credentialKey("slack_channel", "bot_token"),
@@ -68,10 +73,9 @@ describe("CES credential record write-through", () => {
68
73
  setCredentialRecordBackend(backend);
69
74
  _setMetadataPath("/tmp/does-not-write-through-metadata.json");
70
75
 
71
- const created = upsertCredentialMetadata("github", "token", {
76
+ upsertCredentialMetadata("github", "token", {
72
77
  allowedTools: ["bash"],
73
78
  });
74
- await persistCredentialMetadata(created);
75
79
 
76
80
  expect(backend.store.size).toBe(0);
77
81
  });
@@ -3,8 +3,6 @@ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
3
3
 
4
4
  import { drizzle } from "drizzle-orm/bun-sqlite";
5
5
 
6
- const originalBunTest = process.env.BUN_TEST;
7
-
8
6
  import { getSqliteFrom } from "../persistence/db-connection.js";
9
7
  import { migrateConversationForkLineage } from "../persistence/migrations/183-add-conversation-fork-lineage.js";
10
8
  import * as schema from "../persistence/schema/index.js";
@@ -62,12 +60,10 @@ function resetMigrationTestDb(): void {
62
60
 
63
61
  describe("conversation fork lineage migration", () => {
64
62
  beforeEach(() => {
65
- process.env.BUN_TEST = "0";
66
63
  resetMigrationTestDb();
67
64
  });
68
65
 
69
66
  afterAll(() => {
70
- process.env.BUN_TEST = originalBunTest;
71
67
  resetMigrationTestDb();
72
68
  });
73
69
 
@@ -3,8 +3,6 @@ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
3
3
 
4
4
  import { drizzle } from "drizzle-orm/bun-sqlite";
5
5
 
6
- const originalBunTest = process.env.BUN_TEST;
7
-
8
6
  import { _resetDisplayOrderMigrationForTests } from "../persistence/conversation-display-order-migration.js";
9
7
  import { _resetGroupMigrationForTests } from "../persistence/conversation-group-migration.js";
10
8
  import { getSqliteFrom } from "../persistence/db-connection.js";
@@ -68,12 +66,10 @@ function resetMigrationTestDb(): void {
68
66
 
69
67
  describe("conversation inference profile migration", () => {
70
68
  beforeEach(() => {
71
- process.env.BUN_TEST = "0";
72
69
  resetMigrationTestDb();
73
70
  });
74
71
 
75
72
  afterAll(() => {
76
- process.env.BUN_TEST = originalBunTest;
77
73
  resetMigrationTestDb();
78
74
  });
79
75
 
@@ -10,8 +10,6 @@ import {
10
10
 
11
11
  import { drizzle } from "drizzle-orm/bun-sqlite";
12
12
 
13
- const originalBunTest = process.env.BUN_TEST;
14
-
15
13
  import { getSqliteFrom } from "../persistence/db-connection.js";
16
14
  import { migrateLlmRequestLogProvider } from "../persistence/migrations/184-llm-request-log-provider.js";
17
15
  import * as schema from "../persistence/schema/index.js";
@@ -48,7 +46,6 @@ function bootstrapPreProviderLlmRequestLogs(raw: Database): void {
48
46
 
49
47
  describe("llm_request_logs provider migration", () => {
50
48
  beforeEach(() => {
51
- process.env.BUN_TEST = "0";
52
49
  resetDbForTesting();
53
50
  });
54
51
 
@@ -57,11 +54,6 @@ describe("llm_request_logs provider migration", () => {
57
54
  });
58
55
 
59
56
  afterAll(() => {
60
- if (originalBunTest === undefined) {
61
- delete process.env.BUN_TEST;
62
- } else {
63
- process.env.BUN_TEST = originalBunTest;
64
- }
65
57
  resetDbForTesting();
66
58
  });
67
59