@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
@@ -21,7 +21,7 @@ Conversational guardian verification control-plane invocation is guardian-only.
21
21
 
22
22
  ## Memory Provenance Invariant
23
23
 
24
- All memory retrieval decisions must consider actor-role provenance. Untrusted actors (non-guardian, unverified_channel) must not receive memory recall results. This invariant is enforced in `indexer.ts` (write gate) and `conversation-memory.ts` (read gate).
24
+ All memory retrieval decisions must consider actor-role provenance. Untrusted actors (non-guardian, unverified_channel) must not receive memory recall results. This invariant is enforced in `indexer.ts` (write gate) and the memory plugin's `injectors.ts` (read gate), which admits personal-memory content only for a turn whose trust context passes `isPersonalMemoryAllowed`.
25
25
 
26
26
  ## Guardian Privilege Isolation Invariant
27
27
 
@@ -22,10 +22,13 @@
22
22
  * Every surface is attempted independently; one failure never blocks the rest.
23
23
  */
24
24
 
25
+ import { DELIVERY_STATUS } from "@vellumai/gateway-client";
26
+
25
27
  import {
26
28
  type GuardianRequestDeliveryWire,
27
29
  type GuardianRequestStatus,
28
30
  listGuardianRequestDeliveries,
31
+ updateGuardianRequestDelivery,
29
32
  } from "../channels/gateway-guardian-requests.js";
