@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,42 @@
1
+ /**
2
+ * Short-lived tombstones for deleted mirror contacts.
3
+ *
4
+ * The mirror reconciler pulls a gateway identity snapshot and then applies it;
5
+ * a contact deleted (or merged away) while that pull is in flight would be
6
+ * resurrected from the stale snapshot, and the reconciler's additive contract
7
+ * (it never deletes) would keep the ghost forever. Recording each deletion
8
+ * here lets the reconciler refuse to heal an id that died after the snapshot
9
+ * was taken.
10
+ *
11
+ * Contact ids are UUIDs and never come back, so a tombstone can never block
12
+ * a legitimate recreation; the TTL only bounds memory.
13
+ */
14
+
15
+ /** Generously longer than a snapshot pull plus its debounced follow-up. */
16
+ const TOMBSTONE_TTL_MS = 15 * 60 * 1000;
17
+
18
+ const tombstones = new Map<string, number>();
19
+
20
+ /** Record that the mirror contact row for `contactId` was deleted. */
21
+ export function recordContactTombstone(contactId: string): void {
22
+ const now = Date.now();
23
+ tombstones.set(contactId, now + TOMBSTONE_TTL_MS);
24
+ for (const [id, expiresAt] of tombstones) {
25
+ if (expiresAt <= now) {
26
+ tombstones.delete(id);
27
+ }
28
+ }
29
+ }
30
+
31
+ /** Whether `contactId` was deleted recently enough that a snapshot may predate it. */
32
+ export function isContactTombstoned(contactId: string): boolean {
33
+ const expiresAt = tombstones.get(contactId);
34
+ if (expiresAt === undefined) {
35
+ return false;
36
+ }
37
+ if (expiresAt <= Date.now()) {
38
+ tombstones.delete(contactId);
39
+ return false;
40
+ }
41
+ return true;
42
+ }
@@ -7,14 +7,23 @@ import { invalidateGuardianDeliveryCache } from "./guardian-delivery-reader.js";
7
7
  * Notify everything that cares about a contact mutation. Called directly after
8
8
  * a successful contact write:
9
9
  *
10
- * - broadcasts `contacts_changed` so connected clients refresh their view, and
10
+ * - broadcasts `contacts_changed` so connected clients refresh their view,
11
11
  * - drops the guardian-contact, guardian-delivery, and contact-threshold
12
12
  * caches so the next read refetches from the gateway instead of serving
13
- * a stale set.
13
+ * a stale set, and
14
+ * - asks the mirror reconciler for a debounced convergence pass, so identity
15
+ * drift the per-write mirror ops missed heals on the next contact activity.
16
+ *
17
+ * The reconciler is reached via dynamic import: it applies its heals through
18
+ * the contact-store primitives, whose writes land back here, and a static
19
+ * import would close that cycle at module-load time.
14
20
  */
15
21
  export function notifyContactsChanged(): void {
16
22
  broadcastMessage({ type: "contacts_changed" });
17
23
  invalidateGuardianContactCache();
18
24
  invalidateGuardianDeliveryCache();
19
25
  invalidateAllContactThresholdCaches();
26
+ void import("./mirror-reconciler.js")
27
+ .then((m) => m.scheduleMirrorReconcile())
28
+ .catch(() => {});
20
29
  }
@@ -64,17 +64,31 @@ const log = getLogger("compactor");
64
64
  const COMPACTION_CALL_SITE: LLMCallSite = "mainAgent";
65
65
 
66
66
  /**
67
- * Tag stamped on `llm_request_logs.call_site` for compaction-driven rows.
67
+ * Tag stamped on `llm_request_logs.call_site` and the usage event's
68
+ * `callSite` for compaction-driven rows.
68
69
  *
69
70
  * Distinct from `COMPACTION_CALL_SITE` (above) on purpose: that constant
70
- * names the **provider config resolution** site (set to `mainAgent` so we
71
+ * names the provider-config resolution site (set to `mainAgent` so we
71
72
  * inherit the agent's profile and keep the prefix cache warm). This
72
- * constant names the **observability** site — what the row IS — so
73
- * inspectors can filter "show me only compaction calls". They're
73
+ * constant names the observability site (what the row is) so
74
+ * inspectors and usage breakdowns can filter compaction calls. They're
74
75
  * semantically different even though both come from the same enum.
75
76
  */
76
77
  const COMPACTION_LOG_CALL_SITE: LLMCallSite = "compactionAgent";
77
78
 
