@vellumai/assistant 0.12.2 → 0.12.3-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The memory-v3 plugin's own schema on the dedicated memory connection
3
- * (`assistant-memory.db`): the `memory_v3_pools` and
4
- * `memory_v3_injected_sections` tables, and the `section_key` column of
3
+ * (`assistant-memory.db`): the `memory_v3_pools`, `memory_v3_pool_inputs`,
4
+ * `memory_v3_pool_texts`, and `memory_v3_injected_sections` tables, and the
5
+ * `section_key` column of
5
6
  * `memory_v3_selections`. Plugin storage is created by the plugin,
6
7
  * idempotently and fail-open, never by the global migration chain that gates
7
8
  * database readiness: the memory plugin's `init` hook runs every ensure on
@@ -153,6 +154,53 @@ export const ensureMemoryV3PoolsSchemaOnce = ensureOncePerConnection(
153
154
  "failed to ensure memory_v3_pools; pool logging degraded",
154
155
  );
155
156
 
157
+ const POOL_INPUTS_TABLE = "memory_v3_pool_inputs";
158
+ const POOL_TEXTS_TABLE = "memory_v3_pool_texts";
159
+
160
+ /**
161
+ * Create the pool input capture tables, written only under
162
+ * `memory.v3.poolLog.captureInput` (`pool-log-store.ts`):
163
+ * `memory_v3_pool_inputs`, one row per `(conversation, turn)` holding the
164
+ * context strings the selector was given, the gate and keep-all facts, and
165
+ * the content hash of every pooled candidate's rendered text in pool order
166
+ * (aligned with the turn's `memory_v3_pools` candidates); and
167
+ * `memory_v3_pool_texts`, each rendered text once, keyed by that hash, so a
168
+ * stable-prefix card repeated across turns is stored once. Idempotent
169
+ * (`IF NOT EXISTS`).
170
+ */
171
+ export function ensureMemoryV3PoolInputsSchema(memoryRaw: MemorySqlite): void {
172
+ memoryRaw.exec(/*sql*/ `
173
+ CREATE TABLE IF NOT EXISTS ${POOL_INPUTS_TABLE} (
174
+ conversation_id TEXT NOT NULL,
175
+ turn INTEGER NOT NULL,
176
+ created_at INTEGER NOT NULL,
177
+ situational_context TEXT,
178
+ recent_context TEXT NOT NULL,
179
+ current_message TEXT NOT NULL,
180
+ previous_assistant_message TEXT,
181
+ selector_prompt_hash TEXT,
182
+ kept_all INTEGER NOT NULL DEFAULT 0,
183
+ gate_reason TEXT,
184
+ candidate_text_hashes_json TEXT NOT NULL,
185
+ PRIMARY KEY (conversation_id, turn)
186
+ )
187
+ `);
188
+ memoryRaw.exec(/*sql*/ `
189
+ CREATE TABLE IF NOT EXISTS ${POOL_TEXTS_TABLE} (
190
+ text_hash TEXT PRIMARY KEY,
191
+ text TEXT NOT NULL,
192
+ created_at INTEGER NOT NULL
193
+ )
194
+ `);
195
+ }
196
+
197
+ /** {@link ensureMemoryV3PoolInputsSchema} once per connection in this
198
+ * process, for the pool input capture's writer and readers. */
199
+ export const ensureMemoryV3PoolInputsSchemaOnce = ensureOncePerConnection(
200
+ ensureMemoryV3PoolInputsSchema,
201
+ "failed to ensure memory_v3_pool_inputs; pool input capture degraded",
202
+ );
203
+
156
204
  const SECTIONS_TABLE = "memory_v3_injected_sections";
157
205
  const LEGACY_CARDS_TABLE = "memory_v3_ever_injected";
158
206
 
