@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
@@ -28,7 +28,13 @@ const setSecureKeyAsync = mock(
28
28
  );
29
29
  const getSecureKeyAsync = mock(async (_account: string) => getReturn);
30
30
 
31
+ // Spread the real module rather than listing two exports. These cases reach
32
+ // the marker tables, which pulls persistence into the graph, and anything in
33
+ // there that imports another secure-keys export would fail to resolve against
34
+ // a partial factory.
35
+ const realSecureKeys = await import("../../security/secure-keys.js");
31
36
  mock.module("../../security/secure-keys.js", () => ({
37
+ ...realSecureKeys,
32
38
  setSecureKeyAsync,
33
39
  getSecureKeyAsync,
34
40
  }));
@@ -39,7 +45,24 @@ const { _setMetadataPath, getCredentialMetadata, upsertCredentialMetadata } =
39
45
  const { acpSpawnCredentialDenialReason } =
40
46
  await import("../prepare-agent-env.js");
41
47
 
48
+ const { hasAcpConnectCardRaised, markAcpConnectCardRaised } =
49
+ await import("../acp-connect-card-state.js");
50
+ const { installAcpConfigStub } = await import("./helpers/acp-config-stub.js");
51
+ const acpConfig = await installAcpConfigStub();
52
+
53
+ // The registry is retired per conversation by asking whether that conversation
54
+ // still has a marker worth showing, so these cases need a real (empty)
55
+ // database rather than an error path standing in for one.
56
+ const { initializeDb } = await import("../../persistence/db-init.js");
57
+ await initializeDb();
58
+
59
+ const { claudeTokenDigest, noteClaudeTokenRefused } =
60
+ await import("../acp-auth-marker-store.js");
61
+ const { clearHistory, insertHistoryRow } =
62
+ await import("./helpers/acp-history-db.js");
63
+
42
64
  const {
65
+ acpConnectCardStillWarranted,
43
66
  CLAUDE_OAUTH_CONFIG,
44
67
  CLAUDE_MANUAL_REDIRECT_URI,
45
68
  buildClaudeAuthorizeUrl,
@@ -48,6 +71,13 @@ const {
48
71
  hasAcpClaudeToken,
49
72
  } = await import("../acp-claude-oauth.js");
50
73
 
74
+ /**
75
+ * The card notification is detached from the store so it cannot delay a
76
+ * sign-in, so a test asserting on its effects has to let it run.
77
+ */
78
+ const settleNotification = () =>
79
+ new Promise((resolve) => setTimeout(resolve, 10));
80
+
51
81
  const ACP_SERVICE = "acp";
52
82
  const OAUTH_FIELD = "claude_oauth_token";
53
83
  const ACP_SPAWN_TOOL = "acp_spawn";
@@ -280,3 +310,235 @@ describe("hasAcpClaudeToken", () => {
280
310
  expect(oauthMetadata()).toBeUndefined();
281
311
  });
282
312
  });
