@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
@@ -6,12 +6,19 @@
6
6
  */
7
7
  import { randomUUID } from "node:crypto";
8
8
 
9
- import { and, desc, eq, inArray, notInArray } from "drizzle-orm";
9
+ import { and, desc, eq, inArray, isNotNull, notInArray } from "drizzle-orm";
10
10
  import { z } from "zod";
11
11
 
12
+ import {
13
+ acpAuthMarkerStillCurrent,
14
+ MARKER_SCAN_LIMIT,
15
+ } from "../../acp/acp-auth-marker-store.js";
12
16
  import { resolveAgentWithAutoInstall } from "../../acp/auto-install.js";
13
17
  import { getAcpSessionManager } from "../../acp/index.js";
14
- import { prepareAgentEnv } from "../../acp/prepare-agent-env.js";
18
+ import {
19
+ prepareAgentEnv,
20
+ resolvedClaudeCredentialDigest,
21
+ } from "../../acp/prepare-agent-env.js";
15
22
  import { formatResolveFailure } from "../../acp/resolve-agent.js";
16
23
  import {
17
24
  AcpResumeError,
@@ -58,6 +65,9 @@ const sessionEntrySchema = z.object({
58
65
  stopReason: z.string().nullable().optional(),
59
66
  task: z.string().optional(),
60
67
  parentToolUseId: z.string().optional(),
68
+ /** Credential failure that ended the run, when one did. Drives the inline
69
+ * Connect card on reopen; cleared when a replacement token is stored. */
70
+ authErrorCode: z.string().optional(),
61
71
  usedTokens: z.number().optional(),
62
72
  contextSize: z.number().optional(),
63
73
  costAmount: z.number().optional(),
@@ -69,6 +79,24 @@ const sessionEntrySchema = z.object({
69
79
 
70
80
  type SessionEntry = z.infer<typeof sessionEntrySchema>;
71
81
 
82
+ /**
83
+ * A merged session before its marker has been judged.
84
+ *
85
+ * `authErrorCredential` names the credential the failure was refused on. It is
86
+ * what the comparison needs and is dropped before the response goes out: a
87
+ * client has no use for it and no way to resolve the other side of the
88
+ * comparison, so serving it would only widen what leaves the daemon.
89
+ */
90
+ type MergedSession = SessionEntry & { authErrorCredential?: string };
91
+
92
+ /** Drop the comparison's own input before the session goes out on the wire. */
93
+ function stripMarkerCredential({
94
+ authErrorCredential: _dropped,
95
+ ...session
96
+ }: MergedSession): SessionEntry {
97
+ return session;
98
+ }
99
+
72
100
  // ---------------------------------------------------------------------------
73
101
  // Handlers
74
102
  // ---------------------------------------------------------------------------
@@ -424,11 +452,102 @@ function closeSession({ pathParams }: RouteHandlerArgs) {
424
452
  return { acpSessionId: id, closed: true };
425
453
  }
426
454
 
427
- function listSessions({ queryParams }: RouteHandlerArgs) {
455
+ async function listSessions({ queryParams }: RouteHandlerArgs) {
428
456
  const limit = parseLimit(queryParams?.limit);
429
457
  const conversationId = queryParams?.conversationId;
430
- const sessions = listMergedSessions({ limit, conversationId });
431
- return { sessions };
458
+ // Resolved per agent, because precedence is per agent: one alias can carry a
459
+ // configured token while another falls through to the vault. Memoised across
460
+ // the request, since each resolution costs a vault read and a conversation's
461
+ // marked runs are nearly always one agent.
462
+ const resolvedByAgent = new Map<string, string | undefined>();
463
+ const resolvedFor = async (agentId: string) => {
464
+ if (!resolvedByAgent.has(agentId)) {
465
+ resolvedByAgent.set(
466
+ agentId,
467
+ await resolvedClaudeCredentialDigest(agentId),
468
+ );
469
+ }
470
+ return resolvedByAgent.get(agentId);
471
+ };
472
+
473
+ const { sessions: merged, sawEveryHistoryRow } = listMergedSessions({
474
+ limit,
475
+ conversationId,
476
+ });
477
+ // Judged before paging, never after. A stale marker that escaped the page
478
+ // first would stay in the response as an ordinary row once its code was
479
+ // struck, so the retained markers would pile up past the limit.
480
+ const judged = await withCurrentMarkersOnly(merged, resolvedFor);
481
+ const page = judged.slice(0, limit);
482
+ if (!conversationId || page.some((s) => s.authErrorCode !== undefined)) {
483
+ return { sessions: page };
484
+ }
485
+ // Rows the page cut that are already in hand. In-memory sessions merge in on
486
+ // top of the history read, so the merged list can overflow the page even
487
+ // when the query reached the end of the table, and the marker the client
488
+ // needs can be sitting in that overflow. Newest-first, so the first match is
489
+ // the one to surface.
490
+ const overflowMarker = judged
491
+ .slice(limit)
492
+ .find((s) => s.authErrorCode !== undefined);
493
+ if (overflowMarker) {
494
+ return { sessions: [...page, overflowMarker] };
495
+ }
496
+ if (sawEveryHistoryRow) {
497
+ // Every row this conversation has has now been looked at, page and
498
+ // overflow alike, so a marker elsewhere is not a thing that exists. Most
499
+ // conversations have never had a credential failure, and this is what
500
+ // keeps the lookup off their path rather than asking the database to
501
+ // confirm the absence every time.
502
+ return { sessions: page };
503
+ }
504
+ // The page holds no live marker, so the row a client restores the card from
505
+ // is either outside it or absent. One row, fetched only now, rather than
506
+ // every marked row loaded on the chance one of them is needed.
507
+ const marker = await findRecoveryMarker(conversationId, resolvedFor);
508
+ if (!marker || page.some((s) => s.id === marker.id)) {
509
+ return { sessions: page };
510
+ }
511
+ return { sessions: [...page, stripMarkerCredential(marker)] };
512
+ }
513
+
514
+ /**
515
+ * Blank the failure code on any session whose marker no longer describes the
516
+ * credential its agent would resolve.
517
+ *
518
+ * This comparison is what retires a Connect card: not a sweep that has to run
519
+ * at the right moment, but the marker no longer describing the credential in
520
+ * use. Applied after merging rather than inside the query, so live sessions
521
+ * and history rows are judged by exactly the same rule.
522
+ *
523
+ * Resolved per agent, because precedence is per agent: one alias can carry a
524
+ * configured token while another falls through to the vault. Memoised across
525
+ * the request, since a conversation's marked runs are nearly always one agent
526
+ * and each resolution costs a vault read.
527
+ */
528
+ async function withCurrentMarkersOnly(
529
+ sessions: MergedSession[],
530
+ resolvedFor: (agentId: string) => Promise<string | undefined>,
531
+ ): Promise<SessionEntry[]> {
532
+ const strip = stripMarkerCredential;
533
+ if (!sessions.some((s) => s.authErrorCode !== undefined)) {
534
+ return sessions.map(strip);
535
+ }
536
+ const judged: SessionEntry[] = [];
537
+ for (const session of sessions) {
538
+ if (session.authErrorCode === undefined) {
539
+ judged.push(strip(session));
540
+ continue;
541
+ }
542
+ const current = acpAuthMarkerStillCurrent(
543
+ session.authErrorCredential,
544
+ await resolvedFor(session.agentId),
545
+ );
546
+ judged.push(
547
+ strip(current ? session : { ...session, authErrorCode: undefined }),
548
+ );
549
+ }
550
+ return judged;
432
551
  }
433
552
 
434
553
  function bulkDeleteSessions({ queryParams }: RouteHandlerArgs) {
@@ -701,14 +820,20 @@ function parseLimit(raw: string | null | undefined): number {
701
820
  return Math.min(Math.floor(n), MAX_SESSION_LIMIT);
702
821
  }
703
822
 
704
- function listMergedSessions(opts: {
705
- limit: number;
706
- conversationId?: string;
707
- }): SessionEntry[] {
823
+ function listMergedSessions(opts: { limit: number; conversationId?: string }): {
824
+ sessions: MergedSession[];
825
+ /**
826
+ * Whether the history query reached the end of this conversation's rows.
827
+ *
828
+ * A short read means there is nothing beyond what was returned, which is the
829
+ * proof that no marker is hiding outside the page.
830
+ */
831
+ sawEveryHistoryRow: boolean;
832
+ } {
708
833
  const manager = getAcpSessionManager();
709
834
  const inMemory = manager.getStatus() as AcpSessionState[];
710
835
 
711
- const merged = new Map<string, SessionEntry>();
836
+ const merged = new Map<string, MergedSession>();
712
837
  for (const s of inMemory) {
713
838
  if (opts.conversationId && s.parentConversationId !== opts.conversationId) {
714
839
  continue;
@@ -725,6 +850,8 @@ function listMergedSessions(opts: {
725
850
  stopReason: s.stopReason ?? null,
726
851
  task: s.task,
727
852
  parentToolUseId: s.parentToolUseId,
853
+ authErrorCode: s.authErrorCode,
854
+ authErrorCredential: s.authErrorCredential,
728
855
  usedTokens: s.latestUsage?.usedTokens,
729
856
  contextSize: s.latestUsage?.contextSize,
730
857
  costAmount: s.latestUsage?.costAmount,
@@ -748,52 +875,131 @@ function listMergedSessions(opts: {
748
875
  // guarantee we still surface `limit` distinct rows even when every
749
876
  // in-memory session shadows a DB row — without over-fetching when many
750
877
  // unrelated sessions are in memory.
878
+ const historyLimit = opts.limit + merged.size;
751
879
  const historyRows = filtered
752
880
  .orderBy(desc(acpSessionHistory.startedAt))
753
- .limit(opts.limit + merged.size)
881
+ .limit(historyLimit)
754
882
  .all();
755
883
 
756
884
  for (const row of historyRows) {
757
885
  if (merged.has(row.id)) {
758
886
  continue;
759
887
  }
760
- let eventLog: unknown[] = [];
761
- try {
762
- const parsed = JSON.parse(row.eventLogJson) as unknown;
763
- if (Array.isArray(parsed)) {
764
- eventLog = parsed;
765
- }
766
- } catch (err) {
767
- log.warn(
768
- { id: row.id, err },
769
- "Failed to parse event_log_json for ACP session history row",
770
- );
888
+ merged.set(row.id, toMergedSession(row));
889
+ }
890
+
891
+ return {
892
+ sessions: Array.from(merged.values()).sort(
893
+ (a, b) => b.startedAt - a.startedAt,
894
+ ),
895
+ sawEveryHistoryRow: historyRows.length < historyLimit,
896
+ };
897
+ }
898
+
899
+ /** Shape a history row for the response, parsing its stored event log. */
900
+ function toMergedSession(
901
+ row: typeof acpSessionHistory.$inferSelect,
902
+ ): MergedSession {
903
+ let eventLog: unknown[] = [];
904
+ try {
905
+ const parsed = JSON.parse(row.eventLogJson) as unknown;
906
+ if (Array.isArray(parsed)) {
907
+ eventLog = parsed;
771
908
  }
772
- // Rows predating the usage migration carry NULLs for these columns and
773
- // degrade to undefined.
774
- merged.set(row.id, {
775
- id: row.id,
776
- agentId: row.agentId,
777
- acpSessionId: row.acpSessionId,
778
- parentConversationId: row.parentConversationId,
779
- status: row.status,
780
- startedAt: row.startedAt,
781
- completedAt: row.completedAt,
782
- error: row.error,
783
- stopReason: row.stopReason,
784
- task: row.task ?? undefined,
785
- parentToolUseId: row.parentToolUseId ?? undefined,
786
- usedTokens: row.usedTokens ?? undefined,
787
- contextSize: row.contextSize ?? undefined,
788
- costAmount: row.costAmount ?? undefined,
789
- costCurrency: row.costCurrency ?? undefined,
790
- inputTokens: row.inputTokens ?? undefined,
791
- outputTokens: row.outputTokens ?? undefined,
792
- eventLog,
793
- });
909
+ } catch (err) {
910
+ log.warn(
911
+ { id: row.id, err },
912
+ "Failed to parse event_log_json for ACP session history row",
913
+ );
794
914
  }
915
+ // Rows predating the usage migration carry NULLs for these columns and
916
+ // degrade to undefined.
917
+ return {
918
+ id: row.id,
919
+ agentId: row.agentId,
920
+ acpSessionId: row.acpSessionId,
921
+ parentConversationId: row.parentConversationId,
922
+ status: row.status,
923
+ startedAt: row.startedAt,
924
+ completedAt: row.completedAt,
925
+ error: row.error,
926
+ stopReason: row.stopReason,
927
+ task: row.task ?? undefined,
928
+ parentToolUseId: row.parentToolUseId ?? undefined,
929
+ authErrorCode: row.authErrorCode ?? undefined,
930
+ authErrorCredential: row.authErrorCredential ?? undefined,
931
+ usedTokens: row.usedTokens ?? undefined,
932
+ contextSize: row.contextSize ?? undefined,
933
+ costAmount: row.costAmount ?? undefined,
934
+ costCurrency: row.costCurrency ?? undefined,
935
+ inputTokens: row.inputTokens ?? undefined,
936
+ outputTokens: row.outputTokens ?? undefined,
937
+ eventLog,
938
+ };
939
+ }
940
+
941
+ /**
942
+ * Reach past the page for the one marked run a client would restore the card
943
+ * from, when the page itself holds none.
944
+ *
945
+ * Paging it out is the difference between a user having a way back to auth and
946
+ * not: a conversation with more recent runs than the page holds would
947
+ * otherwise hide the one row that matters.
948
+ *
949
+ * Asks the database for that row rather than filtering one that was loaded.
950
+ * Nothing clears a marker, so repeated failures against a credential that is
951
+ * still current accumulate them, and loading every marked row would pull a
952
+ * full event log apiece into memory to parse and then discard. This reads the
953
+ * credential of each marked row (a column, no event log), resolves what its
954
+ * agent would use now, and only then loads the single row that wins.
955
+ *
956
+ * Bounded by the number of distinct agents a conversation has failed under,
957
+ * which is one in practice, rather than by how often it has failed.
958
+ */
959
+ async function findRecoveryMarker(
960
+ conversationId: string,
961
+ resolvedFor: (agentId: string) => Promise<string | undefined>,
962
+ ): Promise<MergedSession | undefined> {
963
+ const db = getDb();
964
+ // One ordered read of the newest markers, then the answer is decided in
965
+ // memory. Filtering by agent and credential in SQL meant the planner could
966
+ // only seek to the conversation and then walk its marked history looking for
967
+ // a match, so a run of failures against a replaced credential put that walk
968
+ // back on every snapshot. This shape is the one the index serves: seek,
969
+ // read in order, stop.
970
+ const markers = db
971
+ .select({
972
+ id: acpSessionHistory.id,
973
+ agentId: acpSessionHistory.agentId,
974
+ credential: acpSessionHistory.authErrorCredential,
975
+ })
976
+ .from(acpSessionHistory)
977
+ .where(
978
+ and(
979
+ eq(acpSessionHistory.parentConversationId, conversationId),
980
+ isNotNull(acpSessionHistory.authErrorCode),
981
+ ),
982
+ )
983
+ .orderBy(desc(acpSessionHistory.startedAt))
984
+ .limit(MARKER_SCAN_LIMIT)
985
+ .all();
795
986
 
796
- return Array.from(merged.values())
797
- .sort((a, b) => b.startedAt - a.startedAt)
798
- .slice(0, opts.limit);
987
+ for (const marker of markers) {
988
+ if (
989
+ acpAuthMarkerStillCurrent(
990
+ marker.credential,
991
+ await resolvedFor(marker.agentId),
992
+ )
993
+ ) {
994
+ // Newest-first, so the first match is the one a client would restore
995
+ // from. Only now is the full row worth loading, by primary key.
996
+ const row = db
997
+ .select()
998
+ .from(acpSessionHistory)
999
+ .where(eq(acpSessionHistory.id, marker.id))
1000
+ .get();
1001
+ return row ? toMergedSession(row) : undefined;
1002
+ }
1003
+ }
1004
+ return undefined;
799
1005
  }
@@ -54,6 +54,7 @@ import { verifyBundleSignature } from "../../bundler/signature-verifier.js";
54
54
  import { compareSemver } from "../../daemon/handlers/shared.js";
55
55
  import { computeContentId } from "../../util/content-id.js";
56
56
  import { getLogger } from "../../util/logger.js";
57
+ import { getUserAppDataDir } from "../../util/platform.js";
57
58
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
58
59
  import {
59
60
  getOriginClientId,
@@ -73,14 +74,26 @@ const log = getLogger("app-management-routes");
73
74
  // Helpers
74
75
  // ---------------------------------------------------------------------------
75
76
 
76
- function getSharedAppsDir(): string {
77
- return join(
77
+ function getSharedAppsDirs(): string[] {
78
+ const canonical = join(
79
+ getUserAppDataDir(),
80
+ "vellum-assistant",
81
+ "shared-apps",
82
+ );
83
+ const legacy = join(
78
84
  homedir(),
79
85
  "Library",
80
86
  "Application Support",
81
87
  "vellum-assistant",
82
88
  "shared-apps",
83
89
  );
90
+ return legacy === canonical ? [canonical] : [canonical, legacy];
91
+ }
92
+
93
+ function resolveSharedAppDir(appUuid: string, dirs: string[]): string | null {
94
+ return (
95
+ dirs.find((dir) => existsSync(join(dir, `${appUuid}-meta.json`))) ?? null
96
+ );
84
97
  }
85
98
 
86
99
  // ---------------------------------------------------------------------------
@@ -218,13 +231,9 @@ function getAppDataResult(
218
231
  }
219
232
  }
220
233
 
221
- function listSharedApps(): Array<Record<string, unknown>> {
222
- const dir = getSharedAppsDir();
223
- if (!existsSync(dir)) {
224
- return [];
225
- }
226
-
227
- const files = readdirSync(dir).filter((f) => f.endsWith("-meta.json"));
234
+ function listSharedApps(
235
+ dirs = getSharedAppsDirs(),
236
+ ): Array<Record<string, unknown>> {
228
237
  const apps: Array<{
229
238
  uuid: string;
230
239
  name: string;
@@ -240,43 +249,54 @@ function listSharedApps(): Array<Record<string, unknown>> {
240
249
  contentId?: string;
241
250
  forked?: boolean;
242
251
  }> = [];
252
+ const seen = new Set<string>();
243
253
 
244
- for (const file of files) {
245
- try {
246
- const raw = readFileSync(join(dir, file), "utf-8");
247
- const meta = JSON.parse(raw);
248
-
249
- let version: string | undefined;
250
- let contentId: string | undefined;
251
- const manifestPath = join(dir, meta.uuid, "manifest.json");
252
- if (existsSync(manifestPath)) {
253
- try {
254
- const manifestRaw = readFileSync(manifestPath, "utf-8");
255
- const manifest = JSON.parse(manifestRaw);
256
- version = manifest.version;
257
- contentId = manifest.content_id;
258
- } catch {
259
- // ignore malformed manifest
254
+ for (const dir of dirs) {
255
+ if (!existsSync(dir)) {
256
+ continue;
257
+ }
258
+ const files = readdirSync(dir).filter((f) => f.endsWith("-meta.json"));
259
+ for (const file of files) {
260
+ try {
261
+ const raw = readFileSync(join(dir, file), "utf-8");
262
+ const meta = JSON.parse(raw);
263
+ if (seen.has(meta.uuid)) {
264
+ continue;
260
265
  }
261
- }
262
266
 
263
- apps.push({
264
- uuid: meta.uuid,
265
- name: meta.name,
266
- description: meta.description,
267
- icon: meta.icon,
268
- preview: meta.preview,
269
- entry: meta.entry,
270
- trustTier: meta.trustTier,
271
- signerDisplayName: meta.signerDisplayName,
272
- bundleSizeBytes: meta.bundleSizeBytes ?? 0,
273
- installedAt: meta.installedAt,
274
- version,
275
- contentId,
276
- forked: meta.forked,
277
- });
278
- } catch {
279
- log.warn({ file }, "Failed to read shared app metadata file");
267
+ let version: string | undefined;
268
+ let contentId: string | undefined;
269
+ const manifestPath = join(dir, meta.uuid, "manifest.json");
270
+ if (existsSync(manifestPath)) {
271
+ try {
272
+ const manifestRaw = readFileSync(manifestPath, "utf-8");
273
+ const manifest = JSON.parse(manifestRaw);
274
+ version = manifest.version;
275
+ contentId = manifest.content_id;
276
+ } catch {
277
+ // ignore malformed manifest
278
+ }
279
+ }
280
+
281
+ apps.push({
282
+ uuid: meta.uuid,
283
+ name: meta.name,
284
+ description: meta.description,
285
+ icon: meta.icon,
286
+ preview: meta.preview,
287
+ entry: meta.entry,
288
+ trustTier: meta.trustTier,
289
+ signerDisplayName: meta.signerDisplayName,
290
+ bundleSizeBytes: meta.bundleSizeBytes ?? 0,
291
+ installedAt: meta.installedAt,
292
+ version,
293
+ contentId,
294
+ forked: meta.forked,
295
+ });
296
+ seen.add(meta.uuid);
297
+ } catch {
298
+ log.warn({ file }, "Failed to read shared app metadata file");
299
+ }
280
300
  }
281
301
  }
282
302
 
@@ -314,6 +334,7 @@ function listSharedApps(): Array<Record<string, unknown>> {
314
334
 
315
335
  function forkSharedApp(
316
336
  appUuid: string,
337
+ dirs = getSharedAppsDirs(),
317
338
  ):
318
339
  | { success: true; appId: string; name: string }
319
340
  | { success: false; error: string } {
@@ -326,12 +347,11 @@ function forkSharedApp(
326
347
  return { success: false, error: "Invalid UUID" };
327
348
  }
328
349
 
329
- const dir = getSharedAppsDir();
330
- const metaFile = join(dir, `${appUuid}-meta.json`);
331
-
332
- if (!existsSync(metaFile)) {
350
+ const dir = resolveSharedAppDir(appUuid, dirs);
351
+ if (!dir) {
333
352
  return { success: false, error: "Shared app not found" };
334
353
  }
354
+ const metaFile = join(dir, `${appUuid}-meta.json`);
335
355
 
336
356
  const metaRaw = readFileSync(metaFile, "utf-8");
337
357
  const meta = JSON.parse(metaRaw);
@@ -392,6 +412,19 @@ function forkSharedApp(
392
412
  return { success: true, appId: newApp.id, name: newApp.name };
393
413
  }
394
414
 
415
+ export function _listSharedAppsForTests(
416
+ dirs: string[],
417
+ ): Array<Record<string, unknown>> {
418
+ return listSharedApps(dirs);
419
+ }
420
+
421
+ export function _resolveSharedAppDirForTests(
422
+ appUuid: string,
423
+ dirs: string[],
424
+ ): string | null {
425
+ return resolveSharedAppDir(appUuid, dirs);
426
+ }
427
+
395
428
  async function openBundle(filePath: string): Promise<Record<string, unknown>> {
396
429
  const fileStat = await stat(filePath);
397
430
  const bundleSizeBytes = fileStat.size;
@@ -10,6 +10,7 @@
10
10
  import { z } from "zod";
11
11
 
12
12
  import { PendingToolQuestionSchema } from "../../api/responses/conversation-message.js";
13
+ import { syncTerminalGuardianRequestStatus } from "../../approvals/guardian-request-status-sync.js";
13
14
  import { findConversation } from "../../daemon/conversation-registry.js";
14
15
  import type {
15
16
  SecretDelivery,
@@ -91,6 +92,14 @@ function handleConfirm({ body }: RouteHandlerArgs) {
91
92
  effectiveDecision === "allow" ? "approved" : "rejected",
92
93
  );
93
94
  interaction.directResolve(effectiveDecision as UserDecision);
95
+ // When a guardian request was promoted for this confirmation, bring its
96
+ // row to the matching terminal status and withdraw its cards; with no
97
+ // such row the CAS misses and this is a no-op.
98
+ void syncTerminalGuardianRequestStatus({
99
+ requestId,
100
+ status: effectiveDecision === "allow" ? "approved" : "denied",
101
+ syncContext: "confirm-direct-resolve",
102
+ });
94
103
  return { accepted: true };
95
104
  }
96
105
 
@@ -25,7 +25,12 @@ export async function createTarGz(
25
25
  const { stdout } = await execFileAsync(
26
26
  "tar",
27
27
  ["czf", "-", "-C", staging, "."],
28
- { maxBuffer: maxBytes, timeout: 30_000, encoding: "buffer" },
28
+ {
29
+ maxBuffer: maxBytes,
30
+ timeout: 30_000,
31
+ encoding: "buffer",
32
+ windowsHide: true,
33
+ },
29
34
  );
30
35
  const buf = Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout);
31
36
  return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
 
4
+ import { getCharacterComponents } from "@vellumai/avatar-catalog";
4
5
  import { AVATAR_TRAITS_FILENAME } from "@vellumai/avatar-manifest";
5
6
  import { z } from "zod";
6
7
 
@@ -16,7 +17,6 @@ import {
16
17
  setCharacter,
17
18
  setImage,
18
19
  } from "../../avatar/avatar-store.js";
19
- import { getCharacterComponents } from "../../avatar/character-components.js";
20
20
  import {
21
21
  ensureAvatarRaster,
22
22
  ensureAvatarRasterPath,
@@ -52,24 +52,22 @@ export function parseCallbackData(
52
52
  return null;
53
53
  }
54
54
  const source =
55
- sourceChannel === "whatsapp"
56
- ? ("whatsapp_button" as const)
57
- : sourceChannel === "slack"
58
- ? ("slack_button" as const)
59
- : sourceChannel === "vellum"
60
- ? ("vellum_surface" as const)
61
- : ("telegram_button" as const);
55
+ sourceChannel === "vellum"
56
+ ? ("vellum_surface" as const)
57
+ : ("button" as const);
62
58
  return { action, source, requestId };
63
59
  }
64
60
 
65
61
  // ---------------------------------------------------------------------------
66
- // Reaction callback data parser — format: "reaction:<emoji_name>"
62
+ // Reaction decision vocabulary
67
63
  // ---------------------------------------------------------------------------
68
64
 
69
65
  /**
70
- * Map of Slack emoji names to approval actions. Multiple emoji names can
71
- * map to the same action to handle Slack's aliasing (e.g. `+1` and `thumbsup`
72
- * both represent the thumbs-up emoji).
66
+ * Map of reaction emoji to approval actions, in each channel's own
67
+ * vocabulary: Slack reactions carry colon names (with aliasing, e.g. `+1`
68
+ * and `thumbsup` are both the thumbs-up emoji), Telegram and Discord carry
69
+ * the unicode character itself. Telegram's fixed reaction set offers the
70
+ * thumbs but not the check mark or alarm clock; Discord offers all four.
73
71
  */
74
72
  const REACTION_EMOJI_MAP: ReadonlyMap<string, ApprovalAction> = new Map([
75
73
  ["+1", "approve_once"],
@@ -78,24 +76,24 @@ const REACTION_EMOJI_MAP: ReadonlyMap<string, ApprovalAction> = new Map([
78
76
  ["alarm_clock", "approve_once"],
79
77
  ["-1", "reject"],
80
78
  ["thumbsdown", "reject"],
79
+ ["\u{1F44D}", "approve_once"], // thumbs up
80
+ ["\u2705", "approve_once"], // check mark button
81
+ ["\u23F0", "approve_once"], // alarm clock
82
+ ["\u{1F44E}", "reject"], // thumbs down
81
83
  ]);
82
84
 
83
85
  /**
84
- * Parse a `reaction:<emoji_name>` callback data string into an approval
85
- * decision. Returns null if the emoji is not mapped to any action.
86
+ * Map a reaction emoji, in the source channel's own vocabulary, to an
87
+ * approval decision. Returns null if the emoji is not mapped to any action.
86
88
  */
87
- export function parseReactionCallbackData(
88
- data: string,
89
+ export function reactionDecisionForEmoji(
90
+ emoji: string,
89
91
  ): ApprovalDecisionResult | null {
90
- if (!data.startsWith("reaction:")) {
91
- return null;
92
- }
93
- const emoji = data.slice("reaction:".length);
94
92
  const action = REACTION_EMOJI_MAP.get(emoji);
95
93
  if (!action) {
96
94
  return null;
97
95
  }
98
- return { action, source: "slack_reaction" };
96
+ return { action, source: "reaction" };
99
97
  }
100
98
 
101
99
  // ---------------------------------------------------------------------------