@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
@@ -184,18 +184,18 @@ export async function getOrCreateConversation(
184
184
  // messages live in memory on the instance being disposed, and the queue
185
185
  // drains via an async dispatch after the current turn releases, so
186
186
  // `isProcessing()` can read false while a queued turn is still pending:
187
- // rebuilding in that gap would silently drop those messages. The conversation
188
- // stays stale and is rebuilt on a later call.
187
+ // rebuilding in that gap would silently drop those messages. In-flight
188
+ // subagents are the same: the parent reads idle between its own tool calls
189
+ // while children still run, and rebuilding would abort them. The
190
+ // conversation stays stale and is rebuilt on a later call.
189
191
  if (
190
192
  !conversation ||
191
- (conversation.isStale() &&
192
- !conversation.isProcessing() &&
193
- !conversation.hasQueuedMessages())
193
+ (conversation.isStale() && !conversation.hasInFlightWork())
194
194
  ) {
195
195
  if (conversation) {
196
- // Stale rebuild: the conversation id lives on, so abort in-flight
197
- // children but keep terminal subagent results readable for the
198
- // retention window.
196
+ // Stale rebuild: the conversation id lives on, so abort any children
197
+ // that raced into flight after the idle check and keep terminal
198
+ // subagent results readable for the retention window.
199
199
  getSubagentManager().abortAllForParent(conversationId);
200
200
  conversation.dispose();
201
201
  }
@@ -400,32 +400,22 @@ export function clearAllActiveConversations(): number {
400
400
 
401
401
  /**
402
402
  * Evict in-memory conversations after a config/prompt/skills reload so the next
403
- * turn rebuilds them against the new config. Idle conversations are disposed and
404
- * dropped; busy ones are marked stale so they're rebuilt once their current turn
405
- * finishes. Conversations with in-flight subagents are left in place so a
406
- * reload does not abort those children. Also used when provider credentials
403
+ * turn rebuilds them against the new config. Conversations with in-flight work
404
+ * (a live turn, a queued successor, or an active subagent) are marked stale
405
+ * and rebuilt by `getOrCreateConversation` once that work finishes. Idle
406
+ * conversations are disposed and dropped. Also used when provider credentials
407
407
  * change.
408
408
  */
409
409
  export function evictConversationsForReload(): void {
410
410
  const subagentManager = getSubagentManager();
411
411
  for (const [id, conversation] of conversationEntries()) {
412
- // A conversation with queued messages is not idle: the queue drains via an
413
- // async dispatch after the current turn releases, so `isProcessing()` can
414
- // read false while a queued turn is still pending. Disposing in that gap
415
- // would silently drop the queued messages, so mark it stale instead and
416
- // let it rebuild once the queue has run.
417
- if (!conversation.isProcessing() && !conversation.hasQueuedMessages()) {
418
- // Same protection the TTL/LRU evictor applies: an idle parent still
419
- // owns mid-task children, and aborting them here would kill that work.
420
- if (subagentManager.hasActiveChildren(id)) {
421
- continue;
422
- }
423
- subagentManager.abortAllForParent(id);
424
- conversation.dispose();
425
- deleteConversation(id);
426
- removeFromEvictor(id);
427
- } else {
412
+ if (conversation.hasInFlightWork()) {
428
413
  conversation.markStale();
414
+ continue;
429
415
  }
416
+ subagentManager.abortAllForParent(id);
417
+ conversation.dispose();
418
+ deleteConversation(id);
419
+ removeFromEvictor(id);
430
420
  }
431
421
  }
@@ -543,19 +543,29 @@ export function findPersistedSurfaceInfo(
543
543
  * user-action completion paths: the underlying request is already resolved, so
544
544
  * withholding the announcement on a transient write failure strands a live,
545
545
  * clickable approval card for a decision that has already been made.
546
+ *
547
+ * Returns whether the completion persisted durably (see
548
+ * {@link markSurfaceCompleted}); the broadcast goes out either way. A caller
549
+ * whose own receipt depends on the card staying completed across a reload
550
+ * (the expiry sweep) holds that receipt back on false and retries.
546
551
  */
547
552
  export function completeSurfaceAndNotify(
548
553
  conversationId: string,
549
554
  surfaceId: string,
550
555
  summary: string,
551
- ): void {
552
- markSurfaceCompleted({ conversationId }, surfaceId, summary);
556
+ ): boolean {
557
+ const persisted = markSurfaceCompleted(
558
+ { conversationId },
559
+ surfaceId,
560
+ summary,
561
+ );
553
562
  broadcastMessage({
554
563
  type: "ui_surface_complete",
555
564
  conversationId,
556
565
  surfaceId,
557
566
  summary,
558
567
  });
568
+ return persisted;
559
569
  }
560
570
 
561
571
  /**
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { AssistantEvent } from "../api/index.js";
10
10
  import {
11
+ type ClientOs,
11
12
  type HostProxyCapability,
12
13
  parseClientOs,
13
14
  supportsHostProxy,
@@ -15,6 +16,7 @@ import {
15
16
  import { getIsPlatform } from "../config/env-registry.js";
16
17
  import { getConfig } from "../config/loader.js";
17
18
  import { isMemoryEnabled } from "../config/memory-v3-gate.js";
19
+ import { supportsChannelReaction } from "../messaging/providers/index.js";
18
20
  import type { PermissionPrompter } from "../permissions/prompter.js";
19
21
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
20
22
  import { getBindingByConversation } from "../persistence/external-conversation-store.js";
@@ -433,8 +435,7 @@ export function createToolExecutor(
433
435
  toolUseId,
434
436
  isPlatformHosted: getIsPlatform(),
435
437
  transportInterface: ctx.transportInterface,
436
- clientOs:
437
- parseClientOs(ctx.currentTurnClientOs ?? ctx.clientOs) ?? undefined,
438
+ clientOs: resolveTurnClientOs(ctx).clientOs,
438
439
  overrideProfile: ctx.currentTurnOverrideProfile,
439
440
  cronRunId: ctx.currentTurnCronRunId,
440
441
  invokingCallSite: ctx.currentCallSite ?? "mainAgent",
@@ -713,14 +714,14 @@ export const ALLOWLIST_ONLY_TOOL_NAMES = new Set<string>([
713
714
  ]);
714
715
 
715
716
  /**
716
- * Windows parity gate: skill tools may declare `supported_client_os`; drop
717
- * them when the turn's client OS (or pinned OS for wakes) is not listed.
717
+ * Host OS of the client driving this turn. The Electron renderer reports
718
+ * `interface: "web"` and carries the real OS in `clientOs`, so this prefers
719
+ * the frozen per-turn value and only falls back to a desktop transport.
718
720
  */
719
- function isToolSupportedOnClientOs(name: string, ctx: Conversation): boolean {
720
- const supportedClientOs = getTool(name)?.supportedClientOs;
721
- if (!supportedClientOs) {
722
- return true;
723
- }
721
+ function resolveTurnClientOs(ctx: Conversation): {
722
+ clientOs: ClientOs | undefined;
723
+ transportInterface: Conversation["transportInterface"];
724
+ } {
724
725
  const pin = ctx.toolContextPin;
725
726
  const transportInterface = pin
726
727
  ? pin.transportInterface
@@ -731,6 +732,19 @@ function isToolSupportedOnClientOs(name: string, ctx: Conversation): boolean {
731
732
  (transportInterface === "macos" || transportInterface === "windows"
732
733
  ? transportInterface
733
734
  : undefined));
735
+ return { clientOs, transportInterface };
736
+ }
737
+
738
+ /**
739
+ * Windows parity gate: skill tools may declare `supported_client_os`; drop
740
+ * them when the turn's client OS (or pinned OS for wakes) is not listed.
741
+ */
742
+ function isToolSupportedOnClientOs(name: string, ctx: Conversation): boolean {
743
+ const supportedClientOs = getTool(name)?.supportedClientOs;
744
+ if (!supportedClientOs) {
745
+ return true;
746
+ }
747
+ const { clientOs, transportInterface } = resolveTurnClientOs(ctx);
734
748
  return supportsClientOsForSkillTool(supportedClientOs, name, {
735
749
  clientOs,
736
750
  transportInterface,
@@ -808,6 +822,20 @@ export function isToolActiveForContext(
808
822
  return true;
809
823
  }
810
824
  }
825
+ // The react capability follows the transport's declaration: the tool is on
826
+ // the wire exactly when the turn's channel transport implements `react`,
827
+ // so the model never sees an option the channel cannot honor. The turn's
828
+ // own capabilities take precedence over the conversation's structural ones
829
+ // (same order as `conversationSupportsDynamicUi`): an app-side turn on a
830
+ // channel-origin conversation has no channel message to react to, and the
831
+ // executor's channel comes from the turn. Wake pins read as no channel
832
+ // and hide it.
833
+ if (name === "react_to_message") {
834
+ const turnChannel = pin
835
+ ? undefined
836
+ : (ctx.currentTurnChannelCapabilities ?? channelCapabilities)?.channel;
837
+ return supportsChannelReaction(turnChannel);
838
+ }
811
839
  if (UI_SURFACE_TOOL_NAMES.has(name)) {
812
840
  if (
813
841
  channelCapabilities?.channel === "slack" &&
@@ -871,7 +899,8 @@ export function isToolActiveForContext(
871
899
  if (PLATFORM_TOOL_NAMES.has(name)) {
872
900
  // Check the *client's* platform, not the daemon's process.platform.
873
901
  // In Docker the daemon runs on Linux but the connected client may be macOS.
874
- return channelCapabilities?.clientOS === "macos" && !hasNoClient;
902
+ const { clientOs } = resolveTurnClientOs(ctx);
903
+ return (clientOs === "macos" || clientOs === "windows") && !hasNoClient;
875
904
  }
876
905
  if (SUBAGENT_ONLY_TOOL_NAMES.has(name)) {
877
906
  return ctx.isSubagent === true;
@@ -21,7 +21,7 @@ import { AgentLoop } from "../agent/loop.js";
21
21
  import type { AssistantActivityStateEvent } from "../api/events/assistant-activity-state.js";
22
22
  import type { ConfirmationStateChangedEvent } from "../api/events/confirmation-state-changed.js";
23
23
  import type { AssistantEvent } from "../api/index.js";
24
- import { decideGuardianRequest } from "../channels/gateway-guardian-requests.js";
24
+ import { syncTerminalGuardianRequestStatus } from "../approvals/guardian-request-status-sync.js";
25
25
  import type {
26
26
  ChannelId,
27
27
  InterfaceId,
@@ -41,6 +41,7 @@ import {
41
41
  derefToolResultReReads,
42
42
  postTurnTruncateToolResults,
43
43
  } from "../context/post-turn-tool-result-truncation.js";
44
+ import { readProviderMetadata } from "../messaging/read-provider-metadata.js";
44
45
  import { isGuardianCardRow } from "../notifications/approval-card-data.js";
45
46
  import { PermissionPrompter } from "../permissions/prompter.js";
46
47
  import { SecretPrompter } from "../permissions/secret-prompter.js";
@@ -55,6 +56,7 @@ import {
55
56
  setConversationProcessingStartedAt,
56
57
  } from "../persistence/conversation-crud.js";
57
58
  import { getResolvedConversationDirPath } from "../persistence/conversation-directories.js";
59
+ import { extractTextFromStoredMessageContent } from "../persistence/message-content.js";
58
60
  import { reportSlowSync } from "../persistence/slow-sync-log.js";
59
61
  import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
60
62
  import {
@@ -137,6 +139,7 @@ import type {
137
139
  } from "./conversation-queue-manager.js";
138
140
  import { MessageQueue } from "./conversation-queue-manager.js";
139
141
  import {
142
+ applySightFrameRetention,
140
143
  type ChannelCapabilities,
141
144
  getSlackCompactionWatermarkForPrefix,
142
145
  getSlackWatermarkAdvanceForRowPrefix,
@@ -172,6 +175,8 @@ import { filterMessagesForUntrustedActor } from "./message-provenance.js";
172
175
  import type { ConversationTransportMetadata } from "./message-types/conversations.js";
173
176
  import { isHostProxyTransport } from "./message-types/conversations.js";
174
177
  import { conversationMetadataSyncTag } from "./message-types/sync.js";
178
+ import { renderReactionHistoryText } from "./reaction-history-render.js";
179
+ import type { QueuedReactionRecord } from "./reaction-record.js";
175
180
  import {
176
181
  resolveSummarizeBoundary,
177
182
  startsNewTurn,
@@ -538,6 +543,14 @@ export class Conversation {
538
543
  /** @internal */ currentTurnSourceActorPrincipalId?: string;
539
544
  /** @internal */ loadedHistoryTrustClass?: TrustClass;
540
545
  /** @internal */ loadedHistoryPersonalMemoryAllowed?: boolean;
546
+ /** @internal */ loadedHistoryStale = false;
547
+ /**
548
+ * @internal Reactions the current turn delivered, awaiting their durable
549
+ * rows. Written mid-turn by the react tool, drained by the agent loop at
550
+ * the turn boundary so the rows never land between a tool_use and its
551
+ * tool_result.
552
+ */
553
+ pendingReactionRecords: QueuedReactionRecord[] = [];
541
554
  /** @internal */ voiceCallControlPrompt?: string;
542
555
  /** @internal */ transportHints?: string[];
543
556
  /**
@@ -887,6 +900,16 @@ export class Conversation {
887
900
  conv.createdAt,
888
901
  );
889
902
  },
903
+ // Compaction rebuilds history from the stored rows, so it can retain
904
+ // camera frames the turn's pre-run pass had already stubbed, and the
905
+ // rebuilt array is sent on the next request of that same turn. Re-running
906
+ // the retention pass here is what holds the frame bound across a
907
+ // compaction instead of only from the next turn onward. The row read
908
+ // repeats rather than sharing the pre-run pass's map: compactions are
909
+ // rare, and a map captured before the turn's awaits would describe a
910
+ // conversation that has since gained rows.
911
+ transformCompactedHistory: (messages) =>
912
+ applySightFrameRetention(messages, this.conversationId),
890
913
  });
891
914
  createContextWindowManager({
892
915
  provider,
@@ -1052,6 +1075,9 @@ export class Conversation {
1052
1075
 
1053
1076
  async loadFromDb(): Promise<LoadFromDbResult> {
1054
1077
  const loadStartedAt = performance.now();
1078
+ // Cleared before the rows are read: a channel event persisted mid-load
1079
+ // re-marks the flag, so the next turn reloads rather than missing it.
1080
+ this.loadedHistoryStale = false;
1055
1081
  const trustClass = this.trustContext?.trustClass;
1056
1082
  const canAccessMemory = resolveCapabilities(trustClass).canAccessMemory;
1057
1083
  const allDbMessages = getMessages(this.conversationId);
@@ -1142,6 +1168,44 @@ export class Conversation {
1142
1168
  }
1143
1169
  return v3PrunedSlugsMemo;
1144
1170
  };
1171
+ // Provider-id → row-text index for reaction target resolution, built
1172
+ // lazily on the first reaction row: most conversations carry none, so
1173
+ // most loads never walk the rows a second time. Keyed over the full
1174
+ // (unsliced) history so a reaction whose target was compacted out of
1175
+ // context still resolves its quote.
1176
+ let reactionTargetIndexMemo: Map<string, string> | null = null;
1177
+ const resolveReactionTarget = (
1178
+ targetMessageId: string,
1179
+ ): string | undefined => {
1180
+ if (reactionTargetIndexMemo === null) {
1181
+ reactionTargetIndexMemo = new Map();
1182
+ for (const row of dbMessages) {
1183
+ const rowMeta = readProviderMetadata(row.metadata);
1184
+ // A deleted target is a logical erasure (same rule as the Slack
1185
+ // transcript renderer), so its text is never quoted back.
1186
+ if (
1187
+ rowMeta?.eventKind === "message" &&
1188
+ rowMeta.deletedAt === undefined &&
1189
+ rowMeta.messageId
1190
+ ) {
1191
+ const text = extractTextFromStoredMessageContent(row.content);
1192
+ if (text) {
1193
+ // A split reply posts several provider messages from one row;
1194
+ // a reaction may name any of them.
1195
+ for (const id of [
1196
+ rowMeta.messageId,
1197
+ ...(rowMeta.additionalMessageIds ?? []),
1198
+ ]) {
1199
+ if (!reactionTargetIndexMemo.has(id)) {
1200
+ reactionTargetIndexMemo.set(id, text);
1201
+ }
1202
+ }
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+ return reactionTargetIndexMemo.get(targetMessageId);
1208
+ };
1145
1209
  const parsedMessages: Message[] = slicedDbMessages.map((m, index, arr) => {
1146
1210
  const isPreStripped = index < preStrippedCount;
1147
1211
  const role = m.role as "user" | "assistant";
@@ -1149,6 +1213,38 @@ export class Conversation {
1149
1213
 
1150
1214
  content = reinjectAttachmentPathAnnotations(content, role, m.metadata);
1151
1215
 
1216
+ // Channel facts stamped in metadata render at load time rather than
1217
+ // at persist time, so every stored row reads correctly whenever it
1218
+ // was written: a reaction row's "[reaction]" sentinel renders as who
1219
+ // reacted with what to which message, and a deleted row's original
1220
+ // text gives way to a deletion marker (the delete intercepts stamp
1221
+ // metadata and deliberately leave the content column untouched). The
1222
+ // substring guard keeps the per-row metadata parse off rows that can
1223
+ // carry neither fact: both envelopes spell these keys literally, in
1224
+ // providerMeta and in nested slackMeta alike.
1225
+ if (
1226
+ m.metadata &&
1227
+ (m.metadata.includes("reaction") || m.metadata.includes("deletedAt"))
1228
+ ) {
1229
+ const providerMeta = readProviderMetadata(m.metadata);
1230
+ if (providerMeta?.eventKind === "reaction") {
1231
+ const rendered = renderReactionHistoryText(
1232
+ providerMeta,
1233
+ resolveReactionTarget,
1234
+ // An assistant-role reaction row is the assistant's own act,
1235
+ // persisted by the react tool.
1236
+ { selfAuthored: role === "assistant" },
1237
+ );
1238
+ if (rendered) {
1239
+ content = [{ type: "text", text: rendered }];
1240
+ }
1241
+ } else if (role === "user" && providerMeta?.deletedAt !== undefined) {
1242
+ // Neutral marker, no actor: Discord deletes can be authorless
1243
+ // (`actorUnattributed`), so the marker never claims who deleted.
1244
+ content = [{ type: "text", text: "[This message was deleted]" }];
1245
+ }
1246
+ }
1247
+
1152
1248
  // Re-inject persisted injection blocks from metadata so it survives
1153
1249
  // conversation reloads (eviction, restart, fork).
1154
1250
  if (role === "user" && m.metadata && !isPreStripped) {
@@ -1522,6 +1618,7 @@ export class Conversation {
1522
1618
  this.trustContext,
1523
1619
  );
1524
1620
  if (
1621
+ !this.loadedHistoryStale &&
1525
1622
  this.loadedHistoryTrustClass === currentTrustClass &&
1526
1623
  this.loadedHistoryPersonalMemoryAllowed === currentPersonalMemoryAllowed
1527
1624
  ) {
@@ -1530,6 +1627,22 @@ export class Conversation {
1530
1627
  await this.loadFromDb();
1531
1628
  }
1532
1629
 
1630
+ /**
1631
+ * Mark the resident history stale: a channel event (a reaction, edit, or
1632
+ * delete) was persisted straight to the store while this conversation was
1633
+ * loaded, so `this.messages` no longer reflects the rows. The next
1634
+ * {@link ensureActorScopedHistory} reloads instead of reusing the resident
1635
+ * history.
1636
+ */
1637
+ markHistoryStale(): void {
1638
+ this.loadedHistoryStale = true;
1639
+ }
1640
+
1641
+ /** Queue a delivered reaction for its durable row at the turn boundary. */
1642
+ queueReactionRecord(record: QueuedReactionRecord): void {
1643
+ this.pendingReactionRecords.push(record);
1644
+ }
1645
+
1533
1646
  /**
1534
1647
  * Deliver an event through the conversation's sink, then to every observer.
1535
1648
  * The single emission point for conversation-level events (activity state,
@@ -1878,6 +1991,18 @@ export class Conversation {
1878
1991
  return !this.queue.isEmpty;
1879
1992
  }
1880
1993
 
1994
+ /**
1995
+ * True when dropping this instance would lose work that is still in flight:
1996
+ * a live turn, a queued successor, or a child subagent.
1997
+ */
1998
+ hasInFlightWork(): boolean {
1999
+ return (
2000
+ this.isProcessing() ||
2001
+ this.hasQueuedMessages() ||
2002
+ getSubagentManager().hasActiveChildren(this.conversationId)
2003
+ );
2004
+ }
2005
+
1881
2006
  /** FIFO snapshot of the messages currently waiting in the in-memory queue.
1882
2007
  * Read-only — used to surface queued user messages in history responses. */
1883
2008
  snapshotQueuedMessages(): QueuedMessage[] {
@@ -1963,20 +2088,16 @@ export class Conversation {
1963
2088
  });
1964
2089
 
1965
2090
  // Sync the gateway request status so stale "pending" records don't get
1966
- // matched by later guardian reply routing. Fire-and-forget: this method
1967
- // is sync with many callers (HTTP handlers, /v1/confirm, channel
1968
- // bridges), the in-memory resolution above is authoritative, and a CAS
1969
- // miss (the decision primitive already resolved it, e.g. the channel
1970
- // approval path) is expected and harmless.
1971
- void decideGuardianRequest({
1972
- id: requestId,
1973
- expectedStatus: "pending",
2091
+ // matched by later guardian reply routing, and withdraw the request's
2092
+ // approval cards when this sync is the write that landed. Fire-and-forget:
2093
+ // this method is sync with many callers (HTTP handlers, /v1/confirm,
2094
+ // channel bridges), the in-memory resolution above is authoritative, and
2095
+ // a CAS miss (the decision primitive already resolved it and withdrew the
2096
+ // cards itself, e.g. the channel approval path) is expected and harmless.
2097
+ void syncTerminalGuardianRequestStatus({
2098
+ requestId,
1974
2099
  status: resolvedState,
1975
- }).catch((err) => {
1976
- log.warn(
1977
- { err, requestId },
1978
- "Post-confirmation guardian request status sync failed",
1979
- );
2100
+ syncContext: "post-confirmation",
1980
2101
  });
1981
2102
  }
1982
2103
 
@@ -2304,6 +2425,20 @@ export class Conversation {
2304
2425
  }
2305
2426
  }
2306
2427
 
2428
+ /**
2429
+ * Trim this conversation's aged camera frames out of a history about to be
2430
+ * sent to a provider, keeping only the newest few as real images.
2431
+ *
2432
+ * Thin binding of {@link applySightFrameRetention} to the conversation's own
2433
+ * id, for the send paths that hold the instance rather than a turn context:
2434
+ * the compaction pipeline's summarizer input, and the wake, which sends its
2435
+ * run input itself. Best-effort inside, so a failed row read degrades to the
2436
+ * untrimmed history.
2437
+ */
2438
+ trimAgedSightFrames(messages: Message[]): Message[] {
2439
+ return applySightFrameRetention(messages, this.conversationId);
2440
+ }
2441
+
2307
2442
  /**
2308
2443
  * Auto-threshold compaction gate. Runs the same durable compaction
2309
2444
  * pipeline as {@link forceCompact} (summary call, circuit-breaker
@@ -2403,8 +2538,16 @@ export class Conversation {
2403
2538
  },
2404
2539
  )
2405
2540
  : null;
2406
- const messagesToCompact =
2407
- slackChronologicalContext?.messages ?? this.messages;
2541
+ // Trim aged camera frames out of the summarizer's own input. Compaction
2542
+ // sends this array to a provider like any other request, so a long camera
2543
+ // session would otherwise put every frame it ever captured into the summary
2544
+ // call, which is the payload-size and many-image rejection retention exists
2545
+ // to prevent. Safe against the caller-fixed boundary below: the pass
2546
+ // replaces blocks inside messages and never adds, drops, or reorders one,
2547
+ // so `fixedTailStartIndex` still names the same message.
2548
+ const messagesToCompact = this.trimAgedSightFrames(
2549
+ slackChronologicalContext?.messages ?? this.messages,
2550
+ );
2408
2551
  const compactedRowCountAtCall = this.contextCompactedMessageCount;
2409
2552
  let result = await defaultCompact({
2410
2553
  conversationId: this.conversationId,
@@ -1,4 +1,4 @@
1
- import { execSync, spawn } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import {
3
3
  closeSync,
4
4
  existsSync,
@@ -23,6 +23,8 @@ import {
23
23
  getPidPath,
24
24
  getWorkspaceConfigPath,
25
25
  } from "../util/platform.js";
26
+ import { readRawProcessCommand } from "../util/process-table.js";
27
+ import { isDaemonCommand } from "../util/worker-ownership.js";
26
28
 
27
29
  const log = getLogger("lifecycle");
28
30
 
@@ -116,24 +118,11 @@ function isProcessRunning(pid: number): boolean {
116
118
  }
117
119
 
118
120
  /**
119
- * Check whether a PID belongs to a vellum daemon process (a bun process
120
- * running the daemon's main.ts). Prevents signaling an unrelated process
121
- * that reused a stale PID.
121
+ * Check whether a PID belongs to a vellum daemon process. Prevents signalling
122
+ * an unrelated process that reused a stale PID.
122
123
  */
123
124
  function isVellumDaemonProcess(pid: number): boolean {
124
- try {
125
- const cmd = execSync(`ps -ww -p ${pid} -o command=`, {
126
- encoding: "utf-8",
127
- timeout: 3000,
128
- stdio: ["ignore", "pipe", "ignore"],
129
- }).trim();
130
- // The daemon is spawned as `bun run <path>/main.ts` — look for bun
131
- // running our daemon entry point.
132
- return cmd.includes("bun") && cmd.includes("daemon/main.ts");
133
- } catch {
134
- // Process exited or ps failed — treat as not ours.
135
- return false;
136
- }
125
+ return isDaemonCommand(readRawProcessCommand(pid));
137
126
  }
138
127
 
139
128
  /** Normalize a bind address to a connectable host for health checks.
@@ -65,6 +65,7 @@ describe("projectProviderForWire", () => {
65
65
  const wire = projectProviderForWire(gemini!);
66
66
  const modelIds = wire.models.map((model) => model.id);
67
67
  const expectedGemini3ModelIds = [
68
+ "gemini-3.7-flash",
68
69
  "gemini-3.6-flash",
69
70
  "gemini-3.5-flash",
70
71
  "gemini-3.5-flash-lite",
@@ -1,5 +1,6 @@
1
1
  import { peekAcpSessionManager } from "../../acp/index.js";
2
- import { decideGuardianRequest } from "../../channels/gateway-guardian-requests.js";
2
+ import { GUARDIAN_TERMINAL_REASON_SUPERSEDED } from "../../api/responses/home.js";
3
+ import { syncTerminalGuardianRequestStatus } from "../../approvals/guardian-request-status-sync.js";
3
4
  import {
4
5
  clearAll,
5
6
  getConversation,
@@ -522,18 +523,16 @@ export function supersedePendingInteractionsOnEnqueue(
522
523
  source: "auto_deny" as const,
523
524
  });
524
525
  // Sync the gateway request so stale "pending" rows aren't matched
525
- // by later guardian reply routing. Fire-and-forget from this sync
526
- // path: the in-memory denial is authoritative, and a CAS miss
526
+ // by later guardian reply routing, and withdraw the request's
527
+ // delivered approval cards so no surface keeps offering a decision
528
+ // that can no longer resolve anything. Fire-and-forget from this
529
+ // sync path: the in-memory denial is authoritative, and a CAS miss
527
530
  // (already decided elsewhere) is expected and harmless.
528
- void decideGuardianRequest({
529
- id: interaction.requestId,
530
- expectedStatus: "pending",
531
+ void syncTerminalGuardianRequestStatus({
532
+ requestId: interaction.requestId,
531
533
  status: "denied",
532
- }).catch((err) => {
533
- log.warn(
534
- { err, requestId: interaction.requestId },
535
- "Auto-deny guardian request status sync failed",
536
- );
534
+ syncContext: "supersede-on-enqueue",
535
+ terminalReason: GUARDIAN_TERMINAL_REASON_SUPERSEDED,
537
536
  });
538
537
  }
539
538
  }
@@ -262,6 +262,7 @@ function commandResolvesElsewhere(
262
262
  const resolved = execFileSync("/usr/bin/which", [commandName], {
263
263
  encoding: "utf-8",
264
264
  stdio: ["ignore", "pipe", "ignore"],
265
+ windowsHide: true,
265
266
  }).trim();
266
267
  return resolved !== "" && !candidatePaths.has(resolved);
267
268
  } catch {
@@ -461,19 +461,16 @@ export async function runDaemon(): Promise<void> {
461
461
  );
462
462
  }
463
463
 
464
- // Expire stale pending guardian requests left over from before this
465
- // process started. Daemon-keyed by design: interaction-bound kinds die
466
- // with THIS process's in-memory pendingInteractions map, so the daemon
467
- // triggers the gateway op at its own boot — the gateway never runs it
468
- // on its own restart. Two categories are cleaned up:
469
- //
470
- // 1. Interaction-bound kinds (tool_approval, pending_question) — their
471
- // in-memory pending-interaction session references are gone, so they
472
- // can never be completed.
473
- // 2. Any pending request whose expiresAt has already passed — persistent
474
- // kinds (access_request, tool_grant_request) that expired while the
475
- // daemon was stopped are transitioned so dedup logic doesn't return
476
- // stale rows.
464
+ // Expire interaction-bound guardian requests (tool_approval,
465
+ // pending_question) left over from before this process started: their
466
+ // in-memory pending-interaction session references are gone, so they
467
+ // can never be completed. Daemon-keyed by design: the daemon triggers
468
+ // the gateway op at its own boot, and the gateway never runs it on its
469
+ // own restart. Persistent kinds (access_request, tool_grant_request) are
470
+ // never touched here, whatever their deadline: their expiry belongs to
471
+ // the periodic sweep, which owns the card-withdrawal and
472
+ // requester-notice fan-out, and the dedupe reads are deadline-aware so
473
+ // a past-deadline row waiting for the sweep suppresses nothing.
477
474
  //
478
475
  // Startup must not block on the gateway (daemon startup philosophy):
479
476
  // on failure the periodic sweep still reaps time-expired rows, and
@@ -20,6 +20,10 @@ export const SYNC_TAGS = {
20
20
  conversationsList: "conversations:list",
21
21
  featureFlagsClient: "feature-flags:client",
22
22
  featureFlagsAssistant: "feature-flags:assistant",
23
+ /** ACP credential-failure markers, which drive the inline Connect card.
24
+ * Invalidated when a token write retires them, so a client holding a
25
+ * restored prompt for the replaced token stops offering it. */
26
+ acpAuthRecovery: "acp:auth-recovery",
23
27
  } as const;
24
28
 
25
29
  export type KnownSyncInvalidationTag =
@@ -19,10 +19,7 @@ import {
19
19
  parseChannelId,
20
20
  parseInterfaceId,
21
21
  } from "../channels/types.js";
22
- import {
23
- getAttachmentsByIds,
24
- getSourcePathsForAttachments,
25
- } from "../persistence/attachments-store.js";
22
+ import { resolveAttachmentsForPersist } from "../persistence/attachments-store.js";
26
23
  import {
27
24
  addMessage,
28
25
  getConversation,
@@ -362,19 +359,7 @@ async function prepareConversationForMessage(
362
359
  });
363
360
 
364
361
  const attachments = attachmentIds
365
- ? (() => {
366
- const resolved = getAttachmentsByIds(attachmentIds, {
367
- hydrateFileData: true,
368
- });
369
- const sourcePaths = getSourcePathsForAttachments(attachmentIds);
370
- return resolved.map((a) => ({
371
- id: a.id,
372
- filename: a.originalFilename,
373
- mimeType: a.mimeType,
374
- data: a.dataBase64,
375
- ...(sourcePaths.has(a.id) ? { filePath: sourcePaths.get(a.id) } : {}),
376
- }));
377
- })()
362
+ ? resolveAttachmentsForPersist(attachmentIds)
378
363
  : [];
379
364
 
380
365
  return { conversation, attachments };