@vellumai/assistant 0.8.5 → 0.8.6

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 (544) hide show
  1. package/AGENTS.md +33 -1
  2. package/ARCHITECTURE.md +1 -1
  3. package/bunfig.toml +6 -1
  4. package/docs/credential-execution-service.md +6 -6
  5. package/docs/plugins.md +4 -3
  6. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +12 -13
  7. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +4 -1
  8. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +16 -14
  9. package/openapi.yaml +1900 -166
  10. package/package.json +1 -1
  11. package/src/__tests__/actor-token-service.test.ts +3 -2
  12. package/src/__tests__/agent-loop-exit-reason.test.ts +102 -9
  13. package/src/__tests__/agent-loop-override-profile.test.ts +2 -1
  14. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
  15. package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
  16. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  17. package/src/__tests__/annotate-risk-options.test.ts +1 -0
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/approval-routes-http.test.ts +9 -13
  20. package/src/__tests__/assert-not-live-db.ts +79 -0
  21. package/src/__tests__/assistant-feature-flags-integration.test.ts +9 -25
  22. package/src/__tests__/audit-log-rotation.test.ts +2 -2
  23. package/src/__tests__/auto-analysis-end-to-end.test.ts +6 -6
  24. package/src/__tests__/background-workers-disk-pressure.test.ts +5 -8
  25. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  26. package/src/__tests__/btw-routes.test.ts +3 -2
  27. package/src/__tests__/call-controller.test.ts +3 -2
  28. package/src/__tests__/channel-approval-routes.test.ts +3 -2
  29. package/src/__tests__/channel-guardian.test.ts +3 -2
  30. package/src/__tests__/channel-readiness-slack-remote.test.ts +175 -0
  31. package/src/__tests__/channel-reply-delivery.test.ts +35 -0
  32. package/src/__tests__/channel-retry-sweep.test.ts +320 -3
  33. package/src/__tests__/checker.test.ts +12 -12
  34. package/src/__tests__/compaction-events.test.ts +1 -0
  35. package/src/__tests__/compaction-trail-store.test.ts +264 -0
  36. package/src/__tests__/compactor-call-site-logging.test.ts +1 -0
  37. package/src/__tests__/compactor-preserved-tail-count.test.ts +1 -0
  38. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +7 -5
  39. package/src/__tests__/computer-use-tools.test.ts +12 -14
  40. package/src/__tests__/config-loader-backfill.test.ts +13 -28
  41. package/src/__tests__/config-loader-corrupt.test.ts +5 -5
  42. package/src/__tests__/config-loader-platform-defaults.test.ts +93 -26
  43. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +3 -3
  44. package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -4
  45. package/src/__tests__/config-schema.test.ts +10 -10
  46. package/src/__tests__/connection-model-compat.test.ts +83 -0
  47. package/src/__tests__/contacts-tools.test.ts +3 -2
  48. package/src/__tests__/context-token-estimator.test.ts +22 -0
  49. package/src/__tests__/conversation-abort-tool-results.test.ts +5 -0
  50. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop-handlers-max-tokens.test.ts +55 -0
  52. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  53. package/src/__tests__/conversation-agent-loop-overflow.test.ts +34 -0
  54. package/src/__tests__/conversation-agent-loop.test.ts +488 -2
  55. package/src/__tests__/conversation-analysis-routes.test.ts +1 -0
  56. package/src/__tests__/conversation-app-control-instantiation.test.ts +29 -19
  57. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -0
  58. package/src/__tests__/conversation-attention-store.test.ts +101 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +3 -2
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  61. package/src/__tests__/conversation-error.test.ts +30 -0
  62. package/src/__tests__/conversation-fork-crud.test.ts +69 -8
  63. package/src/__tests__/conversation-fork-route.test.ts +3 -2
  64. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  65. package/src/__tests__/conversation-inference-profile-list.test.ts +3 -2
  66. package/src/__tests__/conversation-inference-profile-route.test.ts +3 -2
  67. package/src/__tests__/conversation-lifecycle.test.ts +1 -0
  68. package/src/__tests__/conversation-list-source.test.ts +3 -2
  69. package/src/__tests__/conversation-load-history-repair.test.ts +2 -1
  70. package/src/__tests__/conversation-load-history-stripped.test.ts +1 -0
  71. package/src/__tests__/conversation-pairing.test.ts +53 -0
  72. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +26 -7
  73. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  74. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -0
  75. package/src/__tests__/conversation-queue.test.ts +333 -291
  76. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -18
  77. package/src/__tests__/conversation-routes-guardian-reply.test.ts +33 -8
  78. package/src/__tests__/conversation-routes-slash-commands.test.ts +33 -2
  79. package/src/__tests__/conversation-runtime-assembly.test.ts +78 -0
  80. package/src/__tests__/conversation-skill-tools.test.ts +38 -142
  81. package/src/__tests__/conversation-slash-queue.test.ts +84 -32
  82. package/src/__tests__/conversation-slash-unknown.test.ts +5 -0
  83. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  84. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +46 -0
  85. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -0
  86. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +6 -3
  87. package/src/__tests__/conversation-surfaces-standalone.test.ts +6 -3
  88. package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -3
  89. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -17
  90. package/src/__tests__/conversation-sync-tags.test.ts +128 -12
  91. package/src/__tests__/conversation-title-service.test.ts +1 -0
  92. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +30 -0
  93. package/src/__tests__/conversation-usage.test.ts +1 -0
  94. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  95. package/src/__tests__/conversation-workspace-injection.test.ts +5 -0
  96. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -0
  97. package/src/__tests__/credential-broker-browser-fill.test.ts +3 -3
  98. package/src/__tests__/credential-broker-server-use.test.ts +5 -5
  99. package/src/__tests__/credential-execution-client.test.ts +72 -1
  100. package/src/__tests__/credential-execution-feature-gates.test.ts +10 -12
  101. package/src/__tests__/credential-health-service.test.ts +252 -3
  102. package/src/__tests__/credential-security-invariants.test.ts +5 -5
  103. package/src/__tests__/credential-vault-unit.test.ts +19 -19
  104. package/src/__tests__/credential-vault.test.ts +5 -5
  105. package/src/__tests__/cross-provider-web-search.test.ts +56 -2
  106. package/src/__tests__/db-connection-isolation.test.ts +7 -6
  107. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +8 -10
  108. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +7 -10
  109. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +9 -15
  110. package/src/__tests__/db-test-helpers.ts +58 -0
  111. package/src/__tests__/disk-pressure-guard.test.ts +58 -41
  112. package/src/__tests__/disk-pressure-lifecycle.test.ts +13 -10
  113. package/src/__tests__/disk-pressure-routes.test.ts +0 -33
  114. package/src/__tests__/disk-pressure-tools.test.ts +0 -4
  115. package/src/__tests__/dm-persistence.test.ts +26 -40
  116. package/src/__tests__/document-create-dedupe.test.ts +189 -0
  117. package/src/__tests__/document-find-replace.test.ts +3 -2
  118. package/src/__tests__/document-tool-security.test.ts +81 -2
  119. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +5 -4
  120. package/src/__tests__/encrypted-store-test-helpers.ts +56 -0
  121. package/src/__tests__/encrypted-store.test.ts +11 -9
  122. package/src/__tests__/feature-flag-test-helpers.ts +53 -0
  123. package/src/__tests__/filing-service.test.ts +1 -0
  124. package/src/__tests__/first-greeting.test.ts +62 -12
  125. package/src/__tests__/gateway-flag-listener.test.ts +0 -1
  126. package/src/__tests__/gemini-provider.test.ts +26 -0
  127. package/src/__tests__/guardian-action-sweep.test.ts +3 -2
  128. package/src/__tests__/guardian-outbound-http.test.ts +3 -2
  129. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +48 -3
  130. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -0
  131. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  132. package/src/__tests__/heartbeat-service.test.ts +1 -0
  133. package/src/__tests__/helpers/mock-logger.ts +26 -0
  134. package/src/__tests__/host-bash-routes.test.ts +1 -0
  135. package/src/__tests__/host-cu-routes-targeted.test.ts +1 -0
  136. package/src/__tests__/host-file-routes-targeted.test.ts +1 -0
  137. package/src/__tests__/host-shell-tool.test.ts +5 -4
  138. package/src/__tests__/host-transfer-routes-targeted.test.ts +1 -0
  139. package/src/__tests__/http-conversation-lineage.test.ts +3 -2
  140. package/src/__tests__/http-user-message-parity.test.ts +29 -7
  141. package/src/__tests__/identity-intro-cache.test.ts +133 -22
  142. package/src/__tests__/inbound-slack-persistence.test.ts +44 -72
  143. package/src/__tests__/inference-profile-reaper.test.ts +3 -2
  144. package/src/__tests__/inference-profile-session-ipc.test.ts +3 -2
  145. package/src/__tests__/injector-disk-pressure.test.ts +3 -17
  146. package/src/__tests__/inline-skill-load-permissions.test.ts +4 -4
  147. package/src/__tests__/list-messages-hidden-metadata.test.ts +80 -0
  148. package/src/__tests__/llm-context-normalization.test.ts +42 -0
  149. package/src/__tests__/llm-resolver.test.ts +331 -0
  150. package/src/__tests__/llm-schema.test.ts +1 -1
  151. package/src/__tests__/manual-token-reconciliation.test.ts +76 -1
  152. package/src/__tests__/mcp-abort-signal.test.ts +14 -0
  153. package/src/__tests__/mcp-client-auth.test.ts +14 -0
  154. package/src/__tests__/messaging-send-tool.test.ts +1 -0
  155. package/src/__tests__/migration-import-from-url.test.ts +3 -3
  156. package/src/__tests__/mock-gateway-ipc.ts +18 -2
  157. package/src/__tests__/model-intents.test.ts +3 -3
  158. package/src/__tests__/native-web-search.test.ts +30 -2
  159. package/src/__tests__/notification-deep-link.test.ts +62 -0
  160. package/src/__tests__/oauth-commands-routes.test.ts +37 -0
  161. package/src/__tests__/oauth-provider-visibility.test.ts +8 -8
  162. package/src/__tests__/oauth-store.test.ts +3 -2
  163. package/src/__tests__/onboarding-template-contract.test.ts +3 -2
  164. package/src/__tests__/openai-provider.test.ts +8 -9
  165. package/src/__tests__/openai-responses-provider.test.ts +70 -10
  166. package/src/__tests__/openrouter-provider-only.test.ts +27 -5
  167. package/src/__tests__/outbound-slack-persistence.test.ts +46 -1
  168. package/src/__tests__/persistence-pipeline.test.ts +139 -1
  169. package/src/__tests__/persistence-secret-redaction.test.ts +83 -12
  170. package/src/__tests__/plugin-bootstrap.test.ts +9 -11
  171. package/src/__tests__/plugin-tool-contribution.test.ts +41 -38
  172. package/src/__tests__/process-message-background-slack.test.ts +21 -16
  173. package/src/__tests__/process-message-display-content.test.ts +19 -22
  174. package/src/__tests__/provider-catalog-visibility.test.ts +9 -9
  175. package/src/__tests__/provider-platform-proxy-integration.test.ts +216 -4
  176. package/src/__tests__/provider-registry-ollama.test.ts +45 -22
  177. package/src/__tests__/recording-handler.test.ts +1 -0
  178. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  179. package/src/__tests__/registry.test.ts +82 -76
  180. package/src/__tests__/relay-server.test.ts +10 -10
  181. package/src/__tests__/runtime-attachment-metadata.test.ts +3 -2
  182. package/src/__tests__/schedule-store.test.ts +16 -1
  183. package/src/__tests__/scheduler-reuse-conversation.test.ts +48 -3
  184. package/src/__tests__/secret-ingress-http.test.ts +5 -1
  185. package/src/__tests__/secure-keys.test.ts +3 -3
  186. package/src/__tests__/send-endpoint-busy.test.ts +81 -42
  187. package/src/__tests__/server-history-render.test.ts +4 -1
  188. package/src/__tests__/skill-feature-flags-integration.test.ts +8 -10
  189. package/src/__tests__/skill-feature-flags.test.ts +14 -16
  190. package/src/__tests__/skill-load-feature-flag.test.ts +5 -5
  191. package/src/__tests__/skill-projection-feature-flag.test.ts +44 -30
  192. package/src/__tests__/skill-projection.benchmark.test.ts +5 -7
  193. package/src/__tests__/skill-tool-factory.test.ts +96 -95
  194. package/src/__tests__/slack-channel-config.test.ts +3 -3
  195. package/src/__tests__/subagent-call-site-routing.test.ts +11 -3
  196. package/src/__tests__/subagent-disposal.test.ts +27 -8
  197. package/src/__tests__/subagent-fork-notifications.test.ts +24 -9
  198. package/src/__tests__/subagent-fork-spawn.test.ts +13 -4
  199. package/src/__tests__/subagent-manager-notify.test.ts +20 -8
  200. package/src/__tests__/subagent-notify-parent.test.ts +5 -4
  201. package/src/__tests__/subagent-spawn-tool-fork.test.ts +58 -0
  202. package/src/__tests__/subagent-tools.test.ts +2 -1
  203. package/src/__tests__/suggestion-routes.test.ts +1 -0
  204. package/src/__tests__/system-prompt.test.ts +38 -0
  205. package/src/__tests__/test-preload-verifier.ts +68 -0
  206. package/src/__tests__/test-preload.ts +32 -39
  207. package/src/__tests__/tool-executor-lifecycle-events.test.ts +20 -7
  208. package/src/__tests__/tool-executor.test.ts +55 -10
  209. package/src/__tests__/tool-preview-lifecycle.test.ts +1 -0
  210. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  211. package/src/__tests__/twilio-routes.test.ts +3 -2
  212. package/src/__tests__/validate-input.test.ts +381 -0
  213. package/src/__tests__/verification-control-plane-policy.test.ts +1 -0
  214. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -1
  215. package/src/__tests__/voice-session-bridge.test.ts +37 -28
  216. package/src/__tests__/workspace-migration-090-memory-router-cost-optimized-profile.test.ts +326 -0
  217. package/src/__tests__/workspace-migration-091-retighten-migration-onboarding-thread.test.ts +166 -0
  218. package/src/acp/session-manager.ts +5 -6
  219. package/src/agent/loop.ts +80 -0
  220. package/src/api/README.md +124 -2
  221. package/src/api/constants/call-sites.ts +27 -0
  222. package/src/api/events/assistant-outbound-attachment.ts +51 -0
  223. package/src/api/events/assistant-text-delta.ts +32 -0
  224. package/src/api/events/assistant-turn-start.ts +33 -0
  225. package/src/api/events/document-comment-created.ts +48 -0
  226. package/src/api/events/document-comment-deleted.ts +24 -0
  227. package/src/api/events/document-comment-reopened.ts +25 -0
  228. package/src/api/events/document-comment-resolved.ts +27 -0
  229. package/src/api/events/generation-cancelled.ts +24 -0
  230. package/src/api/events/generation-handoff.ts +41 -0
  231. package/src/api/events/message-complete.ts +42 -0
  232. package/src/api/events/open-url.ts +30 -0
  233. package/src/{events → api/events}/relationship-state-updated.ts +3 -3
  234. package/src/api/events/tool-use-start.ts +32 -0
  235. package/src/api/index.ts +128 -3
  236. package/src/api/responses/llm-context-response.ts +39 -0
  237. package/src/api/responses/llm-request-log-entry.ts +93 -0
  238. package/src/api/responses/memory-recall-log.ts +65 -0
  239. package/src/api/responses/memory-v2-activation-log.ts +78 -0
  240. package/src/background-wake/background-wake-routes.test.ts +687 -52
  241. package/src/background-wake/platform-client.test.ts +308 -0
  242. package/src/background-wake/platform-client.ts +167 -0
  243. package/src/background-wake/publisher.ts +91 -0
  244. package/src/background-wake/runtime-registry.ts +2 -2
  245. package/src/background-wake/wake-intent-hooks.test.ts +282 -0
  246. package/src/calls/guardian-dispatch.ts +1 -0
  247. package/src/calls/voice-session-bridge.ts +4 -4
  248. package/src/cli/commands/__tests__/conversations-slack.test.ts +16 -0
  249. package/src/cli/commands/__tests__/notifications.test.ts +184 -40
  250. package/src/cli/commands/channels/__tests__/channels.test.ts +143 -0
  251. package/src/cli/commands/channels/index.ts +229 -0
  252. package/src/cli/commands/memory-v3-render.ts +147 -0
  253. package/src/cli/commands/memory-v3.ts +255 -4
  254. package/src/cli/commands/notifications.ts +365 -55
  255. package/src/cli/lib/open-browser.ts +7 -2
  256. package/src/cli/program.ts +2 -0
  257. package/src/config/assistant-feature-flags.ts +23 -42
  258. package/src/config/bundled-skills/document-editor/SKILL.md +5 -1
  259. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  260. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  261. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -0
  262. package/src/config/call-site-defaults.ts +1 -1
  263. package/src/config/feature-flag-cache.ts +86 -0
  264. package/src/config/feature-flag-registry.json +17 -17
  265. package/src/config/llm-context-resolution.ts +10 -1
  266. package/src/config/llm-resolver.ts +121 -15
  267. package/src/config/loader.ts +4 -5
  268. package/src/config/schemas/__tests__/memory-v2.test.ts +15 -0
  269. package/src/config/schemas/heartbeat.ts +1 -1
  270. package/src/config/schemas/llm.ts +90 -1
  271. package/src/config/schemas/memory-v2.ts +26 -0
  272. package/src/config/schemas/services.ts +6 -2
  273. package/src/config/seed-inference-profiles.ts +36 -16
  274. package/src/context/token-estimator.ts +10 -5
  275. package/src/credential-execution/executable-discovery.ts +40 -0
  276. package/src/credential-execution/process-manager.ts +6 -2
  277. package/src/credential-health/credential-health-service.ts +125 -40
  278. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -6
  279. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +13 -15
  280. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -2
  281. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -0
  282. package/src/daemon/__tests__/meet-manifest-loader.test.ts +25 -12
  283. package/src/daemon/__tests__/native-web-search-metadata.test.ts +1 -0
  284. package/src/daemon/__tests__/switch-inference-profile-tool.test.ts +107 -0
  285. package/src/daemon/__tests__/web-search-status-text.test.ts +1 -0
  286. package/src/daemon/conversation-agent-loop-handlers.ts +389 -68
  287. package/src/daemon/conversation-agent-loop.ts +132 -28
  288. package/src/daemon/conversation-error.ts +33 -5
  289. package/src/daemon/conversation-messaging.ts +84 -43
  290. package/src/daemon/conversation-process.ts +74 -37
  291. package/src/daemon/conversation-runtime-assembly.ts +29 -9
  292. package/src/daemon/conversation-skill-tools.ts +14 -30
  293. package/src/daemon/conversation-surfaces.ts +69 -34
  294. package/src/daemon/conversation-tool-setup.ts +33 -48
  295. package/src/daemon/conversation.ts +26 -46
  296. package/src/daemon/daemon-control.ts +1 -1
  297. package/src/daemon/daemon-skill-host.ts +9 -2
  298. package/src/daemon/disk-pressure-guard.ts +27 -29
  299. package/src/daemon/first-greeting.ts +31 -13
  300. package/src/daemon/handlers/shared.ts +6 -1
  301. package/src/daemon/lifecycle.ts +12 -12
  302. package/src/daemon/mcp-reload-service.ts +1 -1
  303. package/src/daemon/meet-manifest-loader.ts +10 -17
  304. package/src/daemon/message-types/conversations.ts +20 -22
  305. package/src/daemon/message-types/document-comments.ts +8 -44
  306. package/src/daemon/message-types/home.ts +2 -2
  307. package/src/daemon/message-types/integrations.ts +2 -7
  308. package/src/daemon/message-types/messages.ts +23 -38
  309. package/src/daemon/message-types/subagents.ts +6 -0
  310. package/src/daemon/process-message.ts +9 -9
  311. package/src/daemon/providers-setup.ts +1 -1
  312. package/src/daemon/server.ts +16 -0
  313. package/src/daemon/switch-inference-profile-tool.ts +13 -3
  314. package/src/daemon/tool-setup-types.ts +0 -6
  315. package/src/daemon/wake-target-adapter.ts +10 -0
  316. package/src/documents/document-store.ts +38 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +1 -0
  318. package/src/heartbeat/__tests__/heartbeat-service.test.ts +29 -0
  319. package/src/heartbeat/heartbeat-service.ts +63 -0
  320. package/src/home/__tests__/feed-writer.test.ts +161 -0
  321. package/src/home/__tests__/post-connect-feed.test.ts +1 -0
  322. package/src/home/__tests__/suggested-prompts.test.ts +55 -59
  323. package/src/home/feed-writer.ts +146 -7
  324. package/src/home/suggested-prompts.ts +27 -145
  325. package/src/ipc/__tests__/cli-ipc.test.ts +1 -0
  326. package/src/ipc/gateway-client.test.ts +4 -1
  327. package/src/ipc/skill-routes/__tests__/memory.test.ts +1 -0
  328. package/src/ipc/skill-routes/__tests__/registries.test.ts +36 -7
  329. package/src/ipc/skill-routes/memory.ts +4 -3
  330. package/src/ipc/skill-routes/registries.ts +28 -29
  331. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +1 -0
  332. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +26 -5
  333. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +1 -0
  334. package/src/memory/__tests__/memory-retrospective-job.test.ts +1 -0
  335. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +1 -0
  336. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +31 -0
  337. package/src/memory/conversation-attention-store.ts +17 -3
  338. package/src/memory/conversation-crud.ts +352 -112
  339. package/src/memory/db-connection.ts +29 -19
  340. package/src/memory/db-init.ts +4 -0
  341. package/src/memory/db-singleton.ts +77 -0
  342. package/src/memory/delivery-channels.ts +82 -0
  343. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +2 -4
  344. package/src/memory/graph/retriever.test.ts +3 -3
  345. package/src/memory/job-handlers/embedding.test.ts +3 -2
  346. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -2
  347. package/src/memory/jobs-worker.ts +12 -1
  348. package/src/memory/llm-request-log-source-clickhouse.ts +80 -0
  349. package/src/memory/llm-request-log-source-local.ts +24 -0
  350. package/src/memory/llm-request-log-source.ts +31 -0
  351. package/src/memory/llm-request-log-store.ts +188 -3
  352. package/src/memory/memory-v2-activation-log-store.ts +95 -1
  353. package/src/memory/migrations/265-drop-provider-connection-status.ts +26 -0
  354. package/src/memory/migrations/266-messages-client-message-id.ts +43 -0
  355. package/src/memory/migrations/index.ts +2 -0
  356. package/src/memory/schema/conversations.ts +9 -1
  357. package/src/memory/schema/inference.ts +0 -1
  358. package/src/memory/v2/__tests__/backfill-jobs.test.ts +5 -2
  359. package/src/memory/v2/__tests__/harness-metrics.test.ts +9 -0
  360. package/src/memory/v2/__tests__/harness-replay-input.test.ts +9 -4
  361. package/src/memory/v2/__tests__/harness-runner.test.ts +26 -0
  362. package/src/memory/v2/__tests__/sweep-job.test.ts +6 -3
  363. package/src/memory/v2/harness/metrics.ts +5 -1
  364. package/src/memory/v2/harness/replay-input.ts +19 -3
  365. package/src/memory/v2/harness/runner.ts +6 -0
  366. package/src/memory/v2/harness/trace.ts +6 -0
  367. package/src/memory/v3/__tests__/consolidation-job.test.ts +2 -4
  368. package/src/memory/v3/__tests__/coretrieval-seed.test.ts +270 -0
  369. package/src/memory/v3/__tests__/edges.test.ts +144 -1
  370. package/src/memory/v3/__tests__/filter.test.ts +48 -0
  371. package/src/memory/v3/__tests__/gate.test.ts +96 -33
  372. package/src/memory/v3/__tests__/index-composition.test.ts +58 -0
  373. package/src/memory/v3/__tests__/loop.test.ts +250 -5
  374. package/src/memory/v3/__tests__/scouts.test.ts +49 -0
  375. package/src/memory/v3/__tests__/shadow-diff.test.ts +225 -0
  376. package/src/memory/v3/__tests__/shadow-middleware.test.ts +88 -2
  377. package/src/memory/v3/__tests__/traversal.test.ts +39 -0
  378. package/src/memory/v3/__tests__/tree-walk.test.ts +77 -0
  379. package/src/memory/v3/__tests__/validate.test.ts +32 -0
  380. package/src/memory/v3/coretrieval-seed.ts +240 -0
  381. package/src/memory/v3/edges.ts +58 -21
  382. package/src/memory/v3/filter.ts +27 -22
  383. package/src/memory/v3/gate.ts +51 -36
  384. package/src/memory/v3/index-composition.ts +18 -5
  385. package/src/memory/v3/loop.ts +65 -17
  386. package/src/memory/v3/scouts.ts +15 -4
  387. package/src/memory/v3/shadow-diff.ts +287 -0
  388. package/src/memory/v3/shadow-middleware.ts +44 -2
  389. package/src/memory/v3/traversal.ts +6 -1
  390. package/src/memory/v3/tree-walk.ts +6 -1
  391. package/src/memory/v3/validate.ts +56 -33
  392. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  393. package/src/notifications/__tests__/home-feed-side-effect.test.ts +1 -0
  394. package/src/notifications/adapters/slack.ts +45 -11
  395. package/src/notifications/broadcaster.ts +114 -63
  396. package/src/notifications/conversation-pairing.ts +23 -3
  397. package/src/notifications/decisions-store.ts +32 -1
  398. package/src/notifications/deliveries-store.ts +45 -0
  399. package/src/notifications/edit-notification.ts +201 -0
  400. package/src/notifications/emit-signal.ts +11 -1
  401. package/src/notifications/signal.ts +10 -0
  402. package/src/notifications/types.ts +37 -0
  403. package/src/oauth/byo-connection.test.ts +67 -3
  404. package/src/oauth/byo-connection.ts +32 -5
  405. package/src/oauth/connect-orchestrator.ts +9 -0
  406. package/src/oauth/connection-resolver.test.ts +76 -0
  407. package/src/oauth/connection-resolver.ts +49 -10
  408. package/src/oauth/manual-token-connection.ts +51 -3
  409. package/src/oauth/seed-providers.ts +3 -0
  410. package/src/permissions/approval-policy.test.ts +19 -5
  411. package/src/permissions/approval-policy.ts +14 -3
  412. package/src/permissions/checker.ts +21 -8
  413. package/src/platform/client.test.ts +24 -1
  414. package/src/platform/client.ts +8 -0
  415. package/src/platform/feature-gate.ts +15 -0
  416. package/src/plugins/defaults/injectors.ts +2 -8
  417. package/src/plugins/defaults/persistence.ts +25 -6
  418. package/src/plugins/types.ts +57 -13
  419. package/src/proactive-artifact/job.test.ts +1 -0
  420. package/src/prompts/__tests__/system-prompt.test.ts +4 -4
  421. package/src/prompts/system-prompt.ts +38 -40
  422. package/src/prompts/template-detection.ts +10 -4
  423. package/src/prompts/templates/BOOTSTRAP.md +7 -11
  424. package/src/prompts/templates/IDENTITY.md +0 -2
  425. package/src/providers/__tests__/connection-model-compat.test.ts +3 -4
  426. package/src/providers/__tests__/registry-native-web-search.test.ts +122 -0
  427. package/src/providers/call-site-routing.ts +33 -9
  428. package/src/providers/connection-model-compat.ts +23 -0
  429. package/src/providers/connection-resolution.ts +39 -20
  430. package/src/providers/fireworks/client.ts +1 -0
  431. package/src/providers/gemini/client.ts +24 -3
  432. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +0 -2
  433. package/src/providers/inference/__tests__/base-url-security.test.ts +2 -3
  434. package/src/providers/inference/__tests__/{connections-status-label.test.ts → connections-label.test.ts} +12 -111
  435. package/src/providers/inference/auth.ts +0 -8
  436. package/src/providers/inference/connections.ts +3 -66
  437. package/src/providers/inference/resolve-auth.ts +2 -3
  438. package/src/providers/model-catalog.ts +35 -1
  439. package/src/providers/model-intents.ts +3 -3
  440. package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
  441. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +157 -5
  442. package/src/providers/openai/chat-completions-provider.ts +110 -12
  443. package/src/providers/openai/codex-models.ts +2 -0
  444. package/src/providers/openai/responses-provider.ts +53 -53
  445. package/src/providers/openrouter/client.ts +13 -8
  446. package/src/providers/provider-send-message.ts +18 -9
  447. package/src/providers/registry.ts +48 -8
  448. package/src/providers/retry.ts +16 -4
  449. package/src/providers/search-provider-catalog.ts +17 -9
  450. package/src/providers/types.ts +9 -0
  451. package/src/runtime/__tests__/agent-wake.test.ts +1 -0
  452. package/src/runtime/__tests__/background-job-runner.test.ts +1 -0
  453. package/src/runtime/access-request-helper.ts +1 -0
  454. package/src/runtime/auth/route-policy.ts +10 -0
  455. package/src/runtime/channel-readiness-service.ts +68 -0
  456. package/src/runtime/channel-reply-delivery.ts +23 -0
  457. package/src/runtime/channel-retry-sweep.ts +47 -14
  458. package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
  459. package/src/runtime/migrations/vbundle-builder.ts +3 -2
  460. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +1 -0
  461. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +406 -0
  462. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  463. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  464. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +209 -1
  465. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +13 -50
  466. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +51 -3
  467. package/src/runtime/routes/__tests__/memory-v3-simulate-params.test.ts +35 -0
  468. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +3 -2
  469. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +294 -0
  470. package/src/runtime/routes/__tests__/task-routes.test.ts +48 -3
  471. package/src/runtime/routes/acp-routes-list.test.ts +3 -0
  472. package/src/runtime/routes/app-management-routes.ts +111 -4
  473. package/src/runtime/routes/background-wake-routes.ts +188 -20
  474. package/src/runtime/routes/btw-routes.ts +4 -4
  475. package/src/runtime/routes/conversation-analysis-routes.ts +6 -0
  476. package/src/runtime/routes/conversation-compaction-routes.ts +263 -0
  477. package/src/runtime/routes/conversation-list-routes.ts +147 -0
  478. package/src/runtime/routes/conversation-management-routes.ts +39 -14
  479. package/src/runtime/routes/conversation-query-routes.ts +60 -10
  480. package/src/runtime/routes/conversation-routes.ts +186 -140
  481. package/src/runtime/routes/conversations-import-routes.ts +19 -6
  482. package/src/runtime/routes/documents-routes.ts +10 -1
  483. package/src/runtime/routes/group-routes.ts +11 -0
  484. package/src/runtime/routes/home-feed-routes.ts +129 -0
  485. package/src/runtime/routes/identity-intro-cache.ts +61 -16
  486. package/src/runtime/routes/identity-routes.ts +30 -9
  487. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +530 -6
  488. package/src/runtime/routes/inbound-stages/background-dispatch.ts +57 -8
  489. package/src/runtime/routes/index.ts +2 -0
  490. package/src/runtime/routes/inference-provider-connection-routes.ts +5 -26
  491. package/src/runtime/routes/integrations/vercel.ts +15 -0
  492. package/src/runtime/routes/llm-context-normalization.ts +7 -2
  493. package/src/runtime/routes/memory-v3-routes.ts +160 -2
  494. package/src/runtime/routes/migration-routes.ts +20 -13
  495. package/src/runtime/routes/notification-routes.ts +63 -1
  496. package/src/runtime/routes/oauth-commands-routes.ts +6 -1
  497. package/src/runtime/routes/surface-action-routes.ts +1 -38
  498. package/src/runtime/routes/surface-content-routes.ts +12 -5
  499. package/src/runtime/routes/surface-conversation-resolver.ts +65 -0
  500. package/src/runtime/routes/wipe-conversation-routes.ts +3 -0
  501. package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -0
  502. package/src/runtime/slack-dm-text-delivery.ts +177 -0
  503. package/src/runtime/sync/resource-sync-events.ts +1 -1
  504. package/src/runtime/tool-grant-request-helper.ts +1 -0
  505. package/src/schedule/schedule-store.ts +8 -1
  506. package/src/schedule/scheduler.ts +111 -15
  507. package/src/security/__tests__/provider-key-env-fallback.test.ts +3 -3
  508. package/src/security/encrypted-store.ts +7 -16
  509. package/src/security/store-path-override.ts +61 -0
  510. package/src/signals/user-message.ts +5 -8
  511. package/src/skills/validate-input.ts +177 -0
  512. package/src/subagent/manager.ts +13 -13
  513. package/src/subagent/types.ts +6 -0
  514. package/src/tasks/tool-sanitizer.ts +2 -2
  515. package/src/tools/apps/definitions.ts +35 -21
  516. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +2 -8
  517. package/src/tools/computer-use/definitions.ts +268 -266
  518. package/src/tools/document/document-tool.ts +131 -8
  519. package/src/tools/execution-target.ts +2 -5
  520. package/src/tools/executor.ts +18 -55
  521. package/src/tools/host-filesystem/edit.test.ts +1 -0
  522. package/src/tools/host-filesystem/read.test.ts +1 -0
  523. package/src/tools/host-filesystem/transfer.test.ts +31 -6
  524. package/src/tools/host-filesystem/write.test.ts +1 -0
  525. package/src/tools/mcp/mcp-tool-factory.ts +0 -2
  526. package/src/tools/network/__tests__/managed-search-proxy.test.ts +282 -0
  527. package/src/tools/network/__tests__/web-search.test.ts +211 -3
  528. package/src/tools/network/managed-search-proxy.ts +183 -0
  529. package/src/tools/network/web-search.ts +199 -44
  530. package/src/tools/policy-context.ts +3 -1
  531. package/src/tools/registry.ts +146 -103
  532. package/src/tools/schedule/create.ts +1 -1
  533. package/src/tools/skills/skill-tool-factory.ts +17 -36
  534. package/src/tools/subagent/spawn.ts +3 -0
  535. package/src/tools/tool-approval-handler.ts +10 -4
  536. package/src/tools/tool-name-aliases.ts +72 -14
  537. package/src/tools/types.ts +17 -15
  538. package/src/tools/ui-surface/definitions.ts +98 -86
  539. package/src/types/onboarding-context.ts +6 -0
  540. package/src/usage/attribution.ts +32 -1
  541. package/src/util/browser.ts +7 -2
  542. package/src/workspace/migrations/090-memory-router-cost-optimized-profile.ts +109 -0
  543. package/src/workspace/migrations/091-retighten-migration-onboarding-thread.ts +41 -0
  544. package/src/workspace/migrations/registry.ts +4 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Memory v3 — co-retrieval edge seeding.
