@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,304 @@
1
+ /**
2
+ * The agent loop's post-compaction history transform.
3
+ *
4
+ * In-place compaction rebuilds history from the stored rows, so it can
5
+ * reintroduce content the caller had already trimmed out of the array it handed
6
+ * to `run()`. The rebuilt array is installed and sent on the very next request
7
+ * of the same turn, so the caller's trim has to be re-applied at the
8
+ * installation site rather than only before the run. The loop stays
9
+ * conversation-ignorant: the caller injects a closure, and this suite drives
10
+ * that seam with the real camera-frame retention transform.
11
+ */
12
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
13
+
14
+ import type { PostCompactContext } from "@vellumai/plugin-api";
15
+
16
+ import { AgentLoop } from "../agent/loop.js";
17
+ import type { ContextWindowConfig } from "../config/types.js";
18
+ import { stripAgedSightFrames } from "../daemon/conversation-sight-frames.js";
19
+ import type { TrustContext } from "../daemon/trust-context-types.js";
20
+ import { HOOKS } from "../plugin-api/constants.js";
21
+ import {
22
+ createContextWindowManager,
23
+ disposeContextWindowManager,
24
+ getContextWindowManager,
25
+ } from "../plugins/defaults/compaction/manager-store.js";
26
+ import {
27
+ registerPlugin,
28
+ resetPluginRegistryForTests,
29
+ } from "../plugins/registry.js";
30
+ import type {
31
+ ContentBlock,
32
+ Message,
33
+ Provider,
34
+ ProviderResponse,
35
+ } from "../providers/types.js";
36
+ import { ContextOverflowError } from "../providers/types.js";
37
+
38
+ const testPostCompactPlugin = {
39
+ manifest: { name: "test-post-compact-transform", version: "0.0.0" },
40
+ hooks: {
41
+ [HOOKS.POST_COMPACT]: async (input: PostCompactContext): Promise<void> => {
42
+ void input;
43
+ },
44
+ },
45
+ };
46
+
47
+ const CONVERSATION_ID = "compacted-history-transform-conversation";
48
+
49
+ /** Capture times one second apart, the shape the row read produces. */
50
+ const CAPTURE_TIMES = new Map<string, number>([
51
+ ["f1", 1000],
52
+ ["f2", 2000],
53
+ ["f3", 3000],
54
+ ["f4", 4000],
55
+ ]);
56
+
57
+ /** A retained frame as `buildRetainedImageBlocks` rebuilds it: inline + id. */
58
+ function retainedFrame(
59
+ attachmentId: string,
60
+ ): Extract<ContentBlock, { type: "image" }> {
61
+ return {
62
+ type: "image",
63
+ source: { type: "base64", media_type: "image/png", data: "AAAAAAAA" },
64
+ _attachmentId: attachmentId,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * The history compaction rebuilds here: four tagged frames in one synthetic
70
+ * message, which is two more than the retention bound allows.
71
+ */
72
+ function compactedHistoryWithFourFrames(): Message[] {
73
+ return [
74
+ {
75
+ role: "user",
76
+ content: [
77
+ { type: "text", text: "Images retained from the compacted portion:" },
78
+ retainedFrame("f1"),
79
+ retainedFrame("f2"),
80
+ retainedFrame("f3"),
81
+ retainedFrame("f4"),
82
+ ],
83
+ },
84
+ ];
85
+ }
86
+
87
+ function textResponse(text: string): ProviderResponse {
88
+ return {
89
+ content: [{ type: "text", text }],
90
+ model: "mock-model",
91
+ usage: { inputTokens: 10, outputTokens: 5 },
92
+ stopReason: "end_turn",
93
+ };
94
+ }
95
+
96
+ /** Records every history the provider is asked to send. */
97
+ function createRecordingProvider(requests: Message[][]): Provider {
98
+ return {
99
+ name: "mock",
100
+ async sendMessage(messages: Message[]): Promise<ProviderResponse> {
101
+ requests.push(messages);
102
+ return textResponse("done");
103
+ },
104
+ };
105
+ }
106
+
107
+ /** A provider that rejects once as context-too-large, then answers. */
108
+ function createOverflowingProvider(requests: Message[][]): Provider {
109
+ let throwOnce = true;
110
+ return {
111
+ name: "mock",
112
+ async sendMessage(messages: Message[]): Promise<ProviderResponse> {
113
+ requests.push(messages);
114
+ if (throwOnce) {
115
+ throwOnce = false;
116
+ throw new ContextOverflowError("prompt too long", "mock", {
117
+ actualTokens: 999_999,
118
+ });
119
+ }
120
+ return textResponse("done after overflow recovery");
121
+ },
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Install a manager whose compaction returns the four-frame history, so the
127
+ * loop installs more frames than the bound allows.
128
+ */
129
+ function installOverfullCompactionManager(): { trust: TrustContext } {
130
+ createContextWindowManager({
131
+ provider: { name: "mock-provider" } as unknown as Provider,
132
+ config: {} as unknown as ContextWindowConfig,
133
+ conversationId: CONVERSATION_ID,
134
+ });
135
+ const manager = getContextWindowManager(CONVERSATION_ID);
136
+ if (manager) {
137
+ const rebuild = async () => ({
138
+ messages: compactedHistoryWithFourFrames(),
139
+ compacted: true,
140
+ exhausted: false,
141
+ });
142
+ manager.maybeCompact = rebuild as unknown as typeof manager.maybeCompact;
143
+ manager.recoverContextOverflow =
144
+ rebuild as unknown as typeof manager.recoverContextOverflow;
145
+ }
146
+ return { trust: { sourceChannel: "vellum", trustClass: "unknown" } };
147
+ }
148
+
149
+ /** Image blocks in the last history the provider was asked to send. */
150
+ function imagesInLastRequest(requests: Message[][]): ContentBlock[] {
151
+ const last = requests.at(-1) ?? [];
152
+ return last.flatMap((message) =>
153
+ message.content.filter((block) => block.type === "image"),
154
+ );
155
+ }
156
+
157
+ function frameStubsInLastRequest(requests: Message[][]): string[] {
158
+ const last = requests.at(-1) ?? [];
159
+ return last.flatMap((message) =>
160
+ message.content
161
+ .filter((block) => block.type === "text")
162
+ .map((block) => block.text)
163
+ .filter((text) => text.startsWith("[Camera frame omitted from context:")),
164
+ );
165
+ }
166
+
167
+ describe("AgentLoop post-compaction history transform", () => {
168
+ beforeEach(() => {
169
+ resetPluginRegistryForTests();
170
+ registerPlugin(testPostCompactPlugin);
171
+ });
172
+
173
+ afterEach(() => {
174
+ disposeContextWindowManager(CONVERSATION_ID);
175
+ });
176
+
177
+ test("trims a budget-gate compaction before the next provider call", async () => {
178
+ const requests: Message[][] = [];
179
+ const loop = new AgentLoop({
180
+ provider: createRecordingProvider(requests),
181
+ systemPrompt: "system",
182
+ conversationId: CONVERSATION_ID,
183
+ tools: [],
184
+ toolExecutor: async () => ({ content: "ok", isError: false }),
185
+ transformCompactedHistory: (messages) =>
186
+ stripAgedSightFrames(messages, CAPTURE_TIMES).messages,
187
+ });
188
+
189
+ await loop.run({
190
+ requestId: "req",
191
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
192
+ onEvent: () => {},
193
+ modelProfileKey: "balanced",
194
+ resolveContextWindow: () => ({
195
+ maxInputTokens: 10,
196
+ overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
197
+ }),
198
+ compactInPlace: true,
199
+ ...installOverfullCompactionManager(),
200
+ });
201
+
202
+ // The request that went out after compaction carries the bound, not the
203
+ // four frames compaction rebuilt.
204
+ expect(imagesInLastRequest(requests)).toHaveLength(2);
205
+ expect(frameStubsInLastRequest(requests)).toHaveLength(2);
206
+ // The newest two survived.
207
+ expect(imagesInLastRequest(requests)).toEqual([
208
+ retainedFrame("f3"),
209
+ retainedFrame("f4"),
210
+ ]);
211
+ });
212
+
213
+ test("trims an overflow-driven compaction before the retry", async () => {
214
+ const requests: Message[][] = [];
215
+ const loop = new AgentLoop({
216
+ provider: createOverflowingProvider(requests),
217
+ systemPrompt: "system",
218
+ conversationId: CONVERSATION_ID,
219
+ tools: [],
220
+ toolExecutor: async () => ({ content: "ok", isError: false }),
221
+ transformCompactedHistory: (messages) =>
222
+ stripAgedSightFrames(messages, CAPTURE_TIMES).messages,
223
+ });
224
+ // Blocks the budget gate so only the overflow rejection drives compaction.
225
+ loop.compactionCircuit.lastPostCompactionEstimate = 0;
226
+
227
+ await loop.run({
228
+ requestId: "req",
229
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
230
+ onEvent: () => {},
231
+ modelProfileKey: "balanced",
232
+ resolveContextWindow: () => ({
233
+ maxInputTokens: 10,
234
+ overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
235
+ }),
236
+ compactInPlace: false,
237
+ ...installOverfullCompactionManager(),
238
+ });
239
+
240
+ // The overflow retry is a second provider call, and it too is bounded.
241
+ expect(requests.length).toBeGreaterThan(1);
242
+ expect(imagesInLastRequest(requests)).toHaveLength(2);
243
+ expect(frameStubsInLastRequest(requests)).toHaveLength(2);
244
+ });
245
+
246
+ test("installs the compacted history as built when no transform is configured", async () => {
247
+ // The workflow leaf runner builds a loop with no conversation behind it.
248
+ const requests: Message[][] = [];
249
+ const loop = new AgentLoop({
250
+ provider: createRecordingProvider(requests),
251
+ systemPrompt: "system",
252
+ conversationId: CONVERSATION_ID,
253
+ tools: [],
254
+ toolExecutor: async () => ({ content: "ok", isError: false }),
255
+ });
256
+
257
+ await loop.run({
258
+ requestId: "req",
259
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
260
+ onEvent: () => {},
261
+ modelProfileKey: "balanced",
262
+ resolveContextWindow: () => ({
263
+ maxInputTokens: 10,
264
+ overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
265
+ }),
266
+ compactInPlace: true,
267
+ ...installOverfullCompactionManager(),
268
+ });
269
+
270
+ expect(imagesInLastRequest(requests)).toHaveLength(4);
271
+ expect(frameStubsInLastRequest(requests)).toHaveLength(0);
272
+ });
273
+
274
+ test("keeps the compacted history when the transform throws", async () => {
275
+ const requests: Message[][] = [];
276
+ const loop = new AgentLoop({
277
+ provider: createRecordingProvider(requests),
278
+ systemPrompt: "system",
279
+ conversationId: CONVERSATION_ID,
280
+ tools: [],
281
+ toolExecutor: async () => ({ content: "ok", isError: false }),
282
+ transformCompactedHistory: () => {
283
+ throw new Error("row read failed");
284
+ },
285
+ });
286
+
287
+ await loop.run({
288
+ requestId: "req",
289
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
290
+ onEvent: () => {},
291
+ modelProfileKey: "balanced",
292
+ resolveContextWindow: () => ({
293
+ maxInputTokens: 10,
294
+ overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
295
+ }),
296
+ compactInPlace: true,
297
+ ...installOverfullCompactionManager(),
298
+ });
299
+
300
+ // The turn still completed against the untrimmed compacted history.
301
+ expect(requests.length).toBeGreaterThan(0);
302
+ expect(imagesInLastRequest(requests)).toHaveLength(4);
303
+ });
304
+ });
@@ -78,6 +78,9 @@ function makeTarget(): Conversation {
78
78
  }),
79
79
  },
80
80
  messages,
81
+ // The wake trims its own run input; no row here is tagged as a camera
82
+ // frame, so the real pass would return the array unchanged too.
83
+ trimAgedSightFrames: (msgs: Message[]) => msgs,
81
84
  getMessages: () => messages,
82
85
  isProcessing: () => processing,
83
86
  waitForIdle: async () => !processing,
@@ -90,6 +90,9 @@ function makeTarget(): {
90
90
  },
91
91
  },
92
92
  messages,
93
+ // The wake trims its own run input; no row here is tagged as a camera
94
+ // frame, so the real pass would return the array unchanged too.
95
+ trimAgedSightFrames: (msgs: Message[]) => msgs,
93
96
  getMessages: () => messages,
94
97
  isProcessing: () => processing,
95
98
  waitForIdle: async () => !processing,
@@ -2973,6 +2973,62 @@ describe("AnthropicProvider — Managed Proxy Fallback", () => {
2973
2973
  });
2974
2974
  });
