@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
@@ -10,6 +10,7 @@ import { getDb } from "../../../../persistence/db-connection.js";
10
10
  import { initializeDb } from "../../../../persistence/db-init.js";
11
11
  import { messages } from "../../../../persistence/schema/index.js";
12
12
  import {
13
+ countRetrospectiveMessagesAfter,
13
14
  getRetrospectiveMessagesAfter,
14
15
  hasQualifyingUserMessageAfter,
15
16
  messagesHaveUserActivity,
@@ -28,12 +29,61 @@ const MIXED = JSON.stringify([
28
29
  { type: "text", text: "note" },
29
30
  ]);
30
31
 
32
+ /** A kept camera frame's row: a user-role image the call sampled by itself. */
33
+ const SIGHT_FRAME_CONTENT = JSON.stringify([
34
+ { type: "text", text: "(camera frame)" },
35
+ {
36
+ type: "image",
37
+ source: {
38
+ type: "workspace_ref",
39
+ media_type: "image/jpeg",
40
+ attachmentId: "att-frame",
41
+ sizeBytes: 1024,
42
+ },
43
+ },
44
+ ]);
45
+
46
+ /**
47
+ * What `persistLiveVoiceSightFrame` writes: the gate sampled this, nobody
48
+ * spoke it, so `scripted` rides along with the tag.
49
+ */
50
+ function sightFrameMetadata(attachmentId: string): string {
51
+ return JSON.stringify({
52
+ voiceSessionTurn: true,
53
+ provenanceTrustClass: "guardian",
54
+ scripted: true,
55
+ sightFrameAttachmentIds: [attachmentId],
56
+ });
57
+ }
58
+
59
+ /**
60
+ * What the voice bridge writes when a parked frame rides real speech
61
+ * (`calls/voice-session-bridge.ts`, matching its persist test's shape). The
62
+ * bridge leaves `scripted` absent for a genuine turn and the persist stamps
63
+ * the `false` default durably, which is what tells the two apart.
64
+ */
65
+ function riddenTurnMetadata(attachmentId: string): string {
66
+ return JSON.stringify({
67
+ voiceSessionTurn: true,
68
+ provenanceTrustClass: "guardian",
69
+ scripted: false,
70
+ sightFrameAttachmentIds: [attachmentId],
71
+ });
72
+ }
73
+
74
+ /** An auto-sent row with no camera involvement, e.g. an onboarding prompt. */
75
+ const SCRIPTED_UNTAGGED_METADATA = JSON.stringify({
76
+ provenanceTrustClass: "guardian",
77
+ scripted: true,
78
+ });
79
+
31
80
  let seq = 0;
32
81
  function insertRaw(opts: {
33
82
  role: string;
34
83
  content: string;
35
84
  createdAt: number;
36
85
  id?: string;
86
+ metadata?: string;
37
87
  }): string {
38
88
  const id = opts.id ?? `msg-${String(++seq).padStart(4, "0")}`;
39
89
  getDb()
@@ -44,7 +94,7 @@ function insertRaw(opts: {
44
94
  role: opts.role,
45
95
  content: opts.content,
46
96
  createdAt: opts.createdAt,
47
- metadata: null,
97
+ metadata: opts.metadata ?? null,
48
98
  })
49
99
  .run();
50
100
  return id;
@@ -269,3 +319,118 @@ describe("getRetrospectiveMessagesAfter", () => {
269
319
  expect(slice.map((row) => row.id)).not.toContain(streaming);
270
320
  });
271
321
  });