79
+ function compactionUsageAttribution(
80
+ args: Pick<CompactionRunArgs, "overrideProfile">,
81
+ ): Pick<
82
+ CompactionRunResult,
83
+ "summaryCallSite" | "summaryResolutionCallSite" | "summaryOverrideProfile"
84
+ > {
85
+ return {
86
+ summaryCallSite: COMPACTION_LOG_CALL_SITE,
87
+ summaryResolutionCallSite: COMPACTION_CALL_SITE,
88
+ summaryOverrideProfile: args.overrideProfile ?? null,
89
+ };
90
+ }
91
+
78
92
  /**
79
93
  * What actually served a compaction call, for attribution on both the request
80
94
  * log and the usage row. A wrapper may reroute the request away from the
@@ -354,6 +368,14 @@ export interface CompactionRunResult {
354
368
  summaryOutputTokens: number;
355
369
  summaryModel: string;
356
370
  summaryCallSite?: LLMCallSite;
371
+ /**
372
+ * Call site used to resolve the winning inference profile for the
373
+ * summary. Compaction invokes the provider as `mainAgent` so the
374
+ * prefix cache stays warm; {@link summaryCallSite} is the observability
375
+ * tag (`compactionAgent`). Attribution uses this field for profile
376
+ * selection and {@link summaryCallSite} for the usage-row call site.
377
+ */
378
+ summaryResolutionCallSite?: LLMCallSite;
357
379
  summaryOverrideProfile?: string | null;