30
33
  import {
31
34
  completeSurfaceAndNotify,
@@ -35,6 +38,7 @@ import { withdrawDiscordApprovalCard } from "../messaging/providers/discord/with
35
38
  import { withdrawSlackApprovalCard } from "../messaging/providers/slack/withdraw.js";
36
39
  import { withdrawTelegramApprovalCard } from "../messaging/providers/telegram-bot/withdraw.js";
37
40
  import { approvalCardSurfaceId } from "../notifications/approval-card-data.js";
41
+ import { writeGuardianFeedReceipt } from "../notifications/guardian-feed-projection.js";
38
42
  import {
39
43
  type ApprovalAction,
40
44
  resolveDecisionStatusWord,
@@ -66,6 +70,17 @@ export interface WithdrawGuardianCardsParams {
66
70
  * expiry sweep) to broadcast on every surface.
67
71
  */
68
72
  originChannel?: string;
73
+ /**
74
+ * Conversation the in-app decision was made from, when `originChannel` is
75
+ * `vellum`. A request can project a card into more than one conversation
76
+ * (the vellum card, plus any legacy channel rows that paired one), and
77
+ * only the acting conversation's client holds the optimistic completion
78
+ * the broadcast suppression exists to protect; every sibling projection
79
+ * still needs its live `ui_surface_complete`. Absent, every in-app projection is
80
+ * treated as the acting one: a vellum-origin decision then suppresses the
81
+ * broadcast on all of them and only persists their completions.
82
+ */
83
+ originConversationId?: string;
69
84
  /**
70
85
  * The action the guardian took, when the terminal status came from a decision
71
86
  * (omitted for the expiry sweep). A `denied` status can mean either a neutral
@@ -84,23 +99,52 @@ export interface WithdrawGuardianCardsParams {
84
99
  * outcome the guardian's chat gets.
85
100
  */
86
101
  hasOriginGuardianReply?: boolean;
102
+ /**
103
+ * Non-decision cause of the terminal status, for receipt copy only
104
+ * (e.g. "superseded" when a newer inbound message auto-denied the
105
+ * request). Channel card edits render the status word as before; the
106
+ * feed receipt surfaces the reason so the bell can say "Superseded"
107
+ * rather than "Rejected".
108
+ */
109
+ terminalReason?: string;
87
110
  }
88
111
 
89
112
  /**
90
- * Withdraw a resolved request's approval cards across all delivery surfaces.
91
- * Never throws.
113
+ * Withdraw a resolved request's approval cards across all delivery
114
+ * surfaces. Never throws. Each surface that durably withdraws marks its
115
+ * delivery row `withdrawn`; `complete` reports whether every row now
116
+ * carries that mark, so a caller whose own receipt depends on the cards
117
+ * actually being gone (the expiry sweep) can hold its receipt back and
118
+ * retry only what failed, while decision-path callers stay
119
+ * fire-and-forget. A surface with nothing a retry could ever fix (no
120
+ * captured message id, an unusable recorded id, a channel with no
121
+ * in-place edit) marks its row too, so it can never pin a request.
92
122
  */
93
123
  export async function withdrawGuardianRequestCards(
94
124
  params: WithdrawGuardianCardsParams,
95
- ): Promise<void> {
125
+ ): Promise<{ complete: boolean }> {
96
126
  const {
97
127
  request,
98
128
  status,
99
129
  originChannel,
130
+ originConversationId,
100
131
  decidedAction,
101
132
  hasOriginGuardianReply,
102
133
  } = params;
103
134
 
135
+ // The canonical "Needs attention" feed item is a projection like any
136
+ // delivery surface: rewrite it into its terminal receipt first, so it
137
+ // settles even when the gateway (and thus the delivery list) is
138
+ // unreachable. A failed local write gates `complete` the same way a
139
+ // failed surface edit does, so the expiry sweep retries it.
140
+ let complete = await writeGuardianFeedReceipt({
141
+ requestId: request.id,
142
+ status,
143
+ ...(decidedAction ? { decidedAction } : {}),
144
+ decidedAtMs: request.updatedAt,
145
+ ...(params.terminalReason ? { terminalReason: params.terminalReason } : {}),
146
+ });
147
+
104
148
  let deliveries: GuardianRequestDeliveryWire[];
105
149
  try {
106
150
  deliveries = await listGuardianRequestDeliveries(request.id);
@@ -109,35 +153,54 @@ export async function withdrawGuardianRequestCards(
109
153
  { err, requestId: request.id },
110
154
  "Failed to list deliveries for card withdrawal",
111
155
  );
112
- return;
156
+ return { complete: false };
113
157
  }
114
-
115
158
  for (const delivery of deliveries) {
159
+ if (delivery.status === DELIVERY_STATUS.withdrawn) {
160
+ continue;
161
+ }
162
+ let withdrawn = false;
116
163
  try {
164
+ // A delivery can carry two projections of the same card: the
165
+ // channel-native message, and the in-app rendering of any paired
166
+ // conversation (`destinationConversationId`). New channel guardian
167
+ // deliveries pair no conversation, so only the vellum delivery and
168
+ // legacy channel rows carry one; where a row does, withdrawal must
169
+ // settle both projections, or the paired conversation keeps live
170
+ // Approve/Reject buttons after the decision (LUM-3489).
171
+ const inAppWithdrawn = completeInAppProjection(
172
+ request,
173
+ delivery,
174
+ status,
175
+ originChannel,
176
+ originConversationId,
177
+ decidedAction,
178
+ );
117
179
  if (delivery.destinationChannel === "vellum") {
118
- withdrawVellumCard(
119
- request,
120
- delivery,
121
- status,
122
- originChannel,
123
- decidedAction,
124
- );
180
+ withdrawn = inAppWithdrawn;
125
181
  } else if (delivery.destinationChannel === "slack") {
126
182
  await withdrawSlackCard(request, delivery, status, decidedAction);
183
+ withdrawn = inAppWithdrawn;
127
184
  } else if (delivery.destinationChannel === "telegram") {
128
- await withdrawTelegramCard(
185
+ const telegram = await withdrawTelegramCard(
129
186
  delivery,
130
187
  status,
131
188
  originChannel,
132
189
  decidedAction,
133
190
  hasOriginGuardianReply ?? false,
134
191
  );
192
+ withdrawn = telegram.complete && inAppWithdrawn;
135
193
  } else if (delivery.destinationChannel === "discord") {
136
194
  await withdrawDiscordCard(delivery, status, decidedAction);
195
+ withdrawn = inAppWithdrawn;
196
+ } else {
197
+ // WhatsApp direct delivery can't edit a message in place (it would
198
+ // post a new one), so its stale clicks are left to the existing
199
+ // "already resolved" reply until in-place edit support lands.
200
+ // Nothing a channel retry could fix, so only the in-app projection
201
+ // gates the row rather than letting it pin its request.
202
+ withdrawn = inAppWithdrawn;
137
203
  }
138
- // WhatsApp direct delivery can't edit a message in place (it would
139
- // post a new one), so its stale clicks are left to the existing
140
- // "already resolved" reply until in-place edit support lands.
141
204
  } catch (err) {
142
205
  log.warn(
143
206
  {
@@ -148,12 +211,37 @@ export async function withdrawGuardianRequestCards(
148
211
  "Failed to withdraw guardian card on surface (non-fatal)",
149
212
  );
150
213
  }
214
+
215
+ if (!withdrawn) {
216
+ complete = false;
217
+ continue;
218
+ }
219
+ try {
220
+ await updateGuardianRequestDelivery(delivery.id, {
221
+ status: DELIVERY_STATUS.withdrawn,
222
+ });
223
+ } catch (err) {
224
+ // The card is gone but the receipt write failed: the retry round
225
+ // re-runs this surface's idempotent edit and marks it then.
226
+ complete = false;
227
+ log.warn(
228
+ { err, requestId: request.id, deliveryId: delivery.id },
229
+ "Failed to record card withdrawal on the delivery row (non-fatal)",
230
+ );
231
+ }
151
232
  }
233
+ return { complete };
152
234
  }
153
235
 
154
236
  /**
155
- * Withdraw the in-app approval card so it stops offering live actions while
156
- * keeping its content.
237
+ * Complete a delivery's in-app card projection so it stops offering live
238
+ * actions while keeping its content. Applies to any delivery whose row
239
+ * records a paired conversation: the vellum card, and legacy channel rows
240
+ * written before channel deliveries stopped pairing, whose paired
241
+ * conversation renders the same actionable card in-app (the channel edit
242
+ * alone would leave that projection clickable). No-ops (successfully) when
243
+ * the delivery recorded no conversation, which is every new channel
244
+ * delivery.
157
245
  *
158
246
  * The completion is always persisted onto the card's `ui_surface` block. The
159
247
  * acting client's optimistic completion is in-memory only, so without this write
@@ -163,32 +251,41 @@ export async function withdrawGuardianRequestCards(
163
251
  * The `ui_surface_complete` broadcast is the only origin-sensitive half: when the
164
252
  * decision came from in-app, the acting client is already showing the resolver's
165
253
  * guardian-facing reply, and broadcasting the canonical status label back would
166
- * replace that richer summary mid-session.
254
+ * replace that richer summary mid-session. When the acting conversation is
255
+ * known (`originConversationId`), only that conversation's broadcast is
256
+ * suppressed; sibling projections still need theirs to converge live.
167
257
  */
168
- function withdrawVellumCard(
258
+ function completeInAppProjection(
169
259
  request: WithdrawableGuardianRequest,
170
260
  delivery: GuardianRequestDeliveryWire,
171
261
  status: GuardianRequestStatus,
172
262
  originChannel: string | undefined,
263
+ originConversationId: string | undefined,
173
264
  decidedAction: ApprovalAction | undefined,
174
- ): void {
265
+ ): boolean {
175
266
  if (!delivery.destinationConversationId) {
176
- return;
267
+ return true;
177
268
  }
178
269
  const surfaceId = approvalCardSurfaceId(request.kind, request.id);
179
270
  if (!surfaceId) {
180
- return;
271
+ return true;
181
272
  }
182
273
  const summary = resolveDecisionStatusWord(status, decidedAction);
183
- if (originChannel === "vellum") {
184
- markSurfaceCompleted(
274
+ // A false here is a failed durable write: after a reload the persisted
275
+ // block would revert to a pending, clickable card, so it must hold the
276
+ // expiry sweep's receipt back and be retried.
277
+ if (
278
+ originChannel === "vellum" &&
279
+ (!originConversationId ||
280
+ delivery.destinationConversationId === originConversationId)
281
+ ) {
282
+ return markSurfaceCompleted(
185
283
  { conversationId: delivery.destinationConversationId },
186
284
  surfaceId,
187
285
  summary,
188
286
  );
189
- return;
190
287
  }
191
- completeSurfaceAndNotify(
288
+ return completeSurfaceAndNotify(
192
289
  delivery.destinationConversationId,
193
290
  surfaceId,
194
291
  summary,
@@ -259,11 +356,11 @@ async function withdrawTelegramCard(
259
356
  originChannel: string | undefined,
260
357
  decidedAction: ApprovalAction | undefined,
261
358
  hasOriginGuardianReply: boolean,
262
- ): Promise<void> {
359
+ ): Promise<{ complete: boolean }> {
263
360
  if (!delivery.destinationChatId || !delivery.destinationMessageId) {
264
- return;
361
+ return { complete: true };
265
362
  }
266
- await withdrawTelegramApprovalCard({
363
+ return withdrawTelegramApprovalCard({
267
364
  chatId: delivery.destinationChatId,
268
365
  messageId: delivery.destinationMessageId,
269
366
  status,
@@ -155,6 +155,13 @@ export interface ApplyGuardianDecisionParams {
155
155
  action: ApprovalAction;
156
156
  /** Actor context for the entity making the decision. */
157
157
  actorContext: ActorContext;
158
+ /**
159
+ * Conversation an in-app (vellum) decision was made from, when known.
160
+ * Threaded to card withdrawal so only the acting conversation's completion
161
+ * broadcast is suppressed; the request's sibling in-app projections still
162
+ * receive theirs.
163
+ */
164
+ originConversationId?: string;
158
165
  /** Optional user-supplied text (e.g. answer text for pending questions). */
159
166
  userText?: string;
160
167
  /** Optional channel delivery context — present when the decision arrived via a channel message. */
@@ -227,6 +234,7 @@ export async function applyGuardianDecision(
227
234
  requestId,
228
235
  action,
229
236
  actorContext,
237
+ originConversationId,
230
238
  userText,
231
239
  channelDeliveryContext,
232
240
  emissionContext,
@@ -531,6 +539,7 @@ export async function applyGuardianDecision(
531
539
  request: resolved,
532
540
  status: targetStatus,
533
541
  originChannel: actorContext.channel,
542
+ ...(originConversationId ? { originConversationId } : {}),
534
543
  decidedAction: cardAction,
535
544
  // Tells the Telegram projection whether the origin chat is about to get
536
545
  // the resolver's own guardian-facing reply (delivered by our caller), so
@@ -17,9 +17,12 @@
17
17
  * withdrawn card already reflects the expired state, so a fresh ping would be
18
18
  * noise.
19
19
  *
20
- * Best-effort by contract: the request is already resolved (CAS committed)
21
- * before this runs, so a failed notice or interaction release must never
22
- * surface as a sweep failure. Nothing here throws.
20
+ * Never throws; `complete` reports whether the side effects actually ran,
21
+ * because the sweep confirms a request's expiry (the status CAS) only after
22
+ * they did: a failed notice left the requester untold, so it must keep the
23
+ * row pending and retryable rather than vanish into a log line. A request
24
+ * with no deliverable route or no requester chat reports complete: there is
25
+ * nothing a retry could ever deliver.
23
26
  */
24
27
 
25
28
  import type { GuardianRequestWire } from "../channels/gateway-guardian-requests.js";
@@ -36,49 +39,48 @@ import {
36
39
  const log = getLogger("guardian-expiry-notifier");
37
40
 
38
41
  /**
39
- * Run the expiry side effects for a single guardian request that the sweep
40
- * just transitioned to `expired`. Dispatches by kind; never throws.
42
+ * Run the expiry side effects for a single guardian request the sweep is
43
+ * expiring. Dispatches by kind; never throws.
41
44
  */
42
45
  export async function notifyExpiredGuardianRequest(
43
46
  request: GuardianRequestWire,
44
- ): Promise<void> {
47
+ ): Promise<{ complete: boolean }> {
45
48
  try {
46
49
  switch (request.kind) {
47
50
  case "tool_approval":
48
51
  releaseExpiredInteraction(request);
49
- return;
52
+ return { complete: true };
50
53
  case "access_request":
51
54
  // Admitted-mode nudges expire silently for the requester — the
52
55
  // sender made no request and keeps whatever access the floor grants.
53
56
  if (!introductionMode(request.requestTrigger).notifyRequesterOnExpiry) {
54
- break;
57
+ return { complete: true };
55
58
  }
56
- await notifyRequesterOfExpiry(
59
+ return notifyRequesterOfExpiry(
57
60
  request,
58
61
  "Your access request expired before it was reviewed. " +
59
62
  "Send a new message if you still need access.",
60
63
  );
61
- return;
62
64
  case "tool_grant_request":
63
- await notifyRequesterOfExpiry(
65
+ return notifyRequesterOfExpiry(
64
66
  request,
65
67
  `Your request to use "${request.toolName ?? "a tool"}" expired ` +
66
68
  "before it was reviewed. Ask again if you still need it.",
67
69
  );
68
- return;
69
70
  case "pending_question":
70
71
  // Voice call sessions own their own lifecycle and timeout. By the time
71
72
  // the request TTL lapses the call is long over and there is no
72
73
  // durable requester channel to notify, so there is nothing to do.
73
- return;
74
+ return { complete: true };
74
75
  default:
75
- return;
76
+ return { complete: true };
76
77
  }
77
78
  } catch (err) {
78
79
  log.warn(
79
80
  { err, requestId: request.id, kind: request.kind },
80
81
  "Expiry side effects failed for guardian request (non-fatal)",
81
82
  );
83
+ return { complete: false };
82
84
  }
83
85
  }
84
86
 
@@ -121,15 +123,17 @@ function releaseExpiredInteraction(request: GuardianRequestWire): void {
121
123
  async function notifyRequesterOfExpiry(
122
124
  request: GuardianRequestWire,
123
125
  text: string,
124
- ): Promise<void> {
126
+ ): Promise<{ complete: boolean }> {
125
127
  const channel = request.sourceChannel ?? "";
126
128
  const deliverUrl = resolveDeliverCallbackUrlForChannel(channel);
127
129
  const requesterChatId =
128
130
  request.requesterChatId ?? request.requesterExternalUserId ?? "";
129
131
  const requesterExternalUserId = request.requesterExternalUserId ?? "";
130
132
 
133
+ // No deliverable route or no requester chat: nothing a retry could ever
134
+ // deliver, so this counts as complete rather than pinning the request.
131
135
  if (!deliverUrl || !requesterChatId) {
132
- return;
136
+ return { complete: true };
133
137
  }
134
138
 
135
139
  const targetChatId = resolveRequesterDeliveryTarget({
@@ -148,10 +152,12 @@ async function notifyRequesterOfExpiry(
148
152
  { requestId: request.id, kind: request.kind, channel },
149
153
  "Notified requester that guardian request expired",
150
154
  );
155
+ return { complete: true };
151
156
  } catch (err) {
152
157
  log.warn(
153
158
  { err, requestId: request.id, channel },
154
159
  "Failed to notify requester of guardian request expiry (non-fatal)",
155
160
  );
161
+ return { complete: false };
156
162
  }
157
163
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Terminal status sync for guardian requests resolved outside the decision
3
+ * primitive.
4
+ *
5
+ * Some flows resolve a pending tool confirmation in-memory first and treat
6
+ * that as authoritative: the in-app confirm route and the legacy channel
7
+ * approval rail (`Conversation.handleConfirmationResponse`), and the
8
+ * auto-deny sweeps that supersede confirmations when a new message arrives.
9
+ * Those flows still have to move the gateway's `guardian_requests` row to the
10
+ * matching terminal status so stale "pending" rows can't be matched by later
11
+ * guardian reply routing.
12
+ *
13
+ * A status CAS alone is not enough: the request's approval cards are
14
+ * projected onto every surface it was delivered to (the in-app card, channel
15
+ * messages, and each channel card's paired conversation), and a row that goes
16
+ * terminal without card withdrawal leaves every projection offering live
17
+ * Approve/Reject actions for a decision that already happened (LUM-3489). So
18
+ * the sync runs the same cross-surface withdrawal the decision primitive
19
+ * runs, whenever its CAS is the one that actually landed.
20
+ *
21
+ * First-writer-wins: when the primitive already resolved the request (e.g.
22
+ * the channel approval path decided it and ran withdrawal itself), this CAS
23
+ * misses and no second withdrawal runs.
24
+ *
25
+ * Fire-and-forget by contract: the caller's in-memory resolution is
26
+ * authoritative and often synchronous, so failures are logged, never thrown,
27
+ * and lost syncs are reaped by the orphan sweep.
28
+ *
29
+ * Transitional: this exists for the flows that resolve confirmations
30
+ * in-memory first (the legacy rail). New decision paths must route through
31
+ * `applyGuardianDecision`, which owns the CAS, resolver dispatch, grant
32
+ * minting, and withdrawal together; do not add callers here for anything the
33
+ * pipeline can serve.
34
+ */
35
+
36
+ import { decideGuardianRequest } from "../channels/gateway-guardian-requests.js";
37
+ import type { ApprovalAction } from "../runtime/channel-approval-types.js";
38
+ import { getLogger } from "../util/logger.js";
39
+ import { withdrawGuardianRequestCards } from "./guardian-card-withdrawal.js";
40
+
41
+ const log = getLogger("guardian-request-status-sync");
42
+
43
+ export interface SyncTerminalGuardianRequestStatusParams {
44
+ requestId: string;
45
+ status: "approved" | "denied";
46
+ /** Log line context naming the flow that resolved the confirmation. */
47
+ syncContext: string;
48
+ /**
49
+ * Non-decision cause of the terminal status, threaded through to the
50
+ * feed receipt so an auto-deny reads as what it was (e.g.
51
+ * "superseded") rather than as a rejection the guardian chose.
52
+ */
53
+ terminalReason?: string;
54
+ }
55
+
56
+ /**
57
+ * CAS the gateway request to its terminal status and, when this write is the
58
+ * one that landed, project the outcome onto every delivered approval card.
59
+ *
60
+ * No origin channel is passed to withdrawal: the confirmation flows act on
61
+ * the conversation's confirmation prompt (or on no client at all, for the
62
+ * auto-deny sweeps), never on the approval card itself, so every card
63
+ * projection needs its completion broadcast.
64
+ */
65
+ export async function syncTerminalGuardianRequestStatus(
66
+ params: SyncTerminalGuardianRequestStatusParams,
67
+ ): Promise<void> {
68
+ const { requestId, status, syncContext } = params;
69
+ // The confirmation flows only ever approve or reject, so the resolved
70
+ // cards' action is the plain decision pair derived from the status.
71
+ const decidedAction: ApprovalAction =
72
+ status === "approved" ? "approve_once" : "reject";
73
+ try {
74
+ const decided = await decideGuardianRequest({
75
+ id: requestId,
76
+ expectedStatus: "pending",
77
+ status,
78
+ });
79
+ if (!decided.applied) {
80
+ // The decision primitive (or a concurrent sync) resolved it first and
81
+ // owns the card withdrawal.
82
+ return;
83
+ }
84
+ await withdrawGuardianRequestCards({
85
+ request: decided.request,
86
+ status,
87
+ decidedAction,
88
+ ...(params.terminalReason
89
+ ? { terminalReason: params.terminalReason }
90
+ : {}),
91
+ });
92
+ } catch (err) {
93
+ log.warn(
94
+ { err, requestId, syncContext },
95
+ "Guardian request terminal status sync failed",
96
+ );
97
+ }
98
+ }
@@ -1,4 +1,4 @@
1
- import { getCharacterComponents } from "./character-components.js";
1
+ import { getCharacterComponents } from "@vellumai/avatar-catalog";
2
2
 
3
3
  /**
4
4
  * Compose a complete SVG document from a body shape, eye style, and color.
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { mkdirSync, renameSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
+ import { getCharacterComponents } from "@vellumai/avatar-catalog";
5
6
  import {
6
7
  AVATAR_IMAGE_FILENAME,
7
8
  AVATAR_TRAITS_FILENAME,
@@ -11,7 +12,6 @@ import {
11
12
  import { getLogger } from "../util/logger.js";
12
13
  import { getAvatarDir } from "../util/platform.js";
13
14
  import { renderCharacterAscii } from "./ascii-renderer.js";
14
- import { getCharacterComponents } from "./character-components.js";
15
15
  import { renderCharacterPng } from "./png-renderer.js";
16
16
  import { isResvgAvailable } from "./resvg-lazy.js";
17
17
 
@@ -497,6 +497,7 @@ export async function runCompile(
497
497
  stdout: "pipe",
498
498
  stderr: "pipe",
499
499
  env: { ...process.env, NODE_PATH: nodePath },
500
+ windowsHide: true,
500
501
  });
501
502
 
502
503
  await proc.exited;
@@ -170,6 +170,7 @@ async function downloadAndExtract(
170
170
  cmd: ["tar", "xzf", tmpTar, "-C", targetDir, "--strip-components=1"],
171
171
  stdout: "ignore",
172
172
  stderr: "pipe",
173
+ windowsHide: true,
173
174
  });
174
175
  await proc.exited;
175
176
  if (proc.exitCode !== 0) {
@@ -124,6 +124,7 @@ async function installPackage(
124
124
  cwd: cacheDir,
125
125
  stdout: "pipe",
126
126
  stderr: "pipe",
127
+ windowsHide: true,
127
128
  });
128
129
 
129
130
  // Race against timeout