@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
@@ -385,7 +385,13 @@ mock.module("../runtime/access-request-helper.js", () => ({
385
385
  // Gateway guardian-request client polled by the guardian wait controller.
386
386
  let mockGuardianRequest: { status: string } | null = null;
387
387
  const mockGetGuardianRequest = jest.fn(async () => mockGuardianRequest);
388
+ // Spread the actual module so transitive importers of names this
389
+ // factory does not stub keep resolving (a partial factory breaks at
390
+ // import time when the graph gains a new named import).
391
+ const actualGatewayGuardianRequests =
392
+ await import("../channels/gateway-guardian-requests.js");
388
393
  mock.module("../channels/gateway-guardian-requests.js", () => ({
394
+ ...actualGatewayGuardianRequests,
389
395
  getGuardianRequestOrNull: mockGetGuardianRequest,
390
396
  }));
391
397
 
@@ -318,6 +318,18 @@ describe("non-member access request notification", () => {
318
318
  kind: "access_request",
319
319
  });
320
320
  expect(pending.length).toBe(1);
321
+
322
+ // A pending row past its deadline never dedupes: it can sit pending
323
+ // until the sweep expires it, it is already undecidable, and absorbing
324
+ // the fresh attempt would silently drop the new guardian notification.
325
+ pending[0].expiresAt = Date.now() - 1000;
326
+ const req3 = buildInboundRequest({
327
+ externalMessageId: `msg-third-${Date.now()}`,
328
+ content: "Still hoping to get access.",
329
+ });
330
+ await handleChannelInbound(req3, undefined, TEST_BEARER_TOKEN);
331
+
332
+ expect(emitSignalCalls.length).toBe(2);
321
333
  });
322
334
 
323
335
  // A bare guardian deny (a `denied` request with no durable contact seeded)
@@ -10,20 +10,44 @@
10
10
  import { beforeEach, describe, expect, mock, test } from "bun:test";
11
11
 
12
12
  const dmOpens: string[] = [];
13
- const sendCalls: Array<{ channelId: string; text: string }> = [];
13
+ const sendCalls: Array<{
14
+ channelId: string;
15
+ text: string;
16
+ approval?: { requestId: string } | undefined;
17
+ }> = [];
14
18
  const editCalls: Array<{ channelId: string; messageId: string; text: string }> =
15
19
  [];
16
20
 
21
+ // Spread the actual module: the real send.js is imported below for its
22
+ // error class and transitively needs api.js's other named exports.
23
+ const actualApi = await import("../messaging/providers/discord/api.js");
17
24
  mock.module("../messaging/providers/discord/api.js", () => ({
25
+ ...actualApi,
18
26
  openDiscordDmChannel: async (userId: string) => {
19
27
  dmOpens.push(userId);
20
28
  return `dm-for-${userId}`;
21
29
  },
22
30
  }));
23
31
 
32
+ const actualSend = await import("../messaging/providers/discord/send.js");
24
33
  mock.module("../messaging/providers/discord/send.js", () => ({
25
- sendDiscordReply: async (target: { channelId: string }, text: string) => {
26
- sendCalls.push({ channelId: target.channelId, text });
34
+ DiscordPartialSendError: actualSend.DiscordPartialSendError,
35
+ sendDiscordReply: async (
36
+ target: { channelId: string },
37
+ text: string,
38
+ approval?: { requestId: string },
39
+ ) => {
40
+ if (approval && failRichSends) {
41
+ throw failRichSends === "partial"
42
+ ? new actualSend.DiscordPartialSendError(
43
+ new Error("final chunk rejected"),
44
+ 2,
45
+ "tail of the card",
46
+ "1500",
47
+ )
48
+ : new Error("simulated component rejection");
49
+ }
50
+ sendCalls.push({ channelId: target.channelId, text, approval });
27
51
  return { lastMessageId: String(2000 + sendCalls.length) };
28
52
  },
29
53
  editDiscordMessage: async (
@@ -68,10 +92,13 @@ function makeDestination(
68
92
  };
69
93
  }
70
94
 
95
+ let failRichSends: boolean | "partial" = false;
96
+
71
97
  beforeEach(() => {
72
98
  dmOpens.length = 0;
73
99
  sendCalls.length = 0;
74
100
  editCalls.length = 0;
101
+ failRichSends = false;
75
102
  });
76
103
 
77
104
  describe("DiscordAdapter.send", () => {
@@ -87,7 +114,51 @@ describe("DiscordAdapter.send", () => {
87
114
  expect(sendCalls[0].text).toContain("Your task finished.");
88
115
  });
89
116
 
90
- test("approval notifications carry the typed-command instructions", async () => {
117
+ test("approval notifications deliver with component buttons", async () => {
118
+ const adapter = new DiscordAdapter();
119
+ const result = await adapter.send(
120
+ makePayload({
121
+ approvalContext: {
122
+ requestId: "req-1",
123
+ actions: [{ id: "approve_once", label: "Approve once" }],
124
+ plainTextFallback: 'Reply "approve" or "reject" to decide.',
125
+ },
126
+ }),
127
+ makeDestination(),
128
+ );
129
+
130
+ expect(result.success).toBe(true);
131
+ expect(sendCalls).toHaveLength(1);
132
+ expect(sendCalls[0].approval?.requestId).toBe("req-1");
133
+ // The rich card carries no typed-command tail; buttons are the controls.
134
+ expect(sendCalls[0].text).not.toContain("Reply");
135
+ });
136
+
137
+ test("a mid-card failure completes the card instead of replaying it", async () => {
138
+ failRichSends = "partial";
139
+ const adapter = new DiscordAdapter();
140
+ const result = await adapter.send(
141
+ makePayload({
142
+ approvalContext: {
143
+ requestId: "req-1",
144
+ actions: [{ id: "approve_once", label: "Approve once" }],
145
+ plainTextFallback: 'Reply "approve" or "reject" to decide.',
146
+ },
147
+ }),
148
+ makeDestination(),
149
+ );
150
+
151
+ expect(result.success).toBe(true);
152
+ expect(sendCalls).toHaveLength(1);
153
+ // Only the undelivered remainder goes out, with the typed instructions;
154
+ // the delivered chunks are never sent twice.
155
+ expect(sendCalls[0].approval).toBeUndefined();
156
+ expect(sendCalls[0].text).toContain("tail of the card");
157
+ expect(sendCalls[0].text).toContain('Reply "approve" or "reject"');
158
+ });
159
+
160
+ test("a failed rich delivery falls back to the typed-command card", async () => {
161
+ failRichSends = true;
91
162
  const adapter = new DiscordAdapter();
92
163
  const result = await adapter.send(
93
164
  makePayload({
@@ -102,6 +173,7 @@ describe("DiscordAdapter.send", () => {
102
173
 
103
174
  expect(result.success).toBe(true);
104
175
  expect(sendCalls).toHaveLength(1);
176
+ expect(sendCalls[0].approval).toBeUndefined();
105
177
  expect(sendCalls[0].text).toContain(
106
178
  'Reply "approve" or "reject" to decide.',
107
179
  );
@@ -717,6 +717,27 @@ describe("POST oauth/request", () => {
717
717
  expect(req.body).toEqual({ title: "Sheet" });
718
718
  });
719
719
 
720
+ test("decodes a base64 request body into a Buffer", async () => {
721
+ const binary = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0xff, 0x00]);
722
+
723
+ await getRoute("POST", "oauth/request").handler(
724
+ makeArgs({
725
+ body: {
726
+ provider: "google",
727
+ url: "https://api.google.com/upload/drive/v3/files",
728
+ method: "POST",
729
+ headers: { "Content-Type": "application/pdf" },
730
+ parsed_data: binary.toString("base64"),
731
+ body_encoding: "base64",
732
+ },
733
+ }),
734
+ );
735
+
736
+ const req = mockResolveRequests[0] as { body: unknown };
737
+ expect(Buffer.isBuffer(req.body)).toBe(true);
738
+ expect(Buffer.from(req.body as Uint8Array).equals(binary)).toBe(true);
739
+ });
740
+
720
741
  test("base64-encodes binary response bodies for the JSON envelope", async () => {
721
742
  const binary = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0xff, 0x00]);
722
743
  mockResolveResponse = {
@@ -2051,3 +2051,66 @@ describe("FireworksProvider reasoning_effort ceiling", () => {
2051
2051
  expect(lastCreateParams!.reasoning_effort).toBe("max");
2052
2052
  });
2053
2053
  });
2054
+
2055
+ // GLM 5.3 accepts only low|high|max and 4xxes on the in-between tiers, so
2056
+ // the catalog's `supportedEfforts` snaps them down (see
2057
+ // snapReasoningEffortToSupported).
2058
+ describe("FireworksProvider sparse reasoning_effort support (GLM 5.3)", () => {
2059
+ beforeEach(() => {
2060
+ fakeChunks = [];
2061
+ lastCreateParams = null;
2062
+ });
2063
+
2064
+ const send = async (
2065
+ model: string,
2066
+ effort: "none" | "low" | "medium" | "high" | "xhigh" | "max",
2067
+ ) => {
2068
+ const fw = new FireworksProvider("fw-key", model);
2069
+ await fw.sendMessage([userMsg("hi")], {
2070
+ systemPrompt: "system",
2071
+ config: { effort },
2072
+ });
2073
+ return lastCreateParams!.reasoning_effort;
2074
+ };
2075
+
2076
+ test('GLM 5.3 snaps "medium" down to "low"', async () => {
2077
+ expect(await send("accounts/fireworks/models/glm-5p3", "medium")).toBe(
2078
+ "low",
2079
+ );
2080
+ });
2081
+
2082
+ test('GLM 5.3 snaps "xhigh" down to "high"', async () => {
2083
+ expect(await send("accounts/fireworks/models/glm-5p3", "xhigh")).toBe(
2084
+ "high",
2085
+ );
2086
+ });
2087
+
2088
+ test("GLM 5.3 forwards supported values verbatim", async () => {
2089
+ expect(await send("accounts/fireworks/models/glm-5p3", "low")).toBe("low");
2090
+ expect(await send("accounts/fireworks/models/glm-5p3", "high")).toBe(
2091
+ "high",
2092
+ );
2093
+ expect(await send("accounts/fireworks/models/glm-5p3", "max")).toBe("max");
2094
+ });
2095
+
2096
+ test('GLM 5.3 Flash snaps "medium"/"xhigh" the same way', async () => {
2097
+ expect(
2098
+ await send("accounts/fireworks/models/glm-5p3-flash", "medium"),
2099
+ ).toBe("low");
2100
+ expect(await send("accounts/fireworks/models/glm-5p3-flash", "xhigh")).toBe(
2101
+ "high",
2102
+ );
2103
+ });
2104
+
2105
+ test('effort: "none" is not snapped (opt-out keeps its own handling)', async () => {
2106
+ expect(await send("accounts/fireworks/models/glm-5p3", "none")).toBe(
2107
+ "none",
2108
+ );
2109
+ });
2110
+
2111
+ test("models without supportedEfforts are unaffected", async () => {
2112
+ expect(
2113
+ await send("accounts/fireworks/models/deepseek-v4-pro", "medium"),
2114
+ ).toBe("medium");
2115
+ });
2116
+ });
@@ -185,7 +185,7 @@ import { setConfig } from "./helpers/set-config.js";
185
185
  function makeDeps(
186
186
  conversationId: string,
187
187
  overrides: {
188
- assistantMessageChannel?: "slack" | "vellum" | "telegram";
188
+ assistantMessageChannel?: "slack" | "vellum" | "telegram" | "discord";
189
189
  requesterChatId?: string;
190
190
  requesterTimezoneLabel?: string;
191
191
  clientTimezone?: string;
@@ -518,7 +518,119 @@ describe("outbound assistant Slack metadata persistence", () => {
518
518
 
519
519
  const persisted = lastAssistantPersisted();
520
520
  expect(persisted.metadata).toBeDefined();
521
- // Non-Slack channels must leave the existing metadata shape untouched.
521
+ // Non-Slack channels must leave the existing metadata shape untouched,
522
+ // and a vellum turn is not a provider message, so it gets no neutral
523
+ // envelope either.
522
524
  expect(persisted.metadata?.slackMeta).toBeUndefined();
525
+ expect(persisted.metadata?.providerMeta).toBeUndefined();
526
+ });
527
+
528
+ test("stamps a partial neutral envelope on non-Slack channel replies", async () => {
529
+ const conversationId = "conv-discord-stamp";
530
+ const deps = makeDeps(conversationId, {
531
+ assistantMessageChannel: "discord",
532
+ requesterChatId: "9990001112223334445",
533
+ });
534
+ await handleLlmCallStarted(state, deps);
535
+ await handleMessageComplete(
536
+ state,
537
+ deps,
538
+ makeMessageCompleteEvent("discord reply"),
539
+ );
540
+
541
+ const persisted = lastAssistantPersisted();
542
+ const providerMetaRaw = persisted.metadata?.providerMeta;
543
+ expect(typeof providerMetaRaw).toBe("string");
544
+ const providerMeta = JSON.parse(providerMetaRaw as string) as Record<
545
+ string,
546
+ unknown
547
+ >;
548
+ expect(providerMeta.source).toBe("discord");
549
+ expect(providerMeta.conversationExternalId).toBe("9990001112223334445");
550
+ expect(providerMeta.eventKind).toBe("message");
551
+ // Persistence runs BEFORE the transport posts the message, so the
552
+ // provider-assigned id is not yet known; the post-send reconciliation in
553
+ // `deliverReplyViaCallback` fills `messageId` once the transport returns
554
+ // it. `threadId` is never stamped: a value there asserts a thread exists.
555
+ expect(providerMeta.messageId).toBeUndefined();
556
+ expect(providerMeta.threadId).toBeUndefined();
557
+ expect(persisted.metadata?.slackMeta).toBeUndefined();
558
+ });
559
+
560
+ test("post-send reconciliation patches messageId into the neutral envelope", async () => {
561
+ const conversationId = "conv-discord-reconcile";
562
+ const chatId = "9990001112223334445";
563
+ const deps = makeDeps(conversationId, {
564
+ assistantMessageChannel: "discord",
565
+ requesterChatId: chatId,
566
+ });
567
+ await handleLlmCallStarted(state, deps);
568
+ await handleMessageComplete(
569
+ state,
570
+ deps,
571
+ makeMessageCompleteEvent("discord reconciliation reply"),
572
+ );
573
+
574
+ const persisted = lastAssistantPersisted();
575
+ nextDeliveryTs = "1234567890123456789";
576
+ await deliverReplyViaCallback(
577
+ conversationId,
578
+ chatId,
579
+ "http://gateway/deliver/discord",
580
+ "assistant-1",
581
+ { messageId: persisted.id },
582
+ );
583
+
584
+ const row = persistedRows.find(
585
+ (candidate) => candidate.id === persisted.id,
586
+ );
587
+ expect(typeof row?.metadata).toBe("string");
588
+ const envelope = JSON.parse(row!.metadata!) as Record<string, unknown>;
589
+ const reconciled = JSON.parse(envelope.providerMeta as string) as Record<
590
+ string,
591
+ unknown
592
+ >;
593
+ expect(reconciled.messageId).toBe("1234567890123456789");
594
+ expect(reconciled.source).toBe("discord");
595
+ expect(reconciled.conversationExternalId).toBe(chatId);
596
+
597
+ // A later report with a NEW id is another post of the same reply (a
598
+ // split at a tool boundary): it lands in additionalMessageIds and the
599
+ // first id keeps naming the row.
600
+ nextDeliveryTs = "8888888888888888888";
601
+ await deliverReplyViaCallback(
602
+ conversationId,
603
+ chatId,
604
+ "http://gateway/deliver/discord",
605
+ "assistant-1",
606
+ { messageId: persisted.id },
607
+ );
608
+ const rowAfter = persistedRows.find(
609
+ (candidate) => candidate.id === persisted.id,
610
+ );
611
+ const metaAfter = JSON.parse(
612
+ JSON.parse(rowAfter!.metadata!).providerMeta as string,
613
+ ) as Record<string, unknown>;
614
+ expect(metaAfter.messageId).toBe("1234567890123456789");
615
+ expect(metaAfter.additionalMessageIds).toEqual(["8888888888888888888"]);
616
+
617
+ // A redelivery reporting an already-recorded id changes nothing.
618
+ nextDeliveryTs = "8888888888888888888";
619
+ await deliverReplyViaCallback(
620
+ conversationId,
621
+ chatId,
622
+ "http://gateway/deliver/discord",
623
+ "assistant-1",
624
+ { messageId: persisted.id },
625
+ );
626
+ const rowRedelivered = persistedRows.find(
627
+ (candidate) => candidate.id === persisted.id,
628
+ );
629
+ const metaRedelivered = JSON.parse(
630
+ JSON.parse(rowRedelivered!.metadata!).providerMeta as string,
631
+ ) as Record<string, unknown>;
632
+ expect(metaRedelivered.additionalMessageIds).toEqual([
633
+ "8888888888888888888",
634
+ ]);
523
635
  });
524
636
  });
@@ -33,6 +33,7 @@ mock.module("../agent/image-optimize.js", () => ({
33
33
  }),
34
34
  }));
35
35
 
36
+ import { uploadAttachment } from "../persistence/attachments-store.js";
36
37
  import {
37
38
  addMessage,
38
39
  createConversation,
@@ -40,7 +41,10 @@ import {
40
41
  } from "../persistence/conversation-crud.js";
41
42
  import { getDb, getMemorySqlite } from "../persistence/db-connection.js";
42
43
  import { initializeDb } from "../persistence/db-init.js";
43
- import { persistUnsendableImageDowngrades } from "../plugins/defaults/image-recovery/recover.js";
44
+ import {
45
+ persistUnsendableImageDowngrades,
46
+ unsendableImageReplacement,
47
+ } from "../plugins/defaults/image-recovery/recover.js";
44
48
  import { base64Source } from "../providers/media-resolve.js";
45
49
  import type { ContentBlock } from "../providers/types.js";
46
50
 
@@ -147,6 +151,64 @@ describe("persistUnsendableImageDowngrades (downscalable host)", () => {
147
151
  ).toBe(SHRUNK_DATA);
148
152
  });
149
153
 
154
+ /** The resize replaces the block's whole source, so a reference-backed image
155
+ * loses `source.attachmentId` along with the reference. The id has to be
156
+ * derived from either shape and re-stamped, or the durable rewrite untags a
157
+ * camera frame permanently. */
158
+ test("a resized workspace_ref keeps its attachment id on the rebuilt block", async () => {
159
+ const stored = await uploadAttachment(
160
+ "frame.png",
161
+ "image/png",
162
+ oversizedPngBase64(),
163
+ );
164
+ const referenceBlock = {
165
+ type: "image",
166
+ source: {
167
+ type: "workspace_ref",
168
+ media_type: "image/png",
169
+ attachmentId: stored.id,
170
+ sizeBytes: 128,
171
+ // Persisted references carry the dimension hints the provider caps are
172
+ // judged against (see `attachmentsToReferenceBlocks`).
173
+ width: 12000,
174
+ height: 9000,
175
+ },
176
+ } as Extract<ContentBlock, { type: "image" }>;
177
+
178
+ const replacement = await unsendableImageReplacement(referenceBlock);
179
+
180
+ // The source was flattened to the shrunk inline bytes...
181
+ expect(replacement).toMatchObject({
182
+ type: "image",
183
+ source: { type: "base64", data: SHRUNK_DATA },
184
+ });
185
+ // ...and the link to the attachment row survived the flattening.
186
+ expect((replacement as { _attachmentId?: string })._attachmentId).toBe(
187
+ stored.id,
188
+ );
189
+ });
190
+
191
+ /** An inline resize input carries its id on the top-level field instead. */
192
+ test("a resized inline block keeps its top-level attachment id", async () => {
193
+ const inlineBlock = {
194
+ type: "image",
195
+ source: {
196
+ type: "base64",
197
+ media_type: "image/png",
198
+ data: oversizedPngBase64(),
199
+ },
200
+ _attachmentId: "att-inline-1",
201
+ } as Extract<ContentBlock, { type: "image" }>;
202
+
203
+ const replacement = await unsendableImageReplacement(inlineBlock);
204
+
205
+ expect(replacement).toMatchObject({
206
+ type: "image",
207
+ source: { type: "base64", data: SHRUNK_DATA },
208
+ _attachmentId: "att-inline-1",
209
+ });
210
+ });
211
+
150
212
  /** Re-running is a no-op: the downscaled payload is within limits. */
151
213
  test("is idempotent after a downscale rewrite", async () => {
152
214
  // GIVEN a conversation whose oversized screenshot was already downscaled
@@ -392,6 +392,25 @@ describe("unsendableImageReplacement", () => {
392
392
  });
393
393
  });
394
394
 
395
+ /** Relabeling rebuilds the block, and the rebuilt form is written back to
396
+ * the stored row. Losing the internal attachment id there would untag a
397
+ * camera frame for good, so the relabel carries it across. */
398
+ test("relabeling preserves the internal attachment id", async () => {
399
+ const pngData = makePngBase64(1024, 768);
400
+ const mislabeled = {
401
+ ...(imageBlock(pngData, "image/jpeg") as Extract<
402
+ ContentBlock,
403
+ { type: "image" }
404
+ >),
405
+ _attachmentId: "att-frame-1",
406
+ };
407
+ const replacement = await unsendableImageReplacement(mislabeled);
408
+ expect(replacement).toMatchObject({
409
+ type: "image",
410
+ _attachmentId: "att-frame-1",
411
+ });
412
+ });
413
+
395
414
  /** Relabeling a persisted (workspace_ref) image keeps the reference shape —
396
415
  * inlining it would bake the full payload into the stored message row. */
397
416
  test("relabels a mislabeled workspace_ref without inlining the payload", async () => {
@@ -271,10 +271,10 @@ describe("multi-file plugin apps compile on open", () => {
271
271
  expect(result.html).toContain("Compiled Board");
272
272
  expect(result.html).not.toContain("not been compiled");
273
273
  expect(result.origin).toBe("plugin:acme");
274
- // The daemon must not write dist/ into the read-only plugin tree — that is
274
+ // The daemon must not write dist/ into the read-only plugin tree. That is
275
275
  // the monitor's job. The on-open build happened in a throwaway temp dir.
276
276
  expect(existsSync(join(appDir, "dist"))).toBe(false);
277
- });
277
+ }, 15_000);
278
278
 
279
279
  test("apps_open on a malformed plugin app (no src/, no dist/) serves the fallback, not a 500", async () => {
280
280
  // An app dir with neither a root index.html nor src/ nor dist/ is
@@ -86,6 +86,7 @@ const BASELINE: Record<string, readonly string[]> = {
86
86
  "../../../../../messaging/providers/slack/message-metadata.js",
87
87
  "../../../../../persistence/auto-analysis-constants.js",
88
88
  "../../../../../persistence/checkpoints.js",
89
+ "../../../../../persistence/conversation-types.js",
89
90
  "../../../../../persistence/db-connection.js",
90
91
  "../../../../../persistence/embeddings/embed.js",
91
92
  "../../../../../persistence/embeddings/embedding-backend.js",
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  mock.module("../persistence/attachments-store.js", () => ({
4
4
  getAttachmentsByIds: () => [],
5
- getSourcePathsForAttachments: () => new Map<string, string>(),
5
+ resolveAttachmentsForPersist: () => [],
6
6
  }));
7
7
 
8
8
  mock.module("../channels/gateway-guardian-requests.js", () => ({
@@ -27,7 +27,7 @@ let validateShouldFail = false;
27
27
 
28
28
  mock.module("../persistence/attachments-store.js", () => ({
29
29
  getAttachmentsByIds: () => [],
30
- getSourcePathsForAttachments: () => new Map<string, string>(),
30
+ resolveAttachmentsForPersist: () => [],
31
31
  attachmentExists: () => false,
32
32
  linkAttachmentToMessage: () => "att-stored",
33
33
  scopeAttachmentToMessageConversation: () => null,
@@ -97,11 +97,6 @@ mock.module("../persistence/attachments-store.js", () => ({
97
97
  linkAttachmentToMessage: () => {},
98
98
  }));
99
99
 
100
- mock.module("../memory/retriever.js", () => ({
101
- buildMemoryRecall: async () => null,
102
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
103
- }));
104
-
105
100
  mock.module("../memory/query-builder.js", () => ({
106
101
  buildMemoryQuery: () => "",
107
102
  }));
@@ -165,23 +165,6 @@ mock.module("../persistence/attachments-store.js", () => ({
165
165
  uploadAttachment: () => ({ id: "att-1" }),
166
166
  linkAttachmentToMessage: () => {},
167
167
  }));
168
- mock.module("../memory/retriever.js", () => ({
169
- buildMemoryRecall: async () => ({
170
- enabled: false,
171
- degraded: false,
172
- reason: null,
173
- provider: "mock",
174
- model: "mock",
175
- injectedText: "",
176
- semanticHits: 0,
177
- mergedCount: 0,
178
- selectedCount: 0,
179
- injectedTokens: 0,
180
- latencyMs: 0,
181
- topCandidates: [],
182
- }),
183
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
184
- }));
185
168
  mock.module("../memory/query-builder.js", () => ({
186
169
  buildMemoryQuery: () => "",
187
170
  }));
@@ -109,6 +109,12 @@ describe("QdrantManager", () => {
109
109
  });
110
110
  });
111
111
 
112
+ test("falls back to the x64 zip release on Windows arm64", () => {
113
+ expect(resolveQdrantReleaseAsset("win32", "arm64")).toEqual(
114
+ resolveQdrantReleaseAsset("win32", "x64"),
115
+ );
116
+ });
117
+
112
118
  // ── getUrl ───────────────────────────────────────────────────
113
119
 
114
120
  describe("getUrl", () => {