3
+ *
4
+ * Warm-starts the learned association graph (`memory_v3_auto_edges`, migration
5
+ * 263) from the v2 router's selection history. The live edge-learning job
6
+ * (`edge-learning-job.ts`) only accrues edges from v3's own retrievals, which is
7
+ * cold until v3 has run for a long time; the v2 router has thousands of turns of
8
+ * co-selection data already. Seeding projects that history into the same table
9
+ * the `aboveThreshold` read path already consumes, so the edge-expansion lane
10
+ * can merge it (via `expandEdges`'s `extraAdjacency` seam) the moment it's wired.
11
+ *
12
+ * Signal: two pages that the router *selected together* on a turn are associated.
13
+ * Scoring is **NPMI** (normalized pointwise mutual information), not raw
14
+ * co-occurrence — NPMI discounts high-frequency pages, so an always-injected page
15
+ * doesn't edge to everything it merely co-occurs with. A min co-occurrence floor
16
+ * drops noise, and an "always-on" frequency ceiling drops pages selected on a
17
+ * large fraction of all turns (heartbeat/now-md-style infrastructure) that carry
18
+ * no associative signal.
19
+ *
20
+ * `buildCoretrievalGraph` is pure (no I/O) so it is unit-testable; the
21
+ * `seedCoretrievalEdges` driver reads the rows and writes the table.
22
+ */
23
+
24
+ import { getLogger } from "../../util/logger.js";
25
+ import type { DrizzleDb } from "../db-connection.js";
26
+ import { getSqliteFrom } from "../db-connection.js";
27
+
28
+ const log = getLogger("memory-v3-coretrieval-seed");
29
+
30
+ /** A pair must co-occur on at least this many turns to earn an edge. */
31
+ export const DEFAULT_MIN_COUNT = 5;
32
+ /**
33
+ * A candidate *neighbor* selected on more than this fraction of all turns is
34
+ * "always-on" infrastructure (co-occurs with everything) and is excluded — it
35
+ * carries no associative signal, only base-rate noise.
36
+ */
37
+ export const DEFAULT_MAX_NEIGHBOR_FREQ_RATIO = 0.4;
38
+ /** Neighbors kept per source node, ranked by NPMI descending. */
39
+ export const DEFAULT_TOP_K = 20;
40
+ /**
41
+ * Weight of a perfect-association (NPMI = 1) edge. Each edge is persisted at
42
+ * `seedWeight × its NPMI score`, so the read threshold
43
+ * (`memory.v3.edges.learnedAdjacencyThreshold`) acts as a minimum-association
44
+ * cutoff: with the default 2.0, threshold 1.0 ≈ NPMI ≥ 0.5 and 1.2 ≈ NPMI ≥ 0.6.
45
+ */
46
+ export const DEFAULT_SEED_WEIGHT = 2.0;
47
+
48
+ /** Tuning knobs for {@link buildCoretrievalGraph}. */
49
+ export interface CoretrievalGraphOptions {
50
+ minCount: number;
51
+ maxNeighborFreqRatio: number;
52
+ topK: number;
53
+ }
54
+
55
+ /** Driver knobs: graph tuning plus the persisted seed weight. */
56
+ export interface SeedCoretrievalOptions extends CoretrievalGraphOptions {
57
+ seedWeight: number;
58
+ }
59
+
60
+ export const DEFAULT_SEED_OPTIONS: SeedCoretrievalOptions = {
61
+ minCount: DEFAULT_MIN_COUNT,
62
+ maxNeighborFreqRatio: DEFAULT_MAX_NEIGHBOR_FREQ_RATIO,
63
+ topK: DEFAULT_TOP_K,
64
+ seedWeight: DEFAULT_SEED_WEIGHT,
65
+ };
66
+
67
+ /** One scored outgoing edge. */
68
+ export interface ScoredEdge {
69
+ target: string;
70
+ score: number;
71
+ }
72
+
73
+ /** Summary of one seeding run, for the CLI/route report. */
74
+ export interface SeedCoretrievalResult {
75
+ turnsScanned: number;
76
+ nodes: number;
77
+ edgesWritten: number;
78
+ avgDegree: number;
79
+ }
80
+
81
+ /**
82
+ * Build the co-retrieval adjacency from per-turn selected-slug sets.
83
+ *
84
+ * Pure: takes the already-extracted selection sets (one array per turn) and
85
+ * returns `source -> ScoredEdge[]` (top-K NPMI neighbors, heaviest first). Turns
86
+ * with fewer than two distinct slugs contribute no pairs and are ignored.
87
+ */
88
+ export function buildCoretrievalGraph(
89
+ turns: ReadonlyArray<ReadonlyArray<string>>,
90
+ options: CoretrievalGraphOptions = DEFAULT_SEED_OPTIONS,
91
+ ): Map<string, ScoredEdge[]> {
92
+ const sets = turns.map((t) => [...new Set(t)]).filter((s) => s.length >= 2);
93
+ const n = sets.length;
94
+ const graph = new Map<string, ScoredEdge[]>();
95
+ if (n === 0) return graph;
96
+
97
+ const freq = new Map<string, number>();
98
+ const cooccur = new Map<string, Map<string, number>>();
99
+ const bump = (a: string, b: string) => {
100
+ let row = cooccur.get(a);
101
+ if (!row) cooccur.set(a, (row = new Map()));
102
+ row.set(b, (row.get(b) ?? 0) + 1);
103
+ };
104
+ for (const slugs of sets) {
105
+ for (const slug of slugs) freq.set(slug, (freq.get(slug) ?? 0) + 1);
106
+ for (let i = 0; i < slugs.length; i++) {
107
+ for (let j = i + 1; j < slugs.length; j++) {
108
+ bump(slugs[i], slugs[j]);
109
+ bump(slugs[j], slugs[i]);
110
+ }
111
+ }
112
+ }
113
+
114
+ const freqCap = options.maxNeighborFreqRatio * n;
115
+ for (const [source, neighbors] of cooccur) {
116
+ const fSource = freq.get(source)!;
117
+ const scored: ScoredEdge[] = [];
118
+ for (const [target, pairCount] of neighbors) {
119
+ if (pairCount < options.minCount) continue;
120
+ const fTarget = freq.get(target)!;
121
+ if (fTarget > freqCap) continue;
122
+ // NPMI = pmi / -ln(p(a,b)), in [-1, 1]. Higher = stronger association.
123
+ // p(a,b)=1 (the pair co-occurs on every turn) is perfect association — its
124
+ // NPMI limit is 1, but the formula is 0/0, so pin it to avoid NaN.
125
+ const pab = pairCount / n;
126
+ if (pab >= 1) {
127
+ scored.push({ target, score: 1 });
128
+ continue;
129
+ }
130
+ const pmi = Math.log(pab / ((fSource / n) * (fTarget / n)));
131
+ scored.push({ target, score: pmi / -Math.log(pab) });
132
+ }
133
+ scored.sort((a, b) => b.score - a.score);
134
+ if (scored.length > 0) graph.set(source, scored.slice(0, options.topK));
135
+ }
136
+ return graph;
137
+ }
138
+
139
+ /**
140
+ * Extract one selection set per v2 router turn from `memory_v2_activation_logs`.
141
+ * The set is the router's *fresh* per-turn pick (`status === 'injected'`), which
142
+ * is the co-selection signal we want — not the accumulated in-context carry-over.
143
+ *
144
+ * Best-effort: a missing table (db predates the activation log) or an unparseable
145
+ * row yields no turns rather than throwing — the caller degrades to "no seed".
146
+ */
147
+ function readRouterSelections(database: DrizzleDb): string[][] {
148
+ const turns: string[][] = [];
149
+ try {
150
+ const raw = getSqliteFrom(database);
151
+ const rows = raw
152
+ .query(
153
+ `SELECT concepts_json FROM memory_v2_activation_logs WHERE mode = 'router'`,
154
+ )
155
+ .all() as Array<{ concepts_json: string }>;
156
+ for (const row of rows) {
157
+ let concepts: unknown;
158
+ try {
159
+ concepts = JSON.parse(row.concepts_json);
160
+ } catch {
161
+ continue;
162
+ }
163
+ if (!Array.isArray(concepts)) continue;
164
+ const selected = new Set<string>();
165
+ for (const entry of concepts) {
166
+ if (
167
+ entry &&
168
+ typeof entry === "object" &&
169
+ (entry as { status?: unknown }).status === "injected" &&
170
+ typeof (entry as { slug?: unknown }).slug === "string"
171
+ ) {
172
+ selected.add((entry as { slug: string }).slug);
173
+ }
174
+ }
175
+ if (selected.size >= 2) turns.push([...selected]);
176
+ }
177
+ } catch (err) {
178
+ log.warn(
179
+ { err },
180
+ "failed to read router selections for seeding; continuing",
181
+ );
182
+ }
183
+ return turns;
184
+ }
185
+
186
+ /**
187
+ * Build the co-retrieval graph from the v2 router history and persist it into
188
+ * `memory_v3_auto_edges`, weighting each edge by its association strength:
189
+ * `weight = seedWeight × NPMI`. Edges whose weight is non-positive (NPMI ≤ 0 —
190
+ * not an association) are skipped. This makes the read threshold a real
191
+ * minimum-association cutoff rather than all-or-nothing.
192
+ *
193
+ * Idempotent: each edge is upserted by OVERWRITING its weight to the current
194
+ * NPMI-derived value (the seed baseline), so re-running is stable. Tradeoff: the
195
+ * overwrite resets any reinforcement the edge-learning job accrued between seeds.
196
+ * That is acceptable while the seed is the dominant signal and organic
197
+ * co-activation reinforcement is minimal; revisit if/when live reinforcement
198
+ * becomes load-bearing.
199
+ */
200
+ export function seedCoretrievalEdges(
201
+ database: DrizzleDb,
202
+ options: SeedCoretrievalOptions = DEFAULT_SEED_OPTIONS,
203
+ ): SeedCoretrievalResult {
204
+ const turns = readRouterSelections(database);
205
+ const graph = buildCoretrievalGraph(turns, options);
206
+
207
+ const now = Date.now();
208
+ let edgesWritten = 0;
209
+ try {
210
+ const raw = getSqliteFrom(database);
211
+ const upsert = raw.prepare(
212
+ `INSERT INTO memory_v3_auto_edges
213
+ (source_slug, target_slug, weight, last_reinforced_at)
214
+ VALUES (?, ?, ?, ?)
215
+ ON CONFLICT(source_slug, target_slug) DO UPDATE SET
216
+ weight = excluded.weight,
217
+ last_reinforced_at = excluded.last_reinforced_at`,
218
+ );
219
+ const apply = raw.transaction(() => {
220
+ for (const [source, edges] of graph) {
221
+ for (const edge of edges) {
222
+ const weight = options.seedWeight * edge.score;
223
+ if (weight <= 0) continue;
224
+ upsert.run(source, edge.target, weight, now);
225
+ edgesWritten += 1;
226
+ }
227
+ }
228
+ });
229
+ apply();
230
+ } catch (err) {
231
+ log.warn({ err }, "failed to persist seeded co-retrieval edges");
232
+ }
233
+
234
+ return {
235
+ turnsScanned: turns.length,
236
+ nodes: graph.size,
237
+ edgesWritten,
238
+ avgDegree: graph.size > 0 ? edgesWritten / graph.size : 0,
239
+ };
240
+ }
@@ -15,8 +15,9 @@
15
15
  * effective out-neighborhood of a node is `curated[node] ∪ extraAdjacency[node]`.
16
16
  *
17
17
  * The result is the union of every seed's reachable neighborhood (`pulled`,
18
- * with seeds themselves excluded) plus a per-seed `EdgeExpansion[]` trace so a
19
- * harness can attribute each pulled slug to the seed it came from.
18
+ * with the full seed set excluded a seed reachable from another seed is still
19
+ * a seed, not a neighbor) plus a per-seed `EdgeExpansion[]` trace so a harness
20
+ * can attribute each pulled slug to the seed it came from.
20
21
  */