358
380
  /**
359
381
  * Provider that actually served the summary call: the response's
@@ -977,6 +999,18 @@ export async function buildRetainedImageBlocks(
977
999
  media_type: optimized.mediaType,
978
1000
  data: optimized.data,
979
1001
  },
1002
+ // The rebuilt block is inline bytes, so the row it came from is only
1003
+ // recoverable from the id the manifest entry already holds. Carrying it
1004
+ // keeps a retained image traceable to its attachment, which is what
1005
+ // camera-frame retention matches on: without it a frame the compaction
1006
+ // model chose to keep would be invisible to every later pass and could
1007
+ // outlive the retention bound.
1008
+ //
1009
+ // Assigned rather than spread through `attachmentIdFragment`, the helper
1010
+ // the conditional producers share: a manifest entry always names a row,
1011
+ // so making the field conditional here would describe an absence the
1012
+ // type rules out.
1013
+ _attachmentId: entry.attachmentId,
980
1014
  });
981
1015
  resolved.push(name);
982
1016
  }
@@ -1406,8 +1440,7 @@ export async function runAssistantDrivenCompaction(
1406
1440
  summaryCacheCreationInputTokens:
1407
1441
  response.usage.cacheCreationInputTokens ?? 0,
1408
1442
  summaryCacheReadInputTokens: response.usage.cacheReadInputTokens ?? 0,
1409
- summaryCallSite: COMPACTION_CALL_SITE,
1410
- summaryOverrideProfile: args.overrideProfile ?? null,
1443
+ ...compactionUsageAttribution(args),
1411
1444
  summaryRawResponses: response.rawResponse ? [response.rawResponse] : [],
1412
1445
  summaryRequestLogId,
1413
1446
  summaryCalls: 1,
@@ -1445,8 +1478,7 @@ export async function runAssistantDrivenCompaction(
1445
1478
  summaryCacheCreationInputTokens:
1446
1479
  response.usage.cacheCreationInputTokens ?? 0,
1447
1480
  summaryCacheReadInputTokens: response.usage.cacheReadInputTokens ?? 0,
1448
- summaryCallSite: COMPACTION_CALL_SITE,
1449
- summaryOverrideProfile: args.overrideProfile ?? null,
1481
+ ...compactionUsageAttribution(args),
1450
1482
  summaryRawResponses: response.rawResponse ? [response.rawResponse] : [],
1451
1483
  summaryCalls: 1,
1452
1484
  };
@@ -1616,8 +1648,7 @@ export async function runAssistantDrivenCompaction(
1616
1648
  summaryCacheCreationInputTokens:
1617
1649
  response.usage.cacheCreationInputTokens ?? 0,
1618
1650
  summaryCacheReadInputTokens: response.usage.cacheReadInputTokens ?? 0,
1619
- summaryCallSite: COMPACTION_CALL_SITE,
1620
- summaryOverrideProfile: args.overrideProfile ?? null,
1651
+ ...compactionUsageAttribution(args),
1621
1652
  summaryRawResponses: response.rawResponse ? [response.rawResponse] : [],
1622
1653
  summaryRequestLogId,
1623
1654
  summaryCalls: 1,
@@ -1686,8 +1717,7 @@ export async function runAssistantDrivenCompaction(
1686
1717
  summaryInputTokens: response.usage.inputTokens,
1687
1718
  summaryOutputTokens: response.usage.outputTokens,
1688
1719
  summaryModel: response.model,
1689
- summaryCallSite: COMPACTION_CALL_SITE,
1690
- summaryOverrideProfile: args.overrideProfile ?? null,
1720
+ ...compactionUsageAttribution(args),
1691
1721
  ...servedAttribution(response, args.provider),
1692
1722
  summaryCacheCreationInputTokens:
1693
1723
  response.usage.cacheCreationInputTokens ?? 0,
@@ -1874,12 +1904,11 @@ export async function runEmergencyCompaction(
1874
1904
  summaryInputTokens: response.usage.inputTokens,
1875
1905
  summaryOutputTokens: response.usage.outputTokens,
1876
1906
  summaryModel: response.model,
1877
- // This path bills real tokens, so its provider must follow a reroute
1878
- // like every other call-bearing path. It is the one such path that sets
1879
- // no `summaryCallSite`/`summaryOverrideProfile`, which leaves its
1880
- // profile attribution null; that gap predates this change and is not
1881
- // widened by it.
1907
+ // This path bills real tokens, so its provider, call site, and
1908
+ // override profile must follow the same attribution as every other
1909
+ // call-bearing compaction path.
1882
1910
  ...servedAttribution(response, args.provider),
1911
+ ...compactionUsageAttribution(args),
1883
1912
  };
1884
1913
  }
1885
1914
 
@@ -1925,8 +1954,7 @@ export async function runEmergencyCompaction(
1925
1954
  summaryInputTokens: response.usage.inputTokens,
1926
1955
  summaryOutputTokens: response.usage.outputTokens,
1927
1956
  summaryModel: response.model,
1928
- summaryCallSite: COMPACTION_CALL_SITE,
1929
- summaryOverrideProfile: args.overrideProfile ?? null,
1957
+ ...compactionUsageAttribution(args),
1930
1958
  ...servedAttribution(response, args.provider),
1931
1959
  summaryCacheCreationInputTokens:
1932
1960
  response.usage.cacheCreationInputTokens ?? 0,
@@ -20,15 +20,22 @@ const AX_TREE_TAG = "<ax-tree>";
20
20
 
21
21
  /**
22
22
  * Tools whose results are explicit, self-sized reads the model paginates
23
- * itself: `web_fetch` takes `max_chars`/`start_index` and reports a
24
- * "Character Window: X-Y of Z", so the model pages by character index against
25
- * the window it requested. Stubbing such a result at result time hands the
26
- * model ~a tenth of the window it just sized — and it keeps paging blind
27
- * against content it never saw. Like the file-read tools, these explicit
28
- * reads are honored in full for the turn that made them; the post-turn pass
29
- * still truncates them at turn end, after the model has consumed the content.
23
+ * itself: each takes `max_chars`/`start_index` and reports where its window
24
+ * ended (`web_fetch`'s "Character Window: X-Y of Z", the file-read tools'
25
+ * "[Truncated: characters X-Y of Z]" notice), so the model pages by character
26
+ * index against the window it requested. Stubbing such a result at result
27
+ * time would hand the model a fraction of the window it just sized — and it
28
+ * keeps paging blind against content it never saw. These reads are honored in
29
+ * full for the turn that made them; the post-turn pass still truncates them
30
+ * at turn end, after the model has consumed the content. This exemption is
31
+ * also what lets `READ_CHAR_BUDGET` exceed `THRESHOLD_CHARS` without a fresh
32
+ * read being stubbed before the model sees it.
30
33
  */
31
- export const RESULT_TIME_SPOOL_EXEMPT_TOOLS = new Set<string>(["web_fetch"]);
34
+ export const RESULT_TIME_SPOOL_EXEMPT_TOOLS = new Set<string>([
35
+ "web_fetch",
36
+ "file_read",
37
+ "host_file_read",
38
+ ]);
32
39
 