313
+
314
+ describe("storeAcpClaudeToken: retiring the card registry", () => {
315
+ test("clears the registry after the policy repair, not before it", () => {
316
+ // The write happens first and the policy repair second, so at the moment
317
+ // the credential seam asks, a token whose `acp_spawn` read is denied is
318
+ // not usable and nothing is retired. The repair on the next line is what
319
+ // makes it usable, so the notification has to run again after it.
320
+ upsertCredentialMetadata(ACP_SERVICE, OAUTH_FIELD, {
321
+ allowedTools: ["some_other_tool"],
322
+ allowedDomains: [],
323
+ });
324
+ expect(acpSpawnCredentialDenialReason(OAUTH_FIELD)).toBeDefined();
325
+ markAcpConnectCardRaised("conv-with-card");
326
+ getReturn = "sk-ant-oat-token";
327
+
328
+ return storeAcpClaudeToken("sk-ant-oat-token")
329
+ .then(settleNotification)
330
+ .then(() => {
331
+ expect(acpSpawnCredentialDenialReason(OAUTH_FIELD)).toBeUndefined();
332
+ expect(hasAcpConnectCardRaised("conv-with-card")).toBe(false);
333
+ });
334
+ });
335
+
336
+ test("leaves the registry alone when the stored value is unusable", () => {
337
+ // A bulk restore can land an api-key-shaped value. The marker comparison
338
+ // keeps the card up for that reason, so forgetting the registry would open
339
+ // the second prompt it exists to suppress.
340
+ markAcpConnectCardRaised("conv-keeps-card");
341
+ getReturn = "sk-ant-api-key-shaped";
342
+
343
+ return storeAcpClaudeToken("sk-ant-api-key-shaped")
344
+ .then(settleNotification)
345
+ .then(() => {
346
+ expect(hasAcpConnectCardRaised("conv-keeps-card")).toBe(true);
347
+ });
348
+ });
349
+ });
350
+
351
+ describe("storeAcpClaudeToken: a re-written rejected token retires nothing", () => {
352
+ test("keeps the registry entry while the marker still stands", () => {
353
+ // A bulk restore can store the very token Claude rejected. It passes the
354
+ // shape and policy checks, so usability says yes, but nothing about the
355
+ // failure changed: the snapshot goes on serving that marker and the card
356
+ // stays up. Forgetting the entry would open a second prompt beside it.
357
+ clearHistory();
358
+ const token = "sk-ant-oat-still-rejected";
359
+ getReturn = token;
360
+ insertHistoryRow({
361
+ id: "run-still-rejected",
362
+ parentConversationId: "conv-still-broken",
363
+ status: "failed",
364
+ authErrorCode: "acp_claude_auth_required",
365
+ authErrorCredential: claudeTokenDigest(token),
366
+ });
367
+ markAcpConnectCardRaised("conv-still-broken");
368
+
369
+ return storeAcpClaudeToken(token)
370
+ .then(settleNotification)
371
+ .then(() => {
372
+ expect(hasAcpConnectCardRaised("conv-still-broken")).toBe(true);
373
+ });
374
+ });
375
+
376
+ test("drops it once a different token makes the marker stale", () => {
377
+ clearHistory();
378
+ getReturn = "sk-ant-oat-replacement";
379
+ insertHistoryRow({
380
+ id: "run-repaired",
381
+ parentConversationId: "conv-repaired",
382
+ status: "failed",
383
+ authErrorCode: "acp_claude_auth_required",
384
+ authErrorCredential: claudeTokenDigest("sk-ant-oat-old-rejected"),
385
+ });
386
+ markAcpConnectCardRaised("conv-repaired");
387
+
388
+ return storeAcpClaudeToken("sk-ant-oat-replacement")
389
+ .then(settleNotification)
390
+ .then(() => {
391
+ expect(hasAcpConnectCardRaised("conv-repaired")).toBe(false);
392
+ });
393
+ });
394
+ });
395
+
396
+ describe("acpConnectCardStillWarranted", () => {
397
+ test("a mid-run card stands while its marker names the credential in use", () => {
398
+ clearHistory();
399
+ const token = "sk-ant-oat-in-use";
400
+ getReturn = token;
401
+ insertHistoryRow({
402
+ id: "run-warranted",
403
+ parentConversationId: "conv-warranted",
404
+ status: "failed",
405
+ authErrorCode: "acp_claude_auth_required",
406
+ authErrorCredential: claudeTokenDigest(token),
407
+ });
408
+
409
+ return acpConnectCardStillWarranted("conv-warranted").then((warranted) => {
410
+ expect(warranted).toBe(true);
411
+ });
412
+ });
413
+
414
+ test("a mid-run card falls once a different token is stored", () => {
415
+ // The repair need not be a credential write: editing the agent's
416
+ // configured token moves this answer too, which is why it is asked rather
417
+ // than remembered.
418
+ clearHistory();
419
+ getReturn = "sk-ant-oat-replacement";
420
+ insertHistoryRow({
421
+ id: "run-repaired",
422
+ parentConversationId: "conv-repaired-warrant",
423
+ status: "failed",
424
+ authErrorCode: "acp_claude_auth_required",
425
+ authErrorCredential: claudeTokenDigest("sk-ant-oat-old"),
426
+ });
427
+
428
+ return acpConnectCardStillWarranted("conv-repaired-warrant").then(
429
+ (warranted) => {
430
+ expect(warranted).toBe(false);
431
+ },
432
+ );
433
+ });
434
+
435
+ test("a pre-spawn card stands while there is no usable token", () => {
436
+ // The missing-token path has no session to record a marker on, so the
437
+ // absence of one says nothing; what keeps its card meaningful is that a
438
+ // spawn still has nothing to authenticate with.
439
+ clearHistory();
440
+ getReturn = undefined;
441
+
442
+ return acpConnectCardStillWarranted("conv-no-token").then((warranted) => {
443
+ expect(warranted).toBe(true);
444
+ });
445
+ });
446
+
447
+ test("a pre-spawn card falls once a usable token exists", () => {
448
+ clearHistory();
449
+ getReturn = "sk-ant-oat-now-present";
450
+
451
+ return acpConnectCardStillWarranted("conv-no-marker").then((warranted) => {
452
+ expect(warranted).toBe(false);
453
+ });
454
+ });
455
+ });
456
+
457
+ describe("acpConnectCardStillWarranted: a pre-spawn card and configured tokens", () => {
458
+ test("falls once the agent's configured token supplies a credential", () => {
459
+ // Repairing by setting `acp.agents.<id>.env.CLAUDE_CODE_OAUTH_TOKEN` never
460
+ // touches the vault, so asking the vault would call this card warranted
461
+ // forever even though the next spawn authenticates fine.
462
+ clearHistory();
463
+ getReturn = undefined;
464
+ acpConfig.setConfig({
465
+ agents: {
466
+ claude: {
467
+ command: "claude-agent-acp",
468
+ args: [],
469
+ env: { CLAUDE_CODE_OAUTH_TOKEN: "sk-ant-oat-from-config" },
470
+ },
471
+ },
472
+ });
473
+ markAcpConnectCardRaised("conv-config-repair", "claude");
474
+
475
+ return acpConnectCardStillWarranted("conv-config-repair").then(
476
+ (warranted) => {
477
+ expect(warranted).toBe(false);
478
+ },
479
+ );
480
+ });
481
+
482
+ test("stands while neither config nor the vault offers one", () => {
483
+ clearHistory();
484
+ getReturn = undefined;
485
+ acpConfig.setConfig({ agents: {} });
486
+ markAcpConnectCardRaised("conv-still-nothing", "claude");
487
+
488
+ return acpConnectCardStillWarranted("conv-still-nothing").then(
489
+ (warranted) => {
490
+ expect(warranted).toBe(true);
491
+ },
492
+ );
493
+ });
494
+ });
495
+
496
+ describe("acpConnectCardStillWarranted: a resolved credential Claude already refused", () => {
497
+ test("stands while the only credential a spawn would pick is the refused one", () => {
498
+ // Standing down needs a replacement to stand down *to*. With none stored,
499
+ // the resolver still reports the refused configured token, and the next
500
+ // spawn rejects it exactly as this one did. Reading that as repaired drops
501
+ // the card and lets a second prompt open beside it.
502
+ clearHistory();
503
+ const token = "sk-ant-oat-config-refused";
504
+ getReturn = undefined;
505
+ acpConfig.setConfig({
506
+ agents: {
507
+ claude: {
508
+ command: "claude-agent-acp",
509
+ args: [],
510
+ env: { CLAUDE_CODE_OAUTH_TOKEN: token },
511
+ },
512
+ },
513
+ });
514
+ noteClaudeTokenRefused(claudeTokenDigest(token), 1000);
515
+ markAcpConnectCardRaised("conv-refused-config", "claude");
516
+
517
+ return acpConnectCardStillWarranted("conv-refused-config").then(
518
+ (warranted) => {
519
+ expect(warranted).toBe(true);
520
+ },
521
+ );
522
+ });
523
+
524
+ test("falls once the resolved credential is one Claude has not refused", () => {
525
+ clearHistory();
526
+ getReturn = undefined;
527
+ acpConfig.setConfig({
528
+ agents: {
529
+ claude: {
530
+ command: "claude-agent-acp",
531
+ args: [],
532
+ env: { CLAUDE_CODE_OAUTH_TOKEN: "sk-ant-oat-config-fresh" },
533
+ },
534
+ },
535
+ });
536
+ markAcpConnectCardRaised("conv-fresh-config", "claude");
537
+
538
+ return acpConnectCardStillWarranted("conv-fresh-config").then(
539
+ (warranted) => {
540
+ expect(warranted).toBe(false);
541
+ },
542
+ );
543
+ });
544
+ });
@@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test";
3
3
  import {
4
4
  hasAcpConnectCardRaised,
5
5
  markAcpConnectCardRaised,
6
+ takeConversationsWithAcpConnectCard,
6
7
  } from "../acp-connect-card-state.js";