2975
2975
 
2976
+ // ---------------------------------------------------------------------------
2977
+ // Tests - internal `_attachmentId` never reaches the wire
2978
+ // ---------------------------------------------------------------------------
2979
+
2980
+ describe("AnthropicProvider - internal attachment id", () => {
2981
+ let provider: AnthropicProvider;
2982
+
2983
+ beforeEach(() => {
2984
+ lastStreamParams = null;
2985
+ provider = new AnthropicProvider("sk-ant-test", "claude-sonnet-4-6");
2986
+ });
2987
+
2988
+ test("drops _attachmentId from image and file blocks", async () => {
2989
+ // The declared media type matches the bytes, so `resolveMediaReferences`
2990
+ // takes its identity fast path and hands the block through untouched. What
2991
+ // keeps the field off the wire is the client rebuilding each payload out of
2992
+ // `source` alone.
2993
+ const messages: Message[] = [
2994
+ {
2995
+ role: "user",
2996
+ content: [
2997
+ { type: "text", text: "what is this" },
2998
+ {
2999
+ type: "image",
3000
+ source: {
3001
+ type: "base64",
3002
+ media_type: "image/png",
3003
+ data: PNG_B64,
3004
+ },
3005
+ _attachmentId: "att-image-1",
3006
+ },
3007
+ {
3008
+ type: "file",
3009
+ source: {
3010
+ type: "base64",
3011
+ media_type: "application/pdf",
3012
+ data: "cGRm",
3013
+ filename: "notes.pdf",
3014
+ },
3015
+ _attachmentId: "att-file-1",
3016
+ },
3017
+ ],
3018
+ },
3019
+ ];
3020
+
3021
+ await provider.sendMessage(messages);
3022
+
3023
+ const sent = JSON.stringify(lastStreamParams!.messages);
3024
+ expect(sent).not.toContain("_attachmentId");
3025
+ expect(sent).not.toContain("att-image-1");
3026
+ expect(sent).not.toContain("att-file-1");
3027
+ // The image itself still went out.
3028
+ expect(sent).toContain(PNG_B64);
3029
+ });
3030
+ });
3031
+
2976
3032
  // ---------------------------------------------------------------------------