33
40
  /**
34
41
  * Whether a tool result is eligible for the result-time spool/stub pass: the
@@ -39,12 +46,12 @@ export const RESULT_TIME_SPOOL_EXEMPT_TOOLS = new Set<string>(["web_fetch"]);
39
46
  * The spooled-read exemption is what keeps paging possible: a file read is the
40
47
  * model's only way to pull spooled content back into context, so stubbing a
41
48
  * read of a `.tool-results/` file would write a fresh copy and hand back
42
- * another stub, putting that content out of reach for good. It is scoped by
43
- * target path rather than by tool name, because a file read aimed anywhere
44
- * else is ordinary oversized output with no such circularity, and exempting it
45
- * keeps a whole file inline on every LLM call for the rest of the turn.
46
- * Explicit self-sized reads are honored in full; the post-turn pass still
47
- * truncates them at turn end, after the model has consumed the content.
49
+ * another stub, putting that content out of reach for good. The file-read
50
+ * tools are also name-exempt as explicit self-sized reads, but this
51
+ * path-scoped guarantee stands on its own so spooled content stays reachable
52
+ * regardless of how the exempt-tools set evolves. Explicit self-sized reads
53
+ * are honored in full; the post-turn pass still truncates them at turn end,
54
+ * after the model has consumed the content.
48
55
  */