7
8
 
8
9
  describe("acp-connect-card-state", () => {
@@ -20,4 +21,20 @@ describe("acp-connect-card-state", () => {
20
21
  markAcpConnectCardRaised("");
21
22
  expect(hasAcpConnectCardRaised("")).toBe(false);
22
23
  });
24
+
25
+ test("take hands back every raised conversation and forgets them", () => {
26
+ // A new token retires these conversations' persisted markers, so an entry
27
+ // left behind would keep redirecting the fallback at a card nothing can
28
+ // re-raise.
29
+ markAcpConnectCardRaised("conv-take-a");
30
+ markAcpConnectCardRaised("conv-take-b");
31
+
32
+ const taken = takeConversationsWithAcpConnectCard();
33
+
34
+ expect(taken).toContain("conv-take-a");
35
+ expect(taken).toContain("conv-take-b");
36
+ expect(hasAcpConnectCardRaised("conv-take-a")).toBe(false);
37
+ expect(hasAcpConnectCardRaised("conv-take-b")).toBe(false);
38
+ expect(takeConversationsWithAcpConnectCard()).toEqual([]);
39
+ });
23
40
  });
@@ -30,6 +30,8 @@ export interface HistoryRow {
30
30
  cost_currency: string | null;
31
31
  input_tokens: number | null;
32
32
  output_tokens: number | null;
33
+ auth_error_code: string | null;
34
+ auth_error_credential: string | null;
33
35
  }
