@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
@@ -263,19 +263,6 @@ afterAll(() => {
263
263
  );
264
264
  });
265
265
 
266
- mock.module("../memory/retriever.js", () => ({
267
- buildMemoryRecall: async () => ({
268
- enabled: false,
269
- degraded: false,
270
- injectedText: "",
271
-
272
- semanticHits: 0,
273
- injectedTokens: 0,
274
- latencyMs: 0,
275
- }),
276
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
277
- }));
278
-
279
266
  mock.module("../apps/app-store.js", () => ({
280
267
  getApp: () => null,
281
268
  listAppFiles: () => [],
@@ -346,6 +346,10 @@ mock.module("../persistence/conversation-crud.js", () => ({
346
346
  updateMessageMetadata: updateMessageMetadataMock,
347
347
  setConversationHistoryStrippedAt: setConversationHistoryStrippedAtMock,
348
348
  getMessages: () => mockStoredMessages,
349
+ // Read by the pre-run camera-frame retention pass for any history holding
350
+ // attachment references. This suite seeds none, so an empty map is the same
351
+ // answer the real accessor would give.
352
+ selectSightFrameCaptureTimes: () => new Map<string, number>(),
349
353
  getConversation: () => mockConversationRow,
350
354
  provenanceFromTrustContext: () => ({
351
355
  source: "user",
@@ -421,19 +425,6 @@ mock.module("../persistence/conversation-disk-view.js", () => ({
421
425
  rebuildConversationDiskViewFromDbStateMock,
422
426
  }));
423
427
 
424
- mock.module("../memory/retriever.js", () => ({
425
- buildMemoryRecall: async () => ({
426
- enabled: false,
427
- degraded: false,
428
- injectedText: "",
429
-
430
- semanticHits: 0,
431
- injectedTokens: 0,
432
- latencyMs: 0,
433
- }),
434
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
435
- }));
436
-
437
428
  mock.module("../apps/app-store.js", () => ({
438
429
  getApp: () => null,
439
430
  listAppFiles: () => [],
@@ -1708,6 +1699,91 @@ describe("session-agent-loop", () => {
1708
1699
  expect(call[1]).toBe(JSON.stringify(rawRequest));
1709
1700
  expect(call[2]).toBe(JSON.stringify(rawResponse));
1710
1701
  });
1702
+
1703
+ test("request log and usage event share the turn call site", async () => {
1704
+ const rawRequest = {
1705
+ model: "gpt-4.1",
1706
+ messages: [{ role: "user", content: "Hello" }],
1707
+ };
1708
+ const rawResponse = {
1709
+ model: "gpt-4.1-2026-03-01",
1710
+ choices: [
1711
+ {
1712
+ finish_reason: "stop",
1713
+ message: { role: "assistant", content: "Hi there." },
1714
+ },
1715
+ ],
1716
+ };
1717
+ const ctx = makeCtx({
1718
+ providerResponses: [
1719
+ {
1720
+ content: [{ type: "text", text: "Hi there." }],
1721
+ model: "gpt-4.1-2026-03-01",
1722
+ usage: { inputTokens: 12, outputTokens: 3 },
1723
+ stopReason: "end_turn",
1724
+ actualProvider: "openai",
1725
+ rawRequest,
1726
+ rawResponse,
1727
+ },
1728
+ ],
1729
+ });
1730
+
1731
+ await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
1732
+ callSite: "callAgent",
1733
+ });
1734
+
1735
+ expect(recordRequestLogMock).toHaveBeenCalledTimes(1);
1736
+ const call = recordRequestLogMock.mock.calls[0] as unknown as unknown[];
1737
+ expect(call[5]).toBe("callAgent");
1738
+
1739
+ const usageCall = recordUsageMock.mock.calls.find(
1740
+ (entry) => (entry as unknown[])[5] === "main_agent",
1741
+ ) as unknown[] | undefined;
1742
+ expect(usageCall).toBeDefined();
1743
+ const attribution = usageCall?.[12] as UsageAttributionInput;
1744
+ expect(attribution.callSite).toBe("callAgent");
1745
+ });
1746
+
1747
+ test("stamps voiceFrontDoor on the request log for a front-door turn", async () => {
1748
+ const ctx = makeCtx({
1749
+ providerResponses: [
1750
+ {
1751
+ content: [{ type: "text", text: "Hi." }],
1752
+ model: "mock-model",
1753
+ usage: { inputTokens: 8, outputTokens: 2 },
1754
+ stopReason: "end_turn",
1755
+ rawRequest: { model: "mock-model", messages: [] },
1756
+ rawResponse: { id: "resp-1" },
1757
+ },
1758
+ ],
1759
+ });
1760
+
1761
+ await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
1762
+ callSite: "voiceFrontDoor",
1763
+ });
1764
+
1765
+ const call = recordRequestLogMock.mock.calls[0] as unknown as unknown[];
1766
+ expect(call[5]).toBe("voiceFrontDoor");
1767
+ });
1768
+
1769
+ test("provider-error request logs carry the turn call site", async () => {
1770
+ const ctx = makeCtx({
1771
+ loopProvider: {
1772
+ name: "mock-provider",
1773
+ async sendMessage() {
1774
+ throw new Error("upstream 500");
1775
+ },
1776
+ } as unknown as Provider,
1777
+ });
1778
+
1779
+ await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
1780
+ callSite: "callAgent",
1781
+ });
1782
+
1783
+ expect(recordRequestLogMock).toHaveBeenCalledTimes(1);
1784
+ const call = recordRequestLogMock.mock.calls[0] as unknown as unknown[];
1785
+ expect(call[5]).toBe("callAgent");
1786
+ });
1711
1787
  });
1712
1788
 
1713
1789
  describe("usage accounting", () => {
@@ -4164,6 +4240,7 @@ describe("session-agent-loop", () => {
4164
4240
  summaryModel: "claude-sonnet-5",
4165
4241
  summaryText: "summary",
4166
4242
  summaryCallSite: "compactionAgent",
4243
+ summaryResolutionCallSite: "mainAgent",
4167
4244
  // What the compactor resolved before the call...
4168
4245
  summaryOverrideProfile: "primaryProfile",
4169
4246
  // ...and what the reroute actually served.
@@ -4184,6 +4261,7 @@ describe("session-agent-loop", () => {
4184
4261
  expect(compactorCall?.[3]).toBe("claude-sonnet-5");
4185
4262
  expect(compactorCall?.[12]).toEqual({
4186
4263
  callSite: "compactionAgent",
4264
+ profileResolutionCallSite: "mainAgent",
4187
4265
  overrideProfile: "backupProfile",
4188
4266
  forceOverrideProfile: true,
4189
4267
  });
@@ -4210,6 +4288,7 @@ describe("session-agent-loop", () => {
4210
4288
  summaryModel: "mock-model",
4211
4289
  summaryText: "summary",
4212
4290
  summaryCallSite: "compactionAgent",
4291
+ summaryResolutionCallSite: "mainAgent",
4213
4292
  summaryOverrideProfile: "primaryProfile",
4214
4293
  summaryActualProvider: "openai",
4215
4294
  },
@@ -4226,6 +4305,7 @@ describe("session-agent-loop", () => {
4226
4305
  expect(compactorCall?.[3]).toBe("mock-model");
4227
4306
  expect(compactorCall?.[12]).toEqual({
4228
4307
  callSite: "compactionAgent",
4308
+ profileResolutionCallSite: "mainAgent",
4229
4309
  overrideProfile: "primaryProfile",
4230
4310
  });
4231
4311
  });
@@ -4255,6 +4335,7 @@ describe("session-agent-loop", () => {
4255
4335
  summaryModel: "mock-model",
4256
4336
  summaryText: "summary",
4257
4337
  summaryCallSite: "compactionAgent",
4338
+ summaryResolutionCallSite: "mainAgent",
4258
4339
  summaryOverrideProfile: "primaryProfile",
4259
4340
  },
4260
4341
  () => {},
@@ -4271,6 +4352,7 @@ describe("session-agent-loop", () => {
4271
4352
  });
4272
4353
  expect(compactorCall?.[12]).toEqual({
4273
4354
  callSite: "compactionAgent",
4355
+ profileResolutionCallSite: "mainAgent",
4274
4356
  overrideProfile: "primaryProfile",
4275
4357
  });
4276
4358
  });
@@ -86,19 +86,6 @@ mock.module("../persistence/attachments-store.js", () => ({
86
86
  linkAttachmentToMessage: () => {},
87
87
  }));
88
88
 
89
- mock.module("../memory/retriever.js", () => ({
90
- buildMemoryRecall: async () => ({
91
- enabled: false,
92
- degraded: false,
93
- injectedText: "",
94
-
95
- semanticHits: 0,
96
- injectedTokens: 0,
97
- latencyMs: 0,
98
- }),
99
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
100
- }));
101
-
102
89
  mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
103
90
  ContextWindowManager: class {
104
91
  estimateInputTokens() {
@@ -435,6 +435,424 @@ describe("loadFromDb history repair", () => {
435
435
  ]);
436
436
  });
437
437
 
438
+ test("a stored reaction row loads as rendered text, not the sentinel", async () => {
439
+ mockConversation = {
440
+ id: "conv-1",
441
+ contextSummary: null,
442
+ contextCompactedMessageCount: 0,
443
+ totalInputTokens: 0,
444
+ totalOutputTokens: 0,
445
+ totalEstimatedCost: 0,
446
+ };
447
+ mockDbMessages = [
448
+ {
449
+ id: "m1",
450
+ role: "user",
451
+ content: [{ type: "text", text: "Deploy is done" }],
452
+ metadata: JSON.stringify({
453
+ providerMeta: JSON.stringify({
454
+ source: "discord",
455
+ conversationExternalId: "chan-1",
456
+ messageId: "555.1",
457
+ eventKind: "message",
458
+ }),
459
+ }),
460
+ },
461
+ {
462
+ id: "m2",
463
+ role: "user",
464
+ content: [{ type: "text", text: "[reaction]" }],
465
+ metadata: JSON.stringify({
466
+ providerMeta: JSON.stringify({
467
+ source: "discord",
468
+ conversationExternalId: "chan-1",
469
+ eventKind: "reaction",
470
+ reaction: {
471
+ targetMessageId: "555.1",
472
+ emoji: "thumbsup",
473
+ op: "added",
474
+ actorDisplayName: "Alice",
475
+ },
476
+ }),
477
+ }),
478
+ },
479
+ ];
480
+
481
+ const conversation = makeConversation();
482
+ await conversation.loadFromDb();
483
+ const messages = conversation.getMessages();
484
+
485
+ // History repair merges the two adjacent user rows into one message;
486
+ // what matters is that the model reads the rendered fact, never the
487
+ // sentinel.
488
+ const allText = messages
489
+ .flatMap((m) => m.content)
490
+ .filter((b) => b.type === "text")
491
+ .map((b) => (b.type === "text" ? b.text : ""))
492
+ .join("\n");
493
+ expect(allText).not.toContain("[reaction]");
494
+ expect(allText).toContain(
495
+ 'Alice reacted with :thumbsup: to the message "Deploy is done"',
496
+ );
497
+ expect(allText).toContain("<external_content");
498
+ });
499
+
500
+ test("markHistoryStale makes the next ensureActorScopedHistory reload", async () => {
501
+ mockConversation = {
502
+ id: "conv-1",
503
+ contextSummary: null,
504
+ contextCompactedMessageCount: 0,
505
+ totalInputTokens: 0,
506
+ totalOutputTokens: 0,
507
+ totalEstimatedCost: 0,
508
+ };
509
+ mockDbMessages = [
510
+ {
511
+ id: "m1",
512
+ role: "user",
513
+ content: [{ type: "text", text: "Hello" }],
514
+ },
515
+ {
516
+ id: "m2",
517
+ role: "assistant",
518
+ content: [{ type: "text", text: "Hi" }],
519
+ },
520
+ ];
521
+
522
+ const conversation = makeConversation();
523
+ conversation.setTrustContext({
524
+ trustClass: "guardian",
525
+ sourceChannel: "telegram",
526
+ });
527
+ await conversation.loadFromDb();
528
+ expect(conversation.getMessages()).toHaveLength(2);
529
+
530
+ // A channel event lands straight in the store while the conversation
531
+ // stays resident.
532
+ mockDbMessages.push({
533
+ id: "m3",
534
+ role: "user",
535
+ content: [{ type: "text", text: "[reaction]" }],
536
+ });
537
+
538
+ // Same actor, no stale mark: the resident history is reused.
539
+ await conversation.ensureActorScopedHistory();
540
+ expect(conversation.getMessages()).toHaveLength(2);
541
+
542
+ conversation.markHistoryStale();
543
+ await conversation.ensureActorScopedHistory();
544
+ expect(conversation.getMessages()).toHaveLength(3);
545
+ });
546
+
547
+ test("a deleted row loads as the deletion marker, not its original text", async () => {
548
+ mockConversation = {
549
+ id: "conv-1",
550
+ contextSummary: null,
551
+ contextCompactedMessageCount: 0,
552
+ totalInputTokens: 0,
553
+ totalOutputTokens: 0,
554
+ totalEstimatedCost: 0,
555
+ };
556
+ mockDbMessages = [
557
+ {
558
+ id: "m1",
559
+ role: "user",
560
+ content: [
561
+ { type: "text", text: "Please forget this password: hunter2" },
562
+ ],
563
+ metadata: JSON.stringify({
564
+ providerMeta: JSON.stringify({
565
+ source: "discord",
566
+ conversationExternalId: "chan-1",
567
+ messageId: "555.1",
568
+ eventKind: "message",
569
+ deletedAt: 1700000001000,
570
+ }),
571
+ }),
572
+ },
573
+ {
574
+ id: "m2",
575
+ role: "assistant",
576
+ content: [{ type: "text", text: "Understood" }],
577
+ },
578
+ ];
579
+
580
+ const conversation = makeConversation();
581
+ await conversation.loadFromDb();
582
+ const allText = conversation
583
+ .getMessages()
584
+ .flatMap((m) => m.content)
585
+ .filter((b) => b.type === "text")
586
+ .map((b) => (b.type === "text" ? b.text : ""))
587
+ .join("\n");
588
+ expect(allText).toContain("[This message was deleted]");
589
+ expect(allText).not.toContain("hunter2");
590
+ });
591
+
592
+ test("a reaction never quotes a target that was later deleted", async () => {
593
+ mockConversation = {
594
+ id: "conv-1",
595
+ contextSummary: null,
596
+ contextCompactedMessageCount: 0,
597
+ totalInputTokens: 0,
598
+ totalOutputTokens: 0,
599
+ totalEstimatedCost: 0,
600
+ };
601
+ mockDbMessages = [
602
+ {
603
+ id: "m1",
604
+ role: "user",
605
+ content: [{ type: "text", text: "secret text" }],
606
+ metadata: JSON.stringify({
607
+ providerMeta: JSON.stringify({
608
+ source: "discord",
609
+ conversationExternalId: "chan-1",
610
+ messageId: "555.1",
611
+ eventKind: "message",
612
+ deletedAt: 1700000001000,
613
+ }),
614
+ }),
615
+ },
616
+ {
617
+ id: "m2",
618
+ role: "user",
619
+ content: [{ type: "text", text: "[reaction]" }],
620
+ metadata: JSON.stringify({
621
+ providerMeta: JSON.stringify({
622
+ source: "discord",
623
+ conversationExternalId: "chan-1",
624
+ eventKind: "reaction",
625
+ reaction: {
626
+ targetMessageId: "555.1",
627
+ emoji: "thumbsup",
628
+ op: "added",
629
+ actorDisplayName: "Alice",
630
+ },
631
+ }),
632
+ }),
633
+ },
634
+ ];
635
+
636
+ const conversation = makeConversation();
637
+ await conversation.loadFromDb();
638
+ const allText = conversation
639
+ .getMessages()
640
+ .flatMap((m) => m.content)
641
+ .filter((b) => b.type === "text")
642
+ .map((b) => (b.type === "text" ? b.text : ""))
643
+ .join("\n");
644
+ expect(allText).toContain("reacted with :thumbsup: to an earlier message");
645
+ expect(allText).not.toContain("secret text");
646
+ });
647
+
648
+ test("a reaction naming a later post of a split reply still quotes it", async () => {
649
+ mockConversation = {
650
+ id: "conv-1",
651
+ contextSummary: null,
652
+ contextCompactedMessageCount: 0,
653
+ totalInputTokens: 0,
654
+ totalOutputTokens: 0,
655
+ totalEstimatedCost: 0,
656
+ };
657
+ mockDbMessages = [
658
+ {
659
+ id: "m1",
660
+ role: "assistant",
661
+ content: [{ type: "text", text: "Deploy finished cleanly." }],
662
+ metadata: JSON.stringify({
663
+ providerMeta: JSON.stringify({
664
+ source: "discord",
665
+ conversationExternalId: "chan-1",
666
+ messageId: "555.1",
667
+ additionalMessageIds: ["555.2"],
668
+ eventKind: "message",
669
+ }),
670
+ }),
671
+ },
672
+ {
673
+ id: "m2",
674
+ role: "user",
675
+ content: [{ type: "text", text: "[reaction]" }],
676
+ metadata: JSON.stringify({
677
+ providerMeta: JSON.stringify({
678
+ source: "discord",
679
+ conversationExternalId: "chan-1",
680
+ eventKind: "reaction",
681
+ reaction: {
682
+ targetMessageId: "555.2",
683
+ emoji: "tada",
684
+ op: "added",
685
+ actorDisplayName: "Alice",
686
+ },
687
+ }),
688
+ }),
689
+ },
690
+ ];
691
+
692
+ const conversation = makeConversation();
693
+ await conversation.loadFromDb();
694
+ const allText = conversation
695
+ .getMessages()
696
+ .flatMap((m) => m.content)
697
+ .filter((b) => b.type === "text")
698
+ .map((b) => (b.type === "text" ? b.text : ""))
699
+ .join("\n");
700
+ expect(allText).toContain(":tada:");
701
+ // The quote resolves through the split reply's later post id: the line
702
+ // never degrades to the unresolved "an earlier message" form.
703
+ expect(allText).not.toContain("an earlier message");
704
+ });
705
+
706
+ test("the assistant's own reaction row renders second-person", async () => {
707
+ mockConversation = {
708
+ id: "conv-1",
709
+ contextSummary: null,
710
+ contextCompactedMessageCount: 0,
711
+ totalInputTokens: 0,
712
+ totalOutputTokens: 0,
713
+ totalEstimatedCost: 0,
714
+ };
715
+ mockDbMessages = [
716
+ {
717
+ id: "m1",
718
+ role: "user",
719
+ content: [{ type: "text", text: "great work" }],
720
+ metadata: JSON.stringify({
721
+ providerMeta: JSON.stringify({
722
+ source: "discord",
723
+ conversationExternalId: "chan-1",
724
+ messageId: "555.9",
725
+ eventKind: "message",
726
+ }),
727
+ }),
728
+ },
729
+ {
730
+ id: "m2",
731
+ role: "assistant",
732
+ content: [{ type: "text", text: "[reaction]" }],
733
+ metadata: JSON.stringify({
734
+ messageKind: "reaction",
735
+ providerMeta: JSON.stringify({
736
+ source: "discord",
737
+ conversationExternalId: "chan-1",
738
+ eventKind: "reaction",
739
+ reaction: {
740
+ targetMessageId: "555.9",
741
+ emoji: "🎉",
742
+ op: "added",
743
+ },
744
+ }),
745
+ }),
746
+ },
747
+ ];
748
+
749
+ const conversation = makeConversation();
750
+ await conversation.loadFromDb();
751
+ const allText = conversation
752
+ .getMessages()
753
+ .flatMap((m) => m.content)
754
+ .filter((b) => b.type === "text")
755
+ .map((b) => (b.type === "text" ? b.text : ""))
756
+ .join("\n");
757
+ expect(allText).not.toContain("[reaction]");
758
+ expect(allText).toContain("You reacted with 🎉 to this message:");
759
+ // The quoted target is sender text and stays fenced even in the
760
+ // assistant's own row.
761
+ expect(allText).toContain("great work");
762
+ expect(allText).toContain("<external_content");
763
+ });
764
+
765
+ test("a reaction row does not count as a turn on rehydration", async () => {
766
+ mockConversation = {
767
+ id: "conv-1",
768
+ contextSummary: null,
769
+ contextCompactedMessageCount: 0,
770
+ totalInputTokens: 0,
771
+ totalOutputTokens: 0,
772
+ totalEstimatedCost: 0,
773
+ };
774
+ mockDbMessages = [
775
+ {
776
+ id: "m1",
777
+ role: "user",
778
+ content: [{ type: "text", text: "Hello" }],
779
+ },
780
+ {
781
+ id: "m2",
782
+ role: "assistant",
783
+ content: [{ type: "text", text: "Hi" }],
784
+ },
785
+ {
786
+ id: "m3",
787
+ role: "user",
788
+ content: [{ type: "text", text: "[reaction]" }],
789
+ metadata: JSON.stringify({
790
+ providerMeta: JSON.stringify({
791
+ source: "discord",
792
+ conversationExternalId: "chan-1",
793
+ eventKind: "reaction",
794
+ reaction: {
795
+ targetMessageId: "555.1",
796
+ emoji: "thumbsup",
797
+ op: "added",
798
+ },
799
+ }),
800
+ }),
801
+ },
802
+ ];
803
+
804
+ const conversation = makeConversation();
805
+ await conversation.loadFromDb();
806
+ // One real user message; the reaction row never dispatched a turn, so
807
+ // the rehydrated counter must match the live counter's one increment.
808
+ expect(conversation.turnCount).toBe(1);
809
+ });
810
+
811
+ test("a slackMeta-shaped reaction row renders with its actor id as origin", async () => {
812
+ mockConversation = {
813
+ id: "conv-1",
814
+ contextSummary: null,
815
+ contextCompactedMessageCount: 0,
816
+ totalInputTokens: 0,
817
+ totalOutputTokens: 0,
818
+ totalEstimatedCost: 0,
819
+ };
820
+ mockDbMessages = [
821
+ {
822
+ id: "m1",
823
+ role: "user",
824
+ content: [{ type: "text", text: "[reaction]" }],
825
+ metadata: JSON.stringify({
826
+ slackMeta: JSON.stringify({
827
+ source: "slack",
828
+ channelId: "C123",
829
+ channelTs: "1700000000.111111",
830
+ eventKind: "reaction",
831
+ actorExternalUserId: "U_REACTOR",
832
+ displayName: "Alice",
833
+ reaction: {
834
+ emoji: "tada",
835
+ targetChannelTs: "1700000000.111111",
836
+ op: "added",
837
+ actorDisplayName: "Alice",
838
+ },
839
+ }),
840
+ }),
841
+ },
842
+ ];
843
+
844
+ const conversation = makeConversation();
845
+ await conversation.loadFromDb();
846
+ const allText = conversation
847
+ .getMessages()
848
+ .flatMap((m) => m.content)
849
+ .filter((b) => b.type === "text")
850
+ .map((b) => (b.type === "text" ? b.text : ""))
851
+ .join("\n");
852
+ expect(allText).toContain("Alice reacted with :tada:");
853
+ expect(allText).toContain('origin="U_REACTOR"');
854
+ });
855
+
438
856
  test("persistUserMessage reloads actor-scoped history before persisting on role switch", async () => {
439
857
  mockConversation = {
440
858
  id: "conv-1",
@@ -68,9 +68,40 @@ describe("stripMediaPayloadsForRetry", () => {
68
68
  // The image in the first message should be replaced with a text stub
69
69
  const firstMsg = result.messages[0];
70
70
  expect(firstMsg.content[1].type).toBe("text");
71
+ // Pinned whole, not just the prefix: the descriptor half comes from the
72
+ // shared `mediaSourceDescriptor`, and this is what keeps this stub and the
73
+ // camera-frame stub describing dropped media the same way. `makeImageBlock`
74
+ // carries 4 base64 chars, which decode to 3 bytes.
75
+ expect((firstMsg.content[1] as { type: "text"; text: string }).text).toBe(
76
+ "[Image omitted from retry context: image/png, 3 bytes]",
77
+ );
78
+ });
79
+
80
+ test("a stripped file block reports the same descriptor as an image", () => {
81
+ const fileBlock: ContentBlock = {
82
+ type: "file",
83
+ source: {
84
+ type: "workspace_ref",
85
+ media_type: "application/pdf",
86
+ attachmentId: "att-1",
87
+ sizeBytes: 2048,
88
+ filename: "notes.pdf",
89
+ },
90
+ };
91
+ const messages: Message[] = [
92
+ makeUserMessage({ type: "text", text: "old" }, fileBlock),
93
+ makeAssistantMessage({ type: "text", text: "response" }),
94
+ makeUserMessage({ type: "text", text: "new" }),
95
+ ];
96
+
97
+ const result = stripMediaPayloadsForRetry(messages);
98
+
99
+ expect(result.replacedBlocks).toBe(1);
71
100
  expect(
72
- (firstMsg.content[1] as { type: "text"; text: string }).text,
73
- ).toContain("Image omitted");
101
+ (result.messages[0].content[1] as { type: "text"; text: string }).text,
102
+ ).toBe(
103
+ "[File omitted from retry context: notes.pdf (application/pdf, 2048 bytes)]",
104
+ );
74
105
  });
75
106
 
76
107
  test("strips images from older user turns, keeps images in latest kept turn", () => {