49
56
  function isSpoolEligible(
50
57
  tr: ToolResultContent,
@@ -39,8 +39,8 @@ import { getLogger } from "../util/logger.js";
39
39
  const log = getLogger("message-consolidation");
40
40
 
41
41
  /**
42
- * True when an assistant row is a standalone display turn (system card or
43
- * provider-error notice) that never merges with adjacent assistant rows in
42
+ * True when an assistant row is a standalone display turn (system card,
43
+ * provider-error notice, or deliberate-silence marker) that never merges with adjacent assistant rows in
44
44
  * either direction. Merging one into an adjacent run would let the anchor's
45
45
  * metadata win and drop the `messageKind` marker from the wire (or stamp it
46
46
  * onto a bubble containing real assistant text).
@@ -4,9 +4,19 @@
4
4
  * Enumerates all active OAuth connections and validates each one for:
5
5
  * - Token presence in secure storage
6
6
  * - Token expiry (expired or expiring within the warning window)
7
- * - Scope coverage (grantedScopes vs provider defaultScopes)
7
+ * - Scope coverage (grantedScopes vs the request that produced the token)
8
8
  * - Liveness ping (for providers with a pingUrl)
9
9
  *
10
+ * Scope coverage asks whether a connection carries the scopes the provider
11
+ * currently requests for the token it stores, which is how a newly required
12
+ * scope reaches an existing install and prompts a re-authorization. It applies
13
+ * only where an authorization makes a request. A manual-token provider (a bot
14
+ * token pasted in, e.g. `slack_channel`) has no request and carries empty
15
+ * scope lists here; it is still checked for token presence and liveness. What
16
+ * its token actually carries is read live from the `x-oauth-scopes` response
17
+ * header by `runtime/channel-readiness-service.ts`, which is the only source
18
+ * of truth available for a credential this service never negotiated.
19
+ *
10
20
  * Designed to run during the heartbeat cycle. The BYO liveness ping is
11
21
  * routed through `withValidToken`, so a stale-but-refreshable access token
12
22
  * is refreshed transparently before the ping fires — this prevents the
@@ -24,7 +34,10 @@ import {
24
34
  type OAuthConnectionRow,
25
35
  type OAuthProviderRow,
26
36
  } from "../oauth/oauth-store.js";
27
- import { scopeDifference } from "../oauth/scope-utils.js";
37
+ import {
38
+ expectedScopesForStoredToken,
39
+ scopeDifference,
40
+ } from "../oauth/scope-utils.js";
28
41
  import {
29
42
  TokenExpiredError,
30
43
  withValidToken,
@@ -182,6 +195,8 @@ interface CheckConnectionOpts {
182
195
  hasRefreshToken: boolean;
183
196
  grantedScopesRaw: string;
184
197
  defaultScopesRaw: string;
198
+ authorizeParamsRaw: string | null;
199
+ scopeSeparator: string | null;
185
200
  pingUrl: string | null;
186
201
  pingMethod: string | null;
187
202
  pingHeaders: string | null;
@@ -199,6 +214,8 @@ async function checkConnection(
199
214
  hasRefreshToken,
200
215
  grantedScopesRaw,
201
216
  defaultScopesRaw,
217
+ authorizeParamsRaw,
218
+ scopeSeparator,
202
219
  pingUrl,
203
220
  pingMethod,
204
221
  pingHeaders,
@@ -268,11 +285,21 @@ async function checkConnection(
268
285
  // Has refresh token — not an issue, auto-refresh will handle it
269
286
  }
270
287
 
271
- // 3. Check scope coverage
288
+ // 3. Check scope coverage, against the request that produced the stored
289
+ // token rather than the provider's bot-side `scope` parameter. A provider
290
+ // asking for user scopes as well stores the user token and records that
291
+ // token's grant, so the bot request names scopes that grant can never hold.
272
292
  const grantedScopes = safeJsonParse<string[]>(grantedScopesRaw, []);
273
- const defaultScopes = safeJsonParse<string[]>(defaultScopesRaw, []);
274
- if (defaultScopes.length > 0 && grantedScopes.length > 0) {
275
- const missing = scopeDifference(defaultScopes, grantedScopes);
293
+ const expectedScopes = expectedScopesForStoredToken(
294
+ safeJsonParse<string[]>(defaultScopesRaw, []),
295
+ safeJsonParse<Record<string, string> | undefined>(
296
+ authorizeParamsRaw,
297
+ undefined,
298
+ ),
299
+ scopeSeparator ?? undefined,
300
+ );
301
+ if (expectedScopes.length > 0 && grantedScopes.length > 0) {
302
+ const missing = scopeDifference(expectedScopes, grantedScopes);
276
303
  if (missing.length > 0) {
277
304
  return {
278
305
  ...base,
@@ -637,6 +664,8 @@ export async function checkAllCredentials(): Promise<CredentialHealthReport> {
637
664
  hasRefreshToken: !!conn.hasRefreshToken,
638
665
  grantedScopesRaw: conn.grantedScopes,
639
666
  defaultScopesRaw: providerRow.defaultScopes,
667
+ authorizeParamsRaw: providerRow.authorizeParams,
668
+ scopeSeparator: providerRow.scopeSeparator,
640
669
  pingUrl: providerRow.pingUrl,
641
670
  pingMethod: providerRow.pingMethod,
642
671
  pingHeaders: providerRow.pingHeaders,
@@ -750,6 +779,8 @@ export async function checkCredentialForProvider(
750
779
  hasRefreshToken: !!conn.hasRefreshToken,
751
780
  grantedScopesRaw: conn.grantedScopes,
752
781
  defaultScopesRaw: providerRow.defaultScopes,
782
+ authorizeParamsRaw: providerRow.authorizeParams,
783
+ scopeSeparator: providerRow.scopeSeparator,
753
784
  pingUrl: providerRow.pingUrl,
754
785
  pingMethod: providerRow.pingMethod,
755
786
  pingHeaders: providerRow.pingHeaders,
@@ -22,6 +22,7 @@ import { getConfig } from "../config/loader.js";
22
22
  import { recordEstimate } from "../context/estimator-calibration.js";
23
23
  import { stripInjectionsForCompaction } from "../context/strip-injections.js";
24
24
  import { getCalibrationProviderKey } from "../context/token-estimator.js";
25
+ import type { ProviderMessageMetadata } from "../messaging/provider-message-metadata.js";
25
26
  import {
26
27
  formatSlackTimezoneLabel,
27
28
  isSlackTs,
@@ -1399,6 +1400,22 @@ function buildAssistantChannelMetadata(
1399
1400
  partialSlackMeta as SlackMessageMetadata,
1400
1401
  );
1401
1402
  }
1403
+ } else if (deps.turnChannelContext.assistantMessageChannel !== "vellum") {
1404
+ // Every other channel row describes itself in the neutral envelope
1405
+ // `readProviderMetadata` serves, the shape a plugin channel writes too.
1406
+ // `messageId` stays absent here and is back-filled by the post-send
1407
+ // reconciliation once the transport returns the sent message's id, which
1408
+ // is what lets a later reaction on this row resolve back to it. No
1409
+ // `threadId`: a value there asserts a thread exists, and the reply's
1410
+ // routing thread is delivery state, not a fact about this row.
1411
+ const chatId = turnOrRestingTrust(deps.ctx)?.requesterChatId;
1412
+ if (chatId) {
1413
+ metadata.providerMeta = JSON.stringify({
1414
+ source: deps.turnChannelContext.assistantMessageChannel,
1415
+ conversationExternalId: chatId,
1416
+ eventKind: "message",
1417
+ } satisfies ProviderMessageMetadata);
1418
+ }
1402
1419
  }
1403
1420
 
1404
1421
  return metadata;
@@ -3163,7 +3180,7 @@ function handleUsage(
3163
3180
  JSON.stringify(event.rawResponse),
3164
3181
  undefined,
3165
3182
  providerName,
3166
- "mainAgent",
3183
+ deps.ctx.currentCallSite,
3167
3184
  latencyBreakdownJson,
3168
3185
  );
3169
3186
  } catch (err) {
@@ -3244,7 +3261,7 @@ function handleProviderError(
3244
3261
  JSON.stringify(buildProviderErrorResponsePayload(event.error)),
3245
3262
  undefined,
3246
3263
  event.actualProvider,
3247
- "mainAgent",
3264
+ deps.ctx.currentCallSite,
3248
3265
  );
3249
3266
  } catch (err) {
3250
3267
  deps.rlog.warn(
@@ -51,8 +51,10 @@ import {
51
51
  resolveOverrideProfile,
52
52
  updateConversationContextWindow,
53
53
  updateConversationSlackContextWatermark,
54
+ updateMessageMetadata,
54
55
  } from "../persistence/conversation-crud.js";
55
56
  import { isReplaceableTitle } from "../persistence/conversation-title-service.js";
57
+ import { NO_RESPONSE_MESSAGE_KIND } from "../persistence/conversation-types.js";
56
58
  import {
57
59
  backfillMessageIdOnLogs,
58
60
  recordSyntheticAgentErrorMessageLog,
@@ -73,6 +75,7 @@ import {
73
75
  import type { ContentBlock, Message } from "../providers/types.js";
74
76
  import type { Provider } from "../providers/types.js";
75
77
  import { resolveCapabilities } from "../runtime/capabilities.js";
78
+ import { isNoResponseOnlyText } from "../runtime/no-response.js";
76
79
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
77
80
  import { stampTurnOutcome } from "../telemetry/turn-outcome.js";
78
81
  import {
@@ -113,6 +116,7 @@ import {
113
116
  drainConversationNotices,
114
117
  } from "./conversation-notices.js";
115
118
  import {
119
+ applySightFrameRetention,
116
120
  getSlackCompactionWatermarkForPrefix,
117
121
  loadSlackChronologicalContext,
118
122
  resolveTurnInboundActorContext,
@@ -133,6 +137,10 @@ import {
133
137
  unregisterInflightTurn,
134
138
  } from "./inflight-turn-registry.js";
135
139
  import type { UsageStats } from "./message-protocol.js";
140
+ import {
141
+ persistReactionRecords,
142
+ type QueuedReactionRecord,
143
+ } from "./reaction-record.js";
136
144
  import type { TrustContext } from "./trust-context-types.js";
137
145
  import { turnOrRestingTrust } from "./trust-context-types.js";
138
146
  import { resolveTurnCallSite } from "./turn-call-site.js";
@@ -775,6 +783,14 @@ export async function runAgentLoopImpl(
775
783
  // turn's content is settled; the `finally` calls it again as the backstop for
776
784
  // the cancel/error paths that never reached the early release.
777
785
  let turnReleased = false;
786
+ // The turn's own queued reaction records, captured by `releaseTurn` while
787
+ // the conversation is still exclusive. The `finally` drains this array,
788
+ // never the live conversation field: after release a following turn may
789
+ // queue records of its own there, and draining the shared field would
790
+ // consume the new turn's record before its tool_result settles, the
791
+ // cross-turn variant of the pairing corruption the boundary drain
792
+ // prevents.
793
+ const ownedReactionRecords: QueuedReactionRecord[] = [];
778
794
 
779
795
  /**
780
796
  * Free the conversation for its next turn.
@@ -814,6 +830,11 @@ export async function runAgentLoopImpl(
814
830
  );
815
831
  }
816
832
  }
833
+ // Ownership capture happens in the same synchronous step as the
834
+ // release, so records queued after this point belong to the next turn.
835
+ if (ctx.pendingReactionRecords?.length) {
836
+ ownedReactionRecords.push(...ctx.pendingReactionRecords.splice(0));
837
+ }
817
838
  ctx.abortController = null;
818
839
  ctx.setProcessing(false);
819
840
  unregisterInflightTurn(ctx.conversationId, state);
@@ -1255,10 +1276,22 @@ export async function runAgentLoopImpl(
1255
1276
  // immediately before the call, after every hook (memory injection, title,
1256
1277
  // user plugins) has settled its shape. Runs unconditionally — a malformed
1257
1278
  // history is a hard provider rejection, never a per-conversation opt-in.
1258
- const runMessages = repairHistoryForRun(
1279
+ const repairedMessages = repairHistoryForRun(
1259
1280
  finalUserPromptCtx.latestMessages,
1260
1281
  ctx.conversationId,
1261
1282
  );
1283
+ // Camera-frame retention: keep only the newest few ambient frames as real
1284
+ // images. Applied to the per-turn copy, after every hook and the repair, so
1285
+ // every turn this loop drives inherits it and the stored transcript keeps
1286
+ // its attachments. The loop re-runs the same pass on any history its
1287
+ // in-place compaction rebuilds (see the `transformCompactedHistory` closure
1288
+ // the conversation supplies), so the bound holds across a compaction rather
1289
+ // than only from the next turn. The overflow ladder's media stubbing still
1290
+ // governs everything the user deliberately attached.
1291
+ const runMessages = applySightFrameRetention(
1292
+ repairedMessages,
1293
+ ctx.conversationId,
1294
+ );
1262
1295
 
1263
1296
  // Reset the manager's turn-scoped overflow-recovery ladder at the turn
1264
1297
  // boundary so a new turn starts the ladder fresh from the emergency rung.
@@ -1563,6 +1596,32 @@ export async function runAgentLoopImpl(
1563
1596
  // of why the turn stopped.
1564
1597
  const hasAssistantResponse =
1565
1598
  newMessages[newMessages.length - 1]?.role === "assistant";
1599
+ // A reply that is nothing but the `<no_response/>` sentinel is deliberate
1600
+ // silence. Stamp the persisted row so clients render a quiet notice and
1601
+ // resolve their pending-response state, instead of showing the raw
1602
+ // sentinel or waiting forever; the content keeps the sentinel so the
1603
+ // model reads its own convention back from history.
1604
+ if (hasAssistantResponse && state.lastAssistantMessageId) {
1605
+ const tail = newMessages[newMessages.length - 1]!;
1606
+ const tailText = Array.isArray(tail.content)
1607
+ ? tail.content
1608
+ .filter((b) => b.type === "text")
1609
+ .map((b) => (b.type === "text" ? b.text : ""))
1610
+ .join("\n")
1611
+ : "";
1612
+ if (isNoResponseOnlyText(tailText)) {
1613
+ try {
1614
+ updateMessageMetadata(state.lastAssistantMessageId, {
1615
+ messageKind: NO_RESPONSE_MESSAGE_KIND,
1616
+ });
1617
+ } catch (err) {
1618
+ rlog.warn(
1619
+ { err, messageId: state.lastAssistantMessageId },
1620
+ "Failed to stamp deliberate-silence marker (non-fatal)",
1621
+ );
1622
+ }
1623
+ }
1624
+ }
1566
1625
  if (
1567
1626
  !hasAssistantResponse &&
1568
1627
  state.providerErrorUserMessage &&
@@ -1998,6 +2057,16 @@ export async function runAgentLoopImpl(
1998
2057
  // the API, enabling stable prefix caching across turns. Compaction
1999
2058
  // consolidates when it summarizes old messages (cache miss is expected).
2000
2059
  } finally {
2060
+ // Backstop for paths that threw before either release site: still
2061
+ // exclusive here, so the release's ownership capture is race-free.
2062
+ releaseTurn();
2063
+ // Reactions the turn delivered get their durable rows on every
2064
+ // terminal path, error exits included: the reaction already happened
2065
+ // on the channel, and the record must not depend on the turn
2066
+ // finishing cleanly. Only records this turn captured at release are
2067
+ // drained; writing after the turn stops producing rows keeps them out
2068
+ // of any tool_use/tool_result pair.
2069
+ await drainQueuedReactionRecords(ctx, ownedReactionRecords);
2001
2070
  // kickDrainQueue never rejects: a drain failure here would otherwise be
2002
2071
  // an unhandled rejection that strands the queue with nothing left to
2003
2072
  // re-trigger it.
@@ -2009,6 +2078,32 @@ export async function runAgentLoopImpl(
2009
2078
  }
2010
2079
  }
2011
2080
 
2081
+ /**
2082
+ * Persist and clear the turn's queued reaction records. Non-throwing: a
2083
+ * failure here must never mask the turn's own outcome, and
2084
+ * `persistReactionRecords` already logs per-record failures.
2085
+ */
2086
+ export async function drainQueuedReactionRecords(
2087
+ ctx: Pick<Conversation, "conversationId" | "markHistoryStale">,
2088
+ ownedRecords: QueuedReactionRecord[] | undefined,
2089
+ ): Promise<void> {
2090
+ // Runs inside the loop's finally, where a throw would mask the turn's own
2091
+ // outcome, so every access sits inside the guard. Drains only the records
2092
+ // the finished turn captured at release, never the conversation's live
2093
+ // queue, so one turn cannot consume a following turn's records.
2094
+ try {
2095
+ if (!ownedRecords?.length) {
2096
+ return;
2097
+ }
2098
+ const queuedReactions = ownedRecords.splice(0);
2099
+ await persistReactionRecords(ctx.conversationId, queuedReactions);
2100
+ ctx.markHistoryStale();
2101
+ } catch {
2102
+ // persistReactionRecords is itself non-throwing per record; this guard
2103
+ // covers only unexpected faults so the finally block stays safe.
2104
+ }
2105
+ }
2106
+
2012
2107
  // ── Helper ───────────────────────────────────────────────────────────
2013
2108
 
2014
2109
  function emitUsage(
@@ -2110,6 +2205,7 @@ export async function applyCompactionResult(
2110
2205
  summaryCacheReadInputTokens?: number;
2111
2206
  summaryRawResponses?: unknown[];
2112
2207
  summaryCallSite?: LLMCallSite;
2208
+ summaryResolutionCallSite?: LLMCallSite;
2113
2209
  summaryOverrideProfile?: string | null;
2114
2210
  summaryActualProvider?: string;
2115
2211
  summaryActualInferenceProfile?: string;
@@ -2200,6 +2296,9 @@ export async function applyCompactionResult(
2200
2296
  // this parameter independently off their own state and never share it.
2201
2297
  {
2202
2298
  callSite: result.summaryCallSite ?? null,
2299
+ ...(result.summaryResolutionCallSite != null
2300
+ ? { profileResolutionCallSite: result.summaryResolutionCallSite }
2301
+ : {}),
2203
2302
  overrideProfile:
2204
2303
  result.summaryActualInferenceProfile ??
2205
2304
  result.summaryOverrideProfile ??
@@ -4,11 +4,17 @@
4
4
  * When the provider rejects a request because the context is too large,
5
5
  * this module replaces older image and file content blocks with lightweight
6
6
  * text stubs to shrink the payload before retrying.
7
+ *
8
+ * The proactive counterpart, applied on every assembly and only to ambient
9
+ * camera frames, is `stripAgedSightFrames` in `conversation-sight-frames.ts`.
10
+ * Both paths name what they dropped through `mediaSourceDescriptor`, so the
11
+ * stubs describe media identically. A block that path already stubbed is plain
12
+ * text here, so it is neither counted as media nor stubbed twice.
7
13
  */
8
14
 
9
15
  import { estimateContentBlockTokens } from "../context/token-estimator.js";
10
16
  import { getSummaryFromContextMessage } from "../plugins/defaults/compaction/window-manager.js";
11
- import { mediaSourceByteLength } from "../providers/media-resolve.js";
17
+ import { mediaSourceDescriptor } from "../providers/media-resolve.js";
12
18
  import type { ContentBlock, Message } from "../providers/types.js";
13
19
 
14
20
  export interface StripMediaOptions {
@@ -219,17 +225,16 @@ export function estimateUnconditionalStubTokens(
219
225
  function imageBlockToStub(
220
226
  block: Extract<ContentBlock, { type: "image" }>,
221
227
  ): Extract<ContentBlock, { type: "text" }> {
222
- const sizeBytes = mediaSourceByteLength(block.source);
223
228
  return {
224
229
  type: "text",
225
- text: `[Image omitted from retry context: ${block.source.media_type}, ${sizeBytes} bytes]`,
230
+ text: `[Image omitted from retry context: ${mediaSourceDescriptor(block.source)}]`,
226
231
  };
227
232
  }
228
233
 
229
234
  function fileBlockToStub(
230
235
  block: Extract<ContentBlock, { type: "file" }>,
231
236
  ): Extract<ContentBlock, { type: "text" }> {
232
- const sizeBytes = mediaSourceByteLength(block.source);
237
+ const descriptor = mediaSourceDescriptor(block.source);
233
238
  const extracted = (block.extracted_text ?? "").trim();
234
239
  const preview =
235
240
  extracted.length > MAX_MEDIA_STUB_TEXT
@@ -239,8 +244,8 @@ function fileBlockToStub(
239
244
  type: "text",
240
245
  text:
241
246
  preview.length > 0
242
- ? `[File omitted from retry context: ${block.source.filename} (${block.source.media_type}, ${sizeBytes} bytes)]\n${preview}`
243
- : `[File omitted from retry context: ${block.source.filename} (${block.source.media_type}, ${sizeBytes} bytes)]`,
247
+ ? `[File omitted from retry context: ${block.source.filename} (${descriptor})]\n${preview}`
248
+ : `[File omitted from retry context: ${block.source.filename} (${descriptor})]`,
244
249
  };
245
250
  }
246
251