@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
@@ -1,39 +1,71 @@
1
1
  /**
2
- * IPC route for the `contacts/prompt` CLI command.
2
+ * IPC routes for the contact forms the guardian fills in their app.
3
3
  *
4
- * Flow:
5
- * 1. CLI calls `contacts/prompt` IPC route with optional channel/role hints.
6
- * 2. Daemon broadcasts a `contact_request` to all connected clients.
7
- * 3. Client shows a contact address input form.
8
- * 4. User enters an address; client POSTs to the gateway's
9
- * `POST /v1/contacts/prompt` HTTP route.
10
- * 5. Gateway upserts the contact + channel (gateway owns all contact writes).
11
- * 6. If the prompt was opened with --verify, the gateway reads
12
- * `contact_prompt_flags` and attests the new channel.
13
- * 7. Gateway calls daemon IPC `resolve_contact_prompt` with the new contact info.
14
- * 8. Daemon resolves the pending promise; `contacts/prompt` IPC returns to CLI.
4
+ * Two commands park on the guardian-form rail:
5
+ * - `contacts/prompt` collects a channel address and binds a channel.
6
+ * - `contacts/record-prompt` confirms a contact record write the assistant
7
+ * proposed (create, update, delete). No channel is touched.
15
8
  *
16
- * The daemon only broadcasts the prompt and waits. It never writes contacts.
17
- * All writes go through the gateway.
9
+ * Flow, identical for both:
10
+ * 1. CLI calls the IPC route with what the assistant proposes.
11
+ * 2. The route parks the call in the guardian-form registry, which broadcasts
12
+ * a `contact_request` / `contact_record_request` to connected clients.
13
+ * 3. Client shows the form. The guardian edits and submits, or dismisses.
14
+ * 4. Client POSTs to the gateway (`/v1/contacts/prompt/submit` or
15
+ * `/v1/contacts/record/submit`).
16
+ * 5. Gateway performs the write (gateway owns all contact writes), attesting
17
+ * the channel when the guardian left the verify box checked.
18
+ * 6. Gateway calls daemon IPC `resolve_contact_prompt`.
19
+ * 7. The registry resolves the parked promise and the CLI call returns.
20
+ *
21
+ * The daemon only broadcasts and waits. It never writes contacts. That is what
22
+ * makes these commands guardian-gated: with no human at a form, nothing is
23
+ * written, and the call times out.
24
+ *
25
+ * The lifecycle itself lives in `runtime/guardian-form-registry.ts` and is
26
+ * shared by every guardian form. What is here is the contact-shaped half: the
27
+ * schemas, the two wire events, and the result the CLI gets back.
18
28
  */
19
29
 
20
- import { v4 as uuid } from "uuid";
21
30
  import { z } from "zod";
22
31
 
23
- import { getLogger } from "../../util/logger.js";
32
+ import { GUARDIAN_FORM_MAX_TIMEOUT_MS } from "../../util/guardian-form-timeouts.js";
24
33
  import { broadcastMessage } from "../assistant-event-hub.js";
25
34
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
35
+ import {
36
+ getGuardianFormMeta,
37
+ type GuardianFormClosedReason,
38
+ hasUnclaimedGuardianForm,
39
+ openGuardianForm,
40
+ } from "../guardian-form-registry.js";
41
+ import { claimForm, resolveFormFromCallback } from "./guardian-form-routes.js";
26
42
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
27
43
 
28
- const log = getLogger("contact-prompt-routes");
29
-
30
- /** Timeout for waiting on the user to submit the contact form (5 min). */
31
- const CONTACT_PROMPT_TIMEOUT_MS = 300_000;
44
+ /**
45
+ * Form kinds on the guardian-form rail that carry a contact card.
46
+ *
47
+ * The gateway names the same kind when it claims, so these two strings are a
48
+ * cross-package contract. `contact-form-kinds.test.ts` on each side pins the
49
+ * literals, since a rename on one side alone would only surface as claims
50
+ * rejected at runtime.
51
+ */
52
+ export const CONTACT_ADDRESS_FORM_KIND = "contacts.address";
53
+ export const CONTACT_RECORD_FORM_KIND = "contacts.record";
54
+ const ADDRESS_FORM = CONTACT_ADDRESS_FORM_KIND;
55
+ const RECORD_FORM = CONTACT_RECORD_FORM_KIND;
56
+ const CONTACT_FORMS = [ADDRESS_FORM, RECORD_FORM] as const;
32
57
 