@@ -331,8 +379,9 @@ export const ensureMemoryV3SelectionsSectionKeyOnce = ensureOncePerConnection(
331
379
 
332
380
  /**
333
381
  * Ensure the whole plugin-owned schema on the memory connection of this
334
- * process, for the memory plugin's `init` hook: the pools and
335
- * injected-sections tables and the selection log's `section_key` column,
382
+ * process, for the memory plugin's `init` hook: the pools, pool input
383
+ * capture, and injected-sections tables and the selection log's
384
+ * `section_key` column,
336
385
  * each through its once-per-connection wrapper so a store's first use on
337
386
  * the same connection is a no-op. No-op when the connection is unavailable
338
387
  * (the stores degrade to no-ops as on any turn).
@@ -340,6 +389,7 @@ export const ensureMemoryV3SelectionsSectionKeyOnce = ensureOncePerConnection(
340
389
  export function ensureMemoryV3PluginSchema(): void {
341
390
  ensuredMemorySqlite("ensureMemoryV3PluginSchema", (raw) => {
342
391
  ensureMemoryV3PoolsSchemaOnce(raw);
392
+ ensureMemoryV3PoolInputsSchemaOnce(raw);
343
393
  ensureMemoryV3InjectedSectionsSchemaOnce(raw);
344
394
  ensureMemoryV3SelectionsSectionKeyOnce(raw);
345
395
  });
@@ -27,17 +27,39 @@
27
27
  * Rows are per-turn diagnostics (roughly 10KB each) with no retention job;
28
28
  * a conversation delete purges them with the other conversation-keyed memory
29
29
  * tables (`conversation-memory-purge.ts`).
30
+ *
31
+ * Under `memory.v3.poolLog.captureInput` (off by default) the turn's row is
32
+ * joined by the selector's exact input, for offline selector evaluation and
33
+ * training data: `memory_v3_pool_inputs` (one row per `(conversation,
34
+ * turn)`: the context strings the selector was given, the selector prompt's
35
+ * content hash, the gate reason, the keep-all flag, and the content hash of
36
+ * every pooled candidate's rendered text in pool order, aligned with
37
+ * `candidates_json`) and `memory_v3_pool_texts` (each rendered text once,
38
+ * keyed by that hash). `buildPoolInput` derives both from the same
39
+ * orchestrate result and turn as the pool record, and `writePoolInput` runs
40
+ * in `writeTurnLog`'s transaction with the pool and selection rows. The
41
+ * input row is purged with the conversation; the texts are page-derived and
42
+ * content-keyed, so they are not.
30
43
  */
31
44
 
45
+ import { createHash } from "node:crypto";
46
+
32
47
  import { getLogger } from "../logging.js";
33
48
  import type { MemorySqlite } from "../memory-db.js";
34
49
  import type { OrchestrateResult } from "./orchestrate.js";
35
50
  import {
36
51
  ensuredMemorySqlite,
52
+ ensureMemoryV3PoolInputsSchemaOnce,
37
53
  ensureMemoryV3PoolsSchemaOnce,
38
54
  memoryReader,
39
55
  } from "./plugin-schema.js";
40
- import { type FinderLane, sectionKey, type Slug } from "./types.js";
56
+ import { renderFinderLine } from "./pool-select.js";
57
+ import {
58
+ type FinderLane,
59
+ type MemoryRoutingTurn,
60
+ sectionKey,
61
+ type Slug,
62
+ } from "./types.js";
41
63
 
42
64
  const log = getLogger("memory-v3-pool-log");
43
65
 
@@ -289,3 +311,233 @@ export function readPoolForMessageIds(messageIds: string[]): StoredPool | null {
289
311
  .get(...messageIds) as PoolRow | null,
290
312
  );
291
313
  }
