@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
@@ -6,6 +6,92 @@ import {
6
6
  enrichMessageWithSourcePaths,
7
7
  } from "../agent/attachments.js";
8
8
  import { createUserMessage } from "../agent/message-types.js";
9
+ import {
10
+ attachmentIdFragment,
11
+ mediaBlockAttachmentId,
12
+ } from "../providers/types.js";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // attachmentIdFragment
16
+ // ---------------------------------------------------------------------------
17
+
18
+ describe("attachmentIdFragment", () => {
19
+ test("yields the property when an id is present", () => {
20
+ expect(attachmentIdFragment("att-1")).toEqual({ _attachmentId: "att-1" });
21
+ });
22
+
23
+ test("yields nothing for a missing or empty id", () => {
24
+ // Absent rather than present-and-empty: the read side requires a non-empty
25
+ // string before it treats the field as an id.
26
+ expect(attachmentIdFragment(undefined)).toEqual({});
27
+ expect(attachmentIdFragment("")).toEqual({});
28
+ expect("_attachmentId" in attachmentIdFragment("")).toBe(false);
29
+ });
30
+ });
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // mediaBlockAttachmentId
34
+ // ---------------------------------------------------------------------------
35
+
36
+ describe("mediaBlockAttachmentId", () => {
37
+ test("reads a reference's id off its source", () => {
38
+ expect(
39
+ mediaBlockAttachmentId({
40
+ type: "image",
41
+ source: {
42
+ type: "workspace_ref",
43
+ media_type: "image/png",
44
+ attachmentId: "att-ref",
45
+ sizeBytes: 10,
46
+ },
47
+ }),
48
+ ).toBe("att-ref");
49
+ });
50
+
51
+ test("reads an inline block's id off the top-level field", () => {
52
+ expect(
53
+ mediaBlockAttachmentId({
54
+ type: "image",
55
+ source: { type: "base64", media_type: "image/png", data: "AAAA" },
56
+ _attachmentId: "att-inline",
57
+ }),
58
+ ).toBe("att-inline");
59
+ });
60
+
61
+ test("is undefined for a block that came from no attachment", () => {
62
+ expect(
63
+ mediaBlockAttachmentId({
64
+ type: "image",
65
+ source: { type: "base64", media_type: "image/png", data: "AAAA" },
66
+ }),
67
+ ).toBeUndefined();
68
+ });
69
+
70
+ test("covers file blocks in both shapes", () => {
71
+ expect(
72
+ mediaBlockAttachmentId({
73
+ type: "file",
74
+ source: {
75
+ type: "workspace_ref",
76
+ media_type: "application/pdf",
77
+ attachmentId: "att-file-ref",
78
+ sizeBytes: 10,
79
+ },
80
+ }),
81
+ ).toBe("att-file-ref");
82
+ expect(
83
+ mediaBlockAttachmentId({
84
+ type: "file",
85
+ source: {
86
+ type: "base64",
87
+ media_type: "application/pdf",
88
+ data: "AAAA",
89
+ },
90
+ _attachmentId: "att-file-inline",
91
+ }),
92
+ ).toBe("att-file-inline");
93
+ });
94
+ });
9
95
 
10
96
  // ---------------------------------------------------------------------------
11
97
  // attachmentsToReferenceBlocks
@@ -152,6 +238,42 @@ describe("attachmentsToContentBlocks", () => {
152
238
  const blocks = await attachmentsToContentBlocks([]);
153
239
  expect(blocks).toHaveLength(0);
154
240
  });