33
- // ---------------------------------------------------------------------------
34
- // Pending contact prompts
35
- // ---------------------------------------------------------------------------
58
+ const TimeoutMsParam = z
59
+ .number()
60
+ .int()
61
+ .positive()
62
+ .max(GUARDIAN_FORM_MAX_TIMEOUT_MS)
63
+ .optional()
64
+ .describe(
65
+ "How long to hold the form open (ms). The caller waits slightly longer than this, so the form closing is what ends the wait. Defaults to 300000.",
66
+ );
36
67
 
68
+ /** What the CLI gets back once the guardian has answered, or not. */
37
69
  export interface ContactPromptResult {
38
70
  ok: boolean;
39
71
  error?: string;
@@ -41,56 +73,28 @@ export interface ContactPromptResult {
41
73
  channelId?: string;
42
74
  channelType?: string;
43
75
  address?: string;
76
+ /** Whether the channel is attested, as the guardian's checkbox left it. */
77
+ verified?: boolean;
78
+ /**
79
+ * Whether submitted notes reached storage. Absent when none were submitted.
80
+ * False means the contact was written without them.
81
+ */
82
+ notesSaved?: boolean;
83
+ /** Whether nothing the submission asked for landed. */
84
+ nothingWritten?: boolean;
44
85
  }
45
86
 
46
- interface PendingContactPrompt {
47
- resolve: (result: ContactPromptResult) => void;
48
- timer: ReturnType<typeof setTimeout>;
49
- /** When true, the gateway marks the submitted channel verified (manual attest). */
50
- verify: boolean;
51
- }
52
-
53
- const pendingContactPrompts = new Map<string, PendingContactPrompt>();
54
-
55
87
  /**
56
- * Called by the gateway after it writes the contact and channel.
57
- * Resolves the pending promise so the CLI's `contacts/prompt` IPC call returns.
88
+ * Tell every client a contact form is over.
89
+ *
90
+ * The form went to all of them, so one client answering leaves the others
91
+ * holding a card that would now be refused. This is what takes those down.
58
92
  */
59
- function resolveContactPrompt({ body = {} }: RouteHandlerArgs): {
60
- resolved: boolean;
61
- } {
62
- const { requestId, contactId, channelId, channelType, address, error } =
63
- body as {
64
- requestId: string;
65
- contactId?: string;
66
- channelId?: string;
67
- channelType?: string;
68
- address?: string;
69
- error?: string;
70
- };
71
- const pending = pendingContactPrompts.get(requestId);
72
- if (!pending) {
73
- log.warn({ requestId }, "resolve_contact_prompt: no pending prompt found");
74
- return { resolved: false };
75
- }
76
-
77
- clearTimeout(pending.timer);
78
- pendingContactPrompts.delete(requestId);
79
-
80
- if (error) {
81
- pending.resolve({ ok: false, error });
82
- } else {
83
- pending.resolve({
84
- ok: true,
85
- contactId,
86
- channelId,
87
- channelType,
88
- address,
89
- });
90
- }
91
-
92
- log.info({ requestId, contactId }, "Contact prompt resolved");
93
- return { resolved: true };
93
+ function announceContactFormClosed(
94
+ requestId: string,
95
+ reason: GuardianFormClosedReason,
96
+ ): void {
97
+ broadcastMessage({ type: "contact_form_closed", requestId, reason });
94
98
  }
95
99
 
96
100
  // ---------------------------------------------------------------------------
@@ -130,8 +134,62 @@ const ContactPromptParams = z.object({
130
134
  .boolean()
131
135
  .optional()
132
136
  .describe(
133
- "When true, submitting the address also marks the channel verified. Same attest as Contacts Verify me.",
137
+ "Pre-check the form's 'mark verified' box. The guardian's answer on submit decides the attest, so an unchecked box leaves the channel unverified.",
138
+ ),
139
+ timeoutMs: TimeoutMsParam,
140
+ });
141
+
142
+ const ContactRecordPromptParams = z.object({
143
+ operation: z
144
+ .enum(["create", "update", "delete"])
145
+ .describe("Which record write the guardian is being asked to confirm."),
146
+ contactId: z
147
+ .string()
148
+ .optional()
149
+ .describe("Target contact. Required for update and delete."),
150
+ currentDisplayName: z
151
+ .string()
152
+ .optional()
153
+ .describe(
154
+ "The target's current name, resolved by the caller, so the form can show what is changing. Gateway-owned facts are not read here.",
155
+ ),
156
+ currentNotes: z
157
+ .string()
158
+ .optional()
159
+ .describe(
160
+ "The target's current notes, resolved by the caller. The form submits a field only when it differs from what is stored.",
161
+ ),
162
+ channels: z
163
+ .array(z.object({ type: z.string(), address: z.string() }))
164
+ .optional()
165
+ .describe(
166
+ "The target's channels, resolved by the caller, so a delete confirmation can identify the contact and show what access is about to be lost.",
167
+ ),
168
+
169
+ displayName: z
170
+ .string()
171
+ .optional()
172
+ .describe(
173
+ "Proposed name, prefilled into the form. The guardian can edit it.",
174
+ ),
175
+ notes: z
176
+ .string()
177
+ .optional()
178
+ .describe(
179
+ "Proposed notes, prefilled into the form. The guardian can edit them.",
180
+ ),
181
+ notesProposed: z
182
+ .boolean()
183
+ .optional()
184
+ .describe(
185
+ "Whether the caller asked for these notes explicitly, so the form submits them even when they match what is stored.",
134
186
  ),
187
+ label: z.string().optional().describe("Display label shown in the form."),
188
+ description: z
189
+ .string()
190
+ .optional()
191
+ .describe("Longer description shown in the form."),
192
+ timeoutMs: TimeoutMsParam,
135
193
  });
136
194
 
137
195
  const ContactPromptFlagsParams = z.object({
@@ -139,7 +197,7 @@ const ContactPromptFlagsParams = z.object({
139
197
  });
140
198
 
141
199
  // ---------------------------------------------------------------------------
142
- // Handler
200
+ // Handlers
143
201
  // ---------------------------------------------------------------------------
144
202
 
145
203
  async function handleContactPrompt({
@@ -153,52 +211,112 @@ async function handleContactPrompt({
153
211
  description,
154
212
  role,
155
213
  verify,
214
+ timeoutMs,
156
215
  } = ContactPromptParams.parse(body);
157
216
 
158
- const requestId = uuid();
217
+ return openGuardianForm<ContactPromptResult>({
218
+ kind: ADDRESS_FORM,
219
+ timeoutMs,
220
+ // Read back by the gateway when a client too old for the checkbox submits
221
+ // no answer of its own.
222
+ meta: { verify: verify === true },
223
+ logContext: { channel, role, verify: verify === true },
224
+ broadcast: {
225
+ open: (requestId) =>
226
+ broadcastMessage({
227
+ type: "contact_request",
228
+ requestId,
229
+ channel,
230
+ placeholder,
231
+ defaultValue,
232
+ label,
233
+ description,
234
+ role,
235
+ // The checkbox's initial state. Carried in the broadcast so the form
236
+ // can show what submitting will do; the client's answer is what the
237
+ // gateway acts on.
238
+ verify: verify === true,
239
+ }),
240
+ closed: announceContactFormClosed,
241
+ },
242
+ });
243
+ }
159
244
 
160
- return new Promise((resolve) => {
161
- const timer = setTimeout(() => {
162
- pendingContactPrompts.delete(requestId);
163
- log.warn({ requestId }, "Contact prompt timed out");
164
- resolve({ ok: false, error: "Prompt timed out" });
165
- }, CONTACT_PROMPT_TIMEOUT_MS);
245
+ /**
246
+ * Park a proposed contact-record write until the guardian answers the form.
247
+ *
248
+ * The daemon writes nothing. It broadcasts the proposal and waits for the
249
+ * gateway to report what the guardian actually submitted, which may differ
250
+ * from what was proposed or may be a dismissal.
251
+ */
252
+ async function handleContactRecordPrompt({
253
+ body = {},
254
+ }: RouteHandlerArgs): Promise<ContactPromptResult> {
255
+ const {
256
+ operation,
257
+ contactId,
258
+ currentDisplayName,
259
+ currentNotes,
260
+ channels,
261
+ displayName,
262
+ notes,
263
+ notesProposed,
264
+ label,
265
+ description,
266
+ timeoutMs,
267
+ } = ContactRecordPromptParams.parse(body);
166
268
 
167
- pendingContactPrompts.set(requestId, {
168
- resolve,
169
- timer,
170
- verify: verify === true,
171
- });
269
+ if (operation !== "create" && !contactId) {
270
+ return { ok: false, error: `contactId is required to ${operation}` };
271
+ }
172
272
 
173
- broadcastMessage({
174
- type: "contact_request",
175
- requestId,
176
- channel,
177
- placeholder,
178
- defaultValue,
179
- label,
180
- description,
181
- role,
182
- });
273
+ // Clients hold one contact card at a time, so a second broadcast replaces the
274
+ // first and leaves its command waiting on a form nobody can answer. Refusing
275
+ // here fails the second command immediately instead, which is a caller that
276
+ // can retry rather than one that hangs.
277
+ if (hasUnclaimedGuardianForm(CONTACT_FORMS)) {
278
+ return {
279
+ ok: false,
280
+ error:
281
+ "Another contact form is already open. Wait for it to be answered, then try again.",
282
+ };
283
+ }
183
284
 
184
- log.info(
185
- { requestId, channel, role, verify: verify === true },
186
- "Contact prompt broadcast",
187
- );
285
+ return openGuardianForm<ContactPromptResult>({
286
+ kind: RECORD_FORM,
287
+ timeoutMs,
288
+ logContext: { operation, contactId },
289
+ broadcast: {
290
+ open: (requestId) =>
291
+ broadcastMessage({
292
+ type: "contact_record_request",
293
+ requestId,
294
+ operation,
295
+ contactId,
296
+ currentDisplayName,
297
+ currentNotes,
298
+ channels,
299
+ displayName,
300
+ notes,
301
+ notesProposed,
302
+ label,
303
+ description,
304
+ }),
305
+ closed: announceContactFormClosed,
306
+ },
188
307
  });
189
308
  }
190
309
 
191
310
  /**
192
- * Read-only flags for a pending prompt. The gateway asks this after it
193
- * writes the channel so a `--verify` prompt can attest without the client
194
- * having to echo the flag on submit.
311
+ * Read-only flags for a pending prompt. The gateway asks this after it writes
312
+ * the channel so a `--verify` prompt can attest without the client having to
313
+ * echo the flag on submit.
195
314
  */
196
315
  function readContactPromptFlags({ body = {} }: RouteHandlerArgs): {
197
316
  verify: boolean;
198
317
  } {
199
318
  const { requestId } = ContactPromptFlagsParams.parse(body);
200
- const pending = pendingContactPrompts.get(requestId);
201
- return { verify: pending?.verify === true };
319
+ return { verify: getGuardianFormMeta(requestId)?.verify === true };
202
320
  }
203
321
 
204
322
  // ---------------------------------------------------------------------------
@@ -217,7 +335,7 @@ export const CONTACT_PROMPT_ROUTES: RouteDefinition[] = [
217
335
  handler: handleContactPrompt,
218
336
  summary: "Prompt user to register a contact channel",
219
337
  description:
220
- "Broadcasts a contact_request to connected clients, waits for the user to submit an address via the gateway. The gateway owns the contact write and notifies the daemon via resolve_contact_prompt IPC.",
338
+ "Broadcasts a contact_request to connected clients, waits for the user to submit an address via the gateway. The gateway owns the contact write and reports it back via resolve_contact_prompt.",
221
339
  tags: ["contacts"],
222
340
  requestBody: ContactPromptParams,
223
341
  responseBody: z.object({
@@ -227,6 +345,29 @@ export const CONTACT_PROMPT_ROUTES: RouteDefinition[] = [
227
345
  channelId: z.string().optional(),
228
346
  channelType: z.string().optional(),
229
347
  address: z.string().optional(),
348
+ verified: z.boolean().optional(),
349
+ }),
350
+ },
351
+ {
352
+ operationId: "contacts_record_prompt",
353
+ endpoint: "contacts/record-prompt",
354
+ method: "POST",
355
+ policy: {
356
+ requiredScopes: ["settings.write"],
357
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
358
+ },
359
+ handler: handleContactRecordPrompt,
360
+ summary: "Ask the guardian to confirm a contact record write",
361
+ description:
362
+ "Broadcasts a contact_record_request to connected clients and waits for the guardian to submit the form. The gateway owns the write and reports it back via resolve_contact_prompt. Nothing is written if nobody answers.",
363
+ tags: ["contacts"],
364
+ requestBody: ContactRecordPromptParams,
365
+ responseBody: z.object({
366
+ ok: z.boolean(),
367
+ error: z.string().optional(),
368
+ contactId: z.string().optional(),
369
+ notesSaved: z.boolean().optional(),
370
+ nothingWritten: z.boolean().optional(),
230
371
  }),
231
372
  },
232
373
  {
@@ -237,12 +378,37 @@ export const CONTACT_PROMPT_ROUTES: RouteDefinition[] = [
237
378
  requiredScopes: ["settings.write"],
238
379
  allowedPrincipalTypes: ACTOR_PRINCIPALS,
239
380
  },
240
- handler: resolveContactPrompt,
381
+ handler: resolveFormFromCallback,
241
382
  summary: "Gateway callback: resolve a pending contact prompt",
242
383
  description:
243
- "Called by the gateway after it writes the contact and channel. Unblocks the waiting contacts/prompt IPC call.",
384
+ "Called by the gateway after it writes the contact and channel. Unblocks the waiting contacts/prompt call.",
244
385
  tags: ["contacts"],
245
386
  },
387
+ {
388
+ operationId: "contact_prompt_claim",
389
+ endpoint: "contact_prompt_claim",
390
+ method: "POST",
391
+ policy: {
392
+ requiredScopes: ["settings.write"],
393
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
394
+ },
395
+ handler: claimForm,
396
+ summary: "Claim a pending contact form for one submission",
397
+ description:
398
+ "Marks a pending form as answered so a second client submitting the same form writes nothing. Returns claimed=false with reason 'already_claimed' when somebody got there first, or 'unknown' when no such form is pending. A granted claim carries the window its write has to report back in.",
399
+ tags: ["contacts"],
400
+ requestBody: ContactPromptFlagsParams,
401
+ responseBody: z.object({
402
+ claimed: z.boolean(),
403
+ reason: z.enum(["already_claimed", "unknown"]).optional(),
404
+ settleMs: z
405
+ .number()
406
+ .optional()
407
+ .describe(
408
+ "How long a granted claim has to report its write back before the waiting call gives up.",
409
+ ),
410
+ }),
411
+ },
246
412
  {
247
413
  operationId: "contact_prompt_flags",
248
414
  endpoint: "contact_prompt_flags",
@@ -23,8 +23,9 @@ import {
23
23
  type ConversationMessage,
24
24
  ConversationMessageSchema,
25
25
  } from "../../api/responses/conversation-message.js";
26
+ import { GUARDIAN_TERMINAL_REASON_SUPERSEDED } from "../../api/responses/home.js";
27
+ import { syncTerminalGuardianRequestStatus } from "../../approvals/guardian-request-status-sync.js";
26
28
  import {
27
- decideGuardianRequest,
28
29
  expireGuardianRequest,
29
30
  listGuardianRequestsOrEmpty,
30
31
  listPendingRequestsByScopeOrEmpty,
@@ -97,13 +98,14 @@ import {
97
98
  readSlackMetadataFromMessageMetadata,
98
99
  type SlackMessageMetadata,
99
100
  } from "../../messaging/providers/slack/message-metadata.js";
101
+ import { readProviderMetadata } from "../../messaging/read-provider-metadata.js";
100
102
  import { recordOnboardingEvent } from "../../onboarding/onboarding-events-store.js";
101
103
  import {
102
104
  classifyKind,
103
105
  getAttachmentById,
104
106
  getAttachmentMetadataForMessage,
105
107
  getAttachmentsByIds,
106
- getSourcePathsForAttachments,
108
+ resolveAttachmentsForPersist,
107
109
  } from "../../persistence/attachments-store.js";
108
110
  import {
109
111
  addMessage,
@@ -127,6 +129,7 @@ import {
127
129
  getOrCreateConversation,
128
130
  } from "../../persistence/conversation-key-store.js";
129
131
  import { searchConversations } from "../../persistence/conversation-queries.js";
132
+ import { isNoResponseMetadata } from "../../persistence/conversation-types.js";
130
133
  import { linkRequestLogsToMessage } from "../../persistence/llm-request-log-store.js";
131
134
  import { MEMORY_RETROSPECTIVE_FORK_SOURCE } from "../../plugins/defaults/memory/memory-retrospective-constants.js";
132
135
  import { normalizeOnboardingContext } from "../../prompts/normalize-onboarding.js";
@@ -163,6 +166,7 @@ import {
163
166
  resolveActorPrincipalIdForLocalGuardian,
164
167
  } from "../local-actor-identity.js";
165
168
  import { resolveLocalPrincipalTrustContext } from "../local-principal-trust.js";
169
+ import { stripNoResponseMarkers } from "../no-response.js";
166
170
  import * as pendingInteractions from "../pending-interactions.js";
167
171
  import {
168
172
  publishConversationListAndMetadataChanged,
@@ -194,8 +198,6 @@ import { RouteResponse } from "./types.js";
194
198
 
195
199
  const log = getLogger("conversation-routes");
196
200
 
197
- /** Matches the `<no_response/>` sentinel used by channel delivery suppression. */
198
- const NO_RESPONSE_INLINE_RE = /<no_response\s*\/?>/g;
199
201
  const ATTACHMENT_ENTRY_RE = /^attachment:(\d+)$/;
200
202
 
201
203
  /** Rewrites a rendered `contentOrder` to reflect attachment alignment. */
@@ -997,6 +999,8 @@ export async function handleListMessages({
997
999
  {};
998
1000
  let backgroundToolCompletion: ConversationMessage["backgroundToolCompletion"];
999
1001
  let systemCard: boolean | undefined;
1002
+ let noResponse: boolean | undefined;
1003
+ let reaction: ConversationMessage["reaction"];
1000
1004
  let providerError: ConversationMessage["providerError"];
1001
1005
  if (msg.metadata) {
1002
1006
  try {
@@ -1009,6 +1013,25 @@ export async function handleListMessages({
1009
1013
  if (isSystemCardMetadata(meta)) {
1010
1014
  systemCard = true;
1011
1015
  }
1016
+ if (isNoResponseMetadata(meta)) {
1017
+ noResponse = true;
1018
+ }
1019
+ // A reaction row, either direction, projects its structured fact so
1020
+ // clients never render the stored "[reaction]" sentinel.
1021
+ if (msg.metadata.includes("reaction")) {
1022
+ const reactionMeta = readProviderMetadata(msg.metadata);
1023
+ if (reactionMeta?.eventKind === "reaction" && reactionMeta.reaction) {
1024
+ reaction = {
1025
+ emoji: reactionMeta.reaction.emoji,
1026
+ op: reactionMeta.reaction.op,
1027
+ targetMessageId: reactionMeta.reaction.targetMessageId,
1028
+ ...(reactionMeta.reaction.actorDisplayName
1029
+ ? { actorDisplayName: reactionMeta.reaction.actorDisplayName }
1030
+ : {}),
1031
+ ...(msg.role === "assistant" ? { selfAuthored: true } : {}),
1032
+ };
1033
+ }
1034
+ }
1012
1035
  // Daemon-persisted provider-failure notices carry the classified
1013
1036
  // error code/category so clients can render a themed card instead
1014
1037
  // of a persona bubble.
@@ -1061,6 +1084,8 @@ export async function handleListMessages({
1061
1084
  backgroundEventNotification: notifications.backgroundEventNotification,
1062
1085
  backgroundToolCompletion,
1063
1086
  systemCard,
1087
+ noResponse,
1088
+ reaction,
1064
1089
  providerError,
1065
1090
  slackMessage,
1066
1091
  clientMessageId: msg.clientMessageId ?? undefined,
@@ -1175,9 +1200,7 @@ export async function handleListMessages({
1175
1200
  const keepIndices: number[] = [];
1176
1201
  const filteredSegments: string[] = [];
1177
1202
  for (let i = 0; i < rendered.textSegments.length; i++) {
1178
- const cleaned = rendered.textSegments[i]
1179
- .replace(NO_RESPONSE_INLINE_RE, "")
1180
- .trim();
1203
+ const cleaned = stripNoResponseMarkers(rendered.textSegments[i]);
1181
1204
  if (cleaned.length > 0) {
1182
1205
  keepIndices.push(i);
1183
1206
  filteredSegments.push(cleaned);
@@ -1201,7 +1224,7 @@ export async function handleListMessages({
1201
1224
  block.type === "text"
1202
1225
  ? {
1203
1226
  type: "text" as const,
1204
- text: block.text.replace(NO_RESPONSE_INLINE_RE, "").trim(),
1227
+ text: stripNoResponseMarkers(block.text),
1205
1228
  }
1206
1229
  : block,
1207
1230
  )
@@ -1271,6 +1294,8 @@ export async function handleListMessages({
1271
1294
  ? { backgroundToolCompletion: m.backgroundToolCompletion }
1272
1295
  : {}),
1273
1296
  ...(m.systemCard ? { systemCard: true } : {}),
1297
+ ...(m.noResponse ? { noResponse: true } : {}),
1298
+ ...(m.reaction ? { reaction: m.reaction } : {}),
1274
1299
  ...(m.providerError ? { providerError: m.providerError } : {}),
1275
1300
  ...(m.slackMessage ? { slackMessage: m.slackMessage } : {}),
1276
1301
  };
@@ -2323,18 +2348,16 @@ export async function handleSendMessage(
2323
2348
  source: "auto_deny" as const,
2324
2349
  });
2325
2350
  // Sync the gateway request status so stale "pending" records don't
2326
- // get matched by later guardian reply routing. Fire-and-forget: the
2351
+ // get matched by later guardian reply routing, and withdraw the
2352
+ // request's delivered approval cards so no surface keeps offering a
2353
+ // decision that can no longer resolve anything. Fire-and-forget: the
2327
2354
  // in-memory denial is authoritative here; a CAS miss (already
2328
2355
  // decided elsewhere) or a lost sync is reaped by the orphan sweep.
2329
- void decideGuardianRequest({
2330
- id: interaction.requestId,
2331
- expectedStatus: "pending",
2356
+ void syncTerminalGuardianRequestStatus({
2357
+ requestId: interaction.requestId,
2332
2358
  status: "denied",
2333
- }).catch((err) => {
2334
- log.warn(
2335
- { err, requestId: interaction.requestId },
2336
- "Auto-deny guardian request status sync failed",
2337
- );
2359
+ syncContext: "auto-deny-idle-send",
2360
+ terminalReason: GUARDIAN_TERMINAL_REASON_SUPERSEDED,
2338
2361
  });
2339
2362
  }
2340
2363
  }
@@ -3027,20 +3050,6 @@ async function handleSearchConversations({
3027
3050
  const suggestionCache = new Map<string, string>();
3028
3051
  const suggestionInFlight = new Map<string, Promise<string | null>>();
3029
3052
 
3030
- function resolveAttachments(attachmentIds: string[]) {
3031
- const resolved = getAttachmentsByIds(attachmentIds, {
3032
- hydrateFileData: true,
3033
- });
3034
- const sourcePaths = getSourcePathsForAttachments(attachmentIds);
3035
- return resolved.map((a) => ({
3036
- id: a.id,
3037
- filename: a.originalFilename,
3038
- mimeType: a.mimeType,
3039
- data: a.dataBase64,
3040
- ...(sourcePaths.has(a.id) ? { filePath: sourcePaths.get(a.id) } : {}),
3041
- }));
3042
- }
3043
-
3044
3053
  // ---------------------------------------------------------------------------
3045
3054
  // Route definitions
3046
3055
  // ---------------------------------------------------------------------------
@@ -3260,7 +3269,7 @@ export const ROUTES: RouteDefinition[] = [
3260
3269
  sendMessageDeps: {
3261
3270
  getOrCreateConversation: getOrCreateConversationInstance,
3262
3271
  assistantEventHub,
3263
- resolveAttachments,
3272
+ resolveAttachments: resolveAttachmentsForPersist,
3264
3273
  },
3265
3274
  approvalConversationGenerator: createApprovalConversationGenerator(),
3266
3275
  }),
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { z } from "zod";
10
10
 
11
+ import { acpConnectCardStillWarranted } from "../../acp/acp-claude-oauth.js";
11
12
  import { hasAcpConnectCardRaised } from "../../acp/acp-connect-card-state.js";
12
13
  import { isAcpClaudeOauthField } from "../../acp/acp-credentials.js";
13
14
  import {
@@ -112,10 +113,18 @@ async function handleCredentialPrompt({ body = {} }: RouteHandlerArgs) {
112
113
  // fallback, so `claude setup-token` remains usable where no card can appear.
113
114
  if (isAcpClaudeOauthField(validated.service, validated.field)) {
114
115
  const conversation = findConversation(validated.conversationId);
116
+ // The registry says a card was raised; this asks whether it is still one
117
+ // the user can act on. Auth can be repaired without any credential write
118
+ // (a `config.json` edit to the agent's `CLAUDE_CODE_OAUTH_TOKEN` does it),
119
+ // and nothing runs on that path to forget the entry, so an entry alone
120
+ // would keep suppressing the fallback against a card that stopped
121
+ // rendering.
115
122
  if (
116
123
  conversation &&
117
124
  conversationSupportsDynamicUi(conversation) &&
118
- hasAcpConnectCardRaised(validated.conversationId)
125
+ validated.conversationId !== undefined &&
126
+ hasAcpConnectCardRaised(validated.conversationId) &&
127
+ (await acpConnectCardStillWarranted(validated.conversationId))
119
128
  ) {
120
129
  return {
121
130
  ok: false,
@@ -97,6 +97,7 @@ function handleDebugBash({ body }: RouteHandlerArgs): Promise<DebugBashResult> {
97
97
  stdio: ["ignore", "pipe", "pipe"],
98
98
  detached: true,
99
99
  env: buildSanitizedEnv(),
100
+ windowsHide: true,
100
101
  });
101
102
 
102
103
  const timer = setTimeout(() => {