314
+
315
+ /** Content hash of a rendered candidate text or a selector prompt: the key
316
+ * of `memory_v3_pool_texts`. */
317
+ export function hashPoolText(text: string): string {
318
+ return createHash("sha256").update(text).digest("hex");
319
+ }
320
+
321
+ /** The selector's exact input for one turn, persisted under
322
+ * `memory.v3.poolLog.captureInput` beside the turn's pool record. */
323
+ export interface PoolInputRecord {
324
+ situational_context: string | null;
325
+ recent_context: string;
326
+ current_message: string;
327
+ previous_assistant_message: string | null;
328
+ /** Content hash of the selector system prompt the turn ran with; null
329
+ * when the selector was not run. */
330
+ selector_prompt_hash: string | null;
331
+ /** Whether the selector's recall-safe keep-all fallback fired. */
332
+ kept_all: boolean;
333
+ /** The injection gate's reason code; null when the gate did not run. */
334
+ gate_reason: string | null;
335
+ /** Content hash of each pooled candidate's rendered text, in pool order:
336
+ * index `i` describes the turn's pool record candidate `i`, which the
337
+ * selector saw under pool number `i + 1`. Empty when the record is, and
338
+ * when the texts could not be captured (a consumer zips by index and
339
+ * treats a length mismatch as texts unavailable). */
340
+ candidate_text_hashes: string[];
341
+ }
342
+
343
+ /** A turn's input record with the rendered texts its hashes name. */
344
+ export interface PoolInputCapture {
345
+ input: PoolInputRecord;
346
+ texts: Map<string, string>;
347
+ }
348
+
349
+ /**
350
+ * Build the turn's input capture from an orchestrate result and the turn it
351
+ * ran on. Each candidate text is exactly what the selector was shown minus
352
+ * its pool number: a stable-prefix candidate's pre-rendered card, a finder
353
+ * candidate's rendered line ({@link renderFinderLine}), in the pool
354
+ * record's order ({@link buildPoolRecord}). A turn whose pool record is
355
+ * empty (the selector never judged a pool and nothing was selected)
356
+ * captures the context strings and no candidate texts, so the hashes stay
357
+ * aligned with that record; a result that carries no selector pool, or one
358
+ * that disagrees with its lanes on the candidate count, captures no texts
359
+ * either and logs the mismatch.
360
+ */
361
+ export function buildPoolInput(
362
+ result: OrchestrateResult,
363
+ turn: MemoryRoutingTurn,
364
+ selectorPrompt: string | undefined,
365
+ ): PoolInputCapture {
366
+ const texts = new Map<string, string>();
367
+ const hashes: string[] = [];
368
+ const recordsPool = result.selectorRan || result.selections.length > 0;
369
+ if (recordsPool) {
370
+ const { core, hot, fresh, always, finder } = result.lanes;
371
+ const expected =
372
+ core.length + hot.length + fresh.length + always.length + finder.length;
373
+ const rendered =
374
+ result.pool === undefined
375
+ ? undefined
376
+ : [
377
+ ...result.pool.stable.map((candidate) => candidate.card),
378
+ ...result.pool.finder.map((candidate) =>
379
+ renderFinderLine(candidate),
380
+ ),
381
+ ];
382
+ if (rendered === undefined || rendered.length !== expected) {
383
+ log.warn(
384
+ {
385
+ conversationId: turn.conversationId,
386
+ turnNumber: turn.turnNumber,
387
+ rendered: rendered?.length ?? null,
388
+ expected,
389
+ },
390
+ "memory-v3 pool input capture: the result's pool does not match its lanes; capturing no candidate texts",
391
+ );
392
+ } else {
393
+ for (const text of rendered) {
394
+ const hash = hashPoolText(text);
395
+ hashes.push(hash);
396
+ texts.set(hash, text);
397
+ }
398
+ }
399
+ }
400
+ return {
401
+ input: {
402
+ situational_context: turn.situationalContext ?? null,
403
+ recent_context: turn.recentContext,
404
+ current_message: turn.currentMessage,
405
+ previous_assistant_message: turn.previousAssistantMessage ?? null,
406
+ selector_prompt_hash:
407
+ result.selectorRan && selectorPrompt !== undefined
408
+ ? hashPoolText(selectorPrompt)
409
+ : null,
410
+ kept_all: result.keptAll ?? false,
411
+ gate_reason: result.gateReason ?? null,
412
+ candidate_text_hashes: hashes,
413
+ },
414
+ texts,
415
+ };
416
+ }
417
+
418
+ /**
419
+ * Write the turn's input row and its candidate texts on `raw`, their tables
420
+ * ensured first (`plugin-schema.ts`). The row's PK is `(conversation_id,
421
+ * turn)`, so a re-observed turn overwrites it in step with its pool row;
422
+ * each text is inserted by hash and ignored when already stored. Throws on
423
+ * a failed statement: `writeTurnLog` in `shadow-plugin.ts` runs this inside
424
+ * the transaction that writes the turn's pool and selection rows, and owns
425
+ * the best-effort boundary around it.
426
+ */
427
+ export function writePoolInput(
428
+ raw: MemorySqlite,
429
+ conversationId: string,
430
+ turn: number,
431
+ capture: PoolInputCapture,
432
+ ): void {
433
+ ensureMemoryV3PoolInputsSchemaOnce(raw);
434
+ const now = Date.now();
435
+ const { input, texts } = capture;
436
+ raw
437
+ .query(
438
+ /*sql*/ `
439
+ INSERT OR REPLACE INTO memory_v3_pool_inputs (
440
+ conversation_id, turn, created_at, situational_context, recent_context,
441
+ current_message, previous_assistant_message, selector_prompt_hash,
442
+ kept_all, gate_reason, candidate_text_hashes_json
443
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
444
+ `,
445
+ )
446
+ .run(
447
+ conversationId,
448
+ turn,
449
+ now,
450
+ input.situational_context,
451
+ input.recent_context,
452
+ input.current_message,
453
+ input.previous_assistant_message,
454
+ input.selector_prompt_hash,
455
+ input.kept_all ? 1 : 0,
456
+ input.gate_reason,
457
+ JSON.stringify(input.candidate_text_hashes),
458
+ );
459
+ const insertText = raw.query(/*sql*/ `
460
+ INSERT OR IGNORE INTO memory_v3_pool_texts (text_hash, text, created_at)
461
+ VALUES (?, ?, ?)
462
+ `);
463
+ for (const [hash, text] of texts) {
464
+ insertText.run(hash, text, now);
465
+ }
466
+ }
467
+
468
+ interface PoolInputRow {
469
+ situational_context: string | null;
470
+ recent_context: string;
471
+ current_message: string;
472
+ previous_assistant_message: string | null;
473
+ selector_prompt_hash: string | null;
474
+ kept_all: number;
475
+ gate_reason: string | null;
476
+ candidate_text_hashes_json: string;
477
+ }
478
+
479
+ /** Best-effort read over the capture tables, degrading to the fallback when
480
+ * the memory connection is unavailable or a statement fails
481
+ * (`plugin-schema.ts`). */
482
+ const readPoolInputOr = memoryReader(
483
+ (context) => ensuredMemorySqlite(context, ensureMemoryV3PoolInputsSchemaOnce),
484
+ (err, context) =>
485
+ log.warn(
486
+ { err, context },
487
+ "failed to read memory-v3 pool input; treating it as unrecorded",
488
+ ),
489
+ );
490
+
491
+ /**
492
+ * Read the input record for an exact `(conversation, turn)`. `null` when the
493
+ * turn has none (it ran with the capture off or predates it) or the read
494
+ * degraded.
495
+ */
496
+ export function readPoolInputForTurn(
497
+ conversationId: string,
498
+ turn: number,
499
+ ): PoolInputRecord | null {
500
+ return readPoolInputOr("readPoolInputForTurn", null, (raw) => {
501
+ const row = raw
502
+ .query(
503
+ /*sql*/ `
504
+ SELECT situational_context, recent_context, current_message,
505
+ previous_assistant_message, selector_prompt_hash, kept_all,
506
+ gate_reason, candidate_text_hashes_json
507
+ FROM memory_v3_pool_inputs
508
+ WHERE conversation_id = ? AND turn = ?
509
+ `,
510
+ )
511
+ .get(conversationId, turn) as PoolInputRow | null;
512
+ if (!row) {
513
+ return null;
514
+ }
515
+ const hashes: unknown = JSON.parse(row.candidate_text_hashes_json);
516
+ if (!Array.isArray(hashes)) {
517
+ throw new Error("candidate_text_hashes_json is not an array");
518
+ }
519
+ return {
520
+ situational_context: row.situational_context,
521
+ recent_context: row.recent_context,
522
+ current_message: row.current_message,
523
+ previous_assistant_message: row.previous_assistant_message,
524
+ selector_prompt_hash: row.selector_prompt_hash,
525
+ kept_all: row.kept_all === 1,
526
+ gate_reason: row.gate_reason,
527
+ candidate_text_hashes: hashes as string[],
528
+ };
529
+ });
530
+ }
531
+
532
+ /** The rendered text stored under `textHash`, or `null` when none is or the
533
+ * read degraded. */
534
+ export function readPoolText(textHash: string): string | null {
535
+ return readPoolInputOr("readPoolText", null, (raw) => {
536
+ const row = raw
537
+ .query(
538
+ /*sql*/ `SELECT text FROM memory_v3_pool_texts WHERE text_hash = ?`,
539
+ )
540
+ .get(textHash) as { text: string } | null;
541
+ return row?.text ?? null;
542
+ });
543
+ }
@@ -29,10 +29,14 @@ const realPluginApi = await import("@vellumai/plugin-api");
29
29
 
