@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
@@ -18,6 +18,7 @@ import {
18
18
  setSecureKeyAsync,
19
19
  } from "../security/secure-keys.js";
20
20
  import { getLogger } from "../util/logger.js";
21
+ import { claudeTokenDigest } from "./acp-auth-marker-store.js";
21
22
  import {
22
23
  ACP_OAUTH_TOKEN_FIELD,
23
24
  ACP_SERVICE,
@@ -111,6 +112,31 @@ export function parseManualClaudeCode(input: string): {
111
112
  };
112
113
  }
113
114
 
115
+ /**
116
+ * Digest of the Claude OAuth token secure storage holds right now, or
117
+ * `undefined` when it holds none.
118
+ *
119
+ * The credential a marker is judged against. A configured
120
+ * `CLAUDE_CODE_OAUTH_TOKEN` that Claude has already refused stands down at the
121
+ * next spawn, so once a token carries a marker the vault value is what a spawn
122
+ * goes on to resolve, and comparing against it answers whether the marker
123
+ * still describes the credential in use.
124
+ *
125
+ * Read on demand rather than cached. The vault is the authority on what it
126
+ * holds, and a copy of that answer in this process is a second one that has to
127
+ * be kept in step with every write, including writes this process did not
128
+ * make.
129
+ *
130
+ * Lives here rather than beside the comparison it feeds because this module is
131
+ * already the one authorised to read this vault field (see the importer
132
+ * allowlist in `credential-security-invariants`). The digest is all that
133
+ * leaves; the token itself never does.
134
+ */
135
+ export async function storedClaudeTokenDigest(): Promise<string | undefined> {
136
+ const token = await usableStoredClaudeToken();
137
+ return token ? claudeTokenDigest(token) : undefined;
138
+ }
139
+
114
140
  /**
115
141
  * Store a captured Claude OAuth token in the `acp/claude_oauth_token` vault
116
142
  * field and provision the policy the broker applies at spawn time: grant the
@@ -133,6 +159,66 @@ export async function storeAcpClaudeToken(token: string): Promise<void> {
133
159
  ACP_OAUTH_TOKEN_FIELD,
134
160
  ACP_CLAUDE_OAUTH_USAGE_DESCRIPTION,
135
161
  );
162
+ // Again, because the write above already ran this and the policy it repairs
163
+ // is what decides the answer. A token whose `acp_spawn` read was denied is
164
+ // not usable at the moment `setSecureKeyAsync` asks, so that pass declines
165
+ // to retire anything, and the repair on the line above is what makes it
166
+ // usable. Running it a second time is safe: taking the registry is
167
+ // idempotent, and the invalidation is a refetch trigger.
168
+ //
169
+ // Never allowed to fail or delay the store. The token is written either way,
170
+ // and these are notifications: losing one costs a client a stale card until
171
+ // its next snapshot, while waiting on one holds the user at "signing in"
172
+ // while a scan finishes.
173
+ void notifyAcpConnectRetired().catch((err: unknown) => {
174
+ log.warn(
175
+ { err },
176
+ "ACP Connect card notification failed after a token store",
177
+ );
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Tell the rest of the system that a usable Claude token now exists.
183
+ *
184
+ * Two things only clients and the credential prompt can act on. The card
185
+ * registry is in-memory and about a card on screen rather than about the
186
+ * credential, so no marker answers for it, and left standing the prompt keeps
187
+ * redirecting at a card the new token just made stale. The invalidation tells
188
+ * other clients to re-read the snapshot, which they otherwise would not until
189
+ * navigation or reconnect.
190
+ *
191
+ * Neither retires anything by itself. A card is retired by its marker no
192
+ * longer matching the credential a spawn would resolve, which is decided when
193
+ * the marker is read, so a failure here costs freshness rather than
194
+ * correctness.
195
+ *
196
+ * Does nothing for a value a spawn could not use. A bulk restore can land an
197
+ * api-key-shaped token, or one the policy blocks, and the marker comparison
198
+ * keeps the card up for exactly that reason.
199
+ */
200
+ export async function notifyAcpConnectRetired(): Promise<void> {
201
+ if (!(await hasAcpClaudeToken())) {
202
+ return;
203
+ }
204
+ // Dropped per conversation, and only where the failure it stands for has
205
+ // actually stopped being shown. A writer can store the very token Claude
206
+ // rejected, which passes the usability check above and changes nothing about
207
+ // the failure: the snapshot goes on serving that marker, so forgetting the
208
+ // entry would leave the credential prompt opening a second prompt beside a
209
+ // card that is still there. Asking the markers is the same question the
210
+ // snapshot answers, rather than a proxy for it.
211
+ const { conversationsWithRaisedAcpConnectCard, dropAcpConnectCardRaised } =
212
+ await import("./acp-connect-card-state.js");
213
+ for (const conversationId of conversationsWithRaisedAcpConnectCard()) {
214
+ if (!(await acpConnectCardStillWarranted(conversationId))) {
215
+ dropAcpConnectCardRaised(conversationId);
216
+ }
217
+ }
218
+ const { publishSyncInvalidation } =
219
+ await import("../runtime/sync/sync-publisher.js");
220
+ const { SYNC_TAGS } = await import("../daemon/message-types/sync.js");
221
+ await publishSyncInvalidation([SYNC_TAGS.acpAuthRecovery]);
136
222
  }