2977
3033
  // Tests — Orphaned UTF-16 surrogate sanitization
2978
3034
  // ---------------------------------------------------------------------------
@@ -95,19 +95,6 @@ mock.module("../persistence/attachments-store.js", () => ({
95
95
  linkAttachmentToMessage: () => {},
96
96
  }));
97
97
 
98
- mock.module("../memory/retriever.js", () => ({
99
- buildMemoryRecall: async () => ({
100
- enabled: false,
101
- degraded: false,
102
- injectedText: "",
103
-
104
- semanticHits: 0,
105
- injectedTokens: 0,
106
- latencyMs: 0,
107
- }),
108
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
109
- }));
110
-
111
98
  mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
112
99
  ContextWindowManager: class {
113
100
  estimateInputTokens() {
@@ -16,12 +16,14 @@ import {
16
16
  isValidBase64,
17
17
  linkAttachmentToMessage,
18
18
  MAX_UPLOAD_BYTES,
19
+ resolveAttachmentsForPersist,
19
20
  uploadAttachment,
20
21
  validateAttachmentUpload,
21
22
  } from "../persistence/attachments-store.js";
22
23
  import {
23
24
  addMessage,
24
25
  createConversation,
26
+ deleteMessageById,
25
27
  } from "../persistence/conversation-crud.js";
26
28
  import { getConversationDirPath } from "../persistence/conversation-disk-view.js";
27
29
  import { getDb } from "../persistence/db-connection.js";
@@ -675,6 +677,115 @@ describe("deleteOrphanAttachments", () => {
675
677
  });
676
678
  });