322
+
323
+ describe("ambient camera frames are not retrospective work", () => {
324
+ beforeEach(() => {
325
+ const db = getDb();
326
+ db.run(`DELETE FROM messages`);
327
+ db.run(`DELETE FROM conversations`);
328
+ createConversation({ id: CONV });
329
+ });
330
+
331
+ /** One kept frame, as the live-voice persist writes it. */
332
+ function insertSightFrame(createdAt: number, attachmentId: string): string {
333
+ return insertRaw({
334
+ role: "user",
335
+ content: SIGHT_FRAME_CONTENT,
336
+ createdAt,
337
+ metadata: sightFrameMetadata(attachmentId),
338
+ });
339
+ }
340
+
341
+ test("a camera-only tail does not qualify as user activity", () => {
342
+ // A phone lying face up on a desk persists one of these every few
343
+ // seconds. Nobody said anything, so there is nothing to review.
344
+ insertSightFrame(1_000, "att-a");
345
+ insertSightFrame(2_000, "att-b");
346
+ insertSightFrame(3_000, "att-c");
347
+
348
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(false);
349
+ });
350
+
351
+ test("one real message among the frames does qualify", () => {
352
+ insertSightFrame(1_000, "att-a");
353
+ insertRaw({ role: "user", content: TEXT, createdAt: 2_000 });
354
+ insertSightFrame(3_000, "att-b");
355
+
356
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(true);
357
+ });
358
+
359
+ test("frames are left out of the count that decides a run is due", () => {
360
+ insertSightFrame(1_000, "att-a");
361
+ insertSightFrame(2_000, "att-b");
362
+
363
+ expect(countRetrospectiveMessagesAfter(CONV, null)).toBe(0);
364
+
365
+ insertRaw({ role: "user", content: TEXT, createdAt: 3_000 });
366
+
367
+ expect(countRetrospectiveMessagesAfter(CONV, null)).toBe(1);
368
+ });
369
+
370
+ test("frames are left out of the slice a run would review", () => {
371
+ insertSightFrame(1_000, "att-a");
372
+ const real = insertRaw({
373
+ role: "user",
374
+ content: TEXT,
375
+ createdAt: 2_000,
376
+ });
377
+ insertSightFrame(3_000, "att-b");
378
+
379
+ const slice = getRetrospectiveMessagesAfter(CONV, null);
380
+
381
+ // The cutoff lands on the real message, so the frames are neither
382
+ // reviewed nor skipped over.
383
+ expect(slice.map((row) => row.id)).toEqual([real]);
384
+ });
385
+
386
+ test("a spoken turn that carried a frame is real work", () => {
387
+ // Camera mode parks a frame continuously, so in a camera call EVERY
388
+ // spoken turn carries the tag. Excluding on the tag alone would mean a
389
+ // conversation held entirely in camera mode never earns a retrospective.
390
+ const spoken = insertRaw({
391
+ role: "user",
392
+ content: TEXT,
393
+ createdAt: 1_000,
394
+ metadata: riddenTurnMetadata("att-frame-1"),
395
+ });
396
+
397
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(true);
398
+ expect(countRetrospectiveMessagesAfter(CONV, null)).toBe(1);
399
+ expect(getRetrospectiveMessagesAfter(CONV, null).map((r) => r.id)).toEqual([
400
+ spoken,
401
+ ]);
402
+ });
403
+
404
+ test("an auto-sent row without the tag is untouched by this exclusion", () => {
405
+ // `scripted` covers onboarding sends and other machine-authored turns.
406
+ // They counted before the camera existed and must keep counting.
407
+ const onboarding = insertRaw({
408
+ role: "user",
409
+ content: TEXT,
410
+ createdAt: 1_000,
411
+ metadata: SCRIPTED_UNTAGGED_METADATA,
412
+ });
413
+
414
+ expect(countRetrospectiveMessagesAfter(CONV, null)).toBe(1);
415
+ expect(getRetrospectiveMessagesAfter(CONV, null).map((r) => r.id)).toEqual([
416
+ onboarding,
417
+ ]);
418
+ });
419
+
420
+ test("keeps are excluded even among the speech they punctuate", () => {
421
+ insertSightFrame(1_000, "att-a");
422
+ const spoken = insertRaw({
423
+ role: "user",
424
+ content: TEXT,
425
+ createdAt: 2_000,
426
+ metadata: riddenTurnMetadata("att-frame-1"),
427
+ });
428
+ insertSightFrame(3_000, "att-b");
429
+
430
+ expect(countRetrospectiveMessagesAfter(CONV, null)).toBe(1);
431
+ expect(getRetrospectiveMessagesAfter(CONV, null).map((r) => r.id)).toEqual([
432
+ spoken,
433
+ ]);
434
+ expect(hasQualifyingUserMessageAfter(CONV, null)).toBe(true);
435
+ });
436
+ });
@@ -338,6 +338,9 @@ function makeForkConversationDouble(): Conversation {
338
338
  const conversation = {
339
339
  conversationId: FORK_ID,
340
340
  messages,
341
+ // The wake trims its own run input; nothing here is tagged as a camera
342
+ // frame, so the real pass would return the array unchanged too.
343
+ trimAgedSightFrames: (msgs: Message[]) => msgs,
341
344
  getMessages: () => messages,
342
345
  isProcessing: () => false,
343
346
  setProcessing: (_on: boolean) => {},
@@ -176,6 +176,9 @@ function makeForkConversationDouble(forkId: string): Conversation {
176
176
  provider: scriptedProvider,
177
177
  usageStats: {},
178
178
  messages,
179
+ // The wake trims its own run input; nothing here is tagged as a camera
180
+ // frame, so the real pass would return the array unchanged too.
181
+ trimAgedSightFrames: (msgs: Message[]) => msgs,
179
182
  getMessages: () => messages,
180
183
  isProcessing: () => processing,
181
184
  setProcessing: (on: boolean) => {
@@ -192,7 +192,7 @@ export interface MemoryJobsWorker {
192
192
 
193
193
  /**
194
194
  * Daemon-lifecycle entry point: spawn the memory jobs worker as a child of the
195
- * daemon (`detached: false`, so it appears in `assistant ps` and is torn down
195
+ * daemon (so it appears in the daemon's process tree and is torn down
196
196
  * on shutdown). The worker process is the sole drainer of the memory job queue.
197
197
  * Fire-and-forget — a worker failure must never block boot. A worker that comes
198
198
  * up late is still the desired sole drainer, so `terminateOnTimeout` is
@@ -205,7 +205,6 @@ export interface MemoryJobsWorker {
205
205
  export function startMemoryJobsWorker(): void {
206
206
  void spawnMemoryWorkerProcess({
207
207
  terminateOnTimeout: false,
208
- detached: false,
209
208
  })
210
209
  .then(({ pid, alreadyRunning }) =>
211
210
  log.info(
@@ -16,6 +16,14 @@
16
16
  // zero-new-messages early-out) and the job's new-message slice — without
17
17
  // touching the generic persistence helpers' semantics for other callers.
18
18
  //
19
+ // Ambient camera frames are excluded on the same three paths and for the same
20
+ // reason, one step further out: a live-voice call with the camera up persists
21
+ // a frame every few seconds whether or not anyone speaks, so counting them
22
+ // would let a phone lying face up on a desk keep declaring its conversation
23
+ // worth reviewing, and reviewing them would hand the model a wall to remember.
24
+ // The user-activity probe excludes them too, so a tail of nothing but frames
25
+ // never satisfies the `requireUserActivity` gate.
26
+ //
19
27
  // The cursor is never blindly advanced over a card: the job still takes its
20
28
  // cutoff from the last NON-card row of the filtered slice, so a real message
21
29
  // that lands between the cutoff snapshot and the card insert can never be
@@ -30,10 +38,31 @@ import {
30
38
  getMessagesAfter,
31
39
  type MessageRow,
32
40
  } from "../../../persistence/conversation-crud.js";
41
+ import {
42
+ messageMetadataIsAmbientSightKeep,
43
+ SIGHT_FRAME_ATTACHMENT_IDS_KEY,
44
+ } from "../../../persistence/conversation-types.js";
33
45
  import { getDb } from "../../../persistence/db-connection.js";
34
46
  import { messages } from "../../../persistence/schema/index.js";
35
47
  import { SKILL_CARD_MESSAGE_KIND } from "./memory-retrospective-constants.js";
36
48
 
49
+ /**
50
+ * Rows the retrospective must not account for: a prior run's own skill card,
51
+ * and ambient camera frames.
52
+ *
53
+ * A frame is machine-sampled, one every few seconds for as long as the camera
54
+ * is up, so counting them would let an idle camera pointed at a wall keep
55
+ * declaring a conversation worth reviewing. They are also the wrong thing to
56
+ * review: nobody said anything.
57
+ */
58
+ function isExcludedFromRetrospectiveAccounting(row: {
59
+ metadata: string | null;
60
+ }): boolean {
61
+ return (
62
+ isSkillCardMessage(row) || messageMetadataIsAmbientSightKeep(row.metadata)
63
+ );
64
+ }
65
+
37
66
  /** True when a message row's metadata carries the skill-card kind. */
38
67
  export function isSkillCardMessage(row: { metadata: string | null }): boolean {
39
68
  if (!row.metadata) {
@@ -75,7 +104,7 @@ export function getRetrospectiveMessagesAfter(
75
104
  const firstUnfinalized = rows.findIndex((row) => row.finalized !== 1);
76
105
  const bounded =
77
106
  firstUnfinalized === -1 ? rows : rows.slice(0, firstUnfinalized);
78
- return bounded.filter((row) => !isSkillCardMessage(row));
107
+ return bounded.filter((row) => !isExcludedFromRetrospectiveAccounting(row));
79
108
  }
80
109
 
81
110
  /**
@@ -93,8 +122,8 @@ export function countRetrospectiveMessagesAfter(
93
122
  if (total === 0) {
94
123
  return 0;
95
124
  }
96
- const cardCount = countSkillCardMessagesAfter(conversationId, afterMessageId);
97
- return Math.max(0, total - cardCount);
125
+ const excluded = countExcludedMessagesAfter(conversationId, afterMessageId);
126
+ return Math.max(0, total - excluded);
98
127
  }
99
128
 
100
129
  /**
@@ -117,6 +146,12 @@ function blocksCarryNonToolResult(
117
146
  * user-role row whose resolved content carries a non-tool_result block. Tool
118
147
  * results ride on user-role rows, so a bare role check would count any
119
148
  * tool-using assistant stretch as user activity.
149
+ *
150
+ * Takes rows rather than reading any itself, and its one caller passes the
151
+ * slice {@link getRetrospectiveMessagesAfter} already filtered, so ambient
152
+ * camera frames never reach it. A future caller assembling its own rows has to
153
+ * filter them first: a frame is a user-role row carrying an image block, which
154
+ * is indistinguishable from real activity by role and content alone.
120
155
  */
121
156
  export function messagesHaveUserActivity(
122
157
  rows: ReadonlyArray<Pick<MessageRow, "role" | "content">>,
@@ -136,7 +171,9 @@ export function messagesHaveUserActivity(
136
171
  *
137
172
  * Operates on the raw `content` column: rows that do not parse to a block
138
173
  * array (file-backed `{ ref }` rows, legacy plain strings) count as
139
- * user-authored, so the gate fails toward running the retrospective.
174
+ * user-authored, so the gate fails toward running the retrospective. Ambient
175
+ * camera frames are the one user-role row that does NOT count: the camera
176
+ * sampled them, so they must not be what wakes a review.
140
177
  */
141
178
  export function hasQualifyingUserMessageAfter(
142
179
  conversationId: string,
@@ -171,7 +208,7 @@ export function hasQualifyingUserMessageAfter(
171
208
  ]
172
209
  : [];
173
210
  const rows = db
174
- .select({ content: messages.content })
211
+ .select({ content: messages.content, metadata: messages.metadata })
175
212
  .from(messages)
176
213
  .where(
177
214
  and(
@@ -182,7 +219,11 @@ export function hasQualifyingUserMessageAfter(
182
219
  )
183
220
  .all();
184
221
 
185
- return rows.some((row) => rawUserContentCarriesActivity(row.content));
222
+ return rows.some(
223
+ (row) =>
224
+ !messageMetadataIsAmbientSightKeep(row.metadata) &&
225
+ rawUserContentCarriesActivity(row.content),
226
+ );
186
227
  }
187
228
 
188
229
  /** Raw-column twin of the block check used by {@link messagesHaveUserActivity}. */
@@ -211,7 +252,7 @@ function rawUserContentCarriesActivity(raw: string | null): boolean {
211
252
  * tie-breaker semantics, including the null/`""` "count everything" cases
212
253
  * and the vanished-reference "no new work" case.
213
254
  */
214
- function countSkillCardMessagesAfter(
255
+ function countExcludedMessagesAfter(
215
256
  conversationId: string,
216
257
  afterMessageId: string | null,
217
258
  ): number {
@@ -230,7 +271,10 @@ function countSkillCardMessagesAfter(
230
271
  .where(
231
272
  and(
232
273
  eq(messages.conversationId, conversationId),
233
- like(messages.metadata, `%"kind":"${SKILL_CARD_MESSAGE_KIND}"%`),
274
+ or(
275
+ like(messages.metadata, `%"kind":"${SKILL_CARD_MESSAGE_KIND}"%`),
276
+ like(messages.metadata, `%"${SIGHT_FRAME_ATTACHMENT_IDS_KEY}"%`),
277
+ ),
234
278
  ),
235
279
  )
236
280
  .all();
@@ -254,7 +298,7 @@ function countSkillCardMessagesAfter(
254
298
 
255
299
  let count = 0;
256
300
  for (const row of candidates) {
257
- if (!isSkillCardMessage(row)) {
301
+ if (!isExcludedFromRetrospectiveAccounting(row)) {
258
302
  continue;
259
303
  }
260
304
  if (cursorId !== null && ref) {
@@ -4,7 +4,7 @@
4
4
  * The route handlers run inside the daemon and manage the worker process. We
5
5
  * mock worker-control + the embedding backend so the tests assert handler
6
6
  * behaviour:
7
- * - start spawns as a daemon child (detached:false) and throws on failure.
7
+ * - start spawns the worker and throws on failure.
8
8
  * - stop signals the worker and reports its prior state.
9
9
  * - status reports the worker process liveness + the embedding backend.
10
10
  */
@@ -16,7 +16,7 @@ import { getMemoryWorkerPidPath } from "../../../../../util/platform.js";
16
16
  class FakeSpawnError extends Error {}
17
17
 
18
18
  let spawnImpl: () => Promise<{ pid: number; alreadyRunning: boolean }>;
19
- let spawnArgs: Array<{ detached?: boolean; terminateOnTimeout?: boolean }> = [];
19
+ let spawnArgs: Array<{ terminateOnTimeout?: boolean } | undefined> = [];
20
20
  let stopImpl: () => { status: "running" | "not_running"; pid?: number };
21
21
  let workerProbe: { status: "running" | "not_running"; pid?: number } = {
22
22
  status: "not_running",
@@ -37,10 +37,7 @@ let backendStatus: {
37
37
 
38
38
  mock.module("../../worker-control.js", () => ({
39
39
  MemoryWorkerSpawnError: FakeSpawnError,
40
- spawnMemoryWorkerProcess: async (opts: {
41
- detached?: boolean;
42
- terminateOnTimeout?: boolean;
43
- }) => {
40
+ spawnMemoryWorkerProcess: async (opts: { terminateOnTimeout?: boolean }) => {
44
41
  spawnArgs.push(opts);
45
42
  return spawnImpl();
46
43
  },
@@ -86,7 +83,7 @@ describe("memory_worker_start", () => {
86
83
 
87
84
  const res = await handler("memory_worker_start")();
88
85
 
89
- expect(spawnArgs).toEqual([{ detached: false }]);
86
+ expect(spawnArgs).toEqual([undefined]);
90
87
  expect(res).toEqual({
91
88
  pid: 4242,
92
89
  alreadyRunning: false,
@@ -62,7 +62,7 @@ const statusResponseSchema = z.object({
62
62
  async function startMemoryWorker() {
63
63
  let result: { pid: number; alreadyRunning: boolean };
64
64
  try {
65
- result = await spawnMemoryWorkerProcess({ detached: false });
65
+ result = await spawnMemoryWorkerProcess();
66
66
  } catch (err) {
67
67
  const message =
68
68
  err instanceof MemoryWorkerSpawnError || err instanceof Error
@@ -4,6 +4,7 @@ import {
4
4
  } from "@vellumai/plugin-api";
5
5
  import { and, eq, isNotNull, like, ne } from "drizzle-orm";
6
6
 
7
+ import { messageMetadataIsAmbientSightKeep } from "../../../../../persistence/conversation-types.js";
7
8
  import { getDb } from "../../../../../persistence/db-connection.js";
8
9
  import { withQdrantBreaker } from "../../../../../persistence/embeddings/qdrant-circuit-breaker.js";
9
10
  import {
@@ -66,7 +67,11 @@ export async function rebuildIndexJob(): Promise<void> {
66
67
  }
67
68
 
68
69
  const imageMessages = db
69
- .select({ id: messages.id, content: messages.content })
70
+ .select({
71
+ id: messages.id,
72
+ content: messages.content,
73
+ metadata: messages.metadata,
74
+ })
70
75
  .from(messages)
71
76
  .where(
72
77
  and(
@@ -76,6 +81,15 @@ export async function rebuildIndexJob(): Promise<void> {
76
81
  )
77
82
  .all();
78
83
  for (const msg of imageMessages) {
84
+ // Ambient camera keeps were persisted with indexing skipped, and a
85
+ // rebuild must not be the thing that quietly puts them into memory
86
+ // after all. `skipIndexing` is a per-write option with no column behind
87
+ // it, so the row's own tag plus its `scripted` marker is the durable
88
+ // record of that choice. A spoken turn that merely CARRIED a frame is
89
+ // indexed by the live path and so is rebuilt like any other message.
90
+ if (messageMetadataIsAmbientSightKeep(msg.metadata)) {
91
+ continue;
92
+ }
79
93
  const blocks = extractMediaBlockMeta(msg.content);
80
94
  for (const block of blocks) {
81
95
  enqueueMemoryJob("embed_attachment", {
@@ -37,8 +37,7 @@ export class MemoryWorkerSpawnError extends WorkerProcessSpawnError {}
37
37
  * failed to start).
38
38
  *
39
39
  * See {@link SpawnWorkerProcessOptions} for the generic option semantics. The
40
- * daemon's boot spawn passes `detached: false` (so the worker appears in
41
- * `assistant ps` and is torn down with the daemon) and leaves
40
+ * daemon's boot spawn leaves
42
41
  * `terminateOnTimeout` unset (a worker that comes up late is still the desired
43
42
  * sole drainer).
44
43
  */
@@ -74,6 +74,7 @@ const imageContentSchema = z
74
74
  .object({
75
75
  type: z.literal("image"),
76
76
  source: mediaSourceSchema,
77
+ _attachmentId: z.string().optional(),
77
78
  })
78
79
  .passthrough();
79
80
 
@@ -1,4 +1,7 @@
1
- import { modelEffortCeilings } from "../model-catalog.js";
1
+ import {
2
+ modelEffortCeilings,
3
+ modelSupportedEfforts,
4
+ } from "../model-catalog.js";
2
5
  import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
3
6
 
4
7
  export interface FireworksProviderOptions {
@@ -10,6 +13,7 @@ export interface FireworksProviderOptions {
10
13
  const DEFAULT_FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1";
11
14
 
12
15
  const FIREWORKS_MODEL_EFFORT_CEILINGS = modelEffortCeilings("fireworks");
16
+ const FIREWORKS_MODEL_SUPPORTED_EFFORTS = modelSupportedEfforts("fireworks");
13
17
 
14
18
  export class FireworksProvider extends OpenAIChatCompletionsProvider {
15
19
  constructor(
@@ -39,4 +43,8 @@ export class FireworksProvider extends OpenAIChatCompletionsProvider {
39
43
  ): "high" | "xhigh" | "max" {
40
44
  return FIREWORKS_MODEL_EFFORT_CEILINGS.get(model) ?? "high";
41
45
  }
46
+
47
+ protected override resolveSupportedReasoningEfforts(model: string) {
48
+ return FIREWORKS_MODEL_SUPPORTED_EFFORTS.get(model);
49
+ }
42
50
  }
@@ -62,23 +62,36 @@ const THINKING_LEVEL_BY_NAME: Record<ThinkingLevelName, ThinkingLevel> = {
62
62
  */
63
63
  const GEMINI_PRO_DEFAULT_THINKING_LEVEL: ThinkingLevelName = "high";
64
64
 
65
+ function normalizeGeminiModelId(model: string): string {
66
+ return model.startsWith("models/") ? model.slice("models/".length) : model;
67
+ }
68
+
65
69
  /**
66
70
  * Gemini 3.x Pro family accepts only `low`/`medium`/`high` (no `"minimal"`) and
67
71
  * cannot fully disable thinking. Matches `gemini-3.1-pro-preview`,
68
72
  * `gemini-3.1-pro-preview-customtools`, and future `gemini-3*pro*`.
69
73
  */
70
74
  function isGeminiProModel(model: string): boolean {
71
- const normalized = model.startsWith("models/")
72
- ? model.slice("models/".length)
73
- : model;
74
- return /^gemini-3.*pro/.test(normalized);
75
+ return /^gemini-3.*pro/.test(normalizeGeminiModelId(model));
76
+ }
77
+
78
+ function findGeminiCatalogModel(model: string) {
79
+ const normalized = normalizeGeminiModelId(model);
80
+ return PROVIDER_CATALOG.find(
81
+ (provider) => provider.id === "gemini",
82
+ )?.models.find((m) => m.id === normalized);
75
83
  }
76
84
 
77
85
  /**
78
- * Lowest thinking level the model accepts. Pro's floor is `"low"`; every other
86
+ * Lowest thinking level the model accepts. Catalog `thinkingFloor` wins when
87
+ * present. Uncatalogued Gemini 3.x Pro IDs fall back to `"low"`; every other
79
88
  * thinking-capable Gemini model accepts `"minimal"`.
80
89
  */
81
90
  function geminiThinkingFloor(model: string): ThinkingLevelName {
91
+ const catalogFloor = findGeminiCatalogModel(model)?.thinkingFloor;
92
+ if (catalogFloor === "low" || catalogFloor === "minimal") {
93
+ return catalogFloor;
94
+ }
82
95
  return isGeminiProModel(model) ? "low" : "minimal";
83
96
  }
84
97
 
@@ -102,9 +115,9 @@ function clampThinkingLevelToFloor(
102
115
  * Google's per-model default apply (e.g. `gemini-3.5-flash` defaults to
103
116
  * dynamic medium-level thinking).
104
117
  *
105
- * - `enabled: false` maps to the model's floor — the most "off" state it
106
- * allows (`"minimal"` for most models, `"low"` for Pro, which can't disable
107
- * thinking).
118
+ * - `enabled: false` maps to the model's floor (the most "off" state it
119
+ * allows): `"minimal"` for most models, `"low"` for Pro and Gemini 3.7
120
+ * Flash, which reject `"minimal"`.
108
121
  * - An explicit `level` below the floor is raised to the floor.
109
122
  * - When no `level` is pinned, Pro models get the documented default (`"high"`)
110
123
  * because an absent level resolves to the unsupported `"minimal"` upstream;
@@ -161,13 +174,7 @@ function buildThinkingConfig(
161
174
  * behavior); only an explicit `supportsThinking: false` suppresses it.
162
175
  */
163
176
  function geminiModelSupportsThinking(model: string): boolean {
164
- const normalized = model.startsWith("models/")
165
- ? model.slice("models/".length)
166
- : model;
167
- const catalogModel = PROVIDER_CATALOG.find(
168
- (provider) => provider.id === "gemini",
169
- )?.models.find((m) => m.id === normalized);
170
- return catalogModel?.supportsThinking !== false;
177
+ return findGeminiCatalogModel(model)?.supportsThinking !== false;
171
178
  }
172
179
 
173
180
  function stripGeminiHttpOptions(
@@ -28,13 +28,14 @@ import {
28
28
  sniffImageMimeType,
29
29
  } from "../util/image-conversion.js";
30
30
  import { getLogger } from "../util/logger.js";
31
- import type {
32
- Base64MediaSource,
33
- ContentBlock,
34
- FileContent,
35
- ImageContent,
36
- MediaSource,
37
- Message,
31
+ import {
32
+ attachmentIdFragment,
33
+ type Base64MediaSource,
34
+ type ContentBlock,
35
+ type FileContent,
36
+ type ImageContent,
37
+ type MediaSource,
38
+ type Message,
38
39
  } from "./types.js";
39
40
 
40
41
  const log = getLogger("media-resolve");
@@ -143,6 +144,21 @@ export function mediaSourceByteLength(source: MediaSource): number {
143
144
  return Math.floor((source.data.length * 3) / 4);
144
145
  }
145
146
 
147
+ /**
148
+ * What a media source was, as the text that stands in for it once the bytes are
149
+ * gone: its media type and payload size.
150
+ *
151
+ * Every path that replaces a media block with a text stub naming what it
152
+ * dropped reports these same two facts, so they are derived once here. Each
153
+ * stub keeps its own surrounding sentence (which path dropped it, and why) and
154
+ * embeds this for the what. Reads the size through
155
+ * {@link mediaSourceByteLength}, so a reference and an inline block describe
156
+ * themselves the same way.
157
+ */
158
+ export function mediaSourceDescriptor(source: MediaSource): string {
159
+ return `${source.media_type}, ${mediaSourceByteLength(source)} bytes`;
160
+ }
161
+
146
162
  /**
147
163
  * Resolve a media source to inline base64, reading a reference source back from
148
164
  * its workspace location. Returns `null` when a reference can no longer be
@@ -280,9 +296,7 @@ function resolveFileBlock(block: FileContent): ContentBlock {
280
296
  ...(block.extracted_text !== undefined
281
297
  ? { extracted_text: block.extracted_text }
282
298
  : {}),
283
- ...(block._attachmentId !== undefined
284
- ? { _attachmentId: block._attachmentId }
285
- : {}),
299
+ ...attachmentIdFragment(block._attachmentId),
286
300
  };
287
301
  }
288
302