21
22
 
22
23
  import { getEdgeIndex, getReachable } from "../v2/edge-index.js";
@@ -61,10 +62,11 @@ const MAX_SEEDS_EXPANDED = 150;
61
62
  const MAX_PULLS_PER_SEED = 32;
62
63
 
63
64
  /**
64
- * Hard ceiling on the size of the unioned `pulled` set. This is the load-bearing
65
- * bound: it caps the lane's contribution to the gate's pool to a few hundred
66
- * slugs no matter how many seeds or how dense the graph. Once reached, no
67
- * further seeds are expanded.
65
+ * Default ceiling on the size of the unioned `pulled` set. This is the
66
+ * load-bearing bound: it caps the lane's contribution to the gate's pool no
67
+ * matter how many seeds or how dense the graph. Once reached, no further seeds
68
+ * are expanded. Overridable per call via {@link ExpandEdgesArgs.maxTotalPulls}
69
+ * (the loop wires it to `memory.v3.edges.maxPulls`).
68
70
  */
69
71
  const MAX_TOTAL_PULLS = 400;
70
72
 
@@ -119,6 +121,12 @@ export interface ExpandEdgesArgs {
119
121
  * above-threshold edges before passing them in.
120
122
  */
121
123
  extraAdjacency?: ReadonlyMap<string, ReadonlySet<string>>;
124
+ /**
125
+ * Hard ceiling on the unioned `pulled` set — the lane's contribution to the
126
+ * gate pool. Defaults to {@link MAX_TOTAL_PULLS} when omitted or not a finite
127
+ * number ≥ 0.
128
+ */
129
+ maxTotalPulls?: number;
122
130
  }