137
223
 
138
224
  /**
@@ -157,14 +243,28 @@ export async function storeAcpClaudeToken(token: string): Promise<void> {
157
243
  * knows nothing about Anthropic token formats.
158
244
  */
159
245
  export async function hasAcpClaudeToken(): Promise<boolean> {
246
+ return (await usableStoredClaudeToken()) !== undefined;
247
+ }
248
+
249
+ /**
250
+ * The stored Claude token, but only when a spawn would actually get it.
251
+ *
252
+ * One definition of usable for both callers. "Is the account connected" and
253
+ * "which credential would a spawn resolve" are the same question asked for
254
+ * different reasons, and answering them separately is how they drift: a value
255
+ * this rejects still has a digest, so a marker judged against that digest is
256
+ * withheld while the next spawn cannot authenticate, leaving the user with no
257
+ * card and no working token.
258
+ */
259
+ async function usableStoredClaudeToken(): Promise<string | undefined> {
160
260
  const token = await getSecureKeyAsync(
161
261
  credentialKey(ACP_SERVICE, ACP_OAUTH_TOKEN_FIELD),
162
262
  );
163
263
  if (token == null || token.length === 0) {
164
- return false;
264
+ return undefined;
165
265
  }
166
266
  if (classifyAnthropicToken(token) === "api_key") {
167
- return false;
267
+ return undefined;
168
268
  }
169
269
  const denialReason = acpSpawnCredentialDenialReason(ACP_OAUTH_TOKEN_FIELD);
170
270
  if (denialReason !== undefined) {
@@ -172,7 +272,57 @@ export async function hasAcpClaudeToken(): Promise<boolean> {
172
272
  { field: ACP_OAUTH_TOKEN_FIELD, reason: denialReason },
173
273
  "Connect Claude status: token present but spawn read would be denied",
174
274
  );
175
- return false;
275
+ return undefined;
276
+ }
277
+ return token;
278
+ }
279
+
280
+ /**
281
+ * Whether a conversation's raised Connect card is still something to point at.
282
+ *
283
+ * Two ways a card earns its place, matching the two ways one is raised. A
284
+ * mid-run rejection leaves a marker, and the card stands while that marker
285
+ * still names the credential a spawn would resolve. A pre-spawn failure leaves
286
+ * no marker at all, because there was no session to record one, and its card
287
+ * stands while there is still no usable token to spawn with.
288
+ *
289
+ * Asked rather than remembered, so it does not matter what made the situation
290
+ * change. A vault write, a `config.json` edit to
291
+ * `acp.agents.<id>.env.CLAUDE_CODE_OAUTH_TOKEN`, or a policy repair all move
292
+ * the answer, and only one of those runs through a credential write. Hanging
293
+ * this off the write would leave the other two stale.
294
+ */
295
+ export async function acpConnectCardStillWarranted(
296
+ conversationId: string,
297
+ ): Promise<boolean> {
298
+ const { conversationHasCurrentAcpMarker } =
299
+ await import("./acp-auth-marker-store.js");
300
+ if (await conversationHasCurrentAcpMarker(conversationId)) {
301
+ return true;
302
+ }
303
+ // No marker, so this is the pre-spawn card: the failure was that a spawn
304
+ // found nothing to authenticate with, and it stands while that is still
305
+ // true. Asked through the spawn's own resolution rather than of the vault,
306
+ // because a user can repair it by setting the agent's configured token, and
307
+ // a vault-only answer would call that card warranted forever.
308
+ const { raisedAcpConnectCardAgent } =
309
+ await import("./acp-connect-card-state.js");
310
+ const agentId = raisedAcpConnectCardAgent(conversationId);
311
+ if (agentId === undefined) {
312
+ return !(await hasAcpClaudeToken());
313
+ }
314
+ const { resolvedClaudeCredentialDigest } =
315
+ await import("./prepare-agent-env.js");
316
+ const resolved = await resolvedClaudeCredentialDigest(agentId);
317
+ if (resolved === undefined) {
318
+ return true;
176
319
  }
177
- return true;
320
+ // Resolving something is not the same as having a repair. A configured
321
+ // token Claude already refused stands down only once the vault offers an
322
+ // alternative, so with no replacement stored the resolver still reports it
323
+ // and the next spawn rejects it exactly as this one did. Treating that as
324
+ // fixed drops the card and lets a second prompt open beside it.
325
+ const { claudeCredentialRefused } =
326
+ await import("./acp-auth-marker-store.js");
327
+ return claudeCredentialRefused(resolved);
178
328
  }
