@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
@@ -79,10 +79,7 @@ import { initializeDb } from "../persistence/db-init.js";
79
79
  import { linkMessage, recordInbound } from "../persistence/delivery-crud.js";
80
80
  import { messages } from "../persistence/schema/conversations.js";
81
81
  import * as pendingInteractions from "../runtime/pending-interactions.js";
82
- import {
83
- isSlackReactionEvent,
84
- parseSlackReactionCallbackData,
85
- } from "../runtime/routes/inbound-stages/reaction-intercept.js";
82
+ import { isReactionEvent } from "../runtime/routes/inbound-stages/reaction-intercept.js";
86
83
  import {
87
84
  handleChannelInbound,
88
85
  seedContactChannel,
@@ -109,6 +106,7 @@ function resetState(): void {
109
106
  db.run("DELETE FROM contact_channels");
110
107
  db.run("DELETE FROM contacts");
111
108
  gatewayGuardians = [];
109
+ _conversationMocks.clear();
112
110
  }
113
111
 
114
112
  function seedActiveMember(): void {
@@ -202,70 +200,26 @@ function readPersistedMessages(): Array<{
202
200
  // Helper unit tests
203
201
  // ---------------------------------------------------------------------------
204
202
 
205
- describe("isSlackReactionEvent", () => {
206
- test("returns true for reaction added", () => {
207
- expect(
208
- isSlackReactionEvent({
209
- sourceChannel: "slack",
210
- callbackData: "reaction:thumbsup",
211
- }),
212
- ).toBe(true);
213
- });
214
-
215
- test("returns true for reaction removed", () => {
216
- expect(
217
- isSlackReactionEvent({
218
- sourceChannel: "slack",
219
- callbackData: "reaction_removed:eyes",
220
- }),
221
- ).toBe(true);
203
+ describe("isReactionEvent", () => {
204
+ test("returns true for a stamped reaction kind", () => {
205
+ expect(isReactionEvent({ eventKind: "reaction" })).toBe(true);
222
206
  });
223
207
 
224
- test("returns false for non-Slack source", () => {
225
- expect(
226
- isSlackReactionEvent({
227
- sourceChannel: "telegram",
228
- callbackData: "reaction:thumbsup",
229
- }),
230
- ).toBe(false);
208
+ test("returns true for replayed reaction sentinels, added and removed", () => {
209
+ expect(isReactionEvent({ callbackData: "reaction:thumbsup" })).toBe(true);
210
+ expect(isReactionEvent({ callbackData: "reaction_removed:eyes" })).toBe(
211
+ true,
212
+ );
231
213
  });
232
214
 
233
215
  test("returns false for non-reaction callback data", () => {
234
- expect(
235
- isSlackReactionEvent({
236
- sourceChannel: "slack",
237
- callbackData: "apr:req-1:approve_once",
238
- }),
239
- ).toBe(false);
240
- });
241
-
242
- test("returns false when callbackData missing", () => {
243
- expect(isSlackReactionEvent({ sourceChannel: "slack" })).toBe(false);
244
- });
245
- });
246
-
247
- describe("parseSlackReactionCallbackData", () => {
248
- test("parses reaction:<emoji> as added", () => {
249
- expect(parseSlackReactionCallbackData("reaction:thumbsup")).toEqual({
250
- op: "added",
251
- emoji: "thumbsup",
252
- });
253
- });
254
-
255
- test("parses reaction_removed:<emoji> as removed", () => {
256
- expect(parseSlackReactionCallbackData("reaction_removed:eyes")).toEqual({
257
- op: "removed",
258
- emoji: "eyes",
259
- });
260
- });
261
-
262
- test("returns null for empty emoji portion", () => {
263
- expect(parseSlackReactionCallbackData("reaction:")).toBeNull();
264
- expect(parseSlackReactionCallbackData("reaction_removed:")).toBeNull();
216
+ expect(isReactionEvent({ callbackData: "apr:req-1:approve_once" })).toBe(
217
+ false,
218
+ );
265
219
  });
266
220
 
267
- test("returns null for unrelated callback data", () => {
268
- expect(parseSlackReactionCallbackData("apr:req-1:approve")).toBeNull();
221
+ test("returns false for a plain message", () => {
222
+ expect(isReactionEvent({})).toBe(false);
269
223
  });
270
224
  });
271
225
 
@@ -311,6 +265,10 @@ describe("Slack reaction event persistence", () => {
311
265
  expect(row.content).toBe("[reaction]");
312
266
 
313
267
  const envelope = JSON.parse(row.metadata!) as Record<string, unknown>;
268
+ // Provenance keeps the row visible to actor-scoped history loads:
269
+ // filterMessagesForUntrustedActor drops rows with no trust class.
270
+ expect(envelope.provenanceTrustClass).toBe("trusted_contact");
271
+ expect(envelope.provenanceSourceChannel).toBe("slack");
314
272
  const slackMetaRaw = envelope.slackMeta;
315
273
  expect(typeof slackMetaRaw).toBe("string");
316
274
 
@@ -323,6 +281,9 @@ describe("Slack reaction event persistence", () => {
323
281
  // Slack sends no thread on a reaction, so the row claims none.
324
282
  expect(slackMeta!.threadTs).toBeUndefined();
325
283
  expect(slackMeta!.displayName).toBe(SLACK_DISPLAY_NAME);
284
+ // Stable identity, not the sender-controlled label: the history
285
+ // renderer attributes the fenced line's origin by this id.
286
+ expect(slackMeta!.actorExternalUserId).toBe(SLACK_USER_ID);
326
287
  expect(slackMeta!.reaction).toEqual({
327
288
  emoji: "thumbsup",
328
289
  actorDisplayName: SLACK_DISPLAY_NAME,
@@ -356,8 +317,10 @@ describe("Slack reaction event persistence", () => {
356
317
  });
357
318
  const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
358
319
  expect(resp.status).toBe(200);
320
+ // The payload resolver requires a target message id, so the dispatch
321
+ // drops the event before the intercept and it never reads as a message.
359
322
  expect(((await resp.json()) as Record<string, unknown>).reaction).toBe(
360
- "dropped_unknown_target",
323
+ "dropped_unresolvable_payload",
361
324
  );
362
325
 
363
326
  const rows = readPersistedMessages();
@@ -459,6 +422,39 @@ describe("Slack reaction event persistence", () => {
459
422
  expect(rows.length).toBe(1);
460
423
  });
461
424
 
425
+ test("a persisted reaction stale-marks the resident conversation", async () => {
426
+ const conversationId = seedStoredMessage("1700000000.111111");
427
+ const markHistoryStale = mock(() => {});
428
+ _conversationMocks.set(conversationId, {
429
+ markHistoryStale,
430
+ } as unknown as Conversation);
431
+
432
+ const resp = await handleChannelInbound(
433
+ buildReactionRequest("reaction:thumbsup"),
434
+ undefined,
435
+ TEST_BEARER_TOKEN,
436
+ );
437
+ expect(resp.status).toBe(200);
438
+ expect(markHistoryStale).toHaveBeenCalledTimes(1);
439
+ });
440
+
441
+ test("a duplicate reaction does not stale-mark again", async () => {
442
+ const conversationId = seedStoredMessage("1700000000.111111");
443
+ const markHistoryStale = mock(() => {});
444
+ _conversationMocks.set(conversationId, {
445
+ markHistoryStale,
446
+ } as unknown as Conversation);
447
+ const sharedExternalMessageId = `${SLACK_CHANNEL_ID}:1700000000.777777:bob`;
448
+ const makeReq = () =>
449
+ buildReactionRequest("reaction:tada", {
450
+ externalMessageId: sharedExternalMessageId,
451
+ });
452
+
453
+ await handleChannelInbound(makeReq(), undefined, TEST_BEARER_TOKEN);
454
+ await handleChannelInbound(makeReq(), undefined, TEST_BEARER_TOKEN);
455
+ expect(markHistoryStale).toHaveBeenCalledTimes(1);
456
+ });
457
+
462
458
  test("reaction on the assistant's own post lands in that conversation", async () => {
463
459
  // An outbound post opens no inbound event, so the only record of its ts
464
460
  // is the `slackMeta` on the assistant row. Seeded here the way the
@@ -148,6 +148,73 @@ describe("retry normalization: adaptive-only thinking models", () => {
148
148
  expect(lastConfig()?.thinking).toEqual({ type: "adaptive" });
149
149
  });
150
150
 
151
+ test("preserves effort when dropping stale disabled thinking for Fireworks GLM 5.3", async () => {
152
+ // GIVEN a profile that kept `thinking: { enabled: false }` after switching
153
+ // to an always-on GLM 5.3 model, plus an explicit effort
154
+ setLlmConfig({
155
+ callSites: {
156
+ memoryExtraction: {
157
+ provider: "fireworks",
158
+ model: "accounts/fireworks/models/glm-5p3",
159
+ thinking: { enabled: false },
160
+ effort: "high",
161
+ },
162
+ },
163
+ });
164
+ const { provider, lastConfig } = makePipeline("fireworks");
165
+
166
+ // WHEN a request resolves through the call-site config
167
+ await provider.sendMessage([userMessage], {
168
+ config: { callSite: "memoryExtraction" },
169
+ });
170
+
171
+ // THEN the disabled thinking is dropped BEFORE it can be encoded as
172
+ // `effort: "none"` (which GLM 5.3 rejects), and the chosen effort survives
173
+ expect(lastConfig()?.thinking).toBeUndefined();
174
+ expect(lastConfig()?.effort).toBe("high");
175
+ });
176
+
177
+ test("does not generate the opt-out effort for GLM 5.3 Flash with disabled thinking", async () => {
178
+ // GIVEN a pass-through caller with the wire-shape disabled config and no
179
+ // explicit effort
180
+ const { provider, lastConfig } = makePipeline("fireworks");
181
+
182
+ // WHEN sending against the always-on Flash model
183
+ await provider.sendMessage([userMessage], {
184
+ config: {
185
+ model: "accounts/fireworks/models/glm-5p3-flash",
186
+ thinking: { type: "disabled" },
187
+ },
188
+ });
189
+
190
+ // THEN no `effort: "none"` is synthesized: the model runs at its own
191
+ // default instead of tripping the reasoning opt-out rejection retry
192
+ expect(lastConfig()?.thinking).toBeUndefined();
193
+ expect(lastConfig()?.effort).toBeUndefined();
194
+ });
195
+
196
+ test("still encodes the opt-out for Fireworks models that support disabling thinking", async () => {
197
+ // GIVEN a disabled thinking config on GLM 5.2, which is not adaptive-only
198
+ setLlmConfig({
199
+ callSites: {
200
+ memoryExtraction: {
201
+ provider: "fireworks",
202
+ model: "accounts/fireworks/models/glm-5p2",
203
+ thinking: { enabled: false },
204
+ },
205
+ },
206
+ });
207
+ const { provider, lastConfig } = makePipeline("fireworks");
208
+
209
+ // WHEN a request resolves through the call-site config
210
+ await provider.sendMessage([userMessage], {
211
+ config: { callSite: "memoryExtraction" },
212
+ });
213
+
214
+ // THEN the opt-out is still encoded through the effort knob
215
+ expect(lastConfig()?.effort).toBe("none");
216
+ });
217
+
151
218
  test("preserves disabled thinking for models that support disabling it", async () => {
152
219
  // GIVEN a call-site config disabling thinking for a non-adaptive-only model
153
220
  setLlmConfig({
@@ -161,6 +161,28 @@ describe("renderHistoryContent", () => {
161
161
  expect(output.textSegments[1]).toContain("[File attachment] dream-015.md");
162
162
  });
163
163
 
164
+ test("an image block's _attachmentId contributes no attachment ref", () => {
165
+ // Chip positioning joins DB attachment rows against the refs collected
166
+ // here, and only `file` blocks are collected. An image carrying the same
167
+ // internal id must stay out of that join, or a user upload would gain a
168
+ // second, duplicate chip.
169
+ const output = renderHistoryContent([
170
+ { type: "text", text: "what is this" },
171
+ {
172
+ type: "image",
173
+ source: {
174
+ type: "base64",
175
+ media_type: "image/jpeg",
176
+ data: Buffer.from("img").toString("base64"),
177
+ },
178
+ _attachmentId: "att-image-1",
179
+ },
180
+ ]);
181
+
182
+ expect(output.attachments).toEqual([]);
183
+ expect(output.contentOrder).toEqual(["text:0"]);
184
+ });
185
+
164
186
  test("omits attachmentId when file block has no _attachmentId", () => {
165
187
  const output = renderHistoryContent([
166
188
  {
@@ -0,0 +1,362 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ KEEP_LATEST_SIGHT_FRAMES,
5
+ MAX_SIGHT_KEEP_LATEST_FRAMES,
6
+ } from "../config/schemas/sight.js";
7
+ import { applySightFrameRetention } from "../daemon/conversation-runtime-assembly.js";
8
+ import {
9
+ getAttachmentMetadataForMessage,
10
+ linkAttachmentToMessage,
11
+ uploadAttachment,
12
+ } from "../persistence/attachments-store.js";
13
+ import {
14
+ addMessage,
15
+ createConversation,
16
+ forkConversation,
17
+ getMessages,
18
+ selectSightFrameCaptureTimes,
19
+ } from "../persistence/conversation-crud.js";
20
+ import { getDb } from "../persistence/db-connection.js";
21
+ import { initializeDb } from "../persistence/db-init.js";
22
+ import { conversations } from "../persistence/schema.js";
23
+ import type { ContentBlock, Message } from "../providers/types.js";
24
+ import { setConfig } from "./helpers/set-config.js";
25
+
26
+ await initializeDb();
27
+
28
+ function ensureConversation(id: string): void {
29
+ const db = getDb();
30
+ const now = Date.now();
31
+ db.insert(conversations)
32
+ .values({
33
+ id,
34
+ title: "test",
35
+ createdAt: now,
36
+ updatedAt: now,
37
+ lastMessageAt: now,
38
+ })
39
+ .onConflictDoNothing()
40
+ .run();
41
+ }
42
+
43
+ function frameContent(attachmentId: string): string {
44
+ return JSON.stringify([
45
+ { type: "text", text: "what am I looking at" },
46
+ {
47
+ type: "image",
48
+ source: {
49
+ type: "workspace_ref",
50
+ media_type: "image/jpeg",
51
+ attachmentId,
52
+ sizeBytes: 1024,
53
+ },
54
+ },
55
+ ]);
56
+ }
57
+
58
+ function referenceImage(
59
+ attachmentId: string,
60
+ ): Extract<ContentBlock, { type: "image" }> {
61
+ return {
62
+ type: "image",
63
+ source: {
64
+ type: "workspace_ref",
65
+ media_type: "image/jpeg",
66
+ attachmentId,
67
+ sizeBytes: 1024,
68
+ },
69
+ };
70
+ }
71
+
72
+ describe("selectSightFrameCaptureTimes", () => {
73
+ const conversationId = "conv-sight-frame-times";
74
+
75
+ beforeEach(() => {
76
+ ensureConversation(conversationId);
77
+ });
78
+
79
+ test("maps tagged attachment ids to the row that carried them", async () => {
80
+ const first = await addMessage(
81
+ conversationId,
82
+ "user",
83
+ frameContent("att-1"),
84
+ { metadata: { sightFrameAttachmentIds: ["att-1"] } },
85
+ );
86
+ const second = await addMessage(
87
+ conversationId,
88
+ "user",
89
+ frameContent("att-2"),
90
+ { metadata: { sightFrameAttachmentIds: ["att-2", "att-3"] } },
91
+ );
92
+
93
+ const rows = getMessages(conversationId);
94
+ const createdAtById = new Map(rows.map((r) => [r.id, r.createdAt]));
95
+ const captureTimes = selectSightFrameCaptureTimes(conversationId);
96
+
97
+ expect(captureTimes.get("att-1")).toBe(createdAtById.get(first.id)!);
98
+ expect(captureTimes.get("att-2")).toBe(createdAtById.get(second.id)!);
99
+ expect(captureTimes.get("att-3")).toBe(createdAtById.get(second.id)!);
100
+ });
101
+
102
+ test("is empty for a conversation whose rows carry no tag", async () => {
103
+ const untaggedId = "conv-sight-frame-untagged";
104
+ ensureConversation(untaggedId);
105
+ await addMessage(untaggedId, "user", frameContent("att-9"), {
106
+ metadata: { voiceSessionTurn: true },
107
+ });
108
+ await addMessage(untaggedId, "assistant", "sure");
109
+
110
+ expect(selectSightFrameCaptureTimes(untaggedId).size).toBe(0);
111
+ });
112
+
113
+ test("drops a row that only mentions the key in an unrelated value", async () => {
114
+ const mentionId = "conv-sight-frame-mention";
115
+ ensureConversation(mentionId);
116
+ await addMessage(mentionId, "user", "hello", {
117
+ metadata: { backgroundEventSource: "sightFrameAttachmentIds" },
118
+ });
119
+
120
+ expect(selectSightFrameCaptureTimes(mentionId).size).toBe(0);
121
+ });
122
+ });
123
+
124
+ describe("applySightFrameRetention", () => {
125
+ test("stubs the aged frames of a tagged conversation", async () => {
126
+ const conversationId = "conv-sight-frame-retention";
127
+ ensureConversation(conversationId);
128
+ for (const attachmentId of ["att-a", "att-b", "att-c"]) {
129
+ await addMessage(conversationId, "user", frameContent(attachmentId), {
130
+ metadata: { sightFrameAttachmentIds: [attachmentId] },
131
+ });
132
+ }
133
+
134
+ const assembled: Message[] = [
135
+ { role: "user", content: [referenceImage("att-a")] },
136
+ { role: "assistant", content: [{ type: "text", text: "ok" }] },
137
+ { role: "user", content: [referenceImage("att-b")] },
138
+ { role: "user", content: [referenceImage("att-c")] },
139
+ ];
140
+
141
+ const retained = applySightFrameRetention(assembled, conversationId);
142
+
143
+ expect(retained[0].content[0].type).toBe("text");
144
+ expect(retained[2].content[0]).toEqual(referenceImage("att-b"));
145
+ expect(retained[3].content[0]).toEqual(referenceImage("att-c"));
146
+ });
147
+
148
+ test("reaches live inline frames that never reloaded from the DB", async () => {
149
+ const conversationId = "conv-sight-frame-retention-live";
150
+ ensureConversation(conversationId);
151
+ for (const attachmentId of ["live-a", "live-b", "live-c"]) {
152
+ await addMessage(conversationId, "user", frameContent(attachmentId), {
153
+ metadata: { sightFrameAttachmentIds: [attachmentId] },
154
+ });
155
+ }
156
+
157
+ // What `persistQueuedMessageBody` pushes into `ctx.messages`: inline bytes
158
+ // carrying the id of the row persisted as a reference.
159
+ const liveImage = (
160
+ attachmentId: string,
161
+ ): Extract<ContentBlock, { type: "image" }> => ({
162
+ type: "image",
163
+ source: { type: "base64", media_type: "image/jpeg", data: "AAAAAAAA" },
164
+ _attachmentId: attachmentId,
165
+ });
166
+ const assembled: Message[] = [
167
+ { role: "user", content: [liveImage("live-a")] },
168
+ { role: "assistant", content: [{ type: "text", text: "ok" }] },
169
+ { role: "user", content: [liveImage("live-b")] },
170
+ { role: "user", content: [liveImage("live-c")] },
171
+ ];
172
+
173
+ const retained = applySightFrameRetention(assembled, conversationId);
174
+
175
+ expect(retained[0].content[0].type).toBe("text");
176
+ expect(retained[2].content[0]).toEqual(liveImage("live-b"));
177
+ expect(retained[3].content[0]).toEqual(liveImage("live-c"));
178
+ });
179
+
180
+ test("returns the same array for a conversation with no tagged rows", async () => {
181
+ const conversationId = "conv-sight-frame-retention-untagged";
182
+ ensureConversation(conversationId);
183
+ await addMessage(conversationId, "user", frameContent("att-z"));
184
+
185
+ const assembled: Message[] = [
186
+ { role: "user", content: [referenceImage("att-z")] },
187
+ ];
188
+
189
+ expect(applySightFrameRetention(assembled, conversationId)).toBe(assembled);
190
+ });
191
+ });
192
+
193
+ describe("configured retention budget", () => {
194
+ afterEach(() => {
195
+ setConfig("sight", {});
196
+ });
197
+
198
+ /** One tagged standalone frame per row, the shape a keep stream leaves. */
199
+ async function seedFrames(
200
+ conversationId: string,
201
+ count: number,
202
+ ): Promise<Message[]> {
203
+ ensureConversation(conversationId);
204
+ const assembled: Message[] = [];
205
+ for (let index = 0; index < count; index += 1) {
206
+ const attachmentId = `${conversationId}-att-${index}`;
207
+ await addMessage(conversationId, "user", frameContent(attachmentId), {
208
+ metadata: { sightFrameAttachmentIds: [attachmentId] },
209
+ });
210
+ assembled.push({
211
+ role: "user",
212
+ content: [referenceImage(attachmentId)],
213
+ });
214
+ }
215
+ return assembled;
216
+ }
217
+
218
+ function imageCount(messages: Message[]): number {
219
+ return messages.filter((message) => message.content[0].type === "image")
220
+ .length;
221
+ }
222
+
223
+ test("keeps the number of frames the workspace configured", async () => {
224
+ setConfig("sight", { keepLatestFrames: 4 });
225
+ const conversationId = "conv-sight-budget-configured";
226
+ const assembled = await seedFrames(conversationId, 6);
227
+
228
+ const retained = applySightFrameRetention(assembled, conversationId);
229
+
230
+ expect(imageCount(retained)).toBe(4);
231
+ expect(retained.length - imageCount(retained)).toBe(2);
232
+ // The survivors are the newest, which is what recency buys the model.
233
+ expect(retained.slice(2).every((m) => m.content[0].type === "image")).toBe(
234
+ true,
235
+ );
236
+ });
237
+
238
+ test("caps a configured value above the ceiling", async () => {
239
+ setConfig("sight", { keepLatestFrames: 99 });
240
+ const conversationId = "conv-sight-budget-capped";
241
+ const assembled = await seedFrames(conversationId, 8);
242
+
243
+ const retained = applySightFrameRetention(assembled, conversationId);
244
+
245
+ expect(imageCount(retained)).toBe(MAX_SIGHT_KEEP_LATEST_FRAMES);
246
+ });
247
+
248
+ test("falls back to the default when the workspace configures nothing", async () => {
249
+ const conversationId = "conv-sight-budget-default";
250
+ const assembled = await seedFrames(conversationId, 5);
251
+
252
+ const retained = applySightFrameRetention(assembled, conversationId);
253
+
254
+ expect(imageCount(retained)).toBe(KEEP_LATEST_SIGHT_FRAMES);
255
+ });
256
+ });
257
+
258
+ describe("forked conversations", () => {
259
+ test("a fork's tag covers both its source and cloned attachment ids", async () => {
260
+ // A fork copies `messages.content` verbatim, so its blocks still name the
261
+ // SOURCE attachment ids, while `message_attachments` is re-linked to
262
+ // freshly CLONED rows. The compactor reads the link side and stamps cloned
263
+ // ids onto the frames it rebuilds, so a tag naming only one vocabulary
264
+ // goes blind on the other.
265
+ const db = getDb();
266
+ db.run("DELETE FROM message_attachments");
267
+ db.run("DELETE FROM attachments");
268
+
269
+ const source = createConversation("camera call");
270
+ const uploaded = await uploadAttachment(
271
+ "frame.png",
272
+ "image/png",
273
+ "iVBORw0K",
274
+ );
275
+ const row = await addMessage(
276
+ source.id,
277
+ "user",
278
+ JSON.stringify([
279
+ {
280
+ type: "image",
281
+ source: {
282
+ type: "workspace_ref",
283
+ media_type: "image/png",
284
+ attachmentId: uploaded.id,
285
+ sizeBytes: 8,
286
+ },
287
+ },
288
+ ]),
289
+ {
290
+ metadata: { sightFrameAttachmentIds: [uploaded.id] },
291
+ skipIndexing: true,
292
+ },
293
+ );
294
+ linkAttachmentToMessage(row.id, uploaded.id, 0);
295
+
296
+ const fork = forkConversation({ conversationId: source.id });
297
+ const forkRow = getMessages(fork.id)[0];
298
+ const clonedId = getAttachmentMetadataForMessage(forkRow.id)[0]?.id;
299
+
300
+ // The fork really did clone the attachment under a new id.
301
+ expect(clonedId).toBeDefined();
302
+ expect(clonedId).not.toBe(uploaded.id);
303
+
304
+ const captureTimes = selectSightFrameCaptureTimes(fork.id);
305
+ // Both vocabularies resolve, so a frame is matchable whether it arrived
306
+ // through the copied content or through the compactor's manifest.
307
+ expect(captureTimes.has(uploaded.id)).toBe(true);
308
+ expect(captureTimes.has(clonedId!)).toBe(true);
309
+
310
+ // The source conversation is untouched by the widening.
311
+ expect([...selectSightFrameCaptureTimes(source.id).keys()]).toEqual([
312
+ uploaded.id,
313
+ ]);
314
+ });
315
+
316
+ test("retention still matches the frames a fork holds directly", async () => {
317
+ const db = getDb();
318
+ db.run("DELETE FROM message_attachments");
319
+ db.run("DELETE FROM attachments");
320
+
321
+ const source = createConversation("camera call 2");
322
+ const ids: string[] = [];
323
+ for (const name of ["a.png", "b.png", "c.png"]) {
324
+ const uploaded = await uploadAttachment(name, "image/png", "iVBORw0K");
325
+ const row = await addMessage(
326
+ source.id,
327
+ "user",
328
+ JSON.stringify([
329
+ {
330
+ type: "image",
331
+ source: {
332
+ type: "workspace_ref",
333
+ media_type: "image/png",
334
+ attachmentId: uploaded.id,
335
+ sizeBytes: 8,
336
+ },
337
+ },
338
+ ]),
339
+ {
340
+ metadata: { sightFrameAttachmentIds: [uploaded.id] },
341
+ skipIndexing: true,
342
+ },
343
+ );
344
+ linkAttachmentToMessage(row.id, uploaded.id, 0);
345
+ ids.push(uploaded.id);
346
+ }
347
+
348
+ const fork = forkConversation({ conversationId: source.id });
349
+ // The fork's own rows, exactly as its history assembles them.
350
+ const assembled: Message[] = getMessages(fork.id).map((r) => ({
351
+ role: "user" as const,
352
+ content: r.content,
353
+ }));
354
+
355
+ const retained = applySightFrameRetention(assembled, fork.id);
356
+
357
+ // Oldest of the three is stubbed; the newest two survive.
358
+ expect(retained[0].content[0].type).toBe("text");
359
+ expect(retained[1].content[0].type).toBe("image");
360
+ expect(retained[2].content[0].type).toBe("image");
361
+ });
362
+ });