241
+
242
+ test("stamps the attachment id on image and file blocks alike", async () => {
243
+ const blocks = await attachmentsToContentBlocks([
244
+ {
245
+ id: "att-img",
246
+ filename: "photo.jpg",
247
+ mimeType: "image/jpeg",
248
+ data: "imgdata",
249
+ },
250
+ {
251
+ id: "att-doc",
252
+ filename: "doc.pdf",
253
+ mimeType: "application/pdf",
254
+ data: "pdfdata",
255
+ },
256
+ ]);
257
+
258
+ expect(blocks[0].type).toBe("image");
259
+ expect((blocks[0] as { _attachmentId?: string })._attachmentId).toBe(
260
+ "att-img",
261
+ );
262
+ expect(blocks[1].type).toBe("file");
263
+ expect((blocks[1] as { _attachmentId?: string })._attachmentId).toBe(
264
+ "att-doc",
265
+ );
266
+ });
267
+
268
+ test("omits the attachment id when the upload has none", async () => {
269
+ const blocks = await attachmentsToContentBlocks([
270
+ { filename: "photo.jpg", mimeType: "image/jpeg", data: "imgdata" },
271
+ { filename: "doc.pdf", mimeType: "application/pdf", data: "pdfdata" },
272
+ ]);
273
+
274
+ expect("_attachmentId" in blocks[0]).toBe(false);
275
+ expect("_attachmentId" in blocks[1]).toBe(false);
276
+ });
155
277
  });
156
278
 
157
279
  // ---------------------------------------------------------------------------
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
5
 
6
- import { resolveBundledDir } from "../util/bundled-asset.js";
6
+ import { isBunVirtualPath, resolveBundledDir } from "../util/bundled-asset.js";
7
7
 
8
8
  let tempDir: string;
9
9
 
@@ -17,6 +17,13 @@ afterEach(() => {
17
17
  });
18
18
 
