@vellumai/assistant 0.12.3-staging.1 → 0.12.3-staging.2

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 (560) hide show
  1. package/ARCHITECTURE.md +43 -29
  2. package/Dockerfile +63 -0
  3. package/docs/architecture/security.md +10 -5
  4. package/docs/architecture/turn-actor.md +2 -0
  5. package/docs/desktop-browser-cli.md +45 -4
  6. package/docs/live-voice-task-outcomes.md +100 -0
  7. package/docs/mode-sessions.md +183 -0
  8. package/docs/voice-input-diagnostics.md +58 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -1
  17. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +38 -7
  18. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +5 -6
  19. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  20. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  21. package/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  22. package/node_modules/@vellumai/slack-text/src/index.ts +4 -8
  23. package/openapi.yaml +665 -14
  24. package/package.json +1 -1
  25. package/scripts/smoke-desktop-browser-cli.ts +10 -26
  26. package/scripts/smoke-desktop-window-theme.ts +2 -3
  27. package/src/__tests__/agent-loop-output-hooks.test.ts +150 -1
  28. package/src/__tests__/agent-loop-provider-error-recording.test.ts +87 -19
  29. package/src/__tests__/annotate-activity-metadata.test.ts +3 -1
  30. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  31. package/src/__tests__/answered-question-persistence.test.ts +2 -0
  32. package/src/__tests__/anthropic-gateway-shared.test.ts +22 -0
  33. package/src/__tests__/approval-message-composer.test.ts +1 -118
  34. package/src/__tests__/approval-routes-http.test.ts +3 -0
  35. package/src/__tests__/assistant-event-hub-connection-history.test.ts +111 -0
  36. package/src/__tests__/assistant-event-hub-targeted.test.ts +50 -0
  37. package/src/__tests__/assistant-event-targeting.test.ts +186 -0
  38. package/src/__tests__/assistant-stream-state.test.ts +29 -9
  39. package/src/__tests__/browser-fill-credential.test.ts +11 -22
  40. package/src/__tests__/btw-routes.test.ts +4 -1
  41. package/src/__tests__/build-persisted-content.test.ts +1 -1
  42. package/src/__tests__/call-conversation-messages.test.ts +5 -0
  43. package/src/__tests__/call-recovery.test.ts +7 -2
  44. package/src/__tests__/call-routes-http.test.ts +12 -0
  45. package/src/__tests__/call-store.test.ts +7 -2
  46. package/src/__tests__/channel-retry-sweep.test.ts +9 -0
  47. package/src/__tests__/chat-reveal-guard-priming.test.ts +2 -0
  48. package/src/__tests__/checker.test.ts +11 -3
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +35 -1
  50. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +0 -25
  51. package/src/__tests__/computer-use-tools.test.ts +1 -233
  52. package/src/__tests__/config-model-image-provider.test.ts +39 -0
  53. package/src/__tests__/config-schema.test.ts +16 -0
  54. package/src/__tests__/config-watcher.test.ts +24 -0
  55. package/src/__tests__/context-search-agent-runner.test.ts +260 -1
  56. package/src/__tests__/context-search-fanout.test.ts +1 -1
  57. package/src/__tests__/context-search-types.test.ts +7 -7
  58. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +8 -2
  59. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +2 -0
  60. package/src/__tests__/conversation-agent-loop.test.ts +546 -16
  61. package/src/__tests__/conversation-app-control-lifecycle.test.ts +53 -1
  62. package/src/__tests__/conversation-evictor.test.ts +62 -37
  63. package/src/__tests__/conversation-history-boundary-repair-failure.test.ts +174 -0
  64. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +2 -0
  65. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  66. package/src/__tests__/conversation-retry-route.test.ts +22 -0
  67. package/src/__tests__/conversation-routes-disk-view.test.ts +2 -0
  68. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +2 -0
  69. package/src/__tests__/conversation-routes-guardian-reply.test.ts +10 -0
  70. package/src/__tests__/conversation-routes-hidden-queue.test.ts +2 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +29 -0
  73. package/src/__tests__/conversation-surface-snapshots.test.ts +543 -0
  74. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +151 -0
  75. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +63 -1
  76. package/src/__tests__/conversation-tool-setup-attribution.test.ts +23 -0
  77. package/src/__tests__/conversation-undo.test.ts +61 -0
  78. package/src/__tests__/credential-execution-client.test.ts +6 -6
  79. package/src/__tests__/credential-prompt-route.test.ts +35 -3
  80. package/src/__tests__/cu-unified-flow.test.ts +82 -1
  81. package/src/__tests__/db-mode-sessions-migration.test.ts +175 -0
  82. package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -0
  83. package/src/__tests__/dm-backfill.test.ts +91 -1
  84. package/src/__tests__/document-create-route.test.ts +88 -0
  85. package/src/__tests__/document-sync-tags.test.ts +27 -0
  86. package/src/__tests__/emit-signal-routing-intent.test.ts +142 -2
  87. package/src/__tests__/events-tail-route.test.ts +25 -0
  88. package/src/__tests__/evict-conversations-for-reload.test.ts +57 -8
  89. package/src/__tests__/guardian-action-sweep.test.ts +16 -0
  90. package/src/__tests__/guardian-dispatch.test.ts +13 -0
  91. package/src/__tests__/headless-browser-interactions.test.ts +64 -20
  92. package/src/__tests__/heartbeat-service.test.ts +2 -59
  93. package/src/__tests__/helpers/mock-conversation.ts +21 -0
  94. package/src/__tests__/helpers/native-web-search-harness.ts +2 -0
  95. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +5 -0
  96. package/src/__tests__/host-cu-proxy.test.ts +46 -2
  97. package/src/__tests__/http-user-message-parity.test.ts +2 -0
  98. package/src/__tests__/image-credentials.test.ts +49 -0
  99. package/src/__tests__/image-service-dispatcher.test.ts +45 -0
  100. package/src/__tests__/inference-profile-session-handler.test.ts +1 -1
  101. package/src/__tests__/interrupt-on-send.test.ts +57 -43
  102. package/src/__tests__/interrupt-turn-note.test.ts +5 -6
  103. package/src/__tests__/llm-catalog-parity.test.ts +2 -2
  104. package/src/__tests__/media-generate-image.test.ts +49 -1
  105. package/src/__tests__/message-provenance-contact-id.test.ts +69 -0
  106. package/src/__tests__/migration-export-to-gcs.test.ts +142 -0
  107. package/src/__tests__/mode-session-history.test.ts +245 -0
  108. package/src/__tests__/mode-session-recovery.test.ts +222 -0
  109. package/src/__tests__/mode-session-wire.test.ts +141 -0
  110. package/src/__tests__/notification-guardian-path.test.ts +7 -0
  111. package/src/__tests__/notification-telegram-adapter.test.ts +2 -2
  112. package/src/__tests__/notification-vellum-adapter.test.ts +34 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +12 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  115. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  116. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  117. package/src/__tests__/plugin-api-provider.test.ts +5 -0
  118. package/src/__tests__/plugin-api-resolve-credential.test.ts +97 -0
  119. package/src/__tests__/plugin-import-boundary-guard.test.ts +9 -0
  120. package/src/__tests__/process-message-display-content.test.ts +56 -0
  121. package/src/__tests__/reaction-persistence.test.ts +5 -0
  122. package/src/__tests__/request-secret-standalone-channel-gate.test.ts +25 -9
  123. package/src/__tests__/runtime-events-sse-reconnect.test.ts +1 -1
  124. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +73 -0
  125. package/src/__tests__/secret-ingress-http.test.ts +2 -0
  126. package/src/__tests__/secure-keys.test.ts +43 -1
  127. package/src/__tests__/send-endpoint-busy.test.ts +163 -1
  128. package/src/__tests__/server-history-render.test.ts +91 -1
  129. package/src/__tests__/settings-routes.test.ts +50 -1
  130. package/src/__tests__/skill-script-runner-sandbox.test.ts +32 -0
  131. package/src/__tests__/subagent-fork-notifications.test.ts +1 -2
  132. package/src/__tests__/subagent-manager-notify.test.ts +1 -2
  133. package/src/__tests__/subagent-notify-parent.test.ts +146 -39
  134. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +2 -4
  135. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +2 -0
  136. package/src/__tests__/thread-backfill.test.ts +53 -0
  137. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  138. package/src/__tests__/tool-result-metadata-plumbing.test.ts +3 -1
  139. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  140. package/src/__tests__/twilio-routes.test.ts +1 -0
  141. package/src/__tests__/web-search-backend-failure.test.ts +1 -1
  142. package/src/activation/progress-store.test.ts +12 -7
  143. package/src/agent/loop.ts +99 -41
  144. package/src/api/events/assistant-turn-start.ts +3 -0
  145. package/src/api/events/generation-handoff.ts +2 -0
  146. package/src/api/events/message-complete.ts +2 -0
  147. package/src/api/events/notification-conversation-created.ts +14 -16
  148. package/src/api/events/notification-intent.ts +4 -2
  149. package/src/api/events/tool-result.ts +85 -0
  150. package/src/api/events/tool-use-start.ts +3 -0
  151. package/src/api/events/user-message-echo.ts +3 -0
  152. package/src/api/index.ts +31 -6
  153. package/src/api/mode-session.test.ts +131 -0
  154. package/src/api/mode-session.ts +148 -0
  155. package/src/api/remember-tool.ts +15 -0
  156. package/src/api/responses/conversation-message.ts +6 -0
  157. package/src/approvals/AGENTS.md +4 -2
  158. package/src/browser/__tests__/operations.test.ts +29 -0
  159. package/src/browser/operation-meta.ts +2 -1
  160. package/src/browser/operations.ts +29 -0
  161. package/src/browser/virtual-desktop-target.ts +2 -8
  162. package/src/bundler/bundle-signer.ts +1 -3
  163. package/src/calls/__tests__/barge-in-guard.test.ts +3 -0
  164. package/src/calls/__tests__/call-controller.test.ts +94 -0
  165. package/src/calls/__tests__/call-funnel-telemetry.test.ts +204 -0
  166. package/src/calls/__tests__/voice-control-protocol.test.ts +15 -0
  167. package/src/calls/__tests__/voice-escalation-judge.test.ts +209 -0
  168. package/src/calls/__tests__/voice-leg-coordinator.test.ts +168 -0
  169. package/src/{live-voice/__tests__/live-voice-metrics.test.ts → calls/__tests__/voice-metrics.test.ts} +32 -38
  170. package/src/calls/__tests__/voice-session-bridge.test.ts +576 -122
  171. package/src/calls/barge-in-guard.ts +5 -0
  172. package/src/calls/call-controller.ts +196 -1
  173. package/src/calls/call-domain.ts +4 -0
  174. package/src/calls/call-store.ts +87 -2
  175. package/src/calls/guardian-action-sweep.ts +22 -8
  176. package/src/calls/media-stream-audio-transcode.ts +1 -3
  177. package/src/calls/typesafe-noul.ts +156 -0
  178. package/src/calls/voice-control-protocol.ts +6 -0
  179. package/src/calls/voice-escalation-judge.ts +145 -0
  180. package/src/calls/voice-escalation-target.ts +10 -0
  181. package/src/calls/voice-leg-coordinator.ts +121 -3
  182. package/src/{live-voice/live-voice-metrics.ts → calls/voice-metrics.ts} +94 -87
  183. package/src/calls/voice-session-bridge.ts +256 -112
  184. package/src/calls/voice-triage-escalate.ts +7 -0
  185. package/src/cli/__tests__/roadmap-cli.test.ts +4 -41
  186. package/src/cli/commands/__tests__/notifications.test.ts +35 -10
  187. package/src/cli/commands/browser.help.ts +4 -5
  188. package/src/cli/commands/browser.ts +0 -1
  189. package/src/cli/commands/clients.help.ts +46 -0
  190. package/src/cli/commands/clients.ts +116 -0
  191. package/src/cli/commands/credentials.help.ts +6 -1
  192. package/src/cli/commands/notifications.help.ts +16 -2
  193. package/src/cli/commands/notifications.ts +30 -8
  194. package/src/cli/commands/roadmap.help.ts +18 -21
  195. package/src/cli/commands/roadmap.ts +4 -21
  196. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +5 -2
  197. package/src/cli/lib/bundled-marketplace.json +84 -34
  198. package/src/cli/lib/plugin-marketplace.ts +8 -0
  199. package/src/cli/lib/publish-plugin.ts +1 -2
  200. package/src/config/__tests__/default-provider.test.ts +3 -1
  201. package/src/config/__tests__/memory-retrospective-schema.test.ts +14 -1
  202. package/src/config/bundled-skills/computer-use/SKILL.md +1 -1
  203. package/src/config/bundled-skills/computer-use/TOOLS.json +89 -1
  204. package/src/config/bundled-skills/image-studio/SKILL.md +17 -2
  205. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +7 -11
  206. package/src/config/bundled-skills/sequences/TOOLS.json +1 -5
  207. package/src/config/call-site-defaults.ts +9 -0
  208. package/src/config/feature-flag-registry.json +16 -0
  209. package/src/config/profile-text-generation.test.ts +2 -2
  210. package/src/config/schemas/__tests__/tools.test.ts +31 -0
  211. package/src/config/schemas/call-site-catalog.ts +14 -0
  212. package/src/config/schemas/channels.ts +0 -18
  213. package/src/config/schemas/llm.ts +3 -1
  214. package/src/config/schemas/mcp.ts +2 -1
  215. package/src/config/schemas/memory-retrospective.ts +11 -2
  216. package/src/config/schemas/services.ts +15 -6
  217. package/src/config/schemas/tools.ts +10 -0
  218. package/src/config/session-groups-gate.ts +21 -0
  219. package/src/config/skills.ts +2 -6
  220. package/src/conversations/__tests__/message-consolidation.test.ts +149 -0
  221. package/src/conversations/message-consolidation.ts +102 -5
  222. package/src/credential-execution/ces-connect.test.ts +69 -0
  223. package/src/credential-execution/ces-connect.ts +115 -0
  224. package/src/credential-execution/ces-runtime.ts +55 -93
  225. package/src/credential-execution/executable-discovery.ts +1 -11
  226. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +10 -1
  227. package/src/daemon/__tests__/inference-profile-notification.test.ts +2 -0
  228. package/src/daemon/browser-mode-session.test.ts +402 -0
  229. package/src/daemon/browser-mode-session.ts +186 -0
  230. package/src/daemon/computer-use-mode-session.test.ts +210 -0
  231. package/src/daemon/computer-use-mode-session.ts +105 -0
  232. package/src/daemon/config-watcher.ts +10 -0
  233. package/src/daemon/conversation-agent-loop-handlers.ts +58 -10
  234. package/src/daemon/conversation-agent-loop.ts +219 -25
  235. package/src/daemon/conversation-evictor.ts +5 -24
  236. package/src/daemon/conversation-history.ts +10 -0
  237. package/src/daemon/conversation-interrupt-repair.ts +4 -43
  238. package/src/daemon/conversation-interrupt.ts +7 -11
  239. package/src/daemon/conversation-messaging.ts +58 -5
  240. package/src/daemon/conversation-mode-session.test.ts +1667 -0
  241. package/src/daemon/conversation-mode-session.ts +1008 -0
  242. package/src/daemon/conversation-process.ts +5 -0
  243. package/src/daemon/conversation-runtime-assembly.ts +10 -20
  244. package/src/daemon/conversation-skill-tools.ts +1 -0
  245. package/src/daemon/conversation-store.ts +4 -4
  246. package/src/daemon/conversation-surface-snapshots.ts +135 -0
  247. package/src/daemon/conversation-surfaces.ts +113 -5
  248. package/src/daemon/conversation-tool-setup.ts +8 -3
  249. package/src/daemon/conversation.ts +143 -41
  250. package/src/daemon/handlers/config-model.ts +7 -5
  251. package/src/daemon/handlers/shared.ts +55 -16
  252. package/src/daemon/host-cu-proxy.ts +51 -22
  253. package/src/daemon/lifecycle.ts +30 -13
  254. package/src/daemon/message-protocol.ts +0 -1
  255. package/src/daemon/message-provenance.ts +28 -4
  256. package/src/daemon/mode-session-startup-recovery.ts +23 -0
  257. package/src/daemon/mode-session-tracking.ts +54 -0
  258. package/src/daemon/process-message.ts +13 -4
  259. package/src/daemon/virtual-desktop-context.ts +21 -0
  260. package/src/desktop/__tests__/fake-desktop.ts +1 -0
  261. package/src/desktop/desktop-accessibility-script.ts +88 -0
  262. package/src/desktop/desktop-accessibility.test.ts +160 -0
  263. package/src/desktop/desktop-accessibility.ts +169 -0
  264. package/src/desktop/desktop-automation-lease.test.ts +76 -68
  265. package/src/desktop/desktop-automation-lease.ts +41 -50
  266. package/src/desktop/desktop-browser-endpoint.ts +4 -0
  267. package/src/desktop/desktop-computer-use-routing.test.ts +330 -0
  268. package/src/desktop/desktop-computer-use.test.ts +449 -0
  269. package/src/desktop/desktop-computer-use.ts +502 -0
  270. package/src/desktop/desktop-dependencies.ts +47 -293
  271. package/src/desktop/desktop-display.ts +2 -2
  272. package/src/desktop/desktop-panel-config.test.ts +13 -3
  273. package/src/desktop/desktop-panel-config.ts +109 -15
  274. package/src/desktop/desktop-session-manager.test.ts +69 -21
  275. package/src/desktop/desktop-session-manager.ts +76 -22
  276. package/src/desktop/desktop-stream-bridge.test.ts +11 -122
  277. package/src/desktop/desktop-stream-bridge.ts +6 -8
  278. package/src/desktop/desktop-window-theme.ts +5 -25
  279. package/src/desktop/virtual-desktop-feature.ts +52 -2
  280. package/src/desktop/virtual-desktop-platform.test.ts +5 -9
  281. package/src/documents/document-store.ts +23 -0
  282. package/src/heartbeat/__tests__/heartbeat-service.test.ts +22 -26
  283. package/src/heartbeat/heartbeat-service.ts +5 -33
  284. package/src/ipc/__tests__/browser-ipc.test.ts +215 -6
  285. package/src/ipc/assistant-server.ts +2 -0
  286. package/src/ipc/routes/__tests__/channel-reply-ipc-routes.test.ts +111 -0
  287. package/src/ipc/routes/channel-reply-ipc-routes.ts +73 -0
  288. package/src/live-voice/__tests__/continuation-judge.test.ts +151 -0
  289. package/src/live-voice/__tests__/live-activity-reporter.test.ts +154 -15
  290. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +743 -31
  291. package/src/live-voice/__tests__/live-voice-look-follow-up.test.ts +191 -10
  292. package/src/live-voice/__tests__/live-voice-session-groups-gate.test.ts +87 -0
  293. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +5 -1
  294. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +41 -0
  295. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +1017 -9
  296. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +224 -1
  297. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +101 -0
  298. package/src/live-voice/__tests__/live-voice-tts.test.ts +44 -0
  299. package/src/live-voice/__tests__/live-voice-vad.test.ts +939 -75
  300. package/src/live-voice/__tests__/microphone-idle-gate.test.ts +87 -0
  301. package/src/live-voice/__tests__/session-controls.test.ts +23 -0
  302. package/src/live-voice/__tests__/task-outcomes.test.ts +117 -0
  303. package/src/live-voice/__tests__/voice-input-diagnostics.test.ts +117 -0
  304. package/src/live-voice/camera-mode-session.test.ts +265 -0
  305. package/src/live-voice/camera-mode-session.ts +277 -0
  306. package/src/live-voice/continuation-judge.ts +123 -0
  307. package/src/live-voice/live-activity-reporter.ts +115 -6
  308. package/src/live-voice/live-voice-manager.ts +36 -1
  309. package/src/live-voice/live-voice-photo.ts +88 -7
  310. package/src/live-voice/live-voice-session-manager.ts +15 -0
  311. package/src/live-voice/live-voice-session.ts +1443 -747
  312. package/src/live-voice/live-voice-tts.ts +22 -4
  313. package/src/live-voice/microphone-idle-gate.ts +56 -0
  314. package/src/live-voice/protocol.ts +171 -4
  315. package/src/live-voice/session-controls.ts +11 -3
  316. package/src/live-voice/task-outcomes.ts +131 -0
  317. package/src/live-voice/voice-input-diagnostics.ts +117 -0
  318. package/src/mcp/__tests__/manager-tool-caps.test.ts +63 -18
  319. package/src/mcp/__tests__/tool-caps.test.ts +28 -1
  320. package/src/mcp/manager.ts +3 -0
  321. package/src/mcp/tool-caps.ts +12 -4
  322. package/src/media/__tests__/image-models.test.ts +58 -0
  323. package/src/media/image-credentials.ts +5 -0
  324. package/src/media/image-models.ts +52 -0
  325. package/src/media/image-service.ts +8 -0
  326. package/src/media/openrouter-image-service.test.ts +133 -0
  327. package/src/media/openrouter-image-service.ts +153 -0
  328. package/src/media/types.ts +1 -1
  329. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +65 -11
  330. package/src/messaging/providers/discord/send.ts +4 -17
  331. package/src/messaging/providers/discord/transport.ts +2 -2
  332. package/src/messaging/providers/outlook/client.ts +16 -22
  333. package/src/messaging/providers/telegram-bot/send.test.ts +236 -1
  334. package/src/messaging/providers/telegram-bot/send.ts +10 -7
  335. package/src/messaging/providers/telegram-bot/transport.ts +3 -10
  336. package/src/messaging/providers/whatsapp/send.test.ts +169 -4
  337. package/src/monitoring/__tests__/control.test.ts +17 -5
  338. package/src/monitoring/control.ts +15 -5
  339. package/src/notifications/AGENTS.md +1 -1
  340. package/src/notifications/README.md +83 -99
  341. package/src/notifications/__tests__/broadcaster.test.ts +1 -1
  342. package/src/notifications/__tests__/channel-allowlist.test.ts +39 -0
  343. package/src/notifications/__tests__/copy-composer.test.ts +1 -1
  344. package/src/notifications/__tests__/decision-engine.test.ts +79 -2
  345. package/src/notifications/__tests__/deterministic-checks.test.ts +10 -10
  346. package/src/notifications/__tests__/home-feed-side-effect.test.ts +93 -4
  347. package/src/notifications/__tests__/notification-utils.test.ts +18 -0
  348. package/src/notifications/adapters/macos.ts +37 -46
  349. package/src/notifications/broadcaster.ts +5 -6
  350. package/src/notifications/channel-allowlist.ts +44 -0
  351. package/src/notifications/copy-composer.ts +0 -23
  352. package/src/notifications/decision-engine.ts +24 -14
  353. package/src/notifications/deterministic-checks.ts +1 -1
  354. package/src/notifications/emit-signal.ts +123 -37
  355. package/src/notifications/home-feed-side-effect.ts +9 -7
  356. package/src/notifications/notification-utils.ts +19 -0
  357. package/src/notifications/signal.ts +4 -19
  358. package/src/notifications/types.ts +15 -0
  359. package/src/oauth/connection-resolver.ts +1 -2
  360. package/src/oauth/oauth-store.ts +1 -3
  361. package/src/oauth/provider-serializer.ts +7 -0
  362. package/src/oauth/seed-providers.ts +30 -0
  363. package/src/onboarding/onboarding-events-store.ts +99 -12
  364. package/src/persistence/__tests__/client-connection-events-store.test.ts +322 -0
  365. package/src/persistence/client-connection-events-store.ts +377 -0
  366. package/src/persistence/conversation-crud.ts +55 -6
  367. package/src/persistence/conversation-mode-sessions.test.ts +424 -0
  368. package/src/persistence/conversation-mode-sessions.ts +460 -0
  369. package/src/persistence/conversation-queries.ts +6 -19
  370. package/src/persistence/llm-usage-store.ts +3 -9
  371. package/src/persistence/message-metadata.ts +25 -0
  372. package/src/persistence/migrations/381-create-conversation-mode-sessions.ts +41 -0
  373. package/src/persistence/migrations/382-create-client-connection-events.ts +40 -0
  374. package/src/persistence/migrations/__tests__/382-create-client-connection-events.test.ts +56 -0
  375. package/src/persistence/schema/conversations.ts +67 -0
  376. package/src/persistence/schema/infrastructure.ts +29 -0
  377. package/src/persistence/steps.ts +17 -0
  378. package/src/plugin-api/__tests__/plugin-name-env.test.ts +249 -0
  379. package/src/plugin-api/effective-context-window.ts +33 -0
  380. package/src/plugin-api/index.ts +24 -5
  381. package/src/plugin-api/plugin-name-env.ts +125 -0
  382. package/src/plugin-api/resolve-credential.ts +79 -20
  383. package/src/plugin-api/vision-support.test.ts +22 -0
  384. package/src/plugin-api/vision-support.ts +17 -5
  385. package/src/plugins/__tests__/active-plugin-env.test.ts +136 -0
  386. package/src/plugins/active-plugin-env.ts +51 -0
  387. package/src/plugins/defaults/index.ts +24 -0
  388. package/src/plugins/defaults/memory/__tests__/archive-removal-framing.test.ts +64 -0
  389. package/src/plugins/defaults/memory/__tests__/worker-control.test.ts +19 -6
  390. package/src/plugins/defaults/memory/context-search/agent-protocol.ts +9 -12
  391. package/src/plugins/defaults/memory/context-search/agent-runner.ts +78 -22
  392. package/src/plugins/defaults/memory/context-search/format.ts +51 -7
  393. package/src/plugins/defaults/memory/context-search/limits.ts +10 -12
  394. package/src/plugins/defaults/memory/context-search/recall-input.ts +38 -0
  395. package/src/plugins/defaults/memory/context-search/search.ts +8 -14
  396. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +1 -0
  397. package/src/plugins/defaults/memory/context-search/types.ts +1 -11
  398. package/src/plugins/defaults/memory/graph/__tests__/handle-remember-v2.test.ts +11 -4
  399. package/src/plugins/defaults/memory/graph/tool-handlers.ts +12 -21
  400. package/src/plugins/defaults/memory/graph/tools.ts +35 -64
  401. package/src/plugins/defaults/memory/src/memory-eval-routes.ts +2 -3
  402. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +5 -0
  403. package/src/plugins/defaults/memory/src/memory-item-routes.ts +2 -1
  404. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +2 -2
  405. package/src/plugins/defaults/memory/tools.test.ts +124 -9
  406. package/src/plugins/defaults/memory/tools.ts +38 -11
  407. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +165 -27
  408. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +626 -27
  409. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +46 -27
  410. package/src/plugins/defaults/memory/v3/orchestrate.ts +78 -38
  411. package/src/plugins/defaults/memory/v3/pool-log-store.ts +65 -79
  412. package/src/plugins/defaults/memory/v3/pool-select.test.ts +14 -5
  413. package/src/plugins/defaults/memory/v3/pool-select.ts +683 -44
  414. package/src/plugins/defaults/memory/worker-control.ts +15 -5
  415. package/src/plugins/defaults/task-progress/__tests__/registration.test.ts +70 -0
  416. package/src/plugins/defaults/task-progress/__tests__/task-progress-context.test.ts +380 -0
  417. package/src/plugins/defaults/task-progress/hooks/post-compact.ts +19 -0
  418. package/src/plugins/defaults/task-progress/hooks/user-prompt-submit.ts +24 -0
  419. package/src/plugins/defaults/task-progress/package.json +14 -0
  420. package/src/plugins/defaults/task-progress/src/apply-task-progress-context.ts +93 -0
  421. package/src/plugins/defaults/task-progress/src/format-task-progress-context.ts +67 -0
  422. package/src/providers/__tests__/context-overflow-error.test.ts +10 -0
  423. package/src/providers/__tests__/search-provider-catalog.test.ts +18 -0
  424. package/src/providers/anthropic/client.ts +10 -1
  425. package/src/providers/anthropic-gateway-shared.ts +6 -0
  426. package/src/providers/fetch-provider-catalog.ts +11 -0
  427. package/src/providers/gemini/client.ts +25 -8
  428. package/src/providers/inference/adapter-factory.ts +1 -1
  429. package/src/providers/jev/client.test.ts +10 -0
  430. package/src/providers/jev/client.ts +20 -4
  431. package/src/providers/model-catalog.ts +4 -3
  432. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +94 -1
  433. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +106 -1
  434. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +24 -0
  435. package/src/providers/openai/__tests__/google-thought-signature.test.ts +1 -2
  436. package/src/providers/openai/chat-completions-provider.ts +100 -29
  437. package/src/providers/openai/google-thought-signature.ts +0 -6
  438. package/src/providers/openai/responses-provider.ts +23 -1
  439. package/src/providers/ratelimit.ts +1 -3
  440. package/src/providers/search-provider-catalog.ts +24 -0
  441. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +24 -0
  442. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +7 -1
  443. package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +50 -1
  444. package/src/providers/speech-to-text/deepgram-flux-frames.ts +11 -0
  445. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +13 -0
  446. package/src/providers/types.ts +5 -0
  447. package/src/routes/control.ts +7 -5
  448. package/src/runtime/AGENTS.md +4 -2
  449. package/src/runtime/actor-trust-resolver.ts +3 -3
  450. package/src/runtime/agent-wake.ts +1 -1
  451. package/src/runtime/approval-message-composer.ts +3 -69
  452. package/src/runtime/assistant-event-hub.ts +73 -75
  453. package/src/runtime/assistant-event-publish-options.ts +7 -0
  454. package/src/runtime/assistant-event-targeting.ts +79 -0
  455. package/src/runtime/assistant-stream-state.ts +13 -100
  456. package/src/runtime/background-job-runner.ts +5 -5
  457. package/src/runtime/channel-retry-sweep.ts +1 -0
  458. package/src/runtime/guardian-action-message-composer.ts +8 -161
  459. package/src/runtime/guardian-reply-router.ts +28 -32
  460. package/src/runtime/http-types.ts +6 -0
  461. package/src/runtime/message-composer-types.ts +3 -61
  462. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +67 -1
  463. package/src/runtime/migrations/vbundle-builder.ts +24 -1
  464. package/src/runtime/migrations/vbundle-import-analyzer.ts +13 -4
  465. package/src/runtime/migrations/vbundle-import-policy.ts +10 -0
  466. package/src/runtime/migrations/vbundle-importer.ts +4 -1
  467. package/src/runtime/migrations/vbundle-streaming-importer.ts +4 -1
  468. package/src/runtime/migrations/vbundle-validator.ts +1 -0
  469. package/src/runtime/pending-interactions-mode-session.test.ts +62 -0
  470. package/src/runtime/pending-interactions.ts +29 -0
  471. package/src/runtime/routes/__tests__/client-history-routes.test.ts +104 -0
  472. package/src/runtime/routes/__tests__/image-generation-routes.test.ts +97 -0
  473. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +1 -1
  474. package/src/runtime/routes/__tests__/roadmap-routes.test.ts +29 -6
  475. package/src/runtime/routes/browser-context.ts +31 -11
  476. package/src/runtime/routes/browser-routes.ts +43 -4
  477. package/src/runtime/routes/btw-routes.ts +9 -4
  478. package/src/runtime/routes/client-routes.ts +108 -0
  479. package/src/runtime/routes/conversation-management-routes.ts +4 -1
  480. package/src/runtime/routes/conversation-routes.ts +113 -27
  481. package/src/runtime/routes/credential-prompt-routes.ts +4 -3
  482. package/src/runtime/routes/credential-routes.ts +1 -2
  483. package/src/runtime/routes/desktop-setup-routes.test.ts +9 -18
  484. package/src/runtime/routes/desktop-setup-routes.ts +4 -6
  485. package/src/runtime/routes/documents-routes.ts +51 -0
  486. package/src/runtime/routes/email-routes.ts +3 -7
  487. package/src/runtime/routes/events-routes.ts +75 -54
  488. package/src/runtime/routes/identity-routes.ts +1 -5
  489. package/src/runtime/routes/image-generation-routes.ts +8 -14
  490. package/src/runtime/routes/inbound-message-handler.ts +35 -38
  491. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.test.ts +168 -0
  492. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.ts +96 -0
  493. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +38 -9
  494. package/src/runtime/routes/inbound-stages/background-dispatch.ts +2 -5
  495. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +2 -0
  496. package/src/runtime/routes/llm-context-normalization.ts +1 -7
  497. package/src/runtime/routes/migration-routes.ts +73 -2
  498. package/src/runtime/routes/notification-routes.ts +10 -0
  499. package/src/runtime/routes/oauth-providers.ts +7 -0
  500. package/src/runtime/routes/plugins-routes.ts +6 -0
  501. package/src/runtime/routes/roadmap-routes.ts +47 -8
  502. package/src/runtime/routes/secret-routes.ts +1 -1
  503. package/src/runtime/routes/settings-routes.ts +10 -4
  504. package/src/runtime/routes/watcher-routes.ts +1 -3
  505. package/src/runtime/trust-class.test.ts +7 -6
  506. package/src/runtime/trust-class.ts +9 -17
  507. package/src/schedule/__tests__/worker-control.test.ts +36 -5
  508. package/src/schedule/worker-control.ts +15 -5
  509. package/src/security/secure-keys.ts +53 -103
  510. package/src/skills/clawhub.ts +3 -8
  511. package/src/skills/managed-store.ts +1 -5
  512. package/src/stt/types.ts +4 -0
  513. package/src/subagent/notify.ts +54 -34
  514. package/src/subagent/parent-notification.ts +6 -0
  515. package/src/telemetry/__tests__/phone-call-funnel.test.ts +72 -0
  516. package/src/telemetry/phone-call-funnel.ts +144 -0
  517. package/src/tools/__tests__/tool-schema-root-combinator-guard.test.ts +81 -0
  518. package/src/tools/ask-question/ask-question-tool.ts +6 -4
  519. package/src/tools/browser/browser-execution.ts +22 -59
  520. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +561 -39
  521. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +435 -39
  522. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +1 -8
  523. package/src/tools/client-os.ts +15 -0
  524. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -1
  525. package/src/tools/computer-use/target.ts +42 -0
  526. package/src/tools/document/document-tool.ts +13 -18
  527. package/src/tools/execution-target.ts +14 -16
  528. package/src/tools/host-terminal/host-shell.ts +4 -1
  529. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +250 -0
  530. package/src/tools/network/__tests__/web-search.test.ts +203 -0
  531. package/src/tools/network/web-fetch.ts +274 -7
  532. package/src/tools/network/web-search.ts +468 -6
  533. package/src/tools/permission-checker.ts +3 -3
  534. package/src/tools/policy-context.ts +3 -1
  535. package/src/tools/skills/load.ts +2 -1
  536. package/src/tools/skills/sandbox-runner.ts +11 -2
  537. package/src/tools/skills/skill-script-runner.ts +7 -0
  538. package/src/tools/skills/skill-tool-factory.ts +15 -1
  539. package/src/tools/terminal/__tests__/safe-env.test.ts +11 -0
  540. package/src/tools/terminal/safe-env.ts +4 -0
  541. package/src/tools/terminal/shell.ts +2 -0
  542. package/src/tools/tool-approval-handler.ts +5 -1
  543. package/src/tools/tool-types.ts +3 -3
  544. package/src/tools/types.ts +8 -2
  545. package/src/util/__tests__/worker-process-command.test.ts +5 -4
  546. package/src/util/abort-reasons.ts +18 -26
  547. package/src/util/canonicalize-identity.test.ts +21 -0
  548. package/src/util/canonicalize-identity.ts +16 -0
  549. package/src/util/errors.ts +13 -3
  550. package/src/util/provider-error-patterns.ts +3 -0
  551. package/src/util/worker-process.ts +77 -35
  552. package/src/workspace/migrations/157-add-desktop-files-launcher.ts +94 -0
  553. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +4 -10
  554. package/src/workspace/migrations/__tests__/157-add-desktop-files-launcher.test.ts +113 -0
  555. package/src/workspace/migrations/registry.ts +2 -0
  556. package/src/__tests__/computer-use-window-schema.test.ts +0 -24
  557. package/src/api/events/heartbeat-alert.ts +0 -20
  558. package/src/daemon/message-types/web-activity.ts +0 -65
  559. package/src/desktop/desktop-dependencies.test.ts +0 -131
  560. package/src/tools/computer-use/definitions.ts +0 -607
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Memory v3 — single pool selector.
3
3
  *