@@ -11,23 +11,40 @@
11
11
  * leaving auth unset.
12
12
  *
13
13
  * In-memory and ephemeral by design: a daemon restart clears it, which fails
14
- * safe — the redirect simply doesn't fire and the secure prompt is shown. The
15
- * client re-derives the card itself from persisted history, so nothing UI-facing
16
- * depends on this surviving a restart. Entries are never cleared during a run:
17
- * once a card has been raised the redirect stays the right dedup, and a stale
18
- * entry after connect is harmless (the caller is already connected).
14
+ * safe. The redirect simply doesn't fire and the secure prompt is shown.
15
+ *
16
+ * Cleared for real on a successful token write (`storeAcpClaudeToken`), which
17
+ * is the moment the cards these entries stand for stop being the right place
18
+ * to send anyone. Left standing, the redirect keeps pointing the model at a
19
+ * card for auth that already works, and the persisted history markers the
20
+ * entries name keep re-raising it on every reload.
19
21
  */
20
- const conversationsWithAcpConnectCard = new Set<string>();
22
+ const conversationsWithAcpConnectCard = new Map<string, string | undefined>();
21
23
 
22
- /** Record that a Connect Claude card was raised for this conversation. */
24
+ /**
25
+ * Record that a Connect Claude card was raised for this conversation.
26
+ *
27
+ * The agent is kept alongside it because deciding later whether the card still
28
+ * stands means asking what a spawn of that agent would resolve, and precedence
29
+ * is per agent: a configured `CLAUDE_CODE_OAUTH_TOKEN` on one alias is not the
30
+ * vault and is not another alias.
31
+ */
23
32
  export function markAcpConnectCardRaised(
24
33
  conversationId: string | undefined,
34
+ agentId?: string,
25
35
  ): void {
26
36
  if (conversationId) {
27
- conversationsWithAcpConnectCard.add(conversationId);
37
+ conversationsWithAcpConnectCard.set(conversationId, agentId);
28
38
  }
29
39
  }
30
40
 