34
36
 
35
37
  export function clearHistory(): void {
@@ -62,6 +64,8 @@ export function insertHistoryRow(row: {
62
64
  costCurrency?: string | null;
63
65
  inputTokens?: number | null;
64
66
  outputTokens?: number | null;
67
+ authErrorCode?: string | null;
68
+ authErrorCredential?: string | null;
65
69
  }): void {
66
70
  getSqlite()
67
71
  .query(
@@ -70,8 +74,8 @@ export function insertHistoryRow(row: {
70
74
  started_at, completed_at, status, stop_reason, error,
71
75
  event_log_json, cwd, task, parent_tool_use_id,
72
76
  used_tokens, context_size, cost_amount, cost_currency,
73
- input_tokens, output_tokens
74
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
77
+ input_tokens, output_tokens, auth_error_code, auth_error_credential
78
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
75
79
  )
76
80
  .run(
77
81
  row.id,
@@ -93,6 +97,8 @@ export function insertHistoryRow(row: {
93
97
  row.costCurrency ?? null,
94
98
  row.inputTokens ?? null,
95
99
  row.outputTokens ?? null,
100
+ row.authErrorCode ?? null,
101
+ row.authErrorCredential ?? null,
96
102
  );
97
103
  }
98
104
 
@@ -103,7 +109,8 @@ export function readHistoryRow(id: string): HistoryRow | null {
103
109
  started_at, completed_at, status, stop_reason, error,
104
110
  event_log_json, cwd, task, parent_tool_use_id,
105
111
  used_tokens, context_size, cost_amount, cost_currency,
106
- input_tokens, output_tokens
112
+ input_tokens, output_tokens, auth_error_code,
113
+ auth_error_credential
107
114
  FROM acp_session_history WHERE id = ?`,
108
115
  )
109
116
  .get(id) as HistoryRow | null;
@@ -0,0 +1,196 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { and, desc, eq, isNotNull } from "drizzle-orm";
4
+
5
+ import { getDb } from "../persistence/db-connection.js";
6
+ import {
7
+ acpRefusedCredentials,
8
+ acpSessionHistory,
9
+ } from "../persistence/schema/index.js";
10
+ import { getLogger } from "../util/logger.js";
11
+
12
+ const log = getLogger("acp-auth-marker-store");
13
+
14
+ /**
15
+ * How many of a conversation's newest markers are examined before giving up on
16
+ * finding a current one.
17
+ *
18
+ * A conversation keeps every marker it has ever had, so this bounds the read
19
+ * to a fixed cost. Beyond the newest few the answer is nearly always the same,
20
+ * and erring toward "no current marker" only forgets bookkeeping that the next
21
+ * failure recreates.
22
+ */
23
+ export const MARKER_SCAN_LIMIT = 20;
24
+
25
+ /** Digest a Claude token for identity comparison. Never stores the token. */
26
+ export function claudeTokenDigest(token: string): string {
27
+ return createHash("sha256").update(token).digest("hex").slice(0, 32);
28
+ }
29
+
30
+ /**
31
+ * Whether a credential-failure marker is still worth showing a card for.
32
+ *
33
+ * The marker names the credential the run was refused on. It stops being worth
34
+ * showing the moment a spawn would resolve a different one, because that is
35
+ * the user having repaired the auth the card exists to send them to.
36
+ *
37
+ * Answered by comparing, not by remembering. Nothing has to run at the right
38
+ * moment for a repaired marker to stop rendering, which is the difference
39
+ * between this and a retirement sweep: a sweep has to be ordered against every
40
+ * in-flight write and in-flight read, and a daemon restart loses whatever it
41
+ * was tracking. A comparison has no ordering to get wrong and no lifetime.
42
+ *
43
+ * Fails toward showing the card. A marker with no credential named predates
44
+ * this column or was written by a path that had none, and an unknown
45
+ * credential is no evidence the failure was repaired.
46
+ */
47
+ export function acpAuthMarkerStillCurrent(
48
+ markerCredential: string | null | undefined,
49
+ resolvedCredential: string | undefined,
50
+ ): boolean {
51
+ if (markerCredential == null || resolvedCredential === undefined) {
52
+ return true;
53
+ }
54
+ return markerCredential === resolvedCredential;
55
+ }
56
+
57
+ /**
58
+ * Record that Claude refused this token, so no later spawn resolves it again.
59
+ *
60
+ * Kept in its own table rather than read back off the marker rows. A marker is
61
+ * about showing a card for one run and the user may delete it with that run,
62
+ * while this decides which credential a spawn selects, and clearing session
63
+ * history must not change that. A configured
64
+ * `acp.agents.<id>.env.CLAUDE_CODE_OAUTH_TOKEN` lives in config, so forgetting
65
+ * a refusal lets the revoked value win over the vault replacement again.
66
+ *
67
+ * Never throws: a run has already failed by the time this is called, and
68
+ * losing the record costs one more failed spawn rather than the failure
69
+ * handling itself.
70
+ */
71
+ export function noteClaudeTokenRefused(
72
+ digest: string | undefined,
73
+ refusedAt: number,
74
+ ): void {
75
+ if (digest === undefined) {
76
+ return;
77
+ }
78
+ try {
79
+ getDb()
80
+ .insert(acpRefusedCredentials)
81
+ .values({ digest, refusedAt })
82
+ .onConflictDoNothing()
83
+ .run();
84
+ } catch (err) {
85
+ log.error(
86
+ { err },
87
+ "recording a refused Claude credential failed; it may be resolved again",
88
+ );
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Whether Claude has refused this token before.
94
+ *
95
+ * Never throws: this only narrows which token a spawn prefers, and a read
96
+ * failure must not take the spawn with it. Failing open resolves the
97
+ * configured token, which is what would have happened before it was ever
98
+ * refused.
99
+ */
100
+ export function claudeTokenRefusedByClaude(token: string): boolean {
101
+ return claudeCredentialRefused(claudeTokenDigest(token));
102
+ }
103
+
104
+ /**
105
+ * Whether Claude has refused the credential with this digest.
106
+ *
107
+ * The same question as {@link claudeTokenRefusedByClaude}, for callers that
108
+ * hold an identity rather than the token itself: a resolver reports which
109
+ * credential a spawn would pick, and "would pick it" is not the same as
110
+ * "it works".
111
+ */
112
+ export function claudeCredentialRefused(digest: string): boolean {
113
+ try {
114
+ const row = getDb()
115
+ .select({ digest: acpRefusedCredentials.digest })
116
+ .from(acpRefusedCredentials)
117
+ .where(eq(acpRefusedCredentials.digest, digest))
118
+ .limit(1)
119
+ .get();
120
+ return row !== undefined;
121
+ } catch (err) {
122
+ log.error(
123
+ { err },
124
+ "reading refused Claude credentials failed; treating the token as untried",
125
+ );
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Whether this conversation still has a credential failure worth showing a
132
+ * card for.
133
+ *
134
+ * The same question the session list answers per row, asked for one
135
+ * conversation. Callers that hold their own idea of which conversations have a
136
+ * card up use this to decide whether that idea is still true, rather than
137
+ * guessing from whether a token was written: a writer can store the very token
138
+ * Claude rejected, which changes nothing about the failure and must not be
139
+ * mistaken for repairing it.
140
+ *
141
+ * Never throws, and answers `false` when it cannot tell. The caller uses this
142
+ * to decide whether to forget that a conversation has a card up, and the two
143
+ * mistakes are not equal: forgetting wrongly costs a redundant secure prompt
144
+ * beside a card, while remembering wrongly suppresses that prompt and points
145
+ * the user at a card that may no longer be there.
146
+ */
147
+ export async function conversationHasCurrentAcpMarker(
148
+ conversationId: string,
149
+ ): Promise<boolean> {
150
+ try {
151
+ const rows = getDb()
152
+ .select({
153
+ agentId: acpSessionHistory.agentId,
154
+ credential: acpSessionHistory.authErrorCredential,
155
+ })
156
+ .from(acpSessionHistory)
157
+ .where(
158
+ and(
159
+ eq(acpSessionHistory.parentConversationId, conversationId),
160
+ isNotNull(acpSessionHistory.authErrorCode),
161
+ ),
162
+ )
163
+ .orderBy(desc(acpSessionHistory.startedAt))
164
+ .limit(MARKER_SCAN_LIMIT)
165
+ .all();
166
+ if (rows.length === 0) {
167
+ return false;
168
+ }
169
+ // Imported at call time: `prepare-agent-env` owns the precedence a spawn
170
+ // applies, and reaching it from here at load would put the whole spawn
171
+ // graph behind this module.
172
+ const { resolvedClaudeCredentialDigest } =
173
+ await import("./prepare-agent-env.js");
174
+ const resolved = new Map<string, string | undefined>();
175
+ for (const row of rows) {
176
+ if (!resolved.has(row.agentId)) {
177
+ resolved.set(
178
+ row.agentId,
179
+ await resolvedClaudeCredentialDigest(row.agentId),
180
+ );
181
+ }
182
+ if (
183
+ acpAuthMarkerStillCurrent(row.credential, resolved.get(row.agentId))
184
+ ) {
185
+ return true;
186
+ }
187
+ }
188
+ return false;
189
+ } catch (err) {
190
+ log.error(
191
+ { err },
192
+ "reading ACP auth markers failed; treating the card as no longer raised",
193
+ );
194
+ return false;
195
+ }
196
+ }