4
- * Runs a SINGLE forced-tool call over one unified candidate pool rendered in
4
+ * Runs a SINGLE selector call over one unified candidate pool rendered in
5
5
  * two segments that share one numbering:
6
6
  *
7
7
  * 1. STABLE PREFIX — the core+hot lane pages as FULL CARDS (head section +
@@ -52,11 +52,21 @@
52
52
  import type {
53
53
  ContentBlock,
54
54
  Message,
55
+ Provider,
56
+ ProviderResponse,
55
57
  ToolUseContent,
56
58
  } from "@vellumai/plugin-api";
57
- import { getConfiguredProvider, safeStringSlice } from "@vellumai/plugin-api";
59
+ import {
60
+ getConfiguredProvider,
61
+ getEffectiveContextWindow,
62
+ safeStringSlice,
63
+ } from "@vellumai/plugin-api";
58
64
  import { z } from "zod";
59
65
 
66
+ import {
67
+ estimatePromptTokensWithTools,
68
+ estimateTextTokens,
69
+ } from "../../../../context/token-estimator.js";
60
70
  import { classifyConversationError } from "../../../../daemon/conversation-error.js";
61
71
  import type { PendingConversationNotice } from "../../../../daemon/conversation-notices.js";
62
72
  import { redactLogString, truncate } from "../host-utils.js";
@@ -71,6 +81,7 @@ import { retryForResult } from "./llm-retry.js";
71
81
  import { findTerm } from "./section-needle.js";
72
82
  import { sectionBody } from "./sections.js";
73
83
  import {
84
+ type FinderLane,
74
85
  type MemoryRoutingTurn,
75
86
  type Section,
76
87
  sectionKey,
@@ -92,12 +103,16 @@ const log = getLogger("memory-v3-pool-select");
92
103
  */
93
104
  export class MemoryV3RetrievalUnavailableError extends Error {
94
105
  readonly conversationNotice?: PendingConversationNotice;
106
+ readonly pool?: SelectorPool;
107
+ readonly turn?: MemoryRoutingTurn;
95
108
 
96
109
  constructor(
97
110
  message: string,
98
111
  options?: {
99
112
  cause?: unknown;
100
113
  conversationNotice?: PendingConversationNotice;
114
+ pool?: SelectorPool;
115
+ turn?: MemoryRoutingTurn;
101
116
  },
102
117
  ) {
103
118
  super(
@@ -106,6 +121,8 @@ export class MemoryV3RetrievalUnavailableError extends Error {
106
121
  );
107
122
  this.name = "MemoryV3RetrievalUnavailableError";
108
123
  this.conversationNotice = options?.conversationNotice;
124
+ this.pool = options?.pool;
125
+ this.turn = options?.turn;
109
126
  }
110
127
  }
111
128
 
@@ -129,7 +146,7 @@ function providerBillingNoticeFromError(
129
146
  /** A dynamic-tail (finder) candidate: the slug plus the descriptor that
130
147
  * justifies it (a matched section's text for a needle/dense hit, or a
131
148
  * curated link description for an edge page), rendered as a one-line
132
- * snippet and prefixed with the surfacing lane when one is supplied. A
149
+ * snippet and prefixed with the surfacing lane. A
133
150
  * candidate that also carries its matched `section` and the query `terms`
134
151
  * that scored it (best first) renders a keyword-in-context snippet instead:
135
152
  * a window of the section body around the first of those terms that occurs
@@ -140,7 +157,7 @@ function providerBillingNoticeFromError(
140
157
  export interface PoolCandidate {
141
158
  slug: Slug;
142
159
  descriptor: string;
143
- lane?: string;
160
+ lane: FinderLane;
144
161
  section?: Section;
145
162
  terms?: string[];
146
163
  }
@@ -149,9 +166,12 @@ export interface PoolCandidate {
149
166
  * (`renderCard` output — head section + TOC). Cards must be byte-stable
150
167
  * across turns for the prefix to ride the provider KV cache, which is why
151
168
  * callers pre-render them at lane init rather than per turn. */
169
+ export type StableCandidateLane = "core" | "hot" | "fresh" | "always";
170
+
152
171
  export interface StableCandidate {
153
172
  slug: Slug;
154
173
  card: string;
174
+ lane: StableCandidateLane;
155
175
  }
156
176
 
157
177
  /**
@@ -170,6 +190,7 @@ export interface SelectorPool {
170
190
  /** Tool name forced via `tool_choice`. Shared constant so tests can match it. */
171
191
  const SELECT_PAGES_TOOL_NAME = "select_pages";
172
192
  const MEMORY_V3_SELECT_CALL_SITE = "memoryV3SelectL2" as const;
193
+ const SELECTOR_CONTEXT_BUDGET_RATIO = 0.8;
173
194
 
174
195
  /** Finder-line snippets are truncated to keep the dynamic tail compact. */
175
196
  const SNIPPET_MAX_CHARS = 300;
@@ -179,7 +200,8 @@ type PoolSelectorAttemptFailureReason =
179
200
  | "provider_error"
180
201
  | "missing_tool_use"
181
202
  | "unexpected_tool_name"
182
- | "schema_mismatch";
203
+ | "schema_mismatch"
204
+ | "unusable_answers";
183
205
 
184
206
  interface PoolSelectorAttemptFailure {
185
207
  attempt: number;
@@ -396,12 +418,8 @@ function renderCardSegment(stable: StableCandidate[]): string {
396
418
  }
397
419
 
398
420
  /** A finder line's lane tag: `(lane) `, or `(rare: word) ` for a rare-term
399
- * line, keyed on the one word it carries as its term; empty for a candidate
400
- * without a lane. */
421
+ * line, keyed on the one word it carries as its term. */
401
422
  function laneTag(candidate: PoolCandidate): string {
402
- if (candidate.lane === undefined) {
403
- return "";
404
- }
405
423
  const word = candidate.lane === "rare" ? candidate.terms?.[0] : undefined;
406
424
  return word === undefined
407
425
  ? `(${candidate.lane}) `
@@ -410,8 +428,8 @@ function laneTag(candidate: PoolCandidate): string {
410
428
 
411
429
  /**
412
430
  * One finder line as the selector sees it, minus its pool number: the lane
413
- * tag (omitted for a candidate without one, naming the keyed word for a
414
- * rare-term line, `(rare: turnip)`), the slug, and the snippet after a dash
431
+ * tag (naming the keyed word for a rare-term line, `(rare: turnip)`), the
432
+ * slug, and the snippet after a dash
415
433
  * (no dash for a candidate with an empty descriptor). Shared with the pool
416
434
  * input capture (`pool-log-store.ts`), so the persisted text is exactly the
417
435
  * rendered line.
@@ -493,9 +511,563 @@ export function selectAllPoolCandidates(pool: SelectorPool): SelectedPage[] {
493
511
  );
494
512
  }
495
513
 
514
+ /**
515
+ * Inclusive noul threshold for TypeSafe pool selection. 0.5 is calibrated
516
+ * equal yes/no. This sits slightly below that so a plausible candidate is
517
+ * kept, matching the selector's recall-heavy rule.
518
+ */
519
+ export const TYPE_SAFE_POOL_KEEP_NOUL = 0.4;
520
+
521
+ /** Catalog id of the TypeSafe System One provider. */
522
+ const TYPE_SAFE_PROVIDER_ID = "typesafe";
523
+
524
+ type TypesafeNoulQuestion = {
525
+ type: "noul";
526
+ instructions: string;
527
+ criteria: { yes: string; no: string };
528
+ };
529
+
530
+ function isRecord(value: unknown): value is Record<string, unknown> {
531
+ return typeof value === "object" && value !== null && !Array.isArray(value);
532
+ }
533
+
534
+ function noulFromAnswer(answer: unknown): number | undefined {
535
+ if (typeof answer === "number" && Number.isFinite(answer)) {
536
+ return answer;
537
+ }
538
+ if (
539
+ isRecord(answer) &&
540
+ typeof answer.noul === "number" &&
541
+ Number.isFinite(answer.noul)
542
+ ) {
543
+ return answer.noul;
544
+ }
545
+ return undefined;
546
+ }
547
+
548
+ function typesafeCandidateEntries(
549
+ pool: SelectorPool,
550
+ ): Record<string, { slug: Slug; text: string }> {
551
+ const entries: Record<string, { slug: Slug; text: string }> = {};
552
+ pool.stable.forEach((candidate, index) => {
553
+ entries[String(index + 1)] = {
554
+ slug: candidate.slug,
555
+ text: candidate.card,
556
+ };
557
+ });
558
+ pool.finder.forEach((candidate, index) => {
559
+ entries[String(pool.stable.length + index + 1)] = {
560
+ slug: candidate.slug,
561
+ text: renderFinderLine(candidate),
562
+ };
563
+ });
564
+ return entries;
565
+ }
566
+
567
+ function typesafeKeepQuestion(id: string): TypesafeNoulQuestion {
568
+ return {
569
+ type: "noul",
570
+ instructions:
571
+ `Would the upcoming assistant reply draw on \`candidates.${id}\`? ` +
572
+ "Lean inclusive. Facts, current task and event state, register, " +
573
+ "framing, calibration, and relationship texture all count. True when " +
574
+ "the candidate could plausibly inform the reply.",
575
+ criteria: {
576
+ yes: "The reply would draw on this candidate.",
577
+ no: "The reply would not draw on this candidate.",
578
+ },
579
+ };
580
+ }
581
+
582
+ function buildTypesafeSelectorPayload(
583
+ pool: SelectorPool,
584
+ turn: MemoryRoutingTurn,
585
+ systemPrompt: string,
586
+ ): {
587
+ state: Record<string, unknown>;
588
+ questions: Record<string, TypesafeNoulQuestion>;
589
+ } {
590
+ const candidates = typesafeCandidateEntries(pool);
591
+ const questions: Record<string, TypesafeNoulQuestion> = {};
592
+ for (const id of Object.keys(candidates)) {
593
+ questions[id] = typesafeKeepQuestion(id);
594
+ }
595
+ return {
596
+ state: {
597
+ selector_instructions: systemPrompt,
598
+ candidates,
599
+ ...(turn.situationalContext
600
+ ? { situation: turn.situationalContext }
601
+ : {}),
602
+ recent_context: turn.recentContext,
603
+ current_message: turn.currentMessage,
604
+ },
605
+ questions,
606
+ };
607
+ }
608
+
609
+ function buildGenericSelectorMessage(
610
+ pool: SelectorPool,
611
+ turn: MemoryRoutingTurn,
612
+ ): Message {
613
+ const content: ContentBlock[] = [];
614
+ if (pool.stable.length > 0) {
615
+ content.push(cachedTextBlock(renderCardSegment(pool.stable)));
616
+ }
617
+ const tailParts: string[] = [];
618
+ if (pool.finder.length > 0) {
619
+ tailParts.push(renderFinderSegment(pool.finder, pool.stable.length));
620
+ }
621
+ if (turn.situationalContext) {
622
+ tailParts.push(`<situation>${turn.situationalContext}</situation>`);
623
+ }
624
+ tailParts.push(`<recent_context>${turn.recentContext}</recent_context>`);
625
+ tailParts.push(`<current_message>${turn.currentMessage}</current_message>`);
626
+ content.push({ type: "text", text: tailParts.join("\n") });
627
+ return { role: "user", content };
628
+ }
629
+
630
+ const STABLE_CANDIDATE_PRIORITY: Record<StableCandidateLane, number> = {
631
+ core: 0,
632
+ hot: 5,
633
+ fresh: 6,
634
+ always: 7,
635
+ };
636
+
637
+ const FINDER_CANDIDATE_PRIORITY: Record<FinderLane, number> = {
638
+ needle: 1,
639
+ dense: 2,
640
+ reply: 3,
641
+ span: 4,
642
+ entity: 8,
643
+ rare: 9,
644
+ edge: 10,
645
+ learned: 11,
646
+ };
647
+
648
+ function highestPriorityCandidates(
649
+ pool: SelectorPool,
650
+ retainedCount: number,
651
+ ): SelectorPool {
652
+ const ranked = [
653
+ ...pool.stable.map((candidate, index) => ({
654
+ segment: "stable" as const,
655
+ index,
656
+ poolIndex: index,
657
+ priority: STABLE_CANDIDATE_PRIORITY[candidate.lane],
658
+ })),
659
+ ...pool.finder.map((candidate, index) => ({
660
+ segment: "finder" as const,
661
+ index,
662
+ poolIndex: pool.stable.length + index,
663
+ priority: FINDER_CANDIDATE_PRIORITY[candidate.lane],
664
+ })),
665
+ ]
666
+ .sort(
667
+ (left, right) =>
668
+ left.priority - right.priority || left.poolIndex - right.poolIndex,
669
+ )
670
+ .slice(0, retainedCount);
671
+ const retainedStable = new Set(
672
+ ranked
673
+ .filter((candidate) => candidate.segment === "stable")
674
+ .map((candidate) => candidate.index),
675
+ );
676
+ const retainedFinder = new Set(
677
+ ranked
678
+ .filter((candidate) => candidate.segment === "finder")
679
+ .map((candidate) => candidate.index),
680
+ );
681
+ return {
682
+ stable: pool.stable.filter((_, index) => retainedStable.has(index)),
683
+ finder: pool.finder.filter((_, index) => retainedFinder.has(index)),
684
+ };
685
+ }
686
+
687
+ function withoutTrailingCurrentMessage(
688
+ turn: MemoryRoutingTurn,
689
+ ): MemoryRoutingTurn {
690
+ if (turn.recentContext === turn.currentMessage) {
691
+ return { ...turn, recentContext: "" };
692
+ }
693
+ const duplicateSuffix = `\n${turn.currentMessage}`;
694
+ if (
695
+ turn.currentMessage.length === 0 ||
696
+ !turn.recentContext.endsWith(duplicateSuffix)
697
+ ) {
698
+ return turn;
699
+ }
700
+ return {
701
+ ...turn,
702
+ recentContext: turn.recentContext.slice(0, -duplicateSuffix.length),
703
+ };
704
+ }
705
+
706
+ function latestTurnContextSuffix(
707
+ turn: MemoryRoutingTurn,
708
+ retainedChars: number,
709
+ ): MemoryRoutingTurn {
710
+ let remaining = retainedChars;
711
+ const takeSuffix = (value: string): string => {
712
+ const count = Math.min(remaining, value.length);
713
+ remaining -= count;
714
+ return safeStringSlice(value, value.length - count);
715
+ };
716
+ const currentMessage = takeSuffix(turn.currentMessage);
717
+ const recentContext = takeSuffix(turn.recentContext);
718
+ const situationalContext = turn.situationalContext
719
+ ? takeSuffix(turn.situationalContext) || undefined
720
+ : undefined;
721
+ return {
722
+ ...turn,
723
+ currentMessage,
724
+ recentContext,
725
+ situationalContext,
726
+ };
727
+ }
728
+
729
+ function turnContextChars(turn: MemoryRoutingTurn): number {
730
+ return (
731
+ turn.currentMessage.length +
732
+ turn.recentContext.length +
733
+ (turn.situationalContext?.length ?? 0)
734
+ );
735
+ }
736
+
737
+ function estimateSelectorInputTokens({
738
+ pool,
739
+ turn,
740
+ systemPrompt,
741
+ provider,
742
+ model,
743
+ }: {
744
+ pool: SelectorPool;
745
+ turn: MemoryRoutingTurn;
746
+ systemPrompt: string;
747
+ provider: Provider;
748
+ model: string;
749
+ }): number {
750
+ if (provider.name === TYPE_SAFE_PROVIDER_ID) {
751
+ const payload = buildTypesafeSelectorPayload(pool, turn, systemPrompt);
752
+ return estimateTextTokens(
753
+ JSON.stringify({
754
+ state: payload.state,
755
+ model,
756
+ questions: payload.questions,
757
+ }),
758
+ );
759
+ }
760
+ return estimatePromptTokensWithTools(
761
+ [buildGenericSelectorMessage(pool, turn)],
762
+ systemPrompt,
763
+ [SELECT_PAGES_TOOL],
764
+ provider.tokenEstimationProvider ?? provider.name,
765
+ );
766
+ }
767
+
768
+ interface SelectorPoolBudget {
769
+ pool: SelectorPool;
770
+ turn: MemoryRoutingTurn;
771
+ budgetTokens: number;
772
+ estimatedInputTokens: number;
773
+ originalEstimatedInputTokens: number;
774
+ }
775
+
776
+ function budgetSelectorPool({
777
+ pool,
778
+ turn,
779
+ systemPrompt,
780
+ provider,
781
+ model,
782
+ maxInputTokens,
783
+ }: {
784
+ pool: SelectorPool;
785
+ turn: MemoryRoutingTurn;
786
+ systemPrompt: string;
787
+ provider: Provider;
788
+ model: string;
789
+ maxInputTokens: number;
790
+ }): SelectorPoolBudget {
791
+ const budgetTokens = Math.max(
792
+ 1,
793
+ Math.floor(maxInputTokens * SELECTOR_CONTEXT_BUDGET_RATIO),
794
+ );
795
+ const estimate = (
796
+ candidatePool: SelectorPool,
797
+ candidateTurn: MemoryRoutingTurn,
798
+ ): number =>
799
+ estimateSelectorInputTokens({
800
+ pool: candidatePool,
801
+ turn: candidateTurn,
802
+ systemPrompt,
803
+ provider,
804
+ model,
805
+ });
806
+ const originalEstimatedInputTokens = estimate(pool, turn);
807
+ if (originalEstimatedInputTokens <= budgetTokens) {
808
+ return {
809
+ pool,
810
+ turn,
811
+ budgetTokens,
812
+ estimatedInputTokens: originalEstimatedInputTokens,
813
+ originalEstimatedInputTokens,
814
+ };
815
+ }
816
+
817
+ const deduplicatedTurn = withoutTrailingCurrentMessage(turn);
818
+ const deduplicatedEstimatedInputTokens = estimate(pool, deduplicatedTurn);
819
+ if (deduplicatedEstimatedInputTokens <= budgetTokens) {
820
+ return {
821
+ pool,
822
+ turn: deduplicatedTurn,
823
+ budgetTokens,
824
+ estimatedInputTokens: deduplicatedEstimatedInputTokens,
825
+ originalEstimatedInputTokens,
826
+ };
827
+ }
828
+
829
+ const minimumTurn = latestTurnContextSuffix(
830
+ deduplicatedTurn,
831
+ deduplicatedTurn.currentMessage.length,
832
+ );
833
+ const fittingPool: SelectorPool = {
834
+ stable: pool.stable.filter(
835
+ (candidate) =>
836
+ estimate({ stable: [candidate], finder: [] }, minimumTurn) <=
837
+ budgetTokens,
838
+ ),
839
+ finder: pool.finder.filter(
840
+ (candidate) =>
841
+ estimate({ stable: [], finder: [candidate] }, minimumTurn) <=
842
+ budgetTokens,
843
+ ),
844
+ };
845
+ const candidateCount = fittingPool.stable.length + fittingPool.finder.length;
846
+ if (candidateCount === 0) {
847
+ const emptyPool: SelectorPool = { stable: [], finder: [] };
848
+ return {
849
+ pool: emptyPool,
850
+ turn: minimumTurn,
851
+ budgetTokens,
852
+ estimatedInputTokens: estimate(emptyPool, minimumTurn),
853
+ originalEstimatedInputTokens,
854
+ };
855
+ }
856
+
857
+ const preferredCandidatePool = highestPriorityCandidates(fittingPool, 1);
858
+ let budgetedTurn = deduplicatedTurn;
859
+ if (estimate(preferredCandidatePool, budgetedTurn) > budgetTokens) {
860
+ let low = deduplicatedTurn.currentMessage.length;
861
+ let high = turnContextChars(deduplicatedTurn);
862
+ while (low < high) {
863
+ const retainedChars = Math.ceil((low + high) / 2);
864
+ const candidateTurn = latestTurnContextSuffix(
865
+ deduplicatedTurn,
866
+ retainedChars,
867
+ );
868
+ if (estimate(preferredCandidatePool, candidateTurn) <= budgetTokens) {
869
+ low = retainedChars;
870
+ } else {
871
+ high = retainedChars - 1;
872
+ }
873
+ }
874
+ budgetedTurn = latestTurnContextSuffix(deduplicatedTurn, low);
875
+ }
876
+
877
+ let low = 1;
878
+ let high = candidateCount;
879
+ while (low < high) {
880
+ const retainedCount = Math.ceil((low + high) / 2);
881
+ const candidatePool = highestPriorityCandidates(fittingPool, retainedCount);
882
+ if (estimate(candidatePool, budgetedTurn) <= budgetTokens) {
883
+ low = retainedCount;
884
+ } else {
885
+ high = retainedCount - 1;
886
+ }
887
+ }
888
+
889
+ const budgetedPool = highestPriorityCandidates(fittingPool, low);
890
+ return {
891
+ pool: budgetedPool,
892
+ turn: budgetedTurn,
893
+ budgetTokens,
894
+ estimatedInputTokens: estimate(budgetedPool, budgetedTurn),
895
+ originalEstimatedInputTokens,
896
+ };
897
+ }
898
+
899
+ function answersFromSelectorResponse(
900
+ response: ProviderResponse,
901
+ ): Record<string, unknown> | null {
902
+ const raw = response.rawResponse;
903
+ if (isRecord(raw) && isRecord(raw.answers)) {
904
+ return raw.answers;
905
+ }
906
+ const textBlock = response.content.find((block) => block.type === "text");
907
+ if (!textBlock || textBlock.type !== "text") {
908
+ return null;
909
+ }
910
+ try {
911
+ const parsed: unknown = JSON.parse(textBlock.text);
912
+ return isRecord(parsed) ? parsed : null;
913
+ } catch {
914
+ return null;
915
+ }
916
+ }
917
+
918
+ async function selectPoolWithTypesafe(
919
+ pool: SelectorPool,
920
+ turn: MemoryRoutingTurn,
921
+ ordered: PoolLine[],
922
+ systemPrompt: string,
923
+ provider: Provider,
924
+ ): Promise<PoolSelection> {
925
+ const payload = buildTypesafeSelectorPayload(pool, turn, systemPrompt);
926
+ const userMsg: Message = {
927
+ role: "user",
928
+ content: [{ type: "text", text: JSON.stringify(payload) }],
929
+ };
930
+
931
+ const failures: PoolSelectorAttemptFailure[] = [];
932
+ let attempt = 0;
933
+ const recordFailure = (
934
+ failure: Omit<
935
+ PoolSelectorAttemptFailure,
936
+ | "callSite"
937
+ | "providerName"
938
+ | "candidateCount"
939
+ | "stableCount"
940
+ | "finderCount"
941
+ >,
942
+ ): void => {
943
+ const diagnostic: PoolSelectorAttemptFailure = {
944
+ ...failure,
945
+ callSite: MEMORY_V3_SELECT_CALL_SITE,
946
+ providerName: provider.name,
947
+ candidateCount: ordered.length,
948
+ stableCount: pool.stable.length,
949
+ finderCount: pool.finder.length,
950
+ };
951
+ failures.push(diagnostic);
952
+ log.warn(diagnostic, "pool selector attempt failed");
953
+ };
954
+
955
+ let lastError: unknown = null;
956
+ const parsed = await retryForResult(async () => {
957
+ attempt += 1;
958
+ let response: Awaited<ReturnType<typeof provider.sendMessage>>;
959
+ try {
960
+ response = await provider.sendMessage([userMsg], {
961
+ config: {
962
+ callSite: MEMORY_V3_SELECT_CALL_SITE,
963
+ conversationId: turn.conversationId,
964
+ selectionSeed: turn.conversationId,
965
+ disableTurnStartCache: true,
966
+ },
967
+ });
968
+ lastError = null;
969
+ } catch (error) {
970
+ lastError = error;
971
+ recordFailure({
972
+ attempt,
973
+ reason: "provider_error",
974
+ error: summarizeError(error),
975
+ });
976
+ throw error;
977
+ }
978
+ const answers = answersFromSelectorResponse(response);
979
+ if (!answers) {
980
+ recordFailure({
981
+ attempt,
982
+ reason: "unusable_answers",
983
+ response: summarizeResponse(response),
984
+ });
985
+ return null;
986
+ }
987
+ const picked: number[] = [];
988
+ let parsedCount = 0;
989
+ for (let index = 0; index < ordered.length; index++) {
990
+ const noul = noulFromAnswer(answers[String(index + 1)]);
991
+ if (noul === undefined) {
992
+ continue;
993
+ }
994
+ parsedCount += 1;
995
+ if (noul >= TYPE_SAFE_POOL_KEEP_NOUL) {
996
+ picked.push(index);
997
+ }
998
+ }
999
+ if (parsedCount === 0) {
1000
+ recordFailure({
1001
+ attempt,
1002
+ reason: "unusable_answers",
1003
+ response: summarizeResponse(response),
1004
+ });
1005
+ return null;
1006
+ }
1007
+ return {
1008
+ pages: mergeSelectedLines(ordered, picked),
1009
+ keptAll: false,
1010
+ pool,
1011
+ turn,
1012
+ };
1013
+ });
1014
+
1015
+ if (parsed === null) {
1016
+ if (lastError !== null) {
1017
+ const detail =
1018
+ lastError instanceof Error ? lastError.message : String(lastError);
1019
+ const redactedDetail = truncate(
1020
+ redactLogString(detail),
1021
+ ERROR_MESSAGE_MAX_CHARS,
1022
+ );
1023
+ log.warn(
1024
+ {
1025
+ candidateCount: ordered.length,
1026
+ stableCount: pool.stable.length,
1027
+ finderCount: pool.finder.length,
1028
+ callSite: MEMORY_V3_SELECT_CALL_SITE,
1029
+ providerName: provider.name,
1030
+ failures,
1031
+ },
1032
+ "pool selector provider call failed after retries",
1033
+ );
1034
+ throw new MemoryV3RetrievalUnavailableError(
1035
+ `memory-v3 pool selector provider call failed after retries: ${redactedDetail}`,
1036
+ {
1037
+ cause: lastError,
1038
+ conversationNotice: providerBillingNoticeFromError(lastError),
1039
+ pool,
1040
+ turn,
1041
+ },
1042
+ );
1043
+ }
1044
+ log.warn(
1045
+ {
1046
+ candidateCount: ordered.length,
1047
+ stableCount: pool.stable.length,
1048
+ finderCount: pool.finder.length,
1049
+ callSite: MEMORY_V3_SELECT_CALL_SITE,
1050
+ providerName: provider.name,
1051
+ failures,
1052
+ },
1053
+ "pool selector returned no usable TypeSafe answers after retries",
1054
+ );
1055
+ throw new MemoryV3RetrievalUnavailableError(
1056
+ "memory-v3 pool selector returned no usable selection after retries",
1057
+ { pool, turn },
1058
+ );
1059
+ }
1060
+
1061
+ return parsed;
1062
+ }
1063
+
496
1064
  /** A selection plus whether it came from the recall-safe keep-all fallback. */
497
1065
  export interface PoolSelection {
498
1066
  pages: SelectedPage[];
1067
+ /** The exact candidate pool rendered for the selector request. */
1068
+ pool: SelectorPool;
1069
+ /** The exact context strings rendered for the selector request. */
1070
+ turn: MemoryRoutingTurn;
499
1071
  /** True only when the model OMITTED `ids` and every candidate was kept as the
500
1072
  * recall-safe fallback — NOT when it explicitly selected a large set. The two
501
1073
  * produce the same pages but mean different things (the model gave up judging
@@ -504,14 +1076,16 @@ export interface PoolSelection {
504
1076
  }
505
1077
 
506
1078
  /**
507
- * Run the single forced-tool selector over the unified candidate pool. Returns
1079
+ * Run the single selector over the unified candidate pool. Returns
508
1080
  * the pages to inject, merged per slug (a page selected as a card and on
509
1081
  * finder lines yields one entry carrying every selected section), plus a
510
1082
  * `keptAll` flag marking the recall-safe fallback.
511
1083
  *
512
- * An omitted `ids` keeps ALL candidates (the recall-safe "all of these are
513
- * relevant" signal, `keptAll: true`); an explicit `[]` keeps none; an
514
- * infrastructure failure (after a short re-prompt retry) throws
1084
+ * On a chat model, an omitted `ids` keeps ALL candidates (the recall-safe
1085
+ * "all of these are relevant" signal, `keptAll: true`); an explicit `[]`
1086
+ * keeps none. TypeSafe answers one noul per candidate and never omits ids,
1087
+ * so `keptAll` is always false on that path. An infrastructure failure
1088
+ * (after a short re-prompt retry) throws
515
1089
  * {@link MemoryV3RetrievalUnavailableError}, and the orchestrator keeps the
516
1090
  * stable prefix unjudged in its place.
517
1091
  *
@@ -524,17 +1098,19 @@ export async function selectPool(
524
1098
  turn: MemoryRoutingTurn,
525
1099
  systemPrompt: string = SYSTEM_PROMPT,
526
1100
  ): Promise<PoolSelection> {
527
- const ordered = orderedLines(pool);
528
- if (ordered.length === 0) {
529
- return { pages: [], keptAll: false };
1101
+ const originalCandidateCount = pool.stable.length + pool.finder.length;
1102
+ if (originalCandidateCount === 0) {
1103
+ return { pages: [], keptAll: false, pool, turn };
530
1104
  }
531
1105
 
532
- const provider = await getConfiguredProvider(MEMORY_V3_SELECT_CALL_SITE);
1106
+ const provider = await getConfiguredProvider(MEMORY_V3_SELECT_CALL_SITE, {
1107
+ selectionSeed: turn.conversationId,
1108
+ });
533
1109
  if (!provider) {
534
1110
  log.warn(
535
1111
  {
536
1112
  callSite: MEMORY_V3_SELECT_CALL_SITE,
537
- candidateCount: ordered.length,
1113
+ candidateCount: originalCandidateCount,
538
1114
  stableCount: pool.stable.length,
539
1115
  finderCount: pool.finder.length,
540
1116
  },
@@ -542,28 +1118,77 @@ export async function selectPool(
542
1118
  );
543
1119
  throw new MemoryV3RetrievalUnavailableError(
544
1120
  "memory-v3 pool selector provider unavailable",
1121
+ { pool, turn },
545
1122
  );
546
1123
  }
547
1124
 
548
- // Two content blocks: the stable prefix (cards) carries the cache
549
- // breakpoint; the dynamic tail (finder lines + per-turn context) does not.
550
- // See the module doc for the cache contract.
551
- const content: ContentBlock[] = [];
552
- if (pool.stable.length > 0) {
553
- content.push(cachedTextBlock(renderCardSegment(pool.stable)));
1125
+ const effectiveContextWindow = getEffectiveContextWindow(
1126
+ MEMORY_V3_SELECT_CALL_SITE,
1127
+ { selectionSeed: turn.conversationId },
1128
+ );
1129
+ const budget = budgetSelectorPool({
1130
+ pool,
1131
+ turn,
1132
+ systemPrompt,
1133
+ provider,
1134
+ model: effectiveContextWindow.model,
1135
+ maxInputTokens: effectiveContextWindow.maxInputTokens,
1136
+ });
1137
+ const budgetedPool = budget.pool;
1138
+ const budgetedTurn = budget.turn;
1139
+ const ordered = orderedLines(budgetedPool);
1140
+ if (
1141
+ ordered.length < originalCandidateCount ||
1142
+ turnContextChars(budgetedTurn) < turnContextChars(turn)
1143
+ ) {
1144
+ log.info(
1145
+ {
1146
+ callSite: MEMORY_V3_SELECT_CALL_SITE,
1147
+ providerName: provider.name,
1148
+ model: effectiveContextWindow.model,
1149
+ maxInputTokens: effectiveContextWindow.maxInputTokens,
1150
+ budgetRatio: SELECTOR_CONTEXT_BUDGET_RATIO,
1151
+ budgetTokens: budget.budgetTokens,
1152
+ originalEstimatedInputTokens: budget.originalEstimatedInputTokens,
1153
+ estimatedInputTokens: budget.estimatedInputTokens,
1154
+ originalStableCount: pool.stable.length,
1155
+ retainedStableCount: budgetedPool.stable.length,
1156
+ originalFinderCount: pool.finder.length,
1157
+ retainedFinderCount: budgetedPool.finder.length,
1158
+ originalTurnContextChars: turnContextChars(turn),
1159
+ retainedTurnContextChars: turnContextChars(budgetedTurn),
1160
+ },
1161
+ "trimmed pool selector input to the context budget",
1162
+ );
554
1163
  }
555
- const tailParts: string[] = [];
556
- if (pool.finder.length > 0) {
557
- tailParts.push(renderFinderSegment(pool.finder, pool.stable.length));
1164
+ if (ordered.length === 0) {
1165
+ log.warn(
1166
+ {
1167
+ callSite: MEMORY_V3_SELECT_CALL_SITE,
1168
+ providerName: provider.name,
1169
+ model: effectiveContextWindow.model,
1170
+ maxInputTokens: effectiveContextWindow.maxInputTokens,
1171
+ candidateCount: originalCandidateCount,
1172
+ },
1173
+ "no pool selector candidate fits the context budget",
1174
+ );
1175
+ throw new MemoryV3RetrievalUnavailableError(
1176
+ "memory-v3 pool selector has no candidate that fits the context budget",
1177
+ { pool: budgetedPool, turn: budgetedTurn },
1178
+ );
558
1179
  }
559
- if (turn.situationalContext) {
560
- tailParts.push(`<situation>${turn.situationalContext}</situation>`);
1180
+
1181
+ if (provider.name === TYPE_SAFE_PROVIDER_ID) {
1182
+ return selectPoolWithTypesafe(
1183
+ budgetedPool,
1184
+ budgetedTurn,
1185
+ ordered,
1186
+ systemPrompt,
1187
+ provider,
1188
+ );
561
1189
  }
562
- tailParts.push(`<recent_context>${turn.recentContext}</recent_context>`);
563
- tailParts.push(`<current_message>${turn.currentMessage}</current_message>`);
564
- content.push({ type: "text", text: tailParts.join("\n") });
565
1190
 
566
- const userMsg: Message = { role: "user", content };
1191
+ const userMsg = buildGenericSelectorMessage(budgetedPool, budgetedTurn);
567
1192
  const failures: PoolSelectorAttemptFailure[] = [];
568
1193
  let attempt = 0;
569
1194
  const recordFailure = (
@@ -581,8 +1206,8 @@ export async function selectPool(
581
1206
  callSite: MEMORY_V3_SELECT_CALL_SITE,
582
1207
  providerName: provider.name,
583
1208
  candidateCount: ordered.length,
584
- stableCount: pool.stable.length,
585
- finderCount: pool.finder.length,
1209
+ stableCount: budgetedPool.stable.length,
1210
+ finderCount: budgetedPool.finder.length,
586
1211
  };
587
1212
  failures.push(diagnostic);
588
1213
  log.warn(diagnostic, "pool selector attempt failed");
@@ -609,6 +1234,7 @@ export async function selectPool(
609
1234
  config: {
610
1235
  callSite: MEMORY_V3_SELECT_CALL_SITE,
611
1236
  conversationId: turn.conversationId,
1237
+ selectionSeed: turn.conversationId,
612
1238
  tool_choice: { type: "tool" as const, name: SELECT_PAGES_TOOL_NAME },
613
1239
  // The last block of this one-shot message varies every turn; the
614
1240
  // provider's auto-applied turn-start breakpoint would land on it and
@@ -675,8 +1301,8 @@ export async function selectPool(
675
1301
  log.warn(
676
1302
  {
677
1303
  candidateCount: ordered.length,
678
- stableCount: pool.stable.length,
679
- finderCount: pool.finder.length,
1304
+ stableCount: budgetedPool.stable.length,
1305
+ finderCount: budgetedPool.finder.length,
680
1306
  callSite: MEMORY_V3_SELECT_CALL_SITE,
681
1307
  providerName: provider.name,
682
1308
  failures,
@@ -688,14 +1314,16 @@ export async function selectPool(
688
1314
  {
689
1315
  cause: lastError,
690
1316
  conversationNotice: providerBillingNoticeFromError(lastError),
1317
+ pool: budgetedPool,
1318
+ turn: budgetedTurn,
691
1319
  },
692
1320
  );
693
1321
  }
694
1322
  log.warn(
695
1323
  {
696
1324
  candidateCount: ordered.length,
697
- stableCount: pool.stable.length,
698
- finderCount: pool.finder.length,
1325
+ stableCount: budgetedPool.stable.length,
1326
+ finderCount: budgetedPool.finder.length,
699
1327
  callSite: MEMORY_V3_SELECT_CALL_SITE,
700
1328
  providerName: provider.name,
701
1329
  failures,
@@ -704,12 +1332,18 @@ export async function selectPool(
704
1332
  );
705
1333
  throw new MemoryV3RetrievalUnavailableError(
706
1334
  "memory-v3 pool selector returned no usable selection after retries",
1335
+ { pool: budgetedPool, turn: budgetedTurn },
707
1336
  );
708
1337
  }
709
1338
 
710
1339
  // Omitted `ids` is the recall-safe "keep all candidates" signal.
711
1340
  if (parsed.ids === undefined) {
712
- return { pages: selectAllPoolCandidates(pool), keptAll: true };
1341
+ return {
1342
+ pages: selectAllPoolCandidates(budgetedPool),
1343
+ keptAll: true,
1344
+ pool: budgetedPool,
1345
+ turn: budgetedTurn,
1346
+ };
713
1347
  }
714
1348
 
715
1349
  // Map 1-based IDs over the concatenated numbering, dropping out-of-range
@@ -717,5 +1351,10 @@ export async function selectPool(
717
1351
  const picked = parsed.ids
718
1352
  .filter((id) => id >= 1 && id <= ordered.length)
719
1353
  .map((id) => id - 1);
720
- return { pages: mergeSelectedLines(ordered, picked), keptAll: false };
1354
+ return {
1355
+ pages: mergeSelectedLines(ordered, picked),
1356
+ keptAll: false,
1357
+ pool: budgetedPool,
1358
+ turn: budgetedTurn,
1359
+ };
721
1360
  }