41
+ /** The agent whose spawn raised this conversation's card, when it was known. */
42
+ export function raisedAcpConnectCardAgent(
43
+ conversationId: string,
44
+ ): string | undefined {
45
+ return conversationsWithAcpConnectCard.get(conversationId);
46
+ }
47
+
31
48
  /** Whether a Connect Claude card has been raised for this conversation. */
32
49
  export function hasAcpConnectCardRaised(
33
50
  conversationId: string | undefined,
@@ -37,3 +54,27 @@ export function hasAcpConnectCardRaised(
37
54
  conversationsWithAcpConnectCard.has(conversationId)
38
55
  );
39
56
  }
57
+
58
+ /**
59
+ * Hand back every conversation that raised a card and forget them all.
60
+ *
61
+ * Take-and-clear rather than a read plus a separate reset: the caller is
62
+ * retiring these conversations' persisted markers, and an entry left behind
63
+ * after that would redirect the secure-prompt fallback at a card nothing can
64
+ * re-raise.
65
+ */
66
+ export function takeConversationsWithAcpConnectCard(): string[] {
67
+ const conversationIds = [...conversationsWithAcpConnectCard.keys()];
68
+ conversationsWithAcpConnectCard.clear();
69
+ return conversationIds;
70
+ }
71
+
72
+ /** Forget one conversation's raised card. */
73
+ export function dropAcpConnectCardRaised(conversationId: string): void {
74
+ conversationsWithAcpConnectCard.delete(conversationId);
75
+ }
76
+
77
+ /** Every conversation currently believed to have a card up. */
78
+ export function conversationsWithRaisedAcpConnectCard(): string[] {
79
+ return [...conversationsWithAcpConnectCard.keys()];
80
+ }
@@ -92,6 +92,7 @@ export class AcpAgentProcess {
92
92
  cwd,
93
93
  stdio: ["pipe", "pipe", "pipe"],
94
94
  env: this.spawnedEnv,
95
+ windowsHide: true,
95
96
  });
96
97
 