677
679
 
680
+ // ---------------------------------------------------------------------------
681
+ // resolveAttachmentsForPersist
682
+ // ---------------------------------------------------------------------------
683
+
684
+ describe("resolveAttachmentsForPersist", () => {
685
+ beforeEach(resetTables);
686
+
687
+ // One implementation behind every send path that turns ids into a persisted
688
+ // user message (HTTP send, processMessage, the voice bridge), so its shape is
689
+ // pinned here rather than at each caller.
690
+ test("hydrates ids into the shape a persisted message stores", async () => {
691
+ const stored = await uploadAttachment("photo.png", "image/png", "ZnJhbWU=");
692
+
693
+ expect(resolveAttachmentsForPersist([stored.id])).toEqual([
694
+ {
695
+ id: stored.id,
696
+ filename: "photo.png",
697
+ mimeType: "image/png",
698
+ data: "ZnJhbWU=",
699
+ },
700
+ ]);
701
+ });
702
+
703
+ test("carries the source path only for attachments that have one", async () => {
704
+ const withPath = await uploadAttachment(
705
+ "from-disk.txt",
706
+ "text/plain",
707
+ "AA==",
708
+ );
709
+ const plain = await uploadAttachment("typed.txt", "text/plain", "BB==");
710
+ rawRun(
711
+ "test:setSourcePath",
712
+ "UPDATE attachments SET source_path = ? WHERE id = ?",
713
+ "/tmp/from-disk.txt",
714
+ withPath.id,
715
+ );
716
+
717
+ const resolved = resolveAttachmentsForPersist([withPath.id, plain.id]);
718
+
719
+ expect(resolved[0]).toMatchObject({ filePath: "/tmp/from-disk.txt" });
720
+ expect(resolved[1]).not.toHaveProperty("filePath");
721
+ });
722
+
723
+ test("drops ids with no attachment row", async () => {
724
+ const stored = await uploadAttachment("kept.txt", "text/plain", "AA==");
725
+
726
+ const resolved = resolveAttachmentsForPersist([stored.id, "att-missing"]);
727
+
728
+ expect(resolved.map((a) => a.id)).toEqual([stored.id]);
729
+ });
730
+
731
+ test("returns nothing for an empty id list", () => {
732
+ expect(resolveAttachmentsForPersist([])).toEqual([]);
733
+ });
734
+ });
735
+
736
+ // ---------------------------------------------------------------------------
737
+ // deleteMessageById orphan cleanup
738
+ // ---------------------------------------------------------------------------
739
+
740
+ describe("deleteMessageById attachment cleanup", () => {
741
+ beforeEach(resetTables);
742
+
743
+ test("collects an attachment the deleted message alone referenced", async () => {
744
+ const conv = createConversation();
745
+ const msg = await addMessage(conv.id, "user", "With attachment");
746
+ const stored = await uploadAttachment("shot.txt", "text/plain", "ZGF0YQ==");
747
+ linkAttachmentToMessage(msg.id, stored.id, 0);
748
+ const filePath = getFilePathForAttachment(stored.id);
749
+
750
+ deleteMessageById(msg.id);
751
+
752
+ expect(getAttachmentById(stored.id)).toBeNull();
753
+ expect(existsSync(filePath!)).toBe(false);
754
+ });
755
+
756
+ test("retained ids survive the delete, row and bytes", async () => {
757
+ // A caller rolling a message back rather than deleting it: the attachment
758
+ // returns to the uploaded-but-unlinked state every attachment sits in
759
+ // between upload and send, so a later message can still reference it.
760
+ const conv = createConversation();
761
+ const msg = await addMessage(conv.id, "user", "Rolled back");
762
+ const stored = await uploadAttachment("shot.txt", "text/plain", "ZGF0YQ==");
763
+ linkAttachmentToMessage(msg.id, stored.id, 0);
764
+ const filePath = getFilePathForAttachment(stored.id);
765
+
766
+ deleteMessageById(msg.id, { retainAttachmentIds: [stored.id] });
767
+
768
+ expect(getAttachmentById(stored.id)).not.toBeNull();
769
+ expect(existsSync(filePath!)).toBe(true);
770
+ // The row is gone even though its attachment stayed.
771
+ expect(getAttachmentsForMessage(msg.id)).toHaveLength(0);
772
+ });
773
+
774
+ test("retaining one id does not spare the others on the row", async () => {
775
+ const conv = createConversation();
776
+ const msg = await addMessage(conv.id, "user", "Two attachments");
777
+ const kept = await uploadAttachment("kept.txt", "text/plain", "AAAA");
778
+ const dropped = await uploadAttachment("dropped.txt", "text/plain", "BBBB");
779
+ linkAttachmentToMessage(msg.id, kept.id, 0);
780
+ linkAttachmentToMessage(msg.id, dropped.id, 1);
781
+
782
+ deleteMessageById(msg.id, { retainAttachmentIds: [kept.id] });
783
+
784
+ expect(getAttachmentById(kept.id)).not.toBeNull();
785
+ expect(getAttachmentById(dropped.id)).toBeNull();
786
+ });
787
+ });
788
+
678
789
  // ---------------------------------------------------------------------------
