@vellumai/assistant 0.11.7 → 0.11.8-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -0,0 +1,615 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ KEEP_LATEST_SIGHT_FRAMES,
5
+ MAX_SIGHT_KEEP_LATEST_FRAMES,
6
+ MIN_SIGHT_KEEP_LATEST_FRAMES,
7
+ } from "../config/schemas/sight.js";
8
+ import {
9
+ countMediaBlocks,
10
+ stripMediaPayloadsForRetry,
11
+ } from "../daemon/conversation-media-retry.js";
12
+ import {
13
+ resolveSightKeepLatestFrames,
14
+ stripAgedSightFrames,
15
+ } from "../daemon/conversation-sight-frames.js";
16
+ import {
17
+ messageMetadataIsAmbientSightKeep,
18
+ sightFrameAttachmentIdsFromMetadata,
19
+ } from "../persistence/conversation-types.js";
20
+ import type { ContentBlock, Message } from "../providers/types.js";
21
+ import { setConfig } from "./helpers/set-config.js";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /** An attachment reference block, the shape persisted history carries. */
28
+ function referenceImage(
29
+ attachmentId: string,
30
+ sizeBytes = 1024,
31
+ ): Extract<ContentBlock, { type: "image" }> {
32
+ return {
33
+ type: "image",
34
+ source: {
35
+ type: "workspace_ref",
36
+ media_type: "image/jpeg",
37
+ attachmentId,
38
+ sizeBytes,
39
+ },
40
+ };
41
+ }
42
+
43
+ /** An inline upload, which carries no attachment id to match against. */
44
+ function inlineImage(): Extract<ContentBlock, { type: "image" }> {
45
+ return {
46
+ type: "image",
47
+ source: { type: "base64", media_type: "image/png", data: "AAAA" },
48
+ };
49
+ }
50
+
51
+ /**
52
+ * The live in-memory copy of an upload: inline bytes plus the attachment id
53
+ * that ties it to the row persisted as a reference. `data` is 8 base64 chars,
54
+ * so `mediaSourceByteLength` reports 6 bytes.
55
+ */
56
+ function liveImage(
57
+ attachmentId: string,
58
+ ): Extract<ContentBlock, { type: "image" }> {
59
+ return {
60
+ type: "image",
61
+ source: { type: "base64", media_type: "image/jpeg", data: "AAAAAAAA" },
62
+ _attachmentId: attachmentId,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * A block in the shape `buildRetainedImageBlocks` rebuilds: inline bytes
68
+ * re-hydrated from the attachment store, stamped with the manifest entry's id.
69
+ */
70
+ function retainedImage(
71
+ attachmentId: string,
72
+ ): Extract<ContentBlock, { type: "image" }> {
73
+ return {
74
+ type: "image",
75
+ source: { type: "base64", media_type: "image/png", data: "AAAAAAAA" },
76
+ _attachmentId: attachmentId,
77
+ };
78
+ }
79
+
80
+ function user(...blocks: ContentBlock[]): Message {
81
+ return { role: "user", content: blocks };
82
+ }
83
+
84
+ function assistant(text: string): Message {
85
+ return { role: "assistant", content: [{ type: "text", text }] };
86
+ }
87
+
88
+ /** Capture times for tagged frames, one second apart from a fixed instant. */
89
+ function captureTimes(...attachmentIds: string[]): Map<string, number> {
90
+ const base = Date.parse("2026-08-30T14:25:00.000Z");
91
+ return new Map(attachmentIds.map((id, i) => [id, base + i * 1000]));
92
+ }
93
+
94
+ function textOf(message: Message, index: number): string {
95
+ const block = message.content[index];
96
+ return block.type === "text" ? block.text : "";
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // stripAgedSightFrames
101
+ // ---------------------------------------------------------------------------
102
+
103
+ describe("stripAgedSightFrames", () => {
104
+ test("keeps the newest frames and stubs the rest", () => {
105
+ const messages: Message[] = [
106
+ user({ type: "text", text: "one" }, referenceImage("f1")),
107
+ assistant("ok"),
108
+ user({ type: "text", text: "two" }, referenceImage("f2")),
109
+ assistant("ok"),
110
+ user({ type: "text", text: "three" }, referenceImage("f3")),
111
+ assistant("ok"),
112
+ user({ type: "text", text: "four" }, referenceImage("f4")),
113
+ user({ type: "text", text: "five" }, referenceImage("f5")),
114
+ ];
115
+
116
+ const result = stripAgedSightFrames(
117
+ messages,
118
+ captureTimes("f1", "f2", "f3", "f4", "f5"),
119
+ );
120
+
121
+ expect(result.modified).toBe(true);
122
+ expect(result.replacedBlocks).toBe(3);
123
+
124
+ const surviving = result.messages.flatMap((m) =>
125
+ m.content.filter((b) => b.type === "image"),
126
+ );
127
+ expect(surviving).toHaveLength(KEEP_LATEST_SIGHT_FRAMES);
128
+ expect(surviving).toEqual([referenceImage("f4"), referenceImage("f5")]);
129
+
130
+ expect(textOf(result.messages[0], 1)).toBe(
131
+ "[Camera frame omitted from context: captured 2026-08-30T14:25:00.000Z, image/jpeg, 1024 bytes]",
132
+ );
133
+ expect(textOf(result.messages[2], 1)).toBe(
134
+ "[Camera frame omitted from context: captured 2026-08-30T14:25:01.000Z, image/jpeg, 1024 bytes]",
135
+ );
136
+ expect(textOf(result.messages[4], 1)).toBe(
137
+ "[Camera frame omitted from context: captured 2026-08-30T14:25:02.000Z, image/jpeg, 1024 bytes]",
138
+ );
139
+ });
140
+
141
+ test("keeps as many frames as the caller's budget allows", () => {
142
+ const messages: Message[] = [
143
+ user(referenceImage("f1")),
144
+ user(referenceImage("f2")),
145
+ user(referenceImage("f3")),
146
+ user(referenceImage("f4")),
147
+ user(referenceImage("f5")),
148
+ ];
149
+
150
+ const result = stripAgedSightFrames(
151
+ messages,
152
+ captureTimes("f1", "f2", "f3", "f4", "f5"),
153
+ 4,
154
+ );
155
+
156
+ expect(result.replacedBlocks).toBe(1);
157
+ expect(
158
+ result.messages.flatMap((m) =>
159
+ m.content.filter((b) => b.type === "image"),
160
+ ),
161
+ ).toEqual([
162
+ referenceImage("f2"),
163
+ referenceImage("f3"),
164
+ referenceImage("f4"),
165
+ referenceImage("f5"),
166
+ ]);
167
+ });
168
+
169
+ test("stubs several frames on one message independently", () => {
170
+ const messages: Message[] = [
171
+ user(referenceImage("f1"), referenceImage("f2"), referenceImage("f3")),
172
+ user(referenceImage("f4")),
173
+ ];
174
+
175
+ const result = stripAgedSightFrames(
176
+ messages,
177
+ captureTimes("f1", "f2", "f3", "f4"),
178
+ );
179
+
180
+ expect(result.replacedBlocks).toBe(2);
181
+ expect(result.messages[0].content.map((b) => b.type)).toEqual([
182
+ "text",
183
+ "text",
184
+ "image",
185
+ ]);
186
+ expect(result.messages[1].content.map((b) => b.type)).toEqual(["image"]);
187
+ });
188
+
189
+ test("leaves untagged media alone, even interleaved with frames", () => {
190
+ const pickedReference = referenceImage("picked-1", 4096);
191
+ const pickedInline = inlineImage();
192
+ const pickedFile: ContentBlock = {
193
+ type: "file",
194
+ source: {
195
+ type: "workspace_ref",
196
+ media_type: "application/pdf",
197
+ attachmentId: "picked-2",
198
+ sizeBytes: 2048,
199
+ filename: "notes.pdf",
200
+ },
201
+ };
202
+ const messages: Message[] = [
203
+ user(referenceImage("f1"), pickedReference),
204
+ user(pickedInline, referenceImage("f2"), pickedFile),
205
+ user(referenceImage("f3"), referenceImage("f4")),
206
+ ];
207
+
208
+ const result = stripAgedSightFrames(
209
+ messages,
210
+ captureTimes("f1", "f2", "f3", "f4"),
211
+ );
212
+
213
+ expect(result.replacedBlocks).toBe(2);
214
+ expect(result.messages[0].content[1]).toBe(pickedReference);
215
+ expect(result.messages[1].content[0]).toBe(pickedInline);
216
+ expect(result.messages[1].content[2]).toBe(pickedFile);
217
+ expect(result.messages[2]).toBe(messages[2]);
218
+ });
219
+
220
+ test("ignores tagged ids that match no attachment on the row", () => {
221
+ const messages: Message[] = [
222
+ user(referenceImage("f1")),
223
+ user(referenceImage("f2")),
224
+ user(referenceImage("f3")),
225
+ ];
226
+
227
+ const result = stripAgedSightFrames(
228
+ messages,
229
+ captureTimes("gone-1", "gone-2", "gone-3"),
230
+ );
231
+
232
+ expect(result.modified).toBe(false);
233
+ expect(result.replacedBlocks).toBe(0);
234
+ expect(result.messages).toBe(messages);
235
+ });
236
+
237
+ test("is inert when nothing is tagged", () => {
238
+ const messages: Message[] = [
239
+ user({ type: "text", text: "look" }, referenceImage("a1")),
240
+ assistant("ok"),
241
+ user(inlineImage(), referenceImage("a2"), liveImage("a3")),
242
+ assistant("ok"),
243
+ user(liveImage("a4"), { type: "text", text: "and this" }),
244
+ ];
245
+ const before = structuredClone(messages);
246
+
247
+ const result = stripAgedSightFrames(messages, new Map());
248
+
249
+ expect(result.modified).toBe(false);
250
+ expect(result.replacedBlocks).toBe(0);
251
+ expect(result.messages).toBe(messages);
252
+ expect(result.messages).toEqual(before);
253
+ });
254
+
255
+ test("stubs live inline frames on a call that never reloaded", () => {
256
+ // Every frame is the in-memory copy a turn pushed, so nothing in this
257
+ // history is a `workspace_ref`. This is the uninterrupted-call case.
258
+ const messages: Message[] = [
259
+ user({ type: "text", text: "one" }, liveImage("f1")),
260
+ assistant("ok"),
261
+ user({ type: "text", text: "two" }, liveImage("f2")),
262
+ assistant("ok"),
263
+ user({ type: "text", text: "three" }, liveImage("f3")),
264
+ assistant("ok"),
265
+ user({ type: "text", text: "four" }, liveImage("f4")),
266
+ ];
267
+
268
+ const result = stripAgedSightFrames(
269
+ messages,
270
+ captureTimes("f1", "f2", "f3", "f4"),
271
+ );
272
+
273
+ expect(result.modified).toBe(true);
274
+ expect(result.replacedBlocks).toBe(2);
275
+ expect(textOf(result.messages[0], 1)).toBe(
276
+ "[Camera frame omitted from context: captured 2026-08-30T14:25:00.000Z, image/jpeg, 6 bytes]",
277
+ );
278
+ expect(textOf(result.messages[2], 1)).toBe(
279
+ "[Camera frame omitted from context: captured 2026-08-30T14:25:01.000Z, image/jpeg, 6 bytes]",
280
+ );
281
+ expect(result.messages[4].content[1]).toEqual(liveImage("f3"));
282
+ expect(result.messages[6].content[1]).toEqual(liveImage("f4"));
283
+ });
284
+
285
+ test("ranks live and reloaded frames in one pool", () => {
286
+ // A conversation reloaded mid-call: the older turns came back from the DB
287
+ // as references, the newer ones are still the live copies. The newest two
288
+ // survive regardless of which shape they are.
289
+ const messages: Message[] = [
290
+ user(referenceImage("f1")),
291
+ user(referenceImage("f2")),
292
+ user(liveImage("f3")),
293
+ user(referenceImage("f4")),
294
+ user(liveImage("f5")),
295
+ ];
296
+
297
+ const result = stripAgedSightFrames(
298
+ messages,
299
+ captureTimes("f1", "f2", "f3", "f4", "f5"),
300
+ );
301
+
302
+ expect(result.replacedBlocks).toBe(3);
303
+ expect(result.messages[0].content[0].type).toBe("text");
304
+ expect(result.messages[1].content[0].type).toBe("text");
305
+ expect(result.messages[2].content[0].type).toBe("text");
306
+ expect(result.messages[3].content[0]).toEqual(referenceImage("f4"));
307
+ expect(result.messages[4].content[0]).toEqual(liveImage("f5"));
308
+ });
309
+
310
+ test("ignores an inline image whose id was never tagged", () => {
311
+ const messages: Message[] = [
312
+ user(liveImage("picked-1")),
313
+ user(liveImage("f1")),
314
+ user(liveImage("f2")),
315
+ user(liveImage("f3")),
316
+ ];
317
+
318
+ const result = stripAgedSightFrames(
319
+ messages,
320
+ captureTimes("f1", "f2", "f3"),
321
+ );
322
+
323
+ expect(result.replacedBlocks).toBe(1);
324
+ expect(result.messages[0].content[0]).toEqual(liveImage("picked-1"));
325
+ expect(result.messages[1].content[0].type).toBe("text");
326
+ });
327
+
328
+ test("re-stubs frames that compaction pulled back into the history", () => {
329
+ // Compaction builds its image manifest from the stored rows, not from the
330
+ // trimmed copy a turn sent, so a frame this pass already stubbed can come
331
+ // back as a rebuilt inline block. The history below is the shape compaction
332
+ // leaves behind (summary, retained-images message, verbatim tail), written
333
+ // out rather than produced by a real compaction run, which would need a
334
+ // provider call. The bound has to re-apply over it.
335
+ const messages: Message[] = [
336
+ assistant("<context_summary>earlier call</context_summary>"),
337
+ user(
338
+ {
339
+ type: "text",
340
+ text: "Images retained from the compacted portion of the conversation:",
341
+ },
342
+ retainedImage("f1"),
343
+ retainedImage("f2"),
344
+ retainedImage("f3"),
345
+ ),
346
+ user({ type: "text", text: "and now" }, liveImage("f4")),
347
+ ];
348
+
349
+ const result = stripAgedSightFrames(
350
+ messages,
351
+ captureTimes("f1", "f2", "f3", "f4"),
352
+ );
353
+
354
+ expect(result.replacedBlocks).toBe(2);
355
+ expect(textOf(result.messages[1], 1)).toBe(
356
+ "[Camera frame omitted from context: captured 2026-08-30T14:25:00.000Z, image/png, 6 bytes]",
357
+ );
358
+ expect(textOf(result.messages[1], 2)).toBe(
359
+ "[Camera frame omitted from context: captured 2026-08-30T14:25:01.000Z, image/png, 6 bytes]",
360
+ );
361
+ // The newest retained frame and the tail's live frame are the survivors.
362
+ expect(result.messages[1].content[3]).toEqual(retainedImage("f3"));
363
+ expect(result.messages[2].content[1]).toEqual(liveImage("f4"));
364
+ });
365
+
366
+ test("ranks compaction-rebuilt frames by capture time, not position", () => {
367
+ // Compaction lists the frames it keeps in the MODEL's order inside one
368
+ // synthetic message, so their position stops tracking recency. Here the
369
+ // newest frame (f4) sits first and the oldest (f1) last. Ranking by
370
+ // position would stub f4 and keep f1; ranking by capture time keeps f4.
371
+ const times = captureTimes("f1", "f2", "f3", "f4");
372
+ const messages: Message[] = [
373
+ assistant("<context_summary>earlier call</context_summary>"),
374
+ user(
375
+ {
376
+ type: "text",
377
+ text: "Images retained from the compacted portion of the conversation:",
378
+ },
379
+ retainedImage("f4"),
380
+ retainedImage("f3"),
381
+ retainedImage("f2"),
382
+ retainedImage("f1"),
383
+ ),
384
+ ];
385
+
386
+ const result = stripAgedSightFrames(messages, times);
387
+
388
+ expect(result.replacedBlocks).toBe(2);
389
+ const retained = result.messages[1];
390
+ // The two newest survive wherever they sit.
391
+ expect(retained.content[1]).toEqual(retainedImage("f4"));
392
+ expect(retained.content[2]).toEqual(retainedImage("f3"));
393
+ // The two oldest are stubbed, each naming its own capture time.
394
+ expect(textOf(retained, 3)).toBe(
395
+ "[Camera frame omitted from context: captured 2026-08-30T14:25:01.000Z, image/png, 6 bytes]",
396
+ );
397
+ expect(textOf(retained, 4)).toBe(
398
+ "[Camera frame omitted from context: captured 2026-08-30T14:25:00.000Z, image/png, 6 bytes]",
399
+ );
400
+ });
401
+
402
+ test("falls back to history order for frames sharing a capture time", () => {
403
+ // Frames attached to the same row share its `createdAt`, so the tiebreak
404
+ // is the order they sit in the history, which is the order they arrived.
405
+ const sameRow = new Map([
406
+ ["f1", 1000],
407
+ ["f2", 1000],
408
+ ["f3", 1000],
409
+ ]);
410
+ const messages: Message[] = [
411
+ user(retainedImage("f1"), retainedImage("f2"), retainedImage("f3")),
412
+ ];
413
+
414
+ const result = stripAgedSightFrames(messages, sameRow);
415
+
416
+ expect(result.replacedBlocks).toBe(1);
417
+ expect(result.messages[0].content[0].type).toBe("text");
418
+ expect(result.messages[0].content[1]).toEqual(retainedImage("f2"));
419
+ expect(result.messages[0].content[2]).toEqual(retainedImage("f3"));
420
+ });
421
+
422
+ test("leaves a conversation at or under the budget untouched", () => {
423
+ const messages: Message[] = [
424
+ user(referenceImage("f1")),
425
+ user(referenceImage("f2")),
426
+ ];
427
+
428
+ const result = stripAgedSightFrames(messages, captureTimes("f1", "f2"));
429
+
430
+ expect(result.modified).toBe(false);
431
+ expect(result.messages).toBe(messages);
432
+ });
433
+ });
434
+
435
+ // ---------------------------------------------------------------------------
436
+ // Composition with the reactive retry path
437
+ // ---------------------------------------------------------------------------
438
+
439
+ describe("stripAgedSightFrames composed with stripMediaPayloadsForRetry", () => {
440
+ test("proactive stubs are invisible to the retry path", () => {
441
+ const messages: Message[] = [
442
+ user(referenceImage("f1"), referenceImage("picked-1", 4096)),
443
+ assistant("ok"),
444
+ user(referenceImage("f2")),
445
+ assistant("ok"),
446
+ user(
447
+ { type: "text", text: "latest" },
448
+ referenceImage("f3"),
449
+ referenceImage("f4"),
450
+ referenceImage("picked-2", 4096),
451
+ referenceImage("picked-3", 4096),
452
+ ),
453
+ ];
454
+ expect(countMediaBlocks(messages)).toBe(7);
455
+
456
+ const trimmed = stripAgedSightFrames(
457
+ messages,
458
+ captureTimes("f1", "f2", "f3", "f4"),
459
+ ).messages;
460
+ // The two stubbed frames are text now, so the retry path cannot see them.
461
+ expect(countMediaBlocks(trimmed)).toBe(5);
462
+
463
+ const retried = stripMediaPayloadsForRetry(trimmed);
464
+
465
+ // Only the real media the retry path owns is replaced: the older message's
466
+ // picked image, plus the fourth media block of the latest user message.
467
+ expect(retried.replacedBlocks).toBe(2);
468
+ expect(countMediaBlocks(retried.messages)).toBe(3);
469
+
470
+ const stubTexts = retried.messages.flatMap((m) =>
471
+ m.content.filter((b) => b.type === "text").map((b) => b.text),
472
+ );
473
+ const cameraStubs = stubTexts.filter((t) =>
474
+ t.startsWith("[Camera frame omitted from context:"),
475
+ );
476
+ expect(cameraStubs).toHaveLength(2);
477
+ expect(
478
+ stubTexts.filter((t) =>
479
+ t.startsWith("[Image omitted from retry context:"),
480
+ ),
481
+ ).toHaveLength(2);
482
+ // Nothing wrapped a camera stub in a retry stub.
483
+ for (const stub of cameraStubs) {
484
+ expect(stub.includes("retry context")).toBe(false);
485
+ }
486
+ });
487
+ });
488
+
489
+ // ---------------------------------------------------------------------------
490
+ // Configured retention budget
491
+ // ---------------------------------------------------------------------------
492
+
493
+ describe("resolveSightKeepLatestFrames", () => {
494
+ afterEach(() => {
495
+ setConfig("sight", {});
496
+ });
497
+
498
+ test("defaults to the constant when the workspace configures nothing", () => {
499
+ expect(resolveSightKeepLatestFrames()).toBe(KEEP_LATEST_SIGHT_FRAMES);
500
+ });
501
+
502
+ test("honors a configured value inside the guardrail", () => {
503
+ setConfig("sight", { keepLatestFrames: 4 });
504
+ expect(resolveSightKeepLatestFrames()).toBe(4);
505
+ });
506
+
507
+ test("caps a value above the ceiling instead of resetting it", () => {
508
+ // Ten live images per request is what the ceiling exists to prevent, and
509
+ // falling back to the default would show fewer frames than the guardrail
510
+ // itself allows.
511
+ setConfig("sight", { keepLatestFrames: 99 });
512
+ expect(resolveSightKeepLatestFrames()).toBe(MAX_SIGHT_KEEP_LATEST_FRAMES);
513
+ });
514
+
515
+ test("raises a value below the floor", () => {
516
+ setConfig("sight", { keepLatestFrames: 0 });
517
+ expect(resolveSightKeepLatestFrames()).toBe(MIN_SIGHT_KEEP_LATEST_FRAMES);
518
+ });
519
+ });
520
+
521
+ // ---------------------------------------------------------------------------
522
+ // Metadata reader
523
+ // ---------------------------------------------------------------------------
524
+
525
+ describe("sightFrameAttachmentIdsFromMetadata", () => {
526
+ test("reads the tagged ids", () => {
527
+ expect(
528
+ sightFrameAttachmentIdsFromMetadata({
529
+ voiceSessionTurn: true,
530
+ sightFrameAttachmentIds: ["att-1", "att-2"],
531
+ }),
532
+ ).toEqual(["att-1", "att-2"]);
533
+ });
534
+
535
+ test("yields nothing for absent, null, or malformed values", () => {
536
+ expect(sightFrameAttachmentIdsFromMetadata(undefined)).toEqual([]);
537
+ expect(sightFrameAttachmentIdsFromMetadata(null)).toEqual([]);
538
+ expect(sightFrameAttachmentIdsFromMetadata({})).toEqual([]);
539
+ expect(
540
+ sightFrameAttachmentIdsFromMetadata({ sightFrameAttachmentIds: "att-1" }),
541
+ ).toEqual([]);
542
+ expect(
543
+ sightFrameAttachmentIdsFromMetadata({
544
+ sightFrameAttachmentIds: ["att-1", "", 7, null],
545
+ }),
546
+ ).toEqual(["att-1"]);
547
+ });
548
+ });
549
+
550
+ describe("messageMetadataIsAmbientSightKeep", () => {
551
+ test("a standalone keep carries the tag and the scripted marker", () => {
552
+ // The shape `persistLiveVoiceSightFrame` writes.
553
+ expect(
554
+ messageMetadataIsAmbientSightKeep(
555
+ JSON.stringify({
556
+ voiceSessionTurn: true,
557
+ scripted: true,
558
+ sightFrameAttachmentIds: ["att-1"],
559
+ }),
560
+ ),
561
+ ).toBe(true);
562
+ });
563
+
564
+ test("a spoken turn that merely carried a frame is not a keep", () => {
565
+ // The shape the voice bridge writes when a parked frame rides real
566
+ // speech, which in camera mode is every turn the user takes. Keying on
567
+ // the tag alone would read all of it as machine output.
568
+ expect(
569
+ messageMetadataIsAmbientSightKeep(
570
+ JSON.stringify({
571
+ voiceSessionTurn: true,
572
+ scripted: false,
573
+ sightFrameAttachmentIds: ["att-frame-1"],
574
+ }),
575
+ ),
576
+ ).toBe(false);
577
+ // Older rows predating the always-stamped default carry no marker at all.
578
+ expect(
579
+ messageMetadataIsAmbientSightKeep(
580
+ JSON.stringify({
581
+ voiceSessionTurn: true,
582
+ sightFrameAttachmentIds: ["att-frame-1"],
583
+ }),
584
+ ),
585
+ ).toBe(false);
586
+ });
587
+
588
+ test("an auto-sent row without the tag is not a keep", () => {
589
+ // `scripted` covers every machine-authored send, onboarding prompts
590
+ // included. Those are none of this predicate's business.
591
+ expect(
592
+ messageMetadataIsAmbientSightKeep(JSON.stringify({ scripted: true })),
593
+ ).toBe(false);
594
+ });
595
+
596
+ test("ordinary and unreadable rows are not keeps", () => {
597
+ expect(messageMetadataIsAmbientSightKeep(null)).toBe(false);
598
+ expect(messageMetadataIsAmbientSightKeep("")).toBe(false);
599
+ expect(
600
+ messageMetadataIsAmbientSightKeep(JSON.stringify({ livePhoto: true })),
601
+ ).toBe(false);
602
+ expect(
603
+ messageMetadataIsAmbientSightKeep(
604
+ JSON.stringify({ scripted: true, sightFrameAttachmentIds: [] }),
605
+ ),
606
+ ).toBe(false);
607
+ // Unreadable metadata leaves the row ordinary, so it keeps its work.
608
+ expect(messageMetadataIsAmbientSightKeep("{not json")).toBe(false);
609
+ expect(
610
+ messageMetadataIsAmbientSightKeep(
611
+ JSON.stringify({ scripted: true, sightFrameAttachmentIds: "att-1" }),
612
+ ),
613
+ ).toBe(false);
614
+ });
615
+ });
@@ -71,19 +71,6 @@ mock.module("../persistence/conversation-queries.js", () => ({
71
71
  listConversations: () => [],
72
72
  }));
73
73
 
74
- mock.module("../memory/retriever.js", () => ({
75
- buildMemoryRecall: async () => ({
76
- enabled: false,
77
- degraded: false,
78
- injectedText: "",
79
-
80
- semanticHits: 0,
81
- injectedTokens: 0,
82
- latencyMs: 0,
83
- }),
84
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
85
- }));
86
-
87
74
  mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
88
75
  ContextWindowManager: class {
89
76
  estimateInputTokens() {
@@ -70,19 +70,6 @@ mock.module("../persistence/conversation-queries.js", () => ({
70
70
  listConversations: () => [],
71
71
  }));
72
72
 
73
- mock.module("../memory/retriever.js", () => ({
74
- buildMemoryRecall: async () => ({
75
- enabled: false,
76
- degraded: false,
77
- injectedText: "",
78
-
79
- semanticHits: 0,
80
- injectedTokens: 0,
81
- latencyMs: 0,
82
- }),
83
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
84
- }));
85
-
86
73
  mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
87
74
  ContextWindowManager: class {
88
75
  estimateInputTokens() {
@@ -111,18 +111,6 @@ mock.module("../persistence/attachments-store.js", () => ({
111
111
  linkAttachmentToMessage: () => {},
112
112
  }));
113
113
 
114
- mock.module("../memory/retriever.js", () => ({
115
- buildMemoryRecall: async () => ({
116
- enabled: false,
117
- degraded: false,
118
- injectedText: "",
119
- semanticHits: 0,
120
- injectedTokens: 0,
121
- latencyMs: 0,
122
- }),
123
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
124
- }));
125
-
126
114
  mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
127
115
  ContextWindowManager: class {
128
116
  estimateInputTokens() {
@@ -96,18 +96,6 @@ mock.module("../persistence/conversation-queries.js", () => ({
96
96
  listConversations: () => [],
97
97
  }));
98
98
 
99
- mock.module("../memory/retriever.js", () => ({
100
- buildMemoryRecall: async () => ({
101
- enabled: false,
102
- degraded: false,
103
- injectedText: "",
104
- semanticHits: 0,
105
- injectedTokens: 0,
106
- latencyMs: 0,
107
- }),
108
- injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
109
- }));
110
-
111
99
  mock.module("../agent/loop.js", () => ({
112
100
  AgentLoop: class {
113
101
  constructor() {}