97
98
  const stream = acp.ndJsonStream(
@@ -86,6 +86,7 @@ function execFileWithTimeout(
86
86
  encoding: "utf8",
87
87
  cwd: options?.cwd,
88
88
  env: options?.env,
89
+ windowsHide: true,
89
90
  },
90
91
  (err, stdout) => {
91
92
  clearTimeout(timer);
@@ -380,6 +380,7 @@ export class VellumAcpClientHandler implements Client {
380
380
  cwd: params.cwd ?? undefined,
381
381
  stdio: ["ignore", "pipe", "pipe"],
382
382
  env,
383
+ windowsHide: true,
383
384
  });
384
385
 
385
386
  const state: TerminalState = {
@@ -31,11 +31,16 @@ import {
31
31
  } from "../tools/credentials/metadata-store.js";
32
32
  import { serverUseDenialReason } from "../tools/credentials/tool-policy.js";
33
33
  import { getLogger } from "../util/logger.js";
34
+ import {
35
+ claudeTokenDigest,
36
+ claudeTokenRefusedByClaude,
37
+ } from "./acp-auth-marker-store.js";
34
38
  import {
35
39
  ACP_OAUTH_TOKEN_FIELD,
36
40
  ACP_SERVICE,
37
41
  classifyAnthropicToken,
38
42
  } from "./acp-credentials.js";
43
+ import { lookupAcpAgentConfig } from "./resolve-agent.js";
39
44
  import type { AcpAgentConfig } from "./types.js";
40
45
 
41
46
  const log = getLogger("acp:prepare-agent-env");
@@ -240,6 +245,61 @@ async function injectOptionalCredential(
240
245
  }
241
246
  }
242
247
 
248
+ /**
249
+ * Whether a configured `CLAUDE_CODE_OAUTH_TOKEN` should stand down.
250
+ *
251
+ * True once Claude has refused that exact value and the vault offers a
252
+ * different one. Only then: with nothing to fall back to, dropping it would
253
+ * trade a token that fails for no token at all, and the missing-token branch
254
+ * would report the wrong reason.
255
+ *
256
+ * `acp-claude-oauth` is imported at call time because it reaches back into
257
+ * this module for the spawn policy helpers, and it is the module authorised to
258
+ * read this vault field, so the dependency runs one way at load and the other
259
+ * here.
260
+ */
261
+ async function configuredClaudeTokenStandsDown(
262
+ configured: string | undefined,
263
+ ): Promise<boolean> {
264
+ if (!configured || !claudeTokenRefusedByClaude(configured)) {
265
+ return false;
266
+ }
267
+ const { storedClaudeTokenDigest } = await import("./acp-claude-oauth.js");
268
+ const stored = await storedClaudeTokenDigest();
269
+ return stored !== undefined && stored !== claudeTokenDigest(configured);
270
+ }
271
+
272
+ /**
273
+ * Digest of the Claude token a spawn of `agentId` would resolve now, or
274
+ * `undefined` when it would find none.
275
+ *
276
+ * The credential a marker is judged against, and the same precedence
277
+ * `prepareAgentEnv` applies rather than a second derivation of it. A
278
+ * vault-only answer is wrong in both directions: a user who repairs auth by
279
+ * setting `acp.agents.<id>.env.CLAUDE_CODE_OAUTH_TOKEN` would keep seeing a
280
+ * card for a failure the next spawn will not repeat, and one whose configured
281
+ * token is the broken one would see none.
282
+ *
283
+ * Deliberately does not check that the agent's binary exists. A marker
284
+ * outlives the run that wrote it, and an uninstalled adapter says nothing
285
+ * about whether its credential was replaced.
286
+ */
287
+ export async function resolvedClaudeCredentialDigest(
288
+ agentId: string,
289
+ ): Promise<string | undefined> {
290
+ const configured =
291
+ lookupAcpAgentConfig(agentId)?.env?.CLAUDE_CODE_OAUTH_TOKEN;
292
+ const usable =
293
+ configured && classifyAnthropicToken(configured) !== "api_key"
294
+ ? configured
295
+ : undefined;
296
+ if (usable && !(await configuredClaudeTokenStandsDown(usable))) {
297
+ return claudeTokenDigest(usable);
298
+ }
299
+ const { storedClaudeTokenDigest } = await import("./acp-claude-oauth.js");
300
+ return storedClaudeTokenDigest();
301
+ }
302
+
243
303
  /**
244
304
  * Returns a NEW config with any required credentials merged into `env`.
245
305
  * Does NOT mutate the input. Throws `FailedDependencyError` if a required
@@ -281,6 +341,7 @@ export async function prepareAgentEnv(
281
341
  // limitations on the optional `AcpAgentConfig.env` field.
282
342
  const env: Record<string, string> = { ...(agentConfig.env ?? {}) };
283
343
  const adapterCommand = basename(agentConfig.command);
344
+ let credentialDigest: string | undefined;
284
345
 
285
346
  if (adapterCommand === "claude-agent-acp") {
286
347
  // A config `env` override or a legacy vault entry can hold an Anthropic API
@@ -302,6 +363,12 @@ export async function prepareAgentEnv(
302
363
  };
303
364
 
304
365
  dropApiKeyOauthToken();
366
+ // A configured token Claude has already refused stands down in favour of
367
+ // the vault. Config otherwise wins, so honouring it here would resolve the
368
+ // same revoked value and raise the card again on every retry.
369
+ if (await configuredClaudeTokenStandsDown(env.CLAUDE_CODE_OAUTH_TOKEN)) {
370
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
371
+ }
305
372
  let missReason: string | undefined;
306
373
  if (!env.CLAUDE_CODE_OAUTH_TOKEN) {
307
374
  missReason = await injectCredential(
@@ -318,6 +385,13 @@ export async function prepareAgentEnv(
318
385
  env.CLAUDE_CODE_OAUTH_TOKEN !== undefined &&
319
386
  classifyAnthropicToken(env.CLAUDE_CODE_OAUTH_TOKEN) === "api_key";
320
387
  dropApiKeyOauthToken();
388
+ // Identity of the token this spawn will actually run with, whichever
389
+ // source survived the resolution above. Recorded on the history row if
390
+ // Claude refuses it, which is what lets the marker be compared against the
391
+ // credential a later spawn would resolve instead of relying on a sweep.
392
+ credentialDigest = env.CLAUDE_CODE_OAUTH_TOKEN
393
+ ? claudeTokenDigest(env.CLAUDE_CODE_OAUTH_TOKEN)
394
+ : undefined;
321
395
  if (!env.CLAUDE_CODE_OAUTH_TOKEN) {
322
396
  // The operator's record of WHY the spawn has no token. `missReason` is
323
397
  // the broker's own reason string and the rest are policy verdicts, so no
@@ -388,5 +462,9 @@ export async function prepareAgentEnv(
388
462
  ]);
389
463
  }
390
464
 
391
- return { ...agentConfig, env };
465
+ return {
466
+ ...agentConfig,
467
+ env,
468
+ credentialDigest,
469
+ };
392
470
  }
@@ -153,6 +153,18 @@ function normalizeAgentId(id: string): string {
153
153
  * id. The alias is consulted ONLY after both direct lookups miss, so a user
154
154
  * config entry literally keyed "claude code" always wins over the alias.
155
155
  */
156
+ /**
157
+ * The configured agent for an id, without checking that its binary exists.
158
+ *
159
+ * `resolveAcpAgent` answers "can this be run", which a caller that only wants
160
+ * to know which credential the agent would use does not need and cannot always
161
+ * get: a marker outlives the run that wrote it, and the binary may since have
162
+ * gone.
163
+ */
164
+ export function lookupAcpAgentConfig(id: string): AcpAgentConfig | undefined {
165
+ return lookupAgent(getConfig().acp.agents, id)?.agent;
166
+ }
167
+
156
168
  function lookupAgent(
157
169
  userAgents: Record<string, AcpAgentConfig>,
158
170
  id: string,
@@ -5,7 +5,39 @@ import {
5
5
  deleteConversation,
6
6
  setConversation,
7
7
  } from "../daemon/conversation-registry.js";
8
+ import { claudeTokenDigest } from "./acp-auth-marker-store.js";
8
9
  import { hasAcpConnectCardRaised } from "./acp-connect-card-state.js";
10
+
11
+ // The credential a spawn of this agent would resolve now. The failure path
12
+ // asks the same question the read path does before emitting a live event, so
13
+ // a card is not raised into connected clients for auth that was already
14
+ // repaired. Spreading the real module keeps every other export intact.
15
+ // Spy on the refusal record. Spread the real module so every other export
16
+ // still resolves for the graph session-manager pulls in.
17
+ const refusedDigests: Array<string | undefined> = [];
18
+ const realMarkerStoreModule = await import("./acp-auth-marker-store.js");
19
+ mock.module("./acp-auth-marker-store.js", () => ({
20
+ ...realMarkerStoreModule,
21
+ noteClaudeTokenRefused: (digest: string | undefined) => {
22
+ refusedDigests.push(digest);
23
+ },
24
+ }));
25
+
26
+ let fakeResolvedCredential: string | undefined;
27
+ // Held open by a test that needs to land a teardown inside the window this
28
+ // read suspends the failure handler for.
29
+ let resolveGate: Promise<void> | undefined;
30
+ const realPrepareAgentEnv = await import("./prepare-agent-env.js");
31
+ mock.module("./prepare-agent-env.js", () => ({
32
+ ...realPrepareAgentEnv,
33
+ resolvedClaudeCredentialDigest: async () => {
34
+ if (resolveGate) {
35
+ await resolveGate;
36
+ }
37
+ return fakeResolvedCredential;
38
+ },
39
+ }));
40
+
9
41
  import { VellumAcpClientHandler } from "./client-handler.js";
10
42
  import { AcpSessionManager } from "./session-manager.js";
11
43
 
@@ -331,6 +363,7 @@ describe("AcpSessionManager auth-required recovery surface", () => {
331
363
  command: string;
332
364
  parentToolUseId?: string;
333
365
  cancelled?: boolean;
366
+ credentialDigest?: string;
334
367
  }) {
335
368
  const manager = new AcpSessionManager(1);
336
369
  const parentId = `parent-${opts.id}`;
@@ -350,6 +383,10 @@ describe("AcpSessionManager auth-required recovery surface", () => {
350
383
  if (opts.cancelled) {
351
384
  entry.state.status = "cancelled";
352
385
  }
386
+ if (opts.credentialDigest !== undefined) {
387
+ (entry as { credentialDigest?: string }).credentialDigest =
388
+ opts.credentialDigest;
389
+ }
353
390
 
354
391
  await fire(manager, opts.id, entry).catch(() => {});
355
392
  if (!opts.cancelled) {
@@ -366,9 +403,65 @@ describe("AcpSessionManager auth-required recovery surface", () => {
366
403
  parentId,
367
404
  authEvent: events.find((e) => e.type === "acp_auth_required"),
368
405
  persistedContent: firstPersist?.[0].content,
406
+ persistedAuthErrorCredential: (
407
+ entry.state as { authErrorCredential?: string }
408
+ ).authErrorCredential,
369
409
  };
370
410
  }
371
411
 
412
+ test("no live event when the credential was already replaced", async () => {
413
+ // A replacement token and its invalidation can both land before an older
414
+ // run reports its rejection. The card the live event raises deliberately
415
+ // skips connected-state self-healing, so nothing would retire it until the
416
+ // next snapshot, which may be a navigation away.
417
+ fakeResolvedCredential = claudeTokenDigest("sk-ant-oat-replacement");
418
+ const r = await driveAuthFailure({
419
+ id: "sess-auth-replaced",
420
+ command: "claude-agent-acp",
421
+ parentToolUseId: "tool-anchor-replaced",
422
+ credentialDigest: claudeTokenDigest("sk-ant-oat-refused"),
423
+ });
424
+ fakeResolvedCredential = undefined;
425
+
426
+ expect(r.authEvent).toBeUndefined();
427
+ expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
428
+ // The guidance follows the same predicate: pointing the model at a card
429
+ // that was never raised sends it to an affordance that does not exist.
430
+ expect(r.persistedContent).not.toContain("Connect Claude Code");
431
+ });
432
+
433
+ test("still raises when the run holds the credential a spawn would resolve", async () => {
434
+ const digest = claudeTokenDigest("sk-ant-oat-still-current");
435
+ fakeResolvedCredential = digest;
436
+ const r = await driveAuthFailure({
437
+ id: "sess-auth-current",
438
+ command: "claude-agent-acp",
439
+ parentToolUseId: "tool-anchor-current",
440
+ credentialDigest: digest,
441
+ });
442
+ fakeResolvedCredential = undefined;
443
+
444
+ expect(r.authEvent).toBeDefined();
445
+ });
446
+
447
+ test("the marker names the credential the run was refused on", async () => {
448
+ // The failure path does not judge whether the rejection still matters. It
449
+ // records what was refused, and the read path compares that against the
450
+ // credential a spawn would resolve. Deciding here would mean guessing
451
+ // about writes that have not finished, and guessing toward suppression
452
+ // spends the rejection that would have raised the card.
453
+ const digest = claudeTokenDigest("sk-ant-oat-refused");
454
+ const r = await driveAuthFailure({
455
+ id: "sess-auth-credential",
456
+ command: "claude-agent-acp",
457
+ parentToolUseId: "tool-anchor-credential",
458
+ credentialDigest: digest,
459
+ });
460
+
461
+ expect(r.authEvent).toBeDefined();
462
+ expect(r.persistedAuthErrorCredential).toBe(digest);
463
+ });
464
+
372
465
  test("claude failure with an anchor raises the full surface: event, registry mark, guidance", async () => {
373
466
  const r = await driveAuthFailure({
374
467
  id: "sess-auth-anchor",
@@ -429,3 +522,93 @@ describe("AcpSessionManager auth-required recovery surface", () => {
429
522
  expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
430
523
  });
431
524
  });
525
+
526
+ describe("AcpSessionManager: teardown during the credential read", () => {
527
+ test("a session torn down mid-read is left alone", async () => {
528
+ // The credential read suspends the failure handler. A cancel landing in
529
+ // that window runs to completion first: it persists the cancelled row,
530
+ // drops the event buffer and removes the session. Carrying on would emit a
531
+ // card for a run the user stopped, and persist a second time over a buffer
532
+ // that is already gone, replacing the stored event log with an empty one.
533
+ const revoked = () =>
534
+ Promise.reject(
535
+ new Error(
536
+ "Internal error: Failed to authenticate. API Error: 401 OAuth access token has been revoked.",
537
+ ),
538
+ );
539
+ const manager = new AcpSessionManager(1);
540
+ const parentId = "parent-torn-down";
541
+ const { conversation } = mockConversation();
542
+ setConversation(parentId, conversation);
543
+ registered.push(parentId);
544
+
545
+ const entry = injectSession(
546
+ manager,
547
+ "sess-torn-down",
548
+ parentId,
549
+ fakeProcess(revoked),
550
+ );
551
+ entry.command = "claude-agent-acp";
552
+ (entry as { parentToolUseId?: string }).parentToolUseId = "tool-torn";
553
+
554
+ let openGate = () => {};
555
+ resolveGate = new Promise<void>((resolve) => {
556
+ openGate = resolve;
557
+ });
558
+
559
+ const running = fire(manager, "sess-torn-down", entry).catch(() => {});
560
+ // Let the handler reach the suspended read, then take the session out from
561
+ // under it exactly as a cancel would.
562
+ await new Promise((r) => setTimeout(r, 5));
563
+ (manager as unknown as { sessions: Map<string, unknown> }).sessions.delete(
564
+ "sess-torn-down",
565
+ );
566
+ openGate();
567
+ await running;
568
+ resolveGate = undefined;
569
+
570
+ const events = (
571
+ entry.sendToVellum as ReturnType<typeof mock>
572
+ ).mock.calls.map((c) => c[0] as { type: string });
573
+ expect(events.find((e) => e.type === "acp_auth_required")).toBeUndefined();
574
+ });
575
+ });
576
+
577
+ describe("AcpSessionManager.spawn: a credential Claude refused at startup", () => {
578
+ test("records the refusal, so the HTTP route gets it too", async () => {
579
+ // The record lives here rather than at the tool boundary because
580
+ // `POST /v1/acp/spawn` reaches this method directly. Without it a
581
+ // configured CLAUDE_CODE_OAUTH_TOKEN that Claude refused keeps winning
582
+ // over whatever Connect stores, and every later spawn repeats the failure.
583
+ const { AcpAgentProcess: Process } = await import("./agent-process.js");
584
+ const { AcpAuthRequiredError: AuthError } =
585
+ await import("./auth-required.js");
586
+ const originalInitialize = Process.prototype.initialize;
587
+ Process.prototype.initialize = async () => {
588
+ throw new AuthError("claude", "Authentication required");
589
+ };
590
+ refusedDigests.length = 0;
591
+
592
+ try {
593
+ const manager = new AcpSessionManager(5);
594
+ await manager
595
+ .spawn(
596
+ "claude",
597
+ {
598
+ command: "echo",
599
+ args: ["hi"],
600
+ credentialDigest: "digest-refused-at-startup",
601
+ },
602
+ "do something",
603
+ "/tmp",
604
+ "conv-startup-auth",
605
+ () => {},
606
+ )
607
+ .catch(() => {});
608
+ } finally {
609
+ Process.prototype.initialize = originalInitialize;
610
+ }
611
+
612
+ expect(refusedDigests).toContain("digest-refused-at-startup");
613
+ });
614
+ });