19
19
  describe("resolveBundledDir", () => {
20
+ test("recognizes Bun virtual paths on Unix and Windows", () => {
21
+ expect(isBunVirtualPath("/$bunfs/root/src/config")).toBeTrue();
22
+ expect(isBunVirtualPath("B:\\~BUN\\root")).toBeTrue();
23
+ expect(isBunVirtualPath("B:/~BUN/root/src/config")).toBeTrue();
24
+ expect(isBunVirtualPath("C:\\dev\\vellum-assistant")).toBeFalse();
25
+ });
26
+
20
27
  test("source mode: returns join(callerDir, relativePath) when callerDir is a normal path", () => {
21
28
  const result = resolveBundledDir(
22
29
  "/some/source/path",
@@ -82,6 +89,20 @@ describe("resolveBundledDir", () => {
82
89
  expect(result).toBe(join(binDir, "templates"));
83
90
  });
84
91
 
92
+ test("resolves Windows Bun virtual paths beside the executable", () => {
93
+ const binDir = join(tempDir, "bin");
94
+ mkdirSync(join(binDir, "default-plugins"), { recursive: true });
95
+
96
+ process.execPath = join(binDir, "vellum-daemon.exe");
97
+
98
+ const result = resolveBundledDir(
99
+ "B:\\~BUN\\root",
100
+ ".",
101
+ "default-plugins",
102
+ );
103
+ expect(result).toBe(join(binDir, "default-plugins"));
104
+ });
105
+
85
106
  test("falls back to source path when neither Resources nor execDir have the asset", () => {
86
107
  const binDir = join(tempDir, "bin");
87
108
  mkdirSync(binDir, { recursive: true });
@@ -16,7 +16,7 @@ describe("parseCallbackData", () => {
16
16
  expect(result).not.toBeNull();
17
17
  expect(result!.action).toBe(expectedAction);
18
18
  expect(result!.requestId).toBe("req-123");
19
- expect(result!.source).toBe("telegram_button");
19
+ expect(result!.source).toBe("button");
20
20
  });
21
21
 
22
22
  test.each<[string, string]>([
@@ -33,12 +33,19 @@ describe("parseCallbackData", () => {
33
33
  },
34
34
  );
35
35
 
36
- test("parses slack source channel", () => {
37
- const result = parseCallbackData("apr:req-789:approve_once", "slack");
38
- expect(result).not.toBeNull();
39
- expect(result!.action).toBe("approve_once");
40
- expect(result!.requestId).toBe("req-789");
41
- expect(result!.source).toBe("slack_button");
36
+ test("every channel's button press attributes as the button modality", () => {
37
+ for (const channel of ["slack", "telegram", "whatsapp", "discord"]) {
38
+ const result = parseCallbackData("apr:req-789:approve_once", channel);
39
+ expect(result).not.toBeNull();
40
+ expect(result!.action).toBe("approve_once");
41
+ expect(result!.requestId).toBe("req-789");
42
+ expect(result!.source).toBe("button");
43
+ }
44
+ });
45
+
46
+ test("an in-app surface press attributes as vellum_surface", () => {
47
+ const result = parseCallbackData("apr:req-789:approve_once", "vellum");
48
+ expect(result!.source).toBe("vellum_surface");
42
49
  });
43
50
 
44
51
  test("returns null for unknown action", () => {
@@ -241,7 +241,7 @@ describe("handleChannelDecision", () => {
241
241
  const mockConv = conversationMocks.get("conv-1") as Conversation;
242
242
  const decision: ApprovalDecisionResult = {
243
243
  action: "reject",
244
- source: "telegram_button",
244
+ source: "button",
245
245
  };
246
246
 
247
247
  const result = handleChannelDecision("conv-1", decision);
@@ -261,7 +261,7 @@ describe("handleChannelDecision", () => {
261
261
  const newerMock = conversationMocks.get("conv-1") as Conversation;
262
262
  const decision: ApprovalDecisionResult = {
263
263
  action: "approve_once",
264
- source: "telegram_button",
264
+ source: "button",
265
265
  requestId: "req-newer",
266
266
  };
267
267
 
@@ -280,7 +280,7 @@ describe("handleChannelDecision", () => {
280
280
  registerPendingConfirmation("req-1", "conv-1", "shell");
281
281
  const decision: ApprovalDecisionResult = {
282
282
  action: "approve_once",
283
- source: "telegram_button",
283
+ source: "button",
284
284
  requestId: "req-missing",
285
285
  };
286
286
 
@@ -10,6 +10,7 @@ import { getDb } from "../persistence/db-connection.js";
10
10
  import { initializeDb } from "../persistence/db-init.js";
11
11
  import {
12
12
  clearPayload,
13
+ findConversationByProviderMessageId,
13
14
  findMessageBySourceId,
14
15
  linkMessage,
15
16
  recordInbound,
@@ -122,6 +123,63 @@ describe("channel-delivery-store", () => {
122
123
  expect(row!.sourceMessageId).toBe("src-42");
123
124
  });
124
125
 
126
+ test("resolves an assistant post by its provider id on a non-Slack channel", () => {
127
+ const chatId = "999000111222333444";
128
+ const minted = recordInbound("discord", chatId, "evt-seed");
129
+ const db = getDb();
130
+ db.insert(messages)
131
+ .values({
132
+ id: "assistant-post-1",
133
+ conversationId: minted.conversationId,
134
+ role: "assistant",
135
+ content: "outbound reply",
136
+ createdAt: Date.now(),
137
+ metadata: JSON.stringify({
138
+ providerMeta: JSON.stringify({
139
+ source: "discord",
140
+ conversationExternalId: chatId,
141
+ messageId: "1234567890123456789",
142
+ additionalMessageIds: ["2234567890123456789"],
143
+ eventKind: "message",
144
+ }),
145
+ }),
146
+ })
147
+ .run();
148
+
149
+ expect(
150
+ findConversationByProviderMessageId(
151
+ "discord",
152
+ chatId,
153
+ "1234567890123456789",
154
+ ),
155
+ ).toBe(minted.conversationId);
156
+ // A split reply posts several provider messages from one row; any of
157
+ // their ids resolves it.
158
+ expect(
159
+ findConversationByProviderMessageId(
160
+ "discord",
161
+ chatId,
162
+ "2234567890123456789",
163
+ ),
164
+ ).toBe(minted.conversationId);
165
+ // The scan is scoped to the reaction's own channel address: the same id
166
+ // is invisible from another channel's or another chat's key space.
167
+ expect(
168
+ findConversationByProviderMessageId(
169
+ "telegram",
170
+ chatId,
171
+ "1234567890123456789",
172
+ ),
173
+ ).toBeNull();
174
+ expect(
175
+ findConversationByProviderMessageId(
176
+ "discord",
177
+ "other-chat",
178
+ "1234567890123456789",
179
+ ),
180
+ ).toBeNull();
181
+ });
182
+
125
183
  test("same chat on same channel reuses the same conversation", () => {
126
184
  const r1 = recordInbound("telegram", "chat-1", "msg-1");
127
185
  const r2 = recordInbound("telegram", "chat-1", "msg-2");
@@ -243,6 +243,7 @@ describe("channel inbound disk pressure gate", () => {
243
243
  content: "reaction:thumbsup",
244
244
  actorExternalId: "slack-user-1",
245
245
  callbackData: "reaction:thumbsup",
246
+ sourceMetadata: { messageId: "1700000000.1" },
246
247
  replyCallbackUrl: "https://gateway.test/deliver/slack",
247
248
  }),
248
249
  );
@@ -109,6 +109,7 @@ describe("resolveAdapterHandle", () => {
109
109
 
110
110
  describe("createInviteAdapterRegistry", () => {
111
111
  const builtInChannels = [
112
+ "discord",
112
113
  "email",
113
114
  "slack",
114
115
  "telegram",
@@ -174,9 +174,9 @@ describe("slack remote probe (auth.test)", () => {
174
174
 
175
175
  /**
176
176
  * Slack can install an app with a fraction of the manifest's scopes while
177
- * auth.test still succeeds — a live install produced 2 of 18, and the first
178
- * real API call failed with missing_scope. auth.test passing is therefore not
179
- * evidence the install is usable.
177
+ * auth.test still succeeds, leaving the first real API call to fail with
178
+ * missing_scope. auth.test passing is therefore not evidence the install is
179
+ * usable.
180
180
  */
181
181
  describe("slack auth.test response validation", () => {
182
182
  test("reports an unexpected shape instead of trusting the field types", async () => {
@@ -123,18 +123,6 @@ mock.module("../persistence/attachments-store.js", () => ({
123
123
  linkAttachmentToMessage: () => {},
124
124
  }));
125
125
 
126
- mock.module("../memory/retriever.js", () => ({
127
- buildMemoryRecall: async () => ({
128
- enabled: false,
129
- degraded: false,
130
- injectedText: "",
131
- semanticHits: 0,
132
- injectedTokens: 0,
133
- latencyMs: 0,
134
- }),
135
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
136
- }));
137
-
138
126
  // Per-test compaction result — set before calling forceCompact().
139
127
  let mockCompactResult: ContextWindowResult = {
140
128
  messages: [],
@@ -149,10 +149,12 @@ describe("compactor records llm_request_logs with call_site=compactionAgent", ()
149
149
  });
150
150
 
151
151
  test("successful compaction call stamps call_site = compactionAgent", async () => {
152
- await runAssistantDrivenCompaction(args(makeProvider()));
152
+ const result = await runAssistantDrivenCompaction(args(makeProvider()));
153
153
 
154
154
  expect(recordRequestLogCalls.length).toBe(1);
155
155
  expect(recordRequestLogCalls[0]!.callSite).toBe("compactionAgent");
156
+ expect(result.summaryCallSite).toBe("compactionAgent");
157
+ expect(result.summaryResolutionCallSite).toBe("mainAgent");
156
158
  expect(recordRequestLogCalls[0]!.conversationId).toBe(
157
159
  "conv-compaction-log-1",
158
160
  );
@@ -228,6 +230,10 @@ describe("compaction result reports what actually served the summary", () => {
228
230
  expect(result.summaryActualInferenceProfile).toBe("backupProfile");
229
231
  // The primary's resolution is still reported, just outranked downstream.
230
232
  expect(result.summaryOverrideProfile).toBe("primaryProfile");
233
+ // Request log and usage attribution share the observability call site.
234
+ expect(result.summaryCallSite).toBe("compactionAgent");
235
+ expect(result.summaryResolutionCallSite).toBe("mainAgent");
236
+ expect(recordRequestLogCalls[0]!.callSite).toBe("compactionAgent");
231
237
  // Same value the request log recorded: one source of truth, not two.
232
238
  expect(recordRequestLogCalls[0]!.provider).toBe("anthropic");
233
239
  });
@@ -125,6 +125,21 @@ describe("buildRetainedImageBlocks corrupt-bytes gate", () => {
125
125
  expect(blocks).toHaveLength(0);
126
126
  });
127
127
 
128
+ test("a retained block carries the attachment id it rehydrated from", async () => {
129
+ // The rebuilt block is inline bytes, so this id is the only handle left on
130
+ // the row it came from. Camera-frame retention matches on it: without it a
131
+ // frame compaction chose to keep would be invisible to every later pass.
132
+ const conv = createConversation();
133
+ await addImageMessage(conv.id, "good.png", PNG_1X1_BASE64);
134
+ const manifest = collectImageManifest(conv.id, "guardian");
135
+
136
+ const { blocks } = await buildRetainedImageBlocks(["good.png"], manifest);
137
+
138
+ expect(blocks).toHaveLength(1);
139
+ expect(blocks[0]._attachmentId).toBe(manifest[0].attachmentId);
140
+ expect(blocks[0]._attachmentId).toBeTruthy();
141
+ });
142
+
128
143
  test("unresolvable filenames still land in missing", async () => {
129
144
  const conv = createConversation();
130
145
  await addImageMessage(conv.id, "good.png", PNG_1X1_BASE64);
@@ -811,6 +811,40 @@ describe("AssistantConfigSchema", () => {
811
811
  expect(result.success).toBe(false);
812
812
  });
813
813
 
814
+ test("applies sight defaults", () => {
815
+ expect(AssistantConfigSchema.parse({}).sight).toEqual({
816
+ keepLatestFrames: 2,
817
+ });
818
+ });
819
+
820
+ test("accepts a custom sight.keepLatestFrames", () => {
821
+ const result = AssistantConfigSchema.parse({
822
+ sight: { keepLatestFrames: 4 },
823
+ });
824
+ expect(result.sight.keepLatestFrames).toBe(4);
825
+ });
826
+
827
+ test("keeps an out-of-range sight.keepLatestFrames for the reader to clamp", () => {
828
+ // Rejecting it would reset the key to the default, which is fewer frames
829
+ // than either the config or the guardrail asked for. The clamp lives at
830
+ // read (`resolveSightKeepLatestFrames`).
831
+ const result = AssistantConfigSchema.parse({
832
+ sight: { keepLatestFrames: 99 },
833
+ });
834
+ expect(result.sight.keepLatestFrames).toBe(99);
835
+ });
836
+
837
+ test("rejects a non-integer sight.keepLatestFrames", () => {
838
+ expect(
839
+ AssistantConfigSchema.safeParse({ sight: { keepLatestFrames: 2.5 } })
840
+ .success,
841
+ ).toBe(false);
842
+ expect(
843
+ AssistantConfigSchema.safeParse({ sight: { keepLatestFrames: "two" } })
844
+ .success,
845
+ ).toBe(false);
846
+ });
847
+
814
848
  // ── commitMessageLLM config ──────────────────────────────────────────
815
849
 
816
850
  test("default commitMessageLLM values are correct", () => {
@@ -937,6 +971,7 @@ describe("AssistantConfigSchema", () => {
937
971
  mode: "open-mic",
938
972
  vad: {
939
973
  speechEnergyThreshold: 800,
974
+ noiseFloorMargin: 3,
940
975
  silenceThresholdMs: 1200,
941
976
  maxTurnDurationMs: 30000,
942
977
  bargeInMinSpeechMs: 250,
@@ -39,6 +39,24 @@ function channelCount(type: string, address: string): number {
39
39
  return row.n;
40
40
  }
41
41
 
42
+ function contactExists(contactId: string): boolean {
43
+ // bun:sqlite .get() returns null (not undefined) when no row matches.
44
+ const row = getSqlite()
45
+ .query("SELECT 1 AS one FROM contacts WHERE id = ?")
46
+ .get(contactId) as { one: number } | null;
47
+ return row !== null;
48
+ }
49
+
50
+ /** Contacts parenting zero channels: the guardian-duplicate shape. */
51
+ function channelLessContactCount(): number {
52
+ const row = getSqlite()
53
+ .query(
54
+ "SELECT COUNT(*) AS n FROM contacts c WHERE NOT EXISTS (SELECT 1 FROM contact_channels ch WHERE ch.contact_id = c.id)",
55
+ )
56
+ .get() as { n: number };
57
+ return row.n;
58
+ }
59
+
42
60
  function workspaceDir(): string {
43
61
  const dir = process.env.VELLUM_WORKSPACE_DIR;
44
62
  if (!dir) {
@@ -112,6 +130,34 @@ describe("identity-mirror faithful-replica semantics", () => {
112
130
  expect(result?.contact.userFile).toBeNull();
113
131
  });
114
132
 
133
+ test("an op carrying the gateway channel id adopts it onto a divergent legacy row", () => {
134
+ upsertContactChannel({
135
+ sourceChannel: "slack",
136
+ externalUserId: "UADOPTID",
137
+ displayName: "Alice",
138
+ contactId: "co-adopt-id",
139
+ channelId: "legacy-ch",
140
+ refreshDisplayName: true,
141
+ userFileOnCreate: null,
142
+ });
143
+
144
+ // The gateway's row for the same identity lives under a different UUID;
145
+ // the mirror adopts it so both stores key the channel identically.
146
+ upsertContactChannel({
147
+ sourceChannel: "slack",
148
+ externalUserId: "UADOPTID",
149
+ displayName: "Alice",
150
+ contactId: "co-adopt-id",
151
+ channelId: "gw-ch-aligned",
152
+ refreshDisplayName: true,
153
+ userFileOnCreate: null,
154
+ });
155
+
156
+ expect(channelOwnerById("gw-ch-aligned")).toBe("co-adopt-id");
157
+ expect(channelOwnerById("legacy-ch")).toBeUndefined();
158
+ expect(channelCount("slack", "UADOPTID")).toBe(1);
159
+ });
160
+
115
161
  test("Finding B: an explicit (gateway-minted) channel id is reused verbatim", () => {
116
162
  const result = upsertContactChannel({
117
163
  sourceChannel: "slack",
@@ -215,10 +261,102 @@ describe("mirror channel-reparenting race semantics", () => {
215
261
  reassignConflictingChannels: false,
216
262
  });
217
263
 
218
- // Original channel stays under contact #1; no duplicate channel was created.
219
- expect(channelOwnerById("gw-ch-race-1")).toBe("co-race-1");
220
- expect(channelOwnerById("gw-ch-race-2")).toBeUndefined();
264
+ // Ownership never moves without reassign and no duplicate channel is
265
+ // created; the row's id follows the latest gateway-sourced op (id
266
+ // adoption), which is what keeps the two stores' keys aligned.
267
+ expect(channelOwnerById("gw-ch-race-2")).toBe("co-race-1");
268
+ expect(channelOwnerById("gw-ch-race-1")).toBeUndefined();
221
269
  expect(channelCount("slack", "URACE")).toBe(1);
270
+ // The losing seed adopts contact #1 rather than minting a channel-less
271
+ // contact under its own id.
272
+ expect(contactExists("co-race-2")).toBe(false);
273
+ expect(channelLessContactCount()).toBe(0);
274
+ });
275
+
276
+ test("adoption and re-seen seeds never clobber curated record fields", () => {
277
+ // First-seen bot sender: classification and notes land on CREATE.
278
+ const created = upsertContactChannel({
279
+ sourceChannel: "slack",
280
+ externalUserId: "UCURATED",
281
+ externalChatId: "DCURATED",
282
+ displayName: "Peer Assistant",
283
+ contactId: "co-curated",
284
+ channelId: "gw-ch-curated",
285
+ contactType: "assistant",
286
+ notes: "Automated Slack bot",
287
+ refreshDisplayName: true,
288
+ userFileOnCreate: null,
289
+ reassignConflictingChannels: false,
290
+ });
291
+ expect(created?.contact.contactType).toBe("assistant");
292
+ expect(created?.contact.notes).toBe("Automated Slack bot");
293
+
294
+ // A later seed for the same identity (mirror row present) carries record
295
+ // fields for gap recovery, but they are create-only: the stored
296
+ // classification and notes stand.
297
+ const reseen = upsertContactChannel({
298
+ sourceChannel: "slack",
299
+ externalUserId: "UCURATED",
300
+ contactId: "co-curated",
301
+ displayName: "Peer Assistant",
302
+ contactType: "human",
303
+ notes: "seed-refresh notes that must not land",
304
+ refreshDisplayName: true,
305
+ userFileOnCreate: null,
306
+ reassignConflictingChannels: false,
307
+ });
308
+ expect(reseen?.contact.contactType).toBe("assistant");
309
+ expect(reseen?.contact.notes).toBe("Automated Slack bot");
310
+
311
+ // Adoption (a create-shaped op under a foreign id for the same identity)
312
+ // is equally forbidden from touching the adopted contact's record fields.
313
+ const adopted = upsertContactChannel({
314
+ sourceChannel: "slack",
315
+ externalUserId: "UCURATED",
316
+ contactId: "co-curated-foreign",
317
+ displayName: "Peer Assistant",
318
+ contactType: "human",
319
+ notes: "adoption notes that must not land",
320
+ refreshDisplayName: true,
321
+ userFileOnCreate: null,
322
+ reassignConflictingChannels: false,
323
+ });
324
+ expect(adopted?.contact.id).toBe("co-curated");
325
+ expect(adopted?.contact.contactType).toBe("assistant");
326
+ expect(adopted?.contact.notes).toBe("Automated Slack bot");
327
+ expect(contactExists("co-curated-foreign")).toBe(false);
328
+ });
329
+
330
+ test("explicit-id create over a conflicting channel adopts the owner instead of minting an empty contact", () => {
331
+ upsertContactChannel({
332
+ sourceChannel: "slack",
333
+ externalUserId: "UADOPT",
334
+ externalChatId: "DADOPT",
335
+ displayName: "Owner",
336
+ contactId: "co-adopt-owner",
337
+ channelId: "gw-ch-adopt",
338
+ refreshDisplayName: true,
339
+ userFileOnCreate: null,
340
+ reassignConflictingChannels: false,
341
+ });
342
+
343
+ // A divergence-shaped write: the gateway hands down a contact id the
344
+ // mirror has never seen, for an identity the mirror already parents.
345
+ const result = upsertContactChannel({
346
+ sourceChannel: "slack",
347
+ externalUserId: "UADOPT",
348
+ displayName: "Owner Renamed",
349
+ contactId: "co-adopt-foreign",
350
+ refreshDisplayName: true,
351
+ userFileOnCreate: null,
352
+ reassignConflictingChannels: false,
353
+ });
354
+
355
+ expect(result?.contact.id).toBe("co-adopt-owner");
356
+ expect(result?.contact.displayName).toBe("Owner Renamed");
357
+ expect(channelOwnerById("gw-ch-adopt")).toBe("co-adopt-owner");
358
+ expect(contactExists("co-adopt-foreign")).toBe(false);
359
+ expect(channelLessContactCount()).toBe(0);
222
360
  });
223
361
 
224
362
  test("INVITE-binding path (reassignConflictingChannels:true) DOES reparent", () => {
@@ -83,19 +83,6 @@ mock.module("../persistence/conversation-queries.js", () => ({
83
83
  listConversations: () => [],
84
84
  }));
85
85
 
86
- mock.module("../memory/retriever.js", () => ({
87
- buildMemoryRecall: async () => ({
88
- enabled: false,
89
- degraded: false,
90
- injectedText: "",
91
-
92
- semanticHits: 0,
93
- injectedTokens: 0,
94
- latencyMs: 0,
95
- }),
96
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
97
- }));
98
-
99
86
  mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
100
87
  ContextWindowManager: class {
101
88
  estimateInputTokens() {
@@ -165,18 +165,6 @@ mock.module("../persistence/conversation-disk-view.js", () => ({
165
165
  rebuildConversationDiskViewFromDbState: () => {},
166
166
  }));
167
167
 
168
- mock.module("../memory/retriever.js", () => ({
169
- buildMemoryRecall: async () => ({
170
- enabled: false,
171
- degraded: false,
172
- injectedText: "",
173
- semanticHits: 0,
174
- injectedTokens: 0,
175
- latencyMs: 0,
176
- }),
177
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
178
- }));
179
-
180
168
  mock.module("../apps/app-store.js", () => ({
181
169
  getApp: () => null,
182
170
  listAppFiles: () => [],