30
30
  let selectMockActive = false;
31
31
  let sendMessageImpl: (() => Promise<ProviderResponse>) | null = null;
32
+ // The messages of the most recent selector call, for asserting the rendered
33
+ // pool.
34
+ let lastMessages: unknown = null;
32
35
 
33
36
  const mockProvider = {
34
37
  name: "mock-memory-v3-selector",
35
- async sendMessage(): Promise<ProviderResponse> {
38
+ async sendMessage(messages: unknown): Promise<ProviderResponse> {
39
+ lastMessages = messages;
36
40
  if (!sendMessageImpl) {
37
41
  throw new Error("sendMessageImpl not configured");
38
42
  }
@@ -50,7 +54,7 @@ mock.module("@vellumai/plugin-api", () => ({
50
54
  : realPluginApi.getConfiguredProvider(...args),
51
55
  }));
52
56
 
53
- const { MemoryV3RetrievalUnavailableError, selectPool } =
57
+ const { MemoryV3RetrievalUnavailableError, renderFinderLine, selectPool } =
54
58
  await import("./pool-select.js");
55
59
 
56
60
  function response(content: ContentBlock[]): ProviderResponse {
@@ -143,6 +147,35 @@ describe("selectPool", () => {
143
147
  });
144
148
  });
145
149
 
150
+ test("a finder line is shown as its pool number and renderFinderLine", async () => {
151
+ sendMessageImpl = async () =>
152
+ response([
153
+ {
154
+ type: "tool_use",
155
+ id: "call-1",
156
+ name: "select_pages",
157
+ input: { ids: [] },
158
+ },
159
+ ]);
160
+ const candidate = {
161
+ slug: "page-a" as Slug,
162
+ descriptor: "a descriptor",
163
+ lane: "needle",
164
+ };
165
+ await selectPool(
166
+ {
167
+ stable: [{ slug: "page-s" as Slug, card: "card s" }],
168
+ finder: [candidate],
169
+ },
170
+ turn,
171
+ );
172
+ const sent = JSON.stringify(lastMessages);
173
+ expect(sent).toContain(
174
+ JSON.stringify(`[2] ${renderFinderLine(candidate)}`).slice(1, -1),
175
+ );
176
+ expect(sent).toContain(JSON.stringify("[1] card s").slice(1, -1));
177
+ });
178
+
146
179
  test("an empty candidate pool returns no selections", async () => {
147
180
  expect(await selectPool({ stable: [], finder: [] }, turn)).toEqual({
148
181
  pages: [],
@@ -42,9 +42,11 @@
42
42
  * - infrastructure failure (selector provider unavailable — e.g. a transient
43
43
  * CES credential blip drops the API key — or no usable `tool_use` / schema
44
44
  * mismatch surviving the short re-prompt retry) → throw
45
- * {@link MemoryV3RetrievalUnavailableError}. The live injector treats this
46
- * as a logged memory miss for the turn; shadow/observation callers swallow
47
- * it so v2 retrieval can serve the turn.
45
+ * {@link MemoryV3RetrievalUnavailableError}. The orchestrator keeps the
46
+ * stable prefix unjudged for the turn and drops the finder candidates,
47
+ * recording the pool with `selector_ran = 0`; the live injector renders
48
+ * that prefix and queues a notice that the turn drew on core memories
49
+ * only.
48
50
  */
49
51
 
50
52
  import type {
@@ -52,12 +54,12 @@ import type {
52
54
  Message,
53
55
  ToolUseContent,
54
56
  } from "@vellumai/plugin-api";
55
- import { getConfiguredProvider } from "@vellumai/plugin-api";
57
+ import { getConfiguredProvider, safeStringSlice } from "@vellumai/plugin-api";
56
58
  import { z } from "zod";
57
59
 
58
60
  import { classifyConversationError } from "../../../../daemon/conversation-error.js";
59
61
  import type { PendingConversationNotice } from "../../../../daemon/conversation-notices.js";
60
- import { redactLogString, safeStringSlice, truncate } from "../host-utils.js";
62
+ import { redactLogString, truncate } from "../host-utils.js";
61
63
  import {
62
64
  cachedTextBlock,
63
65
  extractToolUse,
@@ -406,22 +408,31 @@ function laneTag(candidate: PoolCandidate): string {
406
408
  : `(${candidate.lane}: ${word}) `;
407
409
  }
408
410
 
411
+ /**
412
+ * 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
415
+ * (no dash for a candidate with an empty descriptor). Shared with the pool
416
+ * input capture (`pool-log-store.ts`), so the persisted text is exactly the
417
+ * rendered line.
418
+ */
419
+ export function renderFinderLine(candidate: PoolCandidate): string {
420
+ const snippet = renderSnippet(candidate);
421
+ const lane = laneTag(candidate);
422
+ return snippet.length > 0
423
+ ? `${lane}${candidate.slug} — ${snippet}`
424
+ : `${lane}${candidate.slug}`;
425
+ }
426
+
409
427
  /**
410
428
  * Render the finder tail: one `[m+i] (lane) slug — snippet` line per
411
- * candidate, numbered continuing after the `offset` stable-prefix cards. The
412
- * lane tag is omitted for a candidate without one and names the keyed word
413
- * for a rare-term line (`(rare: turnip)`); a candidate with an empty
414
- * descriptor renders without the dash.
429
+ * candidate ({@link renderFinderLine}), numbered continuing after the
430
+ * `offset` stable-prefix cards.
415
431
  */
416
432
  function renderFinderSegment(finder: PoolCandidate[], offset: number): string {
417
- const lines = finder.map((c, i) => {
418
- const snippet = renderSnippet(c);
419
- const id = offset + i + 1;
420
- const lane = laneTag(c);
421
- return snippet.length > 0
422
- ? `[${id}] ${lane}${c.slug} — ${snippet}`
423
- : `[${id}] ${lane}${c.slug}`;
424
- });
433
+ const lines = finder.map(
434
+ (c, i) => `[${offset + i + 1}] ${renderFinderLine(c)}`,
435
+ );
425
436
  return `<candidates>\n${lines.join("\n")}\n</candidates>`;
426
437
  }
427
438
 
@@ -500,8 +511,9 @@ export interface PoolSelection {
500
511
  *
501
512
  * An omitted `ids` keeps ALL candidates (the recall-safe "all of these are
502
513
  * relevant" signal, `keptAll: true`); an explicit `[]` keeps none; an
503
- * infrastructure failure (after a short re-prompt retry) keeps none, degrading
504
- * to the deterministic recall lanes the orchestrator unions in.
514
+ * infrastructure failure (after a short re-prompt retry) throws
515
+ * {@link MemoryV3RetrievalUnavailableError}, and the orchestrator keeps the
516
+ * stable prefix unjudged in its place.
505
517
  *
506
518
  * `systemPrompt` is the selector's instruction scaffold; it defaults to the
507
519
  * bundled {@link SYSTEM_PROMPT} and is overridable via `memory.v3.selectorPromptPath`
@@ -1,4 +1,5 @@
1
- import { safeStringSlice } from "../host-utils.js";
1
+ import { safeStringSlice } from "@vellumai/plugin-api";
2
+
2
3
  import type { Section, SectionIndex, Slug } from "./types.js";
3
4
 
4
5
  /**
@@ -19,7 +19,9 @@
19
19
  * 3. Run {@link orchestrate} and record its selection set to
20
20
  * `memory_v3_selections` with a best-effort lane attribution, plus the
21
21
  * full candidate pool and verdict to `memory_v3_pools` for the inspector,
22
- * as one transaction ({@link writeTurnLog}).
22
+ * and, under `memory.v3.poolLog.captureInput`, the selector's exact
23
+ * input to `memory_v3_pool_inputs` / `memory_v3_pool_texts`, as one
24
+ * transaction ({@link writeTurnLog}).
23
25
  *
24
26
  * {@link observeTurn} wraps everything in try/catch — any failure is logged and
25
27
  * swallowed so it can never affect the live turn. The injector treats a
@@ -32,6 +34,7 @@ import {
32
34
  getMessages,
33
35
  listInstalledSkills,
34
36
  parseMessageMetadata,
37
+ safeStringSlice,
35
38
  stringifyMessageContent,
36
39
  VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
37
40
  } from "@vellumai/plugin-api";
@@ -44,7 +47,7 @@ import {
44
47
  timeLatencySubSpan,
45
48
  } from "../../../../daemon/turn-latency-sub-spans.js";
46
49
  import { enqueueMemoryJob } from "../../../../persistence/jobs-store.js";
47
- import { safeStringSlice, stripCommentLines } from "../host-utils.js";
50
+ import { stripCommentLines } from "../host-utils.js";
48
51
  import { getLogger } from "../logging.js";
49
52
  import { type MemorySqlite, memorySqliteOrNull } from "../memory-db.js";
50
53
  import { getWorkspaceDir, getWorkspacePromptPath } from "../paths.js";
@@ -69,9 +72,12 @@ import type { OrchestrateResult } from "./orchestrate.js";
69
72
  import { orchestrate } from "./orchestrate.js";
70
73
  import { ensureMemoryV3SelectionsSectionKeyOnce } from "./plugin-schema.js";
71
74
  import {
75
+ buildPoolInput,
72
76
  buildPoolRecord,
77
+ type PoolInputCapture,
73
78
  type PoolRecord,
74
79
  writePool,
80
+ writePoolInput,
75
81
  } from "./pool-log-store.js";
76
82
  import {
77
83
  MemoryV3RetrievalUnavailableError,
@@ -635,8 +641,16 @@ interface SelectionRow {
635
641
  * The row holds one section per slug: the selection's FIRST selected section
636
642
  * (pool order) stands for the page; a page selected with no section (a card,
637
643
  * or a section-less edge or learned line) logs none.
644
+ *
645
+ * A turn whose selector could not run (`selectorFailure`) logs no rows: its
646
+ * selections are the stable prefix kept unjudged, and the selection table is
647
+ * what the hot set's frecency and the learned-edge graph read as judgments.
648
+ * The pool record still carries the turn for the inspector.
638
649
  */
639
650
  export function attributeSelections(result: OrchestrateResult): SelectionRow[] {
651
+ if (result.selectorFailure) {
652
+ return [];
653
+ }
640
654
  const core = new Set<Slug>(result.lanes.core);
641
655
  const hot = new Set<Slug>(result.lanes.hot);
642
656
  const fresh = new Set<Slug>(result.lanes.fresh);
@@ -729,20 +743,24 @@ function replaceSelections(
729
743
  /**
730
744
  * Write the turn's log over the dedicated memory connection: its attributed
731
745
  * selection rows to `memory_v3_selections` and its candidate pool to
732
- * `memory_v3_pools`, in one transaction. A turn observed again (a retried or
733
- * re-entered turn index) replaces its rows in both tables as a unit, so the
734
- * pool's `chosen` flags and the selection rows always describe the same
735
- * observation. Best-effort: an unavailable memory database or a failed
736
- * statement drops this observation's log rather than affecting the turn, and
737
- * the transaction leaves the earlier observation's rows in place in both
738
- * tables. The selection table's plugin-owned `section_key` column is ensured
739
- * on the first use of a connection in this process (`plugin-schema.ts`).
746
+ * `memory_v3_pools`, plus, when the caller captured it under
747
+ * `memory.v3.poolLog.captureInput`, the selector's exact `input` to
748
+ * `memory_v3_pool_inputs` / `memory_v3_pool_texts`, in one transaction. A
749
+ * turn observed again (a retried or re-entered turn index) replaces its rows
750
+ * in every table as a unit, so the pool's `chosen` flags, the selection rows,
751
+ * and the captured input always describe the same observation. Best-effort:
752
+ * an unavailable memory database or a failed statement drops this
753
+ * observation's log rather than affecting the turn, and the transaction
754
+ * leaves the earlier observation's rows in place in every table. The
755
+ * selection table's plugin-owned `section_key` column is ensured on the
756
+ * first use of a connection in this process (`plugin-schema.ts`).
740
757
  */
741
758
  export function writeTurnLog(
742
759
  conversationId: string,
743
760
  turn: number,
744
761
  rows: SelectionRow[],
745
762
  pool: PoolRecord,
763
+ input?: PoolInputCapture,
746
764
  ): void {
747
765
  try {
748
766
  const raw = memorySqliteOrNull("writeTurnLog");
@@ -753,6 +771,9 @@ export function writeTurnLog(
753
771
  raw.transaction(() => {
754
772
  replaceSelections(raw, conversationId, turn, rows);
755
773
  writePool(raw, conversationId, turn, pool);
774
+ if (input) {
775
+ writePoolInput(raw, conversationId, turn, input);
776
+ }
756
777
  })();
757
778
  } catch (err) {
758
779
  log.warn(
@@ -845,6 +866,10 @@ export async function observeTurn(
845
866
  // against the same store the injector renders from. This turn has not
846
867
  // committed yet, so the set matches what the injector will see.
847
868
  const activeSections = getActiveSections(conversationId);
869
+ const selectorPrompt = resolveSelectorPrompt(
870
+ v3.selectorPromptPath,
871
+ getWorkspaceDir(),
872
+ );
848
873
  const result = await orchestrate(turn, {
849
874
  sectionIndex: lanes.sectionIndex,
850
875
  needle: lanes.needle,
@@ -877,10 +902,7 @@ export async function observeTurn(
877
902
  learnedPerSeed: v3.learnedEdges.perSeed,
878
903
  learnedCap: tuning.learnedEdgesCap,
879
904
  selectorEnabled: tuning.selectorEnabled,
880
- selectorPrompt: resolveSelectorPrompt(
881
- v3.selectorPromptPath,
882
- getWorkspaceDir(),
883
- ),
905
+ selectorPrompt,
884
906
  // Per-turn injection gate: the `memory.v3.gate` tuning, `enabled`
885
907
  // kill-switch included. Read-only downstream, so the config object is
886
908
  // passed as-is.
@@ -910,6 +932,11 @@ export async function observeTurn(
910
932
  turnIndex,
911
933
  attributeSelections(result),
912
934
  buildPoolRecord(result),
935
+ // The selector's exact input, captured only when opted in: the context
936
+ // strings and every pooled candidate's rendered text.
937
+ v3.poolLog.captureInput
938
+ ? buildPoolInput(result, turn, selectorPrompt)
939
+ : undefined,
913
940
  );
914
941
  recordLatencySubSpan(
915
942
  "v3_persist",
@@ -10,7 +10,10 @@
10
10
  * guidance while the client-facing and persisted tool output stay the tool's
11
11
  * actual result. This mirrors how Claude Code (`additionalContext`) and Codex
12
12
  * (`additional_contexts`) surface PostToolUse feedback as separate context
13
- * rather than rewriting the tool response.
13
+ * rather than rewriting the tool response. The one exception is an assistant
14
+ * turn that left a native web search deferred: its answering message must
15
+ * hold tool_result blocks alone, so the loop folds the guidance into the
16
+ * provider-bound copy of the errored tool_result (see `buildToolResultFollowUp`).
14
17
  *
15
18
  * The coaching is bounded per tool: once a single tool has failed
16
19
  * `MAX_CONSECUTIVE_ERROR_NUDGES` times in a row the notice is dropped — the