123
131
 
124
132
  export interface ExpandEdgesResult {
@@ -171,10 +179,11 @@ function reachableMerged(
171
179
  * Expand a set of confident seed slugs to their 1–2 hop curated neighborhood.
172
180
  *
173
181
  * Each expanded seed produces one `EdgeExpansion { from, pulled }` entry (sorted
174
- * slugs for deterministic output); the seed itself is never in its own `pulled`.
175
- * The top-level `pulled` set is the union across all expanded seeds a slug
176
- * pulled by more than one seed appears once there but in each contributing
177
- * seed's expansion.
182
+ * slugs for deterministic output); no seed ever appears in `pulled` not its
183
+ * own entry and not another seed's, even when one seed is a neighbor of another
184
+ * (the seeds-excluded contract). The top-level `pulled` set is the union across
185
+ * all expanded seeds — a slug pulled by more than one seed appears once there
186
+ * but in each contributing seed's expansion.
178
187
  *
179
188
  * Bounded by {@link MAX_SEEDS_EXPANDED}, {@link MAX_PULLS_PER_SEED}, and
180
189
  * {@link MAX_TOTAL_PULLS} so a large seed union or a dense graph can't balloon
@@ -197,6 +206,15 @@ export async function expandEdges(
197
206
  laneBySlug,
198
207
  } = args;
199
208
 
209
+ // Per-call override of the union ceiling, falling back to the default constant
210
+ // for an omitted or invalid (negative/NaN) value.
211
+ const maxTotal =
212
+ typeof args.maxTotalPulls === "number" &&
213
+ Number.isFinite(args.maxTotalPulls) &&
214
+ args.maxTotalPulls >= 0
215
+ ? args.maxTotalPulls
216
+ : MAX_TOTAL_PULLS;
217
+
200
218
  const index = await getEdgeIndex(workspaceDir);
201
219
  const pulled = new Set<string>();
202
220
  const expansions: EdgeExpansion[] = [];
@@ -213,7 +231,14 @@ export async function expandEdges(
213
231
  );
214
232
  }
