@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
@@ -30,7 +30,7 @@
30
30
  */
31
31
 
32
32
  import { createRequire } from "node:module";
33
- import { beforeEach, describe, expect, mock, test } from "bun:test";
33
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
34
34
 
35
35
  import type {
36
36
  Message,
@@ -39,6 +39,10 @@ import type {
39
39
  SendMessageOptions,
40
40
  } from "@vellumai/plugin-api";
41
41
 
42
+ import {
43
+ estimatePromptTokensWithTools,
44
+ estimateTextTokens,
45
+ } from "../../../../../context/token-estimator.js";
42
46
  import { OpenRouterProvider } from "../../../../../providers/openrouter/client.js";
43
47
  import { ProviderError } from "../../../../../util/errors.js";
44
48
  import { stripOrphanedSurrogates } from "../../../../../util/unicode.js";
@@ -51,6 +55,8 @@ import type { MemoryRoutingTurn, Section } from "../types.js";
51
55
  // ---------------------------------------------------------------------------
52
56
 
53
57
  let providerStub: Provider | null = null;
58
+ let selectorContextMockActive = false;
59
+ let selectorMaxInputTokens = 200_000;
54
60
  const registryReal = {
55
61
  ...(createRequire(import.meta.url)(
56
62
  "../../../../../providers/registry.js",
@@ -68,6 +74,16 @@ const realPluginApi = await import("@vellumai/plugin-api");
68
74
  mock.module("@vellumai/plugin-api", () => ({
69
75
  ...realPluginApi,
70
76
  getConfiguredProvider: async () => providerStub,
77
+ getEffectiveContextWindow: (
78
+ ...args: Parameters<typeof realPluginApi.getEffectiveContextWindow>
79
+ ) =>
80
+ selectorContextMockActive
81
+ ? {
82
+ provider: providerStub?.name ?? "stub",
83
+ model: providerStub?.defaultModel ?? "stub-model",
84
+ maxInputTokens: selectorMaxInputTokens,
85
+ }
86
+ : realPluginApi.getEffectiveContextWindow(...args),
71
87
  }));
72
88
 
73
89
  mock.module("../../../../../providers/registry.js", () => ({
@@ -78,15 +94,20 @@ mock.module("../../../../../providers/registry.js", () => ({
78
94
 
79
95
  mock.module("../../../../../util/logger.js", () => ({
80
96
  getLogger: () => ({
97
+ info: () => {},
81
98
  warn: (...args: unknown[]) => warnCalls.push({ args }),
82
99
  child: () => ({
100
+ info: () => {},
83
101
  warn: (...args: unknown[]) => warnCalls.push({ args }),
84
102
  }),
85
103
  }),
86
104
  }));
87
105
 
88
- const { selectPool, MemoryV3RetrievalUnavailableError } =
89
- await import("../pool-select.js");
106
+ const {
107
+ selectPool,
108
+ MemoryV3RetrievalUnavailableError,
109
+ TYPE_SAFE_POOL_KEEP_NOUL,
110
+ } = await import("../pool-select.js");
90
111
  type SelectorPool = Parameters<typeof selectPool>[0];
91
112
 
92
113
  // ---------------------------------------------------------------------------
@@ -166,16 +187,24 @@ const CARD_A =
166
187
  const CARD_B = "# memory/concepts/page-b.md\nlead for page b";
167
188
 
168
189
  /** Two stable-prefix cards (`[1] page-a`, `[2] page-b`) and two finder lines
169
- * (`[3] topic-x`, `[4] page-a` — a finder hit on a stable-prefix page). */
190
+ * (`[3] topic-x`, `[4] page-a`, with the latter also in the prefix). */
170
191
  function makePool(): SelectorPool {
171
192
  return {
172
193
  stable: [
173
- { slug: "page-a", card: CARD_A },
174
- { slug: "page-b", card: CARD_B },
194
+ { slug: "page-a", card: CARD_A, lane: "core" },
195
+ { slug: "page-b", card: CARD_B, lane: "hot" },
175
196
  ],
176
197
  finder: [
177
- { slug: "topic-x", descriptor: "section: about topic x" },
178
- { slug: "page-a", descriptor: "section: the alpha rollout plan" },
198
+ {
199
+ slug: "topic-x",
200
+ descriptor: "section: about topic x",
201
+ lane: "needle",
202
+ },
203
+ {
204
+ slug: "page-a",
205
+ descriptor: "section: the alpha rollout plan",
206
+ lane: "dense",
207
+ },
179
208
  ],
180
209
  };
181
210
  }
@@ -222,10 +251,16 @@ function warnPayloads(): Array<Record<string, unknown>> {
222
251
 
223
252
  beforeEach(() => {
224
253
  providerStub = null;
254
+ selectorContextMockActive = true;
255
+ selectorMaxInputTokens = 200_000;
225
256
  providerCalls.length = 0;
226
257
  warnCalls.length = 0;
227
258
  });
228
259
 
260
+ afterAll(() => {
261
+ selectorContextMockActive = false;
262
+ });
263
+
229
264
  // ---------------------------------------------------------------------------
230
265
  // selectPool — id mapping over the concatenated numbering.
231
266
  // ---------------------------------------------------------------------------
@@ -413,6 +448,94 @@ describe("selectPool — infrastructure failures throw", () => {
413
448
  ]);
414
449
  });
415
450
 
451
+ test("provider failure carries the exact budgeted pool and turn", async () => {
452
+ selectorMaxInputTokens = 8_000;
453
+ const pool: SelectorPool = {
454
+ stable: Array.from({ length: 20 }, (_, index) => ({
455
+ slug: `stable-${index}`,
456
+ card: `stable card ${index} ${"x".repeat(4_000)}`,
457
+ lane: index === 0 ? ("core" as const) : ("hot" as const),
458
+ })),
459
+ finder: [
460
+ {
461
+ slug: "finder-learned",
462
+ descriptor: "learned association",
463
+ lane: "learned",
464
+ },
465
+ {
466
+ slug: "finder-needle",
467
+ descriptor: "direct lexical hit",
468
+ lane: "needle",
469
+ },
470
+ ],
471
+ };
472
+ const turn = {
473
+ ...makeTurn("preserve this current message"),
474
+ recentContext: `${"r".repeat(40_000)}RECENT-END`,
475
+ situationalContext: `old situation ${"s".repeat(20_000)}`,
476
+ };
477
+ providerStub = makeThrowingProvider();
478
+
479
+ let caught: unknown;
480
+ try {
481
+ await selectPool(pool, turn);
482
+ } catch (error) {
483
+ caught = error;
484
+ }
485
+
486
+ expect(caught).toBeInstanceOf(MemoryV3RetrievalUnavailableError);
487
+ const failure = caught as InstanceType<
488
+ typeof MemoryV3RetrievalUnavailableError
489
+ >;
490
+ expect(failure.pool?.stable[0]?.slug).toBe("stable-0");
491
+ expect(
492
+ (failure.pool?.stable.length ?? 0) + (failure.pool?.finder.length ?? 0),
493
+ ).toBeLessThan(pool.stable.length + pool.finder.length);
494
+ expect(
495
+ failure.pool?.finder.map((candidate) => candidate.slug),
496
+ ).not.toContain("finder-learned");
497
+ expect(failure.turn?.currentMessage).toBe("preserve this current message");
498
+ expect(failure.turn?.recentContext).toEndWith("RECENT-END");
499
+ expect(failure.turn?.recentContext.length).toBeLessThan(
500
+ turn.recentContext.length,
501
+ );
502
+ expect(failure.turn?.situationalContext).toBeUndefined();
503
+ });
504
+
505
+ test("throws without sending when no candidate can fit the context budget", async () => {
506
+ selectorMaxInputTokens = 8_000;
507
+ const pool: SelectorPool = {
508
+ stable: [
509
+ {
510
+ slug: "oversized-core",
511
+ card: `oversized core ${"x".repeat(40_000)}`,
512
+ lane: "core",
513
+ },
514
+ ],
515
+ finder: [],
516
+ };
517
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
518
+
519
+ let caught: unknown;
520
+ try {
521
+ await selectPool(pool, makeTurn("anything"));
522
+ } catch (error) {
523
+ caught = error;
524
+ }
525
+
526
+ expect(caught).toBeInstanceOf(MemoryV3RetrievalUnavailableError);
527
+ expect((caught as Error).message).toBe(
528
+ "memory-v3 pool selector has no candidate that fits the context budget",
529
+ );
530
+ expect(providerCalls).toHaveLength(0);
531
+ const failure = caught as InstanceType<
532
+ typeof MemoryV3RetrievalUnavailableError
533
+ >;
534
+ expect(failure.pool).toEqual({ stable: [], finder: [] });
535
+ expect(failure.turn?.currentMessage).toBe("anything");
536
+ expect(failure.turn?.recentContext).toBe("");
537
+ });
538
+
416
539
  test("managed provider 402 attaches a non-terminal credits notice", async () => {
417
540
  providerStub = {
418
541
  name: "managed",
@@ -497,6 +620,7 @@ describe("selectPool — request shape", () => {
497
620
  const [call] = providerCalls;
498
621
  const cfg = call.options?.config as Record<string, unknown>;
499
622
  expect(cfg?.callSite).toBe("memoryV3SelectL2");
623
+ expect(cfg?.selectionSeed).toBe("conv-xyz");
500
624
  expect(cfg?.tool_choice).toEqual({ type: "tool", name: "select_pages" });
501
625
  expect(cfg?.disableTurnStartCache).toBe(true);
502
626
  const tool = call.options?.tools?.[0];
@@ -507,6 +631,134 @@ describe("selectPool — request shape", () => {
507
631
  expect(Object.keys(inputSchema.properties ?? {})).toEqual(["ids"]);
508
632
  });
509
633
 
634
+ test("keeps forced-tool requests within 80% of the resolved context window", async () => {
635
+ selectorMaxInputTokens = 8_000;
636
+ const oversizedPool: SelectorPool = {
637
+ stable: Array.from({ length: 20 }, (_, index) => ({
638
+ slug: `stable-${index}`,
639
+ card: `stable card ${index} ${"x".repeat(4_000)}`,
640
+ lane: index === 0 ? ("core" as const) : ("hot" as const),
641
+ })),
642
+ finder: [
643
+ {
644
+ slug: "finder-learned",
645
+ descriptor: "learned association",
646
+ lane: "learned",
647
+ },
648
+ { slug: "finder-edge", descriptor: "authored link", lane: "edge" },
649
+ {
650
+ slug: "finder-needle",
651
+ descriptor: "direct lexical hit",
652
+ lane: "needle",
653
+ },
654
+ ],
655
+ };
656
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
657
+
658
+ const result = await selectPool(
659
+ oversizedPool,
660
+ makeTurn("preserve this generic current message"),
661
+ );
662
+
663
+ expect(providerCalls).toHaveLength(1);
664
+ const call = providerCalls[0]!;
665
+ const estimatedInputTokens = estimatePromptTokensWithTools(
666
+ call.messages,
667
+ call.options?.systemPrompt,
668
+ call.options?.tools ?? [],
669
+ providerStub.name,
670
+ );
671
+ expect(estimatedInputTokens).toBeLessThanOrEqual(6_400);
672
+ const sent = JSON.stringify(call.messages);
673
+ expect(sent).toContain("preserve this generic current message");
674
+ expect(sent).toContain("stable card 0");
675
+ expect(sent).toContain("finder-needle");
676
+ expect(sent).not.toContain("finder-learned");
677
+ expect(result.pool.stable[0]?.slug).toBe("stable-0");
678
+ expect(result.pool.finder.map((candidate) => candidate.slug)).toContain(
679
+ "finder-needle",
680
+ );
681
+ expect(result.pages[0]?.slug).toBe("stable-0");
682
+ });
683
+
684
+ test("skips a large core card that fits only by dropping the current message", async () => {
685
+ selectorMaxInputTokens = 8_000;
686
+ const currentMessage = `CURRENT-QUERY-${"q".repeat(8_000)}`;
687
+ const pool: SelectorPool = {
688
+ stable: [
689
+ {
690
+ slug: "query-erasing-core",
691
+ card: `large core ${"x".repeat(20_000)}`,
692
+ lane: "core",
693
+ },
694
+ ],
695
+ finder: [
696
+ {
697
+ slug: "usable-needle",
698
+ descriptor: "small direct hit",
699
+ lane: "needle",
700
+ },
701
+ ],
702
+ };
703
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
704
+
705
+ const result = await selectPool(pool, makeTurn(currentMessage));
706
+
707
+ expect(providerCalls).toHaveLength(1);
708
+ const sent = JSON.stringify(providerCalls[0]!.messages);
709
+ expect(sent).toContain(currentMessage);
710
+ expect(sent).not.toContain("query-erasing-core");
711
+ expect(sent).toContain("usable-needle");
712
+ expect(result.pool.stable).toEqual([]);
713
+ expect(result.pool.finder.map((candidate) => candidate.slug)).toEqual([
714
+ "usable-needle",
715
+ ]);
716
+ expect(result.turn.currentMessage).toBe(currentMessage);
717
+ expect(result.pages).toEqual([{ slug: "usable-needle", sections: [] }]);
718
+ });
719
+
720
+ test("skips an individually oversized core card and still sends a smaller direct hit", async () => {
721
+ selectorMaxInputTokens = 8_000;
722
+ const pool: SelectorPool = {
723
+ stable: [
724
+ {
725
+ slug: "oversized-core",
726
+ card: `oversized core ${"x".repeat(40_000)}`,
727
+ lane: "core",
728
+ },
729
+ ],
730
+ finder: [
731
+ {
732
+ slug: "usable-needle",
733
+ descriptor: "small direct hit",
734
+ lane: "needle",
735
+ },
736
+ ],
737
+ };
738
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
739
+
740
+ const result = await selectPool(pool, makeTurn("use the direct hit"));
741
+
742
+ expect(providerCalls).toHaveLength(1);
743
+ expect(JSON.stringify(providerCalls[0]!.messages)).not.toContain(
744
+ "oversized-core",
745
+ );
746
+ expect(JSON.stringify(providerCalls[0]!.messages)).toContain(
747
+ "usable-needle",
748
+ );
749
+ expect(result.pool).toEqual({
750
+ stable: [],
751
+ finder: [
752
+ {
753
+ slug: "usable-needle",
754
+ descriptor: "small direct hit",
755
+ lane: "needle",
756
+ },
757
+ ],
758
+ });
759
+ expect(result.pages).toEqual([{ slug: "usable-needle", sections: [] }]);
760
+ });
761
+
510
762
  test("stable prefix renders full cards in its own block carrying cache_control", async () => {
511
763
  providerStub = makeProvider(toolUseResponse({ ids: [1] }));
512
764
  await selectPool(makePool(), makeTurn("rollout?"));
@@ -526,8 +778,12 @@ describe("selectPool — request shape", () => {
526
778
 
527
779
  // The tail continues the numbering after the cards and is NOT cached.
528
780
  expect(tail.type).toBe("text");
529
- expect(tail.text).toContain("[3] topic-x — section: about topic x");
530
- expect(tail.text).toContain("[4] page-a — section: the alpha rollout plan");
781
+ expect(tail.text).toContain(
782
+ "[3] (needle) topic-x \u2014 section: about topic x",
783
+ );
784
+ expect(tail.text).toContain(
785
+ "[4] (dense) page-a \u2014 section: the alpha rollout plan",
786
+ );
531
787
  expect(tail.text).toContain("<current_message>rollout?</current_message>");
532
788
  expect(tail.text).toContain("<recent_context>");
533
789
  expect(tail.cache_control).toBeUndefined();
@@ -535,7 +791,7 @@ describe("selectPool — request shape", () => {
535
791
  expect(tail.text).not.toContain("<candidate_cards>");
536
792
  });
537
793
 
538
- test("finder lines render the surfacing lane tag when one is supplied", async () => {
794
+ test("finder lines render the surfacing lane tag", async () => {
539
795
  providerStub = makeProvider(toolUseResponse({ ids: [] }));
540
796
  const pool = makePool();
541
797
  pool.finder = [
@@ -546,7 +802,7 @@ describe("selectPool — request shape", () => {
546
802
 
547
803
  const [, tail] = sentBlocks();
548
804
  expect(tail.text).toContain(
549
- "[3] (needle) topic-x — section: about topic x",
805
+ "[3] (needle) topic-x \u2014 section: about topic x",
550
806
  );
551
807
  // Empty descriptor: lane tag still renders, dash omitted.
552
808
  expect(tail.text).toContain("[4] (learned) page-a");
@@ -558,7 +814,13 @@ describe("selectPool — request shape", () => {
558
814
 
559
815
  // Same stable lanes, different finder hits + message (a new turn).
560
816
  const pool2 = makePool();
561
- pool2.finder = [{ slug: "page-c", descriptor: "section: something else" }];
817
+ pool2.finder = [
818
+ {
819
+ slug: "page-c",
820
+ descriptor: "section: something else",
821
+ lane: "needle",
822
+ },
823
+ ];
562
824
  await selectPool(pool2, makeTurn("second question"));
563
825
 
564
826
  const [prefix1, tail1] = sentBlocks(0);
@@ -571,12 +833,15 @@ describe("selectPool — request shape", () => {
571
833
  test("an empty stable prefix renders a single un-cached block", async () => {
572
834
  providerStub = makeProvider(toolUseResponse({ ids: [1] }));
573
835
  await selectPool(
574
- { stable: [], finder: [{ slug: "page-a", descriptor: "d" }] },
836
+ {
837
+ stable: [],
838
+ finder: [{ slug: "page-a", descriptor: "d", lane: "needle" }],
839
+ },
575
840
  makeTurn("x"),
576
841
  );
577
842
  const blocks = sentBlocks();
578
843
  expect(blocks).toHaveLength(1);
579
- expect(blocks[0].text).toContain("[1] page-a — d");
844
+ expect(blocks[0].text).toContain("[1] (needle) page-a \u2014 d");
580
845
  expect(blocks[0].cache_control).toBeUndefined();
581
846
  });
582
847
 
@@ -584,28 +849,38 @@ describe("selectPool — request shape", () => {
584
849
  providerStub = makeProvider(toolUseResponse({ ids: [1] }));
585
850
  const longDescriptor = `padded ${"z".repeat(1000)}`;
586
851
  await selectPool(
587
- { stable: [], finder: [{ slug: "page-a", descriptor: longDescriptor }] },
852
+ {
853
+ stable: [],
854
+ finder: [
855
+ { slug: "page-a", descriptor: longDescriptor, lane: "needle" },
856
+ ],
857
+ },
588
858
  makeTurn("x"),
589
859
  );
590
860
  const [block] = sentBlocks();
591
861
  const line = block.text
592
862
  .split("\n")
593
- .find((l) => l.startsWith("[1] page-a — "))!;
863
+ .find((l) => l.startsWith("[1] (needle) page-a \u2014 "))!;
594
864
  expect(line).toContain("...");
595
865
  expect(line).not.toContain("z".repeat(1000));
596
- // snippet cap (300) + the `[1] page-a — ` prefix.
597
- expect(line.length).toBeLessThanOrEqual(300 + "[1] page-a — ".length);
866
+ // snippet cap (300) plus the numbered lane/slug prefix.
867
+ expect(line.length).toBeLessThanOrEqual(
868
+ 300 + "[1] (needle) page-a \u2014 ".length,
869
+ );
598
870
  });
599
871
 
600
872
  test("a finder candidate with an empty descriptor renders without a dangling dash", async () => {
601
873
  providerStub = makeProvider(toolUseResponse({ ids: [1] }));
602
874
  await selectPool(
603
- { stable: [], finder: [{ slug: "page-a", descriptor: " " }] },
875
+ {
876
+ stable: [],
877
+ finder: [{ slug: "page-a", descriptor: " ", lane: "needle" }],
878
+ },
604
879
  makeTurn("x"),
605
880
  );
606
881
  const [block] = sentBlocks();
607
- expect(block.text).toContain("[1] page-a\n");
608
- expect(block.text).not.toContain("[1] page-a — ");
882
+ expect(block.text).toContain("[1] (needle) page-a\n");
883
+ expect(block.text).not.toContain("[1] (needle) page-a \u2014 ");
609
884
  });
610
885
 
611
886
  test("situational context renders in the tail when present", async () => {
@@ -650,18 +925,24 @@ describe("selectPool: sections and keyword-in-context snippets", () => {
650
925
  function sectionedPool(): SelectorPool {
651
926
  const pool = makePool();
652
927
  pool.finder = [
653
- { slug: "topic-x", descriptor: "section: about topic x" },
928
+ {
929
+ slug: "topic-x",
930
+ descriptor: "section: about topic x",
931
+ lane: "needle",
932
+ },
654
933
  {
655
934
  slug: "page-a",
656
935
  descriptor: alpha.text,
657
936
  section: alpha,
658
937
  terms: ["rollout"],
938
+ lane: "needle",
659
939
  },
660
940
  {
661
941
  slug: "page-a",
662
942
  descriptor: beta.text,
663
943
  section: beta,
664
944
  terms: ["metrics"],
945
+ lane: "dense",
665
946
  },
666
947
  ];
667
948
  return pool;
@@ -753,13 +1034,14 @@ describe("selectPool: sections and keyword-in-context snippets", () => {
753
1034
  descriptor: alpha.text,
754
1035
  section: alpha,
755
1036
  terms: ["missing"],
1037
+ lane: "needle",
756
1038
  }),
757
1039
  makeTurn("x"),
758
1040
  );
759
1041
  const [block] = sentBlocks();
760
1042
  const line = block.text
761
1043
  .split("\n")
762
- .find((l) => l.startsWith("[1] page-a "))!;
1044
+ .find((l) => l.startsWith("[1] (needle) page-a "))!;
763
1045
  expect(
764
1046
  line.endsWith("page-a - Alpha the alpha rollout plan in detail"),
765
1047
  ).toBe(true);
@@ -779,13 +1061,14 @@ describe("selectPool: sections and keyword-in-context snippets", () => {
779
1061
  descriptor: lead.text,
780
1062
  section: lead,
781
1063
  terms: ["rollout"],
1064
+ lane: "needle",
782
1065
  }),
783
1066
  makeTurn("x"),
784
1067
  );
785
1068
  const [block] = sentBlocks();
786
1069
  const line = block.text
787
1070
  .split("\n")
788
- .find((l) => l.startsWith("[1] page-a "))!;
1071
+ .find((l) => l.startsWith("[1] (needle) page-a "))!;
789
1072
  expect(line.endsWith("the lead mentions the rollout early on")).toBe(true);
790
1073
  expect(line).not.toContain("§");
791
1074
  expect(line).not.toContain("…");
@@ -833,13 +1116,14 @@ describe("selectPool: sections and keyword-in-context snippets", () => {
833
1116
  descriptor: notes.text,
834
1117
  section: notes,
835
1118
  terms: ["weekly_turnip"],
1119
+ lane: "needle",
836
1120
  }),
837
1121
  makeTurn("x"),
838
1122
  );
839
1123
  const [block] = sentBlocks();
840
1124
  const line = block.text
841
1125
  .split("\n")
842
- .find((l) => l.startsWith("[1] page-a "))!;
1126
+ .find((l) => l.startsWith("[1] (needle) page-a "))!;
843
1127
  expect(line.endsWith("§Notes: we said: weekly, turnip is the label")).toBe(
844
1128
  true,
845
1129
  );
@@ -863,6 +1147,7 @@ describe("selectPool: sections and keyword-in-context snippets", () => {
863
1147
  descriptor: section.text,
864
1148
  section,
865
1149
  terms: ["turnip"],
1150
+ lane: "needle",
866
1151
  }),
867
1152
  makeTurn("turnip?"),
868
1153
  );
@@ -870,7 +1155,7 @@ describe("selectPool: sections and keyword-in-context snippets", () => {
870
1155
  expect(stripOrphanedSurrogates(block.text)).toBe(block.text);
871
1156
  const line = block.text
872
1157
  .split("\n")
873
- .find((l) => l.startsWith("[1] page-a "))!;
1158
+ .find((l) => l.startsWith("[1] (needle) page-a "))!;
874
1159
  expect(line).toContain("§Rollout: … ");
875
1160
  expect(line).toContain("turnip");
876
1161
  expect(line.endsWith(" …")).toBe(true);
@@ -968,3 +1253,317 @@ describe("selectPool: cataloged thinking and forced-tool compatibility", () => {
968
1253
  expect(selection.pages).toEqual([{ slug: "topic-x", sections: [] }]);
969
1254
  });
970
1255
  });
1256
+
1257
+ // ---------------------------------------------------------------------------
1258
+ // selectPool: TypeSafe System One noul-per-candidate path.
1259
+ // ---------------------------------------------------------------------------
1260
+
1261
+ function typesafeResponse(answers: Record<string, unknown>): ProviderResponse {
1262
+ return {
1263
+ model: "jev-latest",
1264
+ stopReason: "end_turn",
1265
+ usage: { inputTokens: 0, outputTokens: 0 },
1266
+ content: [{ type: "text", text: JSON.stringify(answers, null, 2) }],
1267
+ rawResponse: { answers },
1268
+ };
1269
+ }
1270
+
1271
+ function makeTypesafeProvider(response: ProviderResponse): Provider {
1272
+ return {
1273
+ name: "typesafe",
1274
+ sendMessage: async (messages, options) => {
1275
+ providerCalls.push({ messages, options });
1276
+ return response;
1277
+ },
1278
+ };
1279
+ }
1280
+
1281
+ function noulAnswer(noul: number): { type: "noul"; noul: number } {
1282
+ return { type: "noul", noul };
1283
+ }
1284
+
1285
+ describe("selectPool: TypeSafe System One", () => {
1286
+ test("keeps selector requests within 80% of the resolved context window while preserving the newest candidates", async () => {
1287
+ selectorMaxInputTokens = 32_000;
1288
+ const oversizedPool: SelectorPool = {
1289
+ stable: Array.from({ length: 40 }, (_, index) => ({
1290
+ slug: `stable-${index}`,
1291
+ card: `stable card ${index} ${"x".repeat(5_000)}`,
1292
+ lane: index === 0 ? ("core" as const) : ("hot" as const),
1293
+ })),
1294
+ finder: [
1295
+ { slug: "finder-needle", descriptor: "needle", lane: "needle" },
1296
+ { slug: "finder-dense", descriptor: "dense", lane: "dense" },
1297
+ { slug: "finder-edge", descriptor: "edge", lane: "edge" },
1298
+ { slug: "finder-learned", descriptor: "learned", lane: "learned" },
1299
+ ],
1300
+ };
1301
+ let retainedSlugs: string[] = [];
1302
+ providerStub = {
1303
+ name: "typesafe",
1304
+ defaultModel: "jev-latest",
1305
+ sendMessage: async (messages, options) => {
1306
+ providerCalls.push({ messages, options });
1307
+ const payload = JSON.parse(
1308
+ (messages[0]!.content[0] as { text: string }).text,
1309
+ ) as {
1310
+ state: {
1311
+ candidates: Record<string, { slug: string; text: string }>;
1312
+ current_message: string;
1313
+ };
1314
+ questions: Record<string, unknown>;
1315
+ };
1316
+ retainedSlugs = Object.values(payload.state.candidates).map(
1317
+ (candidate) => candidate.slug,
1318
+ );
1319
+ const answers = Object.fromEntries(
1320
+ retainedSlugs.map((_, index) => [
1321
+ String(index + 1),
1322
+ noulAnswer(
1323
+ index === 0 || index === retainedSlugs.length - 1 ? 1 : 0,
1324
+ ),
1325
+ ]),
1326
+ );
1327
+ return typesafeResponse(answers);
1328
+ },
1329
+ };
1330
+
1331
+ const currentMessage = "preserve this current message";
1332
+ const result = await selectPool(oversizedPool, makeTurn(currentMessage));
1333
+
1334
+ expect(providerCalls).toHaveLength(1);
1335
+ const payloadText = (
1336
+ providerCalls[0]!.messages[0]!.content[0] as { text: string }
1337
+ ).text;
1338
+ const payload = JSON.parse(payloadText) as {
1339
+ state: {
1340
+ candidates: Record<string, { slug: string; text: string }>;
1341
+ current_message: string;
1342
+ };
1343
+ questions: Record<string, unknown>;
1344
+ };
1345
+ const estimatedWireTokens = estimateTextTokens(
1346
+ JSON.stringify({
1347
+ state: payload.state,
1348
+ model: "jev-latest",
1349
+ questions: payload.questions,
1350
+ }),
1351
+ );
1352
+ expect(estimatedWireTokens).toBeLessThanOrEqual(25_600);
1353
+ expect(payload.state.current_message).toBe(currentMessage);
1354
+ expect(retainedSlugs.length).toBeLessThan(44);
1355
+ expect(retainedSlugs).toContain("stable-0");
1356
+ expect(retainedSlugs).toContain("finder-needle");
1357
+ expect(retainedSlugs).toContain("finder-dense");
1358
+ expect(retainedSlugs).not.toContain("finder-learned");
1359
+ expect(Object.keys(payload.questions)).toHaveLength(retainedSlugs.length);
1360
+ expect(result.pool.stable[0]?.slug).toBe("stable-0");
1361
+ expect(result.pool.finder.map((candidate) => candidate.slug)).toEqual([
1362
+ "finder-needle",
1363
+ "finder-dense",
1364
+ ]);
1365
+ expect(result.pages).toEqual([
1366
+ { slug: retainedSlugs[0], sections: [] },
1367
+ { slug: retainedSlugs[retainedSlugs.length - 1], sections: [] },
1368
+ ]);
1369
+ expect(result.keptAll).toBe(false);
1370
+ });
1371
+
1372
+ test("drops the trailing current-message duplicate before trimming recent context", async () => {
1373
+ selectorMaxInputTokens = 8_000;
1374
+ let payload: {
1375
+ state: {
1376
+ current_message: string;
1377
+ recent_context: string;
1378
+ situation?: string;
1379
+ };
1380
+ } | null = null;
1381
+ providerStub = {
1382
+ name: "typesafe",
1383
+ defaultModel: "jev-latest",
1384
+ sendMessage: async (messages, options) => {
1385
+ providerCalls.push({ messages, options });
1386
+ payload = JSON.parse(
1387
+ (messages[0]!.content[0] as { text: string }).text,
1388
+ ) as typeof payload;
1389
+ return typesafeResponse({
1390
+ "1": noulAnswer(1),
1391
+ "2": noulAnswer(0),
1392
+ "3": noulAnswer(0),
1393
+ "4": noulAnswer(0),
1394
+ });
1395
+ },
1396
+ };
1397
+ const currentMessage = `current query ${"q".repeat(2_000)}`;
1398
+ const previousAssistant = `preceding assistant reply ${"a".repeat(2_000)}`;
1399
+ const turn = {
1400
+ ...makeTurn(currentMessage),
1401
+ recentContext: `${previousAssistant}\n${currentMessage}`,
1402
+ situationalContext: `old situation ${"s".repeat(30_000)}`,
1403
+ };
1404
+
1405
+ const result = await selectPool(makePool(), turn);
1406
+
1407
+ expect(payload).not.toBeNull();
1408
+ expect(payload!.state.current_message).toBe(currentMessage);
1409
+ expect(payload!.state.recent_context).toBe(previousAssistant);
1410
+ expect(payload!.state.recent_context).not.toContain(currentMessage);
1411
+ expect(payload!.state.situation?.length ?? 0).toBeLessThan(
1412
+ turn.situationalContext.length,
1413
+ );
1414
+ expect(result.turn.currentMessage).toBe(currentMessage);
1415
+ expect(result.turn.recentContext).toBe(previousAssistant);
1416
+ });
1417
+
1418
+ test("preserves the current message and newest recent-context suffix when turn context is oversized", async () => {
1419
+ selectorMaxInputTokens = 8_000;
1420
+ let payload: {
1421
+ state: {
1422
+ candidates: Record<string, { slug: string; text: string }>;
1423
+ current_message: string;
1424
+ recent_context: string;
1425
+ situation?: string;
1426
+ };
1427
+ questions: Record<string, unknown>;
1428
+ } | null = null;
1429
+ providerStub = {
1430
+ name: "typesafe",
1431
+ defaultModel: "jev-latest",
1432
+ sendMessage: async (messages, options) => {
1433
+ providerCalls.push({ messages, options });
1434
+ payload = JSON.parse(
1435
+ (messages[0]!.content[0] as { text: string }).text,
1436
+ ) as typeof payload;
1437
+ const ids = Object.keys(payload!.state.candidates);
1438
+ return typesafeResponse(
1439
+ Object.fromEntries(ids.map((id) => [id, noulAnswer(1)])),
1440
+ );
1441
+ },
1442
+ };
1443
+ const currentMessage = "keep the complete current message";
1444
+ const recentSuffix = "RECENT-CONTEXT-END";
1445
+ const turn = {
1446
+ ...makeTurn(currentMessage),
1447
+ recentContext: `${"r".repeat(40_000)}${recentSuffix}`,
1448
+ situationalContext: `old situation ${"s".repeat(20_000)}`,
1449
+ };
1450
+
1451
+ await selectPool(makePool(), turn);
1452
+
1453
+ expect(payload).not.toBeNull();
1454
+ expect(payload!.state.current_message).toBe(currentMessage);
1455
+ expect(payload!.state.recent_context).toEndWith(recentSuffix);
1456
+ expect(payload!.state.recent_context.length).toBeLessThan(
1457
+ turn.recentContext.length,
1458
+ );
1459
+ expect(payload!.state.situation).toBeUndefined();
1460
+ const estimatedWireTokens = estimateTextTokens(
1461
+ JSON.stringify({
1462
+ state: payload!.state,
1463
+ model: "jev-latest",
1464
+ questions: payload!.questions,
1465
+ }),
1466
+ );
1467
+ expect(estimatedWireTokens).toBeLessThanOrEqual(6_400);
1468
+ });
1469
+
1470
+ test("sends one noul per candidate and no select_pages tool", async () => {
1471
+ providerStub = makeTypesafeProvider(
1472
+ typesafeResponse({
1473
+ "1": noulAnswer(0.9),
1474
+ "2": noulAnswer(0.1),
1475
+ "3": noulAnswer(0.8),
1476
+ "4": noulAnswer(0.2),
1477
+ }),
1478
+ );
1479
+
1480
+ await selectPool(makePool(), makeTurn("rollout?"));
1481
+
1482
+ expect(providerCalls).toHaveLength(1);
1483
+ const [call] = providerCalls;
1484
+ expect(call.options?.tools).toBeUndefined();
1485
+ expect(
1486
+ (call.options?.config as Record<string, unknown> | undefined)
1487
+ ?.tool_choice,
1488
+ ).toBeUndefined();
1489
+ expect(
1490
+ (call.options?.config as Record<string, unknown> | undefined)?.callSite,
1491
+ ).toBe("memoryV3SelectL2");
1492
+ expect(
1493
+ (call.options?.config as Record<string, unknown> | undefined)
1494
+ ?.selectionSeed,
1495
+ ).toBe("conv-xyz");
1496
+
1497
+ const payload = JSON.parse(
1498
+ (call.messages[0]!.content[0] as { text: string }).text,
1499
+ ) as {
1500
+ state: {
1501
+ candidates: Record<string, { slug: string; text: string }>;
1502
+ current_message: string;
1503
+ selector_instructions: string;
1504
+ };
1505
+ questions: Record<string, { type: string; instructions: string }>;
1506
+ };
1507
+ expect(Object.keys(payload.questions)).toEqual(["1", "2", "3", "4"]);
1508
+ expect(payload.questions["1"]?.type).toBe("noul");
1509
+ expect(payload.questions["1"]?.instructions).toContain("`candidates.1`");
1510
+ expect(payload.state.candidates["1"]?.slug).toBe("page-a");
1511
+ expect(payload.state.candidates["1"]?.text).toBe(CARD_A);
1512
+ expect(payload.state.candidates["3"]?.slug).toBe("topic-x");
1513
+ expect(payload.state.current_message).toBe("rollout?");
1514
+ expect(payload.state.selector_instructions.length).toBeGreaterThan(0);
1515
+ });
1516
+
1517
+ test("keeps candidates at or above the inclusive noul threshold", async () => {
1518
+ providerStub = makeTypesafeProvider(
1519
+ typesafeResponse({
1520
+ "1": noulAnswer(TYPE_SAFE_POOL_KEEP_NOUL),
1521
+ "2": noulAnswer(TYPE_SAFE_POOL_KEEP_NOUL - 0.01),
1522
+ "3": noulAnswer(0.91),
1523
+ "4": noulAnswer(0.12),
1524
+ }),
1525
+ );
1526
+
1527
+ const result = await selectPool(makePool(), makeTurn("rollout?"));
1528
+ expect(result.keptAll).toBe(false);
1529
+ expect(result.pages).toEqual([
1530
+ { slug: "page-a", sections: [] },
1531
+ { slug: "topic-x", sections: [] },
1532
+ ]);
1533
+ });
1534
+
1535
+ test("an all-below-threshold pool is a deliberate empty selection", async () => {
1536
+ providerStub = makeTypesafeProvider(
1537
+ typesafeResponse({
1538
+ "1": noulAnswer(0.1),
1539
+ "2": noulAnswer(0.2),
1540
+ "3": noulAnswer(0.05),
1541
+ "4": noulAnswer(0.3),
1542
+ }),
1543
+ );
1544
+
1545
+ const pool = makePool();
1546
+ const turn = makeTurn("nothing relevant");
1547
+ const result = await selectPool(pool, turn);
1548
+ expect(result).toEqual({
1549
+ pages: [],
1550
+ keptAll: false,
1551
+ pool,
1552
+ turn,
1553
+ });
1554
+ });
1555
+
1556
+ test("unusable answers throw after the re-prompt retry", async () => {
1557
+ providerStub = makeTypesafeProvider({
1558
+ model: "jev-latest",
1559
+ stopReason: "end_turn",
1560
+ usage: { inputTokens: 0, outputTokens: 0 },
1561
+ content: [{ type: "text", text: "not-json" }],
1562
+ });
1563
+
1564
+ await expect(selectPool(makePool(), makeTurn("x"))).rejects.toThrow(
1565
+ MemoryV3RetrievalUnavailableError,
1566
+ );
1567
+ expect(providerCalls).toHaveLength(3);
1568
+ });
1569
+ });