@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
@@ -0,0 +1,162 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { ProviderMessageMetadata } from "../messaging/provider-message-metadata.js";
4
+ import { renderReactionHistoryText } from "./reaction-history-render.js";
5
+
6
+ function reactionMeta(
7
+ overrides: Omit<Partial<ProviderMessageMetadata>, "reaction"> & {
8
+ reaction?: Partial<NonNullable<ProviderMessageMetadata["reaction"]>>;
9
+ } = {},
10
+ ): ProviderMessageMetadata {
11
+ const { reaction, ...rest } = overrides;
12
+ return {
13
+ source: "slack",
14
+ conversationExternalId: "C123",
15
+ eventKind: "reaction",
16
+ reaction: {
17
+ targetMessageId: "1716000000.000001",
18
+ emoji: "thumbsup",
19
+ op: "added",
20
+ actorDisplayName: "Alice",
21
+ ...reaction,
22
+ },
23
+ ...rest,
24
+ } as ProviderMessageMetadata;
25
+ }
26
+
27
+ const noTarget = () => undefined;
28
+
29
+ describe("renderReactionHistoryText", () => {
30
+ test("renders an added reaction with the quoted target", () => {
31
+ const rendered = renderReactionHistoryText(
32
+ reactionMeta(),
33
+ () => "Deploy is done",
34
+ );
35
+ expect(rendered).toContain(
36
+ 'Alice reacted with :thumbsup: to the message "Deploy is done"',
37
+ );
38
+ });
39
+
40
+ test("renders a removed reaction", () => {
41
+ const rendered = renderReactionHistoryText(
42
+ reactionMeta({ reaction: { op: "removed" } }),
43
+ noTarget,
44
+ );
45
+ expect(rendered).toContain(
46
+ "Alice removed their :thumbsup: reaction from an earlier message",
47
+ );
48
+ });
49
+
50
+ test("fences the line as untrusted channel content", () => {
51
+ const rendered = renderReactionHistoryText(
52
+ reactionMeta({ actorExternalId: "U123" }),
53
+ noTarget,
54
+ );
55
+ expect(rendered).toContain('<external_content source="slack"');
56
+ expect(rendered).toContain('origin="U123"');
57
+ expect(rendered).toContain("</external_content>");
58
+ });
59
+
60
+ test("non-slack channels fence with the webhook source", () => {
61
+ const rendered = renderReactionHistoryText(
62
+ reactionMeta({ source: "discord" }),
63
+ noTarget,
64
+ );
65
+ expect(rendered).toContain('<external_content source="webhook"');
66
+ });
67
+
68
+ test("keeps a Discord custom-emoji mention form as-is", () => {
69
+ const rendered = renderReactionHistoryText(
70
+ reactionMeta({ reaction: { emoji: "<:vex:12345>" } }),
71
+ noTarget,
72
+ );
73
+ expect(rendered).toContain("reacted with <:vex:12345> to");
74
+ });
75
+
76
+ test("keeps a unicode emoji as-is", () => {
77
+ const rendered = renderReactionHistoryText(
78
+ reactionMeta({ reaction: { emoji: "👍" } }),
79
+ noTarget,
80
+ );
81
+ expect(rendered).toContain("reacted with 👍 to");
82
+ });
83
+
84
+ test("strips ingress fences and collapses whitespace in the quote", () => {
85
+ const rendered = renderReactionHistoryText(
86
+ reactionMeta(),
87
+ () =>
88
+ '<external_content source="slack">\nline one\n line two\n</external_content>',
89
+ );
90
+ expect(rendered).toContain('the message "line one line two"');
91
+ });
92
+
93
+ test("truncates a long quoted target", () => {
94
+ const rendered = renderReactionHistoryText(reactionMeta(), () =>
95
+ "x".repeat(500),
96
+ );
97
+ expect(rendered).toContain(`"${"x".repeat(120)}..."`);
98
+ expect(rendered).not.toContain("x".repeat(121));
99
+ });
100
+
101
+ test("falls back to Someone when no display name is stored", () => {
102
+ const rendered = renderReactionHistoryText(
103
+ reactionMeta({ reaction: { actorDisplayName: undefined } }),
104
+ noTarget,
105
+ );
106
+ expect(rendered).toContain("Someone reacted with");
107
+ });
108
+
109
+ test("prefers the row-level display name over the fallback", () => {
110
+ const rendered = renderReactionHistoryText(
111
+ reactionMeta({
112
+ displayName: "Bob",
113
+ reaction: { actorDisplayName: undefined },
114
+ }),
115
+ noTarget,
116
+ );
117
+ expect(rendered).toContain("Bob reacted with");
118
+ });
119
+
120
+ test("a self-authored row is second-person with the quoted target fenced", () => {
121
+ const rendered = renderReactionHistoryText(
122
+ reactionMeta({ source: "discord" }),
123
+ () => "Deploy is done",
124
+ { selfAuthored: true },
125
+ );
126
+ expect(rendered).toContain("You reacted with :thumbsup: to this message:");
127
+ expect(rendered).toContain('<external_content source="webhook"');
128
+ expect(rendered).toContain("Deploy is done");
129
+ expect(rendered).toContain("</external_content>");
130
+ });
131
+
132
+ test("a self-authored row with no target stays unfenced", () => {
133
+ const rendered = renderReactionHistoryText(
134
+ reactionMeta({ source: "discord" }),
135
+ noTarget,
136
+ { selfAuthored: true },
137
+ );
138
+ expect(rendered).toBe("You reacted with :thumbsup: to an earlier message");
139
+ expect(rendered).not.toContain("<external_content");
140
+ });
141
+
142
+ test("a self-authored removal reads as your reaction", () => {
143
+ const rendered = renderReactionHistoryText(
144
+ reactionMeta({ reaction: { op: "removed" } }),
145
+ noTarget,
146
+ { selfAuthored: true },
147
+ );
148
+ expect(rendered).toContain(
149
+ "You removed your :thumbsup: reaction from an earlier message",
150
+ );
151
+ });
152
+
153
+ test("returns null for a message row", () => {
154
+ const meta: ProviderMessageMetadata = {
155
+ source: "slack",
156
+ conversationExternalId: "C123",
157
+ messageId: "1716000000.000001",
158
+ eventKind: "message",
159
+ } as ProviderMessageMetadata;
160
+ expect(renderReactionHistoryText(meta, noTarget)).toBeNull();
161
+ });
162
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Render a persisted reaction row into the text the model reads.
3
+ *
4
+ * A reaction is stored as a user row whose content is the "[reaction]"
5
+ * sentinel; the fact lives in the row's metadata (`providerMeta.reaction`:
6
+ * who reacted, with which emoji, on which message). The stored row stays
7
+ * canonical and rendering happens at history-load time, so every stored row
8
+ * renders regardless of when it was written and no formatting is frozen
9
+ * into persistence.
10
+ *
11
+ * Two authorship contracts share the renderer. An inbound row (role user)
12
+ * is sender activity: the actor's display name and the quoted target are
13
+ * sender-authored, so the whole line is fenced as untrusted content the
14
+ * same way channel text is fenced at ingress (`inbound-content-prep.ts`).
15
+ * A self-authored row (role assistant, written by the react tool) renders
16
+ * second-person with the verb unfenced, but the quoted target stays
17
+ * fenced: the emoji and act are the model's own output, the text it
18
+ * reacted to is not.
19
+ *
20
+ * Serves consumers of `Conversation.loadFromDb` history. Slack channel
21
+ * turns build their provider history from rows instead and render
22
+ * reactions through their own transcript renderer
23
+ * (`messaging/providers/slack/render-transcript.ts`), whose tag-line
24
+ * format references targets by alias rather than by quote.
25
+ */
26
+ import type { ProviderMessageMetadata } from "../messaging/provider-message-metadata.js";
27
+ import {
28
+ unwrapExternalContentForDisplay,
29
+ wrapUntrustedContent,
30
+ } from "../security/untrusted-content.js";
31
+
32
+ /**
33
+ * Slack-style emoji names render in colon form. Anything else (a unicode
34
+ * emoji, Discord's `<:name:id>` mention form) is already self-delimiting.
35
+ */
36
+ const EMOJI_NAME_PATTERN = /^[\w+'-]+$/;
37
+
38
+ const TARGET_SNIPPET_MAX_CHARS = 120;
39
+
40
+ function formatEmoji(emoji: string): string {
41
+ return EMOJI_NAME_PATTERN.test(emoji) ? `:${emoji}:` : emoji;
42
+ }
43
+
44
+ /**
45
+ * Flatten a quoted target to one line: the ingress fence is markup around
46
+ * the target's own text, not part of it, so it is unwrapped rather than
47
+ * quoted.
48
+ */
49
+ function snippetOf(text: string): string {
50
+ const flat = unwrapExternalContentForDisplay(text)
51
+ .replace(/\s+/g, " ")
52
+ .trim();
53
+ if (flat.length <= TARGET_SNIPPET_MAX_CHARS) {
54
+ return flat;
55
+ }
56
+ return `${flat.slice(0, TARGET_SNIPPET_MAX_CHARS)}...`;
57
+ }
58
+
59
+ /**
60
+ * The readable line for a reaction row, fenced, or null when the metadata
61
+ * does not describe a reaction (callers keep the row's stored content then).
62
+ *
63
+ * `resolveTargetText` maps the reaction's `targetMessageId` to the target
64
+ * row's text in the same provider id namespace; returning undefined (target
65
+ * compacted away, or never stored) degrades to "an earlier message".
66
+ */
67
+ export function renderReactionHistoryText(
68
+ meta: ProviderMessageMetadata,
69
+ resolveTargetText: (targetMessageId: string) => string | undefined,
70
+ options?: {
71
+ /**
72
+ * The row is the assistant's own reaction (an assistant-role row the
73
+ * react tool persisted). Rendered second-person and unfenced: the actor
74
+ * and emoji are the model's own output, not sender-authored text.
75
+ */
76
+ selfAuthored?: boolean;
77
+ },
78
+ ): string | null {
79
+ if (meta.eventKind !== "reaction" || !meta.reaction) {
80
+ return null;
81
+ }
82
+ const { emoji, op, targetMessageId } = meta.reaction;
83
+ const target = resolveTargetText(targetMessageId);
84
+ const snippet = target ? snippetOf(target) : "";
85
+
86
+ if (options?.selfAuthored) {
87
+ const verb =
88
+ op === "removed"
89
+ ? `removed your ${formatEmoji(emoji)} reaction from`
90
+ : `reacted with ${formatEmoji(emoji)} to`;
91
+ if (!snippet) {
92
+ return `You ${verb} an earlier message`;
93
+ }
94
+ // The quoted target is sender-authored text entering an assistant-role
95
+ // message; unfenced it would replay as trusted content, so a sender
96
+ // could plant instructions and induce a reaction to launder them.
97
+ const fencedSnippet = wrapUntrustedContent(snippet, {
98
+ source: meta.source === "slack" ? "slack" : "webhook",
99
+ });
100
+ return `You ${verb} this message: ${fencedSnippet}`;
101
+ }
102
+
103
+ const actor = meta.reaction.actorDisplayName ?? meta.displayName ?? "Someone";
104
+ const verb =
105
+ op === "removed"
106
+ ? `removed their ${formatEmoji(emoji)} reaction from`
107
+ : `reacted with ${formatEmoji(emoji)} to`;
108
+ const line = snippet
109
+ ? `${actor} ${verb} the message "${snippet}"`
110
+ : `${actor} ${verb} an earlier message`;
111
+ return wrapUntrustedContent(line, {
112
+ source: meta.source === "slack" ? "slack" : "webhook",
113
+ ...(meta.actorExternalId ? { sourceDetail: meta.actorExternalId } : {}),
114
+ });
115
+ }
@@ -0,0 +1,162 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { QueuedReactionRecord } from "./reaction-record.js";
4
+
5
+ let persisted: Array<{
6
+ conversationId: string;
7
+ role: string;
8
+ content: string;
9
+ metadata: Record<string, unknown> | undefined;
10
+ }> = [];
11
+ let failNext = false;
12
+ const actualCrud = await import("../persistence/conversation-crud.js");
13
+ mock.module("../persistence/conversation-crud.js", () => ({
14
+ ...actualCrud,
15
+ addMessage: async (
16
+ conversationId: string,
17
+ role: string,
18
+ content: string,
19
+ opts?: { metadata?: Record<string, unknown> },
20
+ ) => {
21
+ if (failNext) {
22
+ failNext = false;
23
+ throw new Error("db closed");
24
+ }
25
+ persisted.push({ conversationId, role, content, metadata: opts?.metadata });
26
+ return { id: `row-${persisted.length}` };
27
+ },
28
+ }));
29
+
30
+ const { persistReactionRecords } = await import("./reaction-record.js");
31
+ const { drainQueuedReactionRecords } =
32
+ await import("./conversation-agent-loop.js");
33
+
34
+ function record(
35
+ overrides: Partial<QueuedReactionRecord> = {},
36
+ ): QueuedReactionRecord {
37
+ return {
38
+ channel: "discord",
39
+ chatId: "chan-1",
40
+ messageId: "555.1",
41
+ emoji: "🎉",
42
+ op: "added",
43
+ provenanceTrustClass: "guardian",
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ describe("persistReactionRecords", () => {
49
+ beforeEach(() => {
50
+ persisted = [];
51
+ failNext = false;
52
+ });
53
+
54
+ test("a neutral-channel record writes the providerMeta envelope", async () => {
55
+ await persistReactionRecords("conv-1", [record()]);
56
+ expect(persisted).toHaveLength(1);
57
+ const row = persisted[0]!;
58
+ expect(row.role).toBe("assistant");
59
+ expect(row.content).toBe("[reaction]");
60
+ expect(row.metadata?.messageKind).toBe("reaction");
61
+ expect(row.metadata?.provenanceTrustClass).toBe("guardian");
62
+ expect(row.metadata?.provenanceSourceChannel).toBe("discord");
63
+ const envelope = JSON.parse(String(row.metadata?.providerMeta)) as {
64
+ source: string;
65
+ eventKind: string;
66
+ reaction: { targetMessageId: string; emoji: string; op: string };
67
+ };
68
+ expect(envelope.source).toBe("discord");
69
+ expect(envelope.eventKind).toBe("reaction");
70
+ expect(envelope.reaction).toEqual({
71
+ targetMessageId: "555.1",
72
+ emoji: "🎉",
73
+ op: "added",
74
+ });
75
+ expect(row.metadata?.slackMeta).toBeUndefined();
76
+ });
77
+
78
+ test("a Slack record writes the slackMeta envelope the Slack context reads", async () => {
79
+ await persistReactionRecords("conv-1", [
80
+ record({ channel: "slack", chatId: "C1", messageId: "1700.1" }),
81
+ ]);
82
+ const row = persisted[0]!;
83
+ expect(row.metadata?.providerMeta).toBeUndefined();
84
+ const slackMeta = JSON.parse(String(row.metadata?.slackMeta)) as {
85
+ source: string;
86
+ channelId: string;
87
+ channelTs: string;
88
+ eventKind: string;
89
+ reaction: { emoji: string; targetChannelTs: string; op: string };
90
+ };
91
+ expect(slackMeta.source).toBe("slack");
92
+ expect(slackMeta.channelId).toBe("C1");
93
+ expect(slackMeta.channelTs).toBe("1700.1");
94
+ expect(slackMeta.eventKind).toBe("reaction");
95
+ expect(slackMeta.reaction).toEqual({
96
+ emoji: "🎉",
97
+ targetChannelTs: "1700.1",
98
+ op: "added",
99
+ });
100
+ });
101
+
102
+ test("the terminal drain persists the turn's owned records and stale-marks", async () => {
103
+ // The drain runs in the agent loop's finally, so an error exit still
104
+ // reaches it: a delivered reaction's record must not depend on the turn
105
+ // finishing cleanly.
106
+ let staleMarked = 0;
107
+ const owned = [record(), record({ emoji: "👍" })];
108
+ await drainQueuedReactionRecords(
109
+ {
110
+ conversationId: "conv-1",
111
+ markHistoryStale: () => {
112
+ staleMarked += 1;
113
+ },
114
+ },
115
+ owned,
116
+ );
117
+ expect(persisted).toHaveLength(2);
118
+ expect(owned).toHaveLength(0);
119
+ expect(staleMarked).toBe(1);
120
+ });
121
+
122
+ test("one turn's drain never consumes a following turn's live queue", async () => {
123
+ // Ownership is captured at release, while the turn is still exclusive;
124
+ // the drain takes only that captured array. A record a following turn
125
+ // queued on the conversation's live field stays untouched until that
126
+ // turn's own boundary, so its row cannot land before its tool_result.
127
+ const followingTurnQueue = [record({ emoji: "🆕" })];
128
+ const ctx = {
129
+ conversationId: "conv-1",
130
+ pendingReactionRecords: followingTurnQueue,
131
+ markHistoryStale: () => {},
132
+ };
133
+ await drainQueuedReactionRecords(ctx, []);
134
+ expect(persisted).toHaveLength(0);
135
+ expect(followingTurnQueue).toHaveLength(1);
136
+ });
137
+
138
+ test("the terminal drain is a no-op on an empty or absent owned set", async () => {
139
+ let staleMarked = 0;
140
+ await drainQueuedReactionRecords(
141
+ {
142
+ conversationId: "conv-1",
143
+ markHistoryStale: () => {
144
+ staleMarked += 1;
145
+ },
146
+ },
147
+ undefined,
148
+ );
149
+ expect(persisted).toHaveLength(0);
150
+ expect(staleMarked).toBe(0);
151
+ });
152
+
153
+ test("a failed write is logged, not thrown, and later records still persist", async () => {
154
+ failNext = true;
155
+ await persistReactionRecords("conv-1", [record(), record({ emoji: "👍" })]);
156
+ expect(persisted).toHaveLength(1);
157
+ const envelope = JSON.parse(
158
+ String(persisted[0]?.metadata?.providerMeta),
159
+ ) as { reaction: { emoji: string } };
160
+ expect(envelope.reaction.emoji).toBe("👍");
161
+ });
162
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Durable records of the assistant's own delivered reactions.
3
+ *
4
+ * A reaction happens mid-turn (the react tool fans out to the channel while
5
+ * the agent loop is still running), but its row must not be written then: an
6
+ * assistant row inserted between a `tool_use` and its `tool_result` breaks
7
+ * the pairing history repair enforces, and a reload would then teach the
8
+ * model its own reaction failed. The tool queues the record on the live
9
+ * conversation instead, and the agent loop drains the queue at the turn
10
+ * boundary, after the turn's rows are settled.
11
+ *
12
+ * Slack rows write the `slackMeta` envelope (the Slack transcript context
13
+ * builds provider history from rows and reads only that shape); every other
14
+ * channel writes the neutral `providerMeta` that `readProviderMetadata`
15
+ * serves to channel-agnostic readers, `Conversation.loadFromDb`'s renderer
16
+ * included.
17
+ */
18
+ import type { ChannelId } from "../channels/types.js";
19
+ import { writeSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
20
+ import {
21
+ buildNeutralReactionMeta,
22
+ buildSlackReactionMeta,
23
+ } from "../messaging/reaction-envelopes.js";
24
+ import {
25
+ addMessage,
26
+ REACTION_MESSAGE_KIND,
27
+ } from "../persistence/conversation-crud.js";
28
+ import { getLogger } from "../util/logger.js";
29
+
30
+ const log = getLogger("reaction-record");
31
+
32
+ export interface QueuedReactionRecord {
33
+ channel: ChannelId;
34
+ chatId: string;
35
+ /** Target message id, in the channel's own id space. */
36
+ messageId: string;
37
+ emoji: string;
38
+ op: "added" | "removed";
39
+ /** The turn's provenance, so actor-scoped loads keep the row. */
40
+ provenanceTrustClass?: string;
41
+ }
42
+
43
+ /**
44
+ * Persist queued reaction records as standalone assistant rows. Non-throwing:
45
+ * the reactions already happened on the channel, so a persistence failure
46
+ * loses the record, never the act, and is logged rather than raised.
47
+ */
48
+ export async function persistReactionRecords(
49
+ conversationId: string,
50
+ records: readonly QueuedReactionRecord[],
51
+ ): Promise<void> {
52
+ for (const record of records) {
53
+ try {
54
+ const facts = {
55
+ channel: record.channel,
56
+ chatId: record.chatId,
57
+ targetMessageId: record.messageId,
58
+ emoji: record.emoji,
59
+ op: record.op,
60
+ };
61
+ const envelope =
62
+ record.channel === "slack"
63
+ ? { slackMeta: writeSlackMetadata(buildSlackReactionMeta(facts)) }
64
+ : { providerMeta: JSON.stringify(buildNeutralReactionMeta(facts)) };
65
+ await addMessage(conversationId, "assistant", "[reaction]", {
66
+ metadata: {
67
+ messageKind: REACTION_MESSAGE_KIND,
68
+ ...(record.provenanceTrustClass
69
+ ? { provenanceTrustClass: record.provenanceTrustClass }
70
+ : {}),
71
+ provenanceSourceChannel: record.channel,
72
+ ...envelope,
73
+ },
74
+ skipIndexing: true,
75
+ });
76
+ } catch (err) {
77
+ log.warn(
78
+ { err, conversationId, channel: record.channel, op: record.op },
79
+ "Failed to persist the assistant's reaction record",
80
+ );
81
+ }
82
+ }
83
+ }
@@ -21,7 +21,6 @@ import { stopScheduler } from "../schedule/scheduler.js";
21
21
  import { getSubagentManager } from "../subagent/index.js";
22
22
  import { stopUsageTelemetryReporter } from "../telemetry/usage-telemetry-reporter.js";
23
23
  import { browserManager } from "../tools/browser/browser-manager.js";
24
- import { cleanupShellOutputTempFiles } from "../tools/shared/shell-output.js";
25
24
  import { getLogger } from "../util/logger.js";
26
25
  import { APP_VERSION } from "../version.js";
27
26
  import { getEnrichmentService } from "../workspace/commit-message-enrichment-service.js";
@@ -167,7 +166,6 @@ async function shutdown(): Promise<void> {
167
166
 
168
167
  await stopRuntimeHttpServer();
169
168
  await browserManager.closeAllPages();
170
- cleanupShellOutputTempFiles();
171
169
  stopScheduler();
172
170
 
173
171
  // The memory jobs worker process is SIGTERM'd by the memory plugin's own
@@ -1,4 +1,5 @@
1
1
  import { isToolResultOnlyUserMessage } from "../conversations/message-consolidation.js";
2
+ import { readProviderMetadata } from "../messaging/read-provider-metadata.js";
2
3
  import type { MessageRow } from "../persistence/conversation-crud.js";
3
4
  import { UserError } from "../util/errors.js";
4
5
 
@@ -19,6 +20,17 @@ export function startsNewTurn(msg: MessageRow): boolean {
19
20
  if (msg.role !== "user") {
20
21
  return false;
21
22
  }
23
+ // A reaction row is a channel fact attached to the turn it landed in, not
24
+ // a message that ran one: the agent loop never dispatches for reactions,
25
+ // so counting them would rehydrate a `turnCount` above the live counter
26
+ // and shift turn-indexed memory selection. The substring guard keeps the
27
+ // metadata parse off rows that cannot carry the envelope.
28
+ if (
29
+ msg.metadata?.includes("reaction") &&
30
+ readProviderMetadata(msg.metadata)?.eventKind === "reaction"
31
+ ) {
32
+ return false;
33
+ }
22
34
  return !isToolResultOnlyUserMessage(msg);
23
35
  }
24
36
 
@@ -35,7 +35,7 @@ async function extractThumbnail(inputPath: string): Promise<string | null> {
35
35
  "5",
36
36
  outputPath,
37
37
  ],
38
- { stderr: "pipe" },
38
+ { stderr: "pipe", windowsHide: true },
39
39
  );
40
40
 
41
41
  let timer: ReturnType<typeof setTimeout>;
@@ -33,10 +33,14 @@ export {
33
33
  type FeedItemCategory,
34
34
  type FeedItemDetailPanel,
35
35
  type FeedItemDetailPanelKind,
36
+ type FeedItemGuardianIntent,
37
+ type FeedItemGuardianRequest,
38
+ type FeedItemGuardianStatus,
36
39
  FeedItemSchema as feedItemSchema,
37
40
  type FeedItemStatus,
38
41
  type FeedItemType,
39
42
  type FeedItemUrgency,
43
+ isPendingGuardianFeedItem,
40
44
  type SuggestedPrompt,
41
45
  SuggestedPromptSchema as suggestedPromptSchema,
42
46
  type SuggestedPromptSource,
@@ -37,6 +37,10 @@
37
37
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
38
38
  import { join } from "node:path";
39
39
 
40
+ import {
41
+ type FeedItemGuardianRequest,
42
+ isPendingGuardianFeedItem,
43
+ } from "../api/responses/home.js";
40
44
  import { broadcastMessage } from "../runtime/assistant-event-hub.js";
41
45
  import { getLogger } from "../util/logger.js";
42
46
  import { getDataDir } from "../util/platform.js";
@@ -182,6 +186,16 @@ export interface FeedItemContentPatch {
182
186
  summary?: string;
183
187
  urgency?: FeedItemUrgency;
184
188
  status?: FeedItemStatus;
189
+ /**
190
+ * Updater for the item's guardian projection, applied inside the
191
+ * coalescing queue so it reads the projection as of write time (a
192
+ * plain replacement value would race concurrent patches). Only runs
193
+ * when the item carries a `guardianRequest`; items without one are
194
+ * left untouched.
195
+ */
196
+ guardianRequest?: (
197
+ existing: FeedItemGuardianRequest,
198
+ ) => FeedItemGuardianRequest;
185
199
  }
186
200
 
187
201
  export async function patchFeedItemContent(
@@ -387,6 +401,9 @@ async function runWrite(): Promise<void> {
387
401
  if (patch.status !== undefined) {
388
402
  updated.status = patch.status;
389
403
  }
404
+ if (patch.guardianRequest !== undefined && existing.guardianRequest) {
405
+ updated.guardianRequest = patch.guardianRequest(existing.guardianRequest);
406
+ }
390
407
  items[idx] = updated;
391
408
  contentPatchResults.push({ resolve, value: updated });
392
409
  }
@@ -431,6 +448,14 @@ async function runWrite(): Promise<void> {
431
448
  if (idSet && !idSet.has(current.id)) {
432
449
  continue;
433
450
  }
451
+ // Bulk dismissal ("Clear all") never clears the live projection of
452
+ // an unresolved guardian request: the item is the request's one
453
+ // canonical home, and only resolution may retire it. A terminal
454
+ // receipt is not pending and clears like any other item, and a
455
+ // deliberate single-item dismissal stays available.
456
+ if (op.to === "dismissed" && isPendingGuardianFeedItem(current)) {
457
+ continue;
458
+ }
434
459
  items[i] = { ...current, status: op.to };
435
460
  count++;
436
461
  }