215
233
 
216
- // De-dupe seeds while preserving (ranked) order for a stable trace.
234
+ // De-dupe seeds while preserving (ranked) order for a stable trace. The full
235
+ // de-duped seed set is also the exclusion set: a seed reachable from another
236
+ // seed is itself a confident hit, not a neighbor, so it must never land in
237
+ // `pulled` (the seeds-excluded contract) — `getReachable` only excludes the
238
+ // walk's own start node, so a B reachable from A would otherwise leak in.
239
+ const seedSet = new Set<string>();
240
+ for (const seed of orderedSeeds) seedSet.add(seed);
241
+
217
242
  const seenSeeds = new Set<string>();
218
243
 
219
244
  for (const seed of orderedSeeds) {
@@ -223,7 +248,7 @@ export async function expandEdges(
223
248
  // Bound the number of seeds expanded, and stop once the union is full —
224
249
  // the remaining seeds would only inflate the gate's pool. Checked before
225
250
  // doing any per-seed work so an oversized seed set is cheap to truncate.
226
- if (seenSeeds.size > MAX_SEEDS_EXPANDED || pulled.size >= MAX_TOTAL_PULLS) {
251
+ if (seenSeeds.size > MAX_SEEDS_EXPANDED || pulled.size >= maxTotal) {
227
252
  break;
228
253
  }
229
254
 
@@ -231,18 +256,30 @@ export async function expandEdges(
231
256
  ? reachableMerged(index.outgoing, extraAdjacency, seed, hops)
232
257
  : getReachable(index, seed, hops, "out");
233
258
 
234
- // Per-seed fan-out cap, then trim to the union's remaining headroom so the
235
- // total ceiling is hard (never overshot) and the trace entry lists exactly
236
- // the slugs this seed contributed to the bounded union. Sorting first keeps
237
- // truncation deterministic a hub seed always yields the same slice. Slugs
238
- // already pulled by an earlier seed don't consume headroom (the union Set
239
- // de-dupes), so this is a conservative upper bound on what's admitted.
240
- const remaining = MAX_TOTAL_PULLS - pulled.size;
259
+ // Drop any other seed from this seed's neighborhood (seeds-excluded
260
+ // contract) and split the rest into slugs already in the union vs. fresh
261
+ // ones. The per-seed fan-out cap and the union's remaining headroom apply
262
+ // only to fresh slugs, so a slot is never spent on a duplicate an earlier
263
+ // seed already pulled at the cap that would silently drop a unique
264
+ // neighbor. Sorting first keeps truncation deterministic. The trace lists
265
+ // every reached slug that made it into the bounded union (fresh admissions
266
+ // plus the duplicates this seed also reaches), so it stays a faithful
267
+ // attribution while the budget is reserved for new recall.
268
+ const sorted = [...reachable].sort();
269
+ const alreadyPulled = sorted.filter(
270
+ (slug) => !seedSet.has(slug) && pulled.has(slug),
271
+ );
272
+ const fresh = sorted.filter(
273
+ (slug) => !seedSet.has(slug) && !pulled.has(slug),
274
+ );
275
+
276
+ const remaining = maxTotal - pulled.size;
241
277
  const perSeedCap = Math.min(MAX_PULLS_PER_SEED, remaining);
242
- const seedPulled = [...reachable].sort().slice(0, perSeedCap);
278
+ const admitted = fresh.slice(0, perSeedCap);
279
+ for (const slug of admitted) pulled.add(slug);
243
280
 
281
+ const seedPulled = [...alreadyPulled, ...admitted].sort();
244
282
  expansions.push({ from: seed, pulled: seedPulled });
245
- for (const slug of seedPulled) pulled.add(slug);
246
283
  }
247
284
 
248
285
  return { pulled, expansions };
@@ -12,20 +12,19 @@
12
12
  * corpus). Hot pages and near-exact sparse hits arrive via the scouts'
13
13
  * `sticky` / `bypass` sets and are **never judged**: a literal keyword hit or a
14
14
  * page the user has been touching is a strong enough signal that we shouldn't
15
- * make it earn its place through a fallible cheap judgment. They are unioned
16
- * straight into `kept`.
15
+ * make it earn its place through a fallible cheap judgment, and the downstream
16
+ * gate force-injects every sticky slug regardless — judging it could not change
17
+ * its fate. The `bypass` subset is additionally unioned straight into `kept`.
17
18
  *
18
19
  * Fail-open. If no provider is configured or the call errors / returns an
19
- * unusable response, the filter keeps *all* dense candidates and surfaces a
20
- * `failureReason` so the loop can record that the filter was bypassed. Dropping
21
- * candidates on a model outage would silently starve retrieval; keeping them is
22
- * the safe degradation (the downstream gate still narrows the slate).
20
+ * unusable response, the filter keeps *all* judged dense candidates and surfaces
21
+ * a `failureReason` so the loop can record that the filter was bypassed.
22
+ * Dropping candidates on a model outage would silently starve retrieval; keeping
23
+ * them is the safe degradation (the downstream gate still narrows the slate).
23
24
  *
24
- * No LLM call when there is nothing to judge. An empty dense set short-circuits
25
- * to `kept` = the bypass-relevant slugs (no judged additions), with no provider
26
- * round-trip.
27
- *
28
- * This module is currently unwired — a later PR composes it into the loop.
25
+ * No LLM call when there is nothing to judge. A dense set fully covered by
26
+ * sticky short-circuits to `kept` = the bypass-relevant slugs (no judged
27
+ * additions), with no provider round-trip.
29
28
  */
30
29
 
31
30
  import { z } from "zod";
@@ -58,10 +57,12 @@ const FILTER_TOOL_NAME = "filter_dense_hits";
58
57
  * Arguments to one filter invocation.
59
58
  *
60
59
  * `dense` is the bounded dense scout result; only its slugs that are *not*
61
- * already in `bypass` are judged. `sticky` is the broader keep-in-the-running
62
- * set (hot + near-exact sparse); `bypass` is the subset strong enough to skip
63
- * judgment entirely. Bypass slugs that also appear in the dense lane are kept
64
- * unconditionally and never sent to the model.
60
+ * already in `sticky` are judged. `sticky` is the keep-in-the-running set (hot +
61
+ * near-exact sparse) the downstream gate force-injects regardless of this
62
+ * filter, so judging a sticky page wastes an LLM call that can never change its
63
+ * fate. `bypass` is the subset of sticky strong enough to skip judgment that the
64
+ * filter also unions straight into `kept`. Sticky slugs that also appear in the
65
+ * dense lane are excluded from the judged set and never sent to the model.
65
66
  */
66
67
  export interface FilterDenseHitsArgs {
67
68
  input: RetrievalInput;
@@ -163,19 +164,23 @@ function buildResult(
163
164
  * Run the fast dense-hit filter for one pass.
164
165
  *
165
166
  * Makes at most one forced-tool LLM call over the *judged* set (dense slugs not
166
- * already in `bypass`). Bypass slugs are kept unconditionally. On an empty
167
- * judged set no call is made. Any failure (no provider, provider throw, missing
168
- * tool_use, schema mismatch) fails open: every dense candidate is kept and a
167
+ * already in `sticky`). Sticky slugs are force-selected by the downstream gate
168
+ * regardless of this filter, so they are excluded from judgment; bypass slugs
169
+ * are additionally kept unconditionally here. On an empty judged set no call is
170
+ * made. Any failure (no provider, provider throw, missing tool_use, schema
171
+ * mismatch) fails open: every judged dense candidate is kept and a
169
172
  * `failureReason` is returned.
170
173
  */
171
174
  export async function filterDenseHits(
172
175
  args: FilterDenseHitsArgs,
173
176
  ): Promise<FilterDenseHitsResult> {
174
- const { input, dense, bypass } = args;
177
+ const { input, dense, sticky, bypass } = args;
175
178
 
176
- // Dense slugs that bypass judgment (near-exact sparse / hot) are kept as-is;
177
- // only the remainder is judged.
178
- const judged = dense.slugs.filter((slug) => !bypass.has(slug));
179
+ // Sticky slugs (hot + near-exact sparse) are force-selected by the gate
180
+ // regardless of this filter, so judging them wastes an LLM call that can't
181
+ // change their fate. Exclude the full sticky set (a superset of bypass) from
182
+ // the judged set; only the remaining dense near-neighbors are judged.
183
+ const judged = dense.slugs.filter((slug) => !sticky.has(slug));
179
184
 
180
185
  // Nothing to judge → no LLM call. Kept is just the bypass-relevant slugs.
181
186
  if (judged.length === 0) {
@@ -79,9 +79,9 @@ export interface RunGateArgs {
79
79
  * Per-candidate one-line summaries, keyed by slug. When present, candidates
80
80
  * are rendered to the model as `slug — summary` so the gate can judge
81
81
  * relevance on page content rather than the slug alone. Missing entries fall
82
- * back to the bare slug; the forced tool's `selected_slugs` enum stays
83
- * slug-only. The loop passes this only when `memory.v3.gateCandidateSummaries`
84
- * is set.
82
+ * back to the bare slug; the model answers with the candidate's `[N]` number
83
+ * (`selected_ids`) regardless, so summaries never change the answer format. The
84
+ * loop passes this only when `memory.v3.gateCandidateSummaries` is set.
85
85
  */
86
86
  summaryBySlug?: ReadonlyMap<string, string>;
87
87
  /** Optional debug sink — emits one record for the gate's LLM call. */
@@ -102,17 +102,20 @@ export interface RunGateResult {
102
102
  }
103
103
 
104
104
  /**
105
- * Build the forced tool definition. `selected_slugs` is the ordered final
106
- * selection; `decision` is the ready/more verdict; `questions` carries the
107
- * generated follow-up queries on "more" (ignored on "ready"). Mirrors the
108
- * forced-tool pattern of v2's `select_pages_to_inject`.
105
+ * Build the forced tool definition. `selected_ids` is the list of bracketed
106
+ * `[N]` candidate numbers to keep; `decision` is the ready/more verdict;
107
+ * `questions` carries the generated follow-up queries on "more" (ignored on
108
+ * "ready"). The schema is candidate-independent no per-turn enum — so it stays
109
+ * byte-identical across turns (cache-friendly) and the model answers with the
110
+ * line numbers rather than reproducing exact slug strings, mirroring the
111
+ * integer-id output of v2's `select_pages_to_inject`.
109
112
  */
110
- function buildGateTool(candidateSlugs: readonly string[]): ToolDefinition {
113
+ function buildGateTool(): ToolDefinition {
111
114
  return {
112
115
  name: GATE_TOOL_NAME,
113
116
  description:
114
117
  "Decide whether the accumulated candidate pages are sufficient to answer " +
115
- "the next turn. Return decision='ready' with the final ordered selection " +
118
+ "the next turn. Return decision='ready' with the final selection " +
116
119
  "when the candidates cover the turn; return decision='more' with one or " +
117
120
  "more generated follow-up questions (NOT the original message) to seed " +
118
121
  "another retrieval pass when coverage is incomplete.",
@@ -120,16 +123,14 @@ function buildGateTool(candidateSlugs: readonly string[]): ToolDefinition {
120
123
  type: "object",
121
124
  properties: {
122
125
  decision: { type: "string", enum: ["ready", "more"] },
123
- selected_slugs: {
126
+ selected_ids: {
124
127
  type: "array",
125
- items: { type: "string", enum: [...candidateSlugs] },
128
+ items: { type: "integer" },
126
129
  description:
127
- "Final ordered page slugs to inject. Each candidate is listed as " +
128
- "`slug summary` when summaries are available; return only the slug " +
129
- "(left of the em-dash), and only from the candidate set. Prefer keeping " +
130
- "a plausibly-relevant page over dropping it; for a list / 'all of X' / " +
131
- "breadth request, include every candidate that plausibly applies rather " +
132
- "than trimming to the most prominent few.",
130
+ "The bracketed `[N]` numbers of the candidate pages to keep. Prefer " +
131
+ "keeping a plausibly-relevant page over dropping it; for a list / " +
132
+ "'all of X' / breadth request, include every candidate that plausibly " +
133
+ "applies rather than trimming to the most prominent few.",
133
134
  },
134
135
  questions: {
135
136
  type: "array",
@@ -144,32 +145,33 @@ function buildGateTool(candidateSlugs: readonly string[]): ToolDefinition {
144
145
  "candidates were kept or dropped.",
145
146
  },
146
147
  },
147
- required: ["decision"],
148
+ required: ["decision", "selected_ids"],
148
149
  },
149
150
  };
150
151
  }
151
152
 
152
153
  const GateToolResultSchema = z.object({
153
154
  decision: z.enum(["ready", "more"]),
154
- selected_slugs: z.array(z.string()).optional(),
155
+ selected_ids: z.array(z.coerce.number()).optional(),
155
156
  questions: z.array(z.string()).optional(),
156
157
  reasoning: z.string().optional(),
157
158
  });
158
159
 
159
160
  /**
160
- * Render the candidate list for the prompt. With summaries available each line
161
- * is `slug — summary` so the model can judge relevance on content; without them
162
- * it falls back to the bare slug. The slug (left of the em-dash) is what the
163
- * `selected_slugs` enum constrains, so the model always answers in slugs.
161
+ * Render the candidate list for the prompt. Each line is `[N] slug — summary`
162
+ * (or `[N] slug` without a summary), numbered from 1 in candidate order. The
163
+ * model answers with the `[N]` numbers via `selected_ids`, which the caller maps
164
+ * back to slugs by index — so the tool schema stays candidate-independent.
164
165
  */
165
166
  function renderCandidateLines(
166
167
  slugs: readonly string[],
167
168
  summaryBySlug: ReadonlyMap<string, string> | undefined,
168
169
  ): string {
169
170
  return slugs
170
- .map((slug) => {
171
+ .map((slug, i) => {
171
172
  const summary = summaryBySlug?.get(slug);
172
- return summary ? `${slug} — ${summary}` : slug;
173
+ const body = summary ? `${slug} — ${summary}` : slug;
174
+ return `[${i + 1}] ${body}`;
173
175
  })
174
176
  .join("\n");
175
177
  }
@@ -267,7 +269,7 @@ export async function runGate(args: RunGateArgs): Promise<RunGateResult> {
267
269
  ],
268
270
  };
269
271
 
270
- const gateTool = buildGateTool(candidateSlugs);
272
+ const gateTool = buildGateTool();
271
273
 
272
274
  const startedAt = Date.now();
273
275
  let response;
@@ -310,25 +312,38 @@ export async function runGate(args: RunGateArgs): Promise<RunGateResult> {
310
312
  return failSafe(candidates, sticky);
311
313
  }
312
314
 
313
- const selectedSlugs = orderSelection(
314
- parsed.data.selected_slugs ?? [],
315
- candidates,
316
- sticky,
317
- );
315
+ // Recall-safe fallback: an *omitted* `selected_ids` means the model gave no
316
+ // instruction, so keep everything that was surfaced — dropping all non-sticky
317
+ // context on a silent omission is the worse failure. An *explicit* `[]` is the
318
+ // model genuinely choosing nothing and is honored as-is. Ids are 1-based
319
+ // indices into the rendered candidate list; out-of-range ids are dropped.
320
+ const modelSlugs =
321
+ parsed.data.selected_ids === undefined
322
+ ? [...candidates]
323
+ : parsed.data.selected_ids
324
+ .map((id) => candidateSlugs[Math.trunc(id) - 1])
325
+ .filter((slug): slug is string => slug !== undefined);
326
+ const selectedSlugs = orderSelection(modelSlugs, candidates, sticky);
327
+
328
+ const reasoning = parsed.data.reasoning?.trim() || undefined;
318
329
 
319
330
  if (parsed.data.decision === "more") {
320
331
  const questions = (parsed.data.questions ?? []).filter(
321
332
  (q) => q.trim().length > 0,
322
333
  );
323
- const decision: GateDecision =
324
- questions.length > 0
325
- ? { decision: "more", questions }
326
- : { decision: "more" };
334
+ const decision: GateDecision = {
335
+ decision: "more",
336
+ ...(questions.length > 0 ? { questions } : {}),
337
+ ...(reasoning ? { reasoning } : {}),
338
+ };
327
339
  return { decision, selectedSlugs };
328
340
  }
329
341
 
330
342
  // brief generation lands at cutover (P5) — shadow mode injects v2, so this
331
343
  // gate produces only the selection + decision. Do NOT synthesize a voice
332
344
  // brief here.
333
- return { decision: { decision: "ready" }, selectedSlugs };
345
+ return {
346
+ decision: { decision: "ready", ...(reasoning ? { reasoning } : {}) },
347
+ selectedSlugs,
348
+ };
334
349
  }