679
790
  // validateAttachmentUpload
680
791
  // ---------------------------------------------------------------------------
@@ -735,9 +846,9 @@ describe("validateAttachmentUpload", () => {
735
846
  test("accepts shell scripts as text attachments", () => {
736
847
  expect(validateAttachmentUpload("script.sh", "text/plain").ok).toBe(true);
737
848
  expect(validateAttachmentUpload("script.SH", "text/plain").ok).toBe(true);
738
- expect(
739
- validateAttachmentUpload("setup.sh", "application/x-sh").ok,
740
- ).toBe(true);
849
+ expect(validateAttachmentUpload("setup.sh", "application/x-sh").ok).toBe(
850
+ true,
851
+ );
741
852
  expect(
742
853
  validateAttachmentUpload("run.sh", "application/x-shellscript").ok,
743
854
  ).toBe(true);
@@ -760,9 +871,9 @@ describe("validateAttachmentUpload", () => {
760
871
  expect(
761
872
  validateAttachmentUpload("PROGRAM.EXE", "application/octet-stream").ok,
762
873
  ).toBe(false);
763
- expect(validateAttachmentUpload("INSTALL.DMG", "application/octet-stream").ok).toBe(
764
- false,
765
- );
874
+ expect(
875
+ validateAttachmentUpload("INSTALL.DMG", "application/octet-stream").ok,
876
+ ).toBe(false);
766
877
  });
767
878
 
768
879
  test("rejects unsupported MIME types", () => {