@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
@@ -41,6 +41,12 @@ export interface GateDecision {
41
41
  decision: "ready" | "more";
42
42
  /** When "more", the generated follow-up queries seeding the next pass. */
43
43
  questions?: string[];
44
+ /**
45
+ * The gate's one-line rationale for this verdict, when it supplied one.
46
+ * Surfaced in the descent trace and the live-shadow telemetry so a run can be
47
+ * analyzed after the fact ("why did the gate keep this set?").
48
+ */
49
+ reasoning?: string;
44
50
  }
45
51
 
46
52
  /** Everything that happened on one pass of the loop. */
@@ -38,11 +38,9 @@ import {
38
38
 
39
39
  import { eq } from "drizzle-orm";
40
40
 
41
- import { makeMockLogger } from "../../../__tests__/helpers/mock-logger.js";
41
+ import { createMockLoggerModule } from "../../../__tests__/helpers/mock-logger.js";
42
42
 
43
- mock.module("../../../util/logger.js", () => ({
44
- getLogger: () => makeMockLogger(),
45
- }));
43
+ mock.module("../../../util/logger.js", () => createMockLoggerModule());
46
44
 
47
45
  // ── runBackgroundJob mock ───────────────────────────────────────────
48
46
  //
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Tests for `assistant/src/memory/v3/coretrieval-seed.ts`.
3
+ *
4
+ * - `buildCoretrievalGraph` (pure): co-occurrence counting, the min-count
5
+ * floor, the always-on frequency exclusion, NPMI ranking, and the top-K cap.
6
+ * - `seedCoretrievalEdges` (driver): reads router selections from
7
+ * `memory_v2_activation_logs`, persists the graph into `memory_v3_auto_edges`
8
+ * in the shape `aboveThreshold` reads back, and is idempotent.
9
+ *
10
+ * Generic slugs only (page-a, topic-x, …) — no real content.
11
+ */
12
+
13
+ import { Database } from "bun:sqlite";
14
+ import { describe, expect, mock, test } from "bun:test";
15
+
16
+ import { drizzle } from "drizzle-orm/bun-sqlite";
17
+
18
+ import { makeMockLogger } from "../../../__tests__/helpers/mock-logger.js";
19
+
20
+ mock.module("../../../util/logger.js", () => ({
21
+ getLogger: () => makeMockLogger(),
22
+ }));
23
+
24
+ import type { DrizzleDb } from "../../db-connection.js";
25
+ import {
26
+ buildCoretrievalGraph,
27
+ seedCoretrievalEdges,
28
+ } from "../coretrieval-seed.js";
29
+
30
+ describe("buildCoretrievalGraph", () => {
31
+ const opts = { minCount: 2, maxNeighborFreqRatio: 1, topK: 10 };
32
+
33
+ test("ignores turns with fewer than two distinct slugs", () => {
34
+ const graph = buildCoretrievalGraph(
35
+ [["page-a"], ["page-b"], ["page-a", "page-a"]],
36
+ opts,
37
+ );
38
+ expect(graph.size).toBe(0);
39
+ });
40
+
41
+ test("ranks a surprising association above a high-frequency one (NPMI)", () => {
42
+ // page-b appears only ever alongside page-a (perfectly associated); topic-c
43
+ // is high-frequency (appears all over), so its co-occurrence with page-a is
44
+ // unsurprising. NPMI must rank page-b above topic-c for source page-a.
45
+ const turns = [
46
+ ["page-a", "page-b"],
47
+ ["page-a", "page-b"],
48
+ ["page-a", "page-b"],
49
+ ["page-a", "page-b"],
50
+ ["page-a", "topic-c"],
51
+ ["page-a", "topic-c"],
52
+ ["page-a", "topic-c"],
53
+ ["page-a", "topic-c"],
54
+ ["topic-c", "page-d"],
55
+ ["topic-c", "page-d"],
56
+ ["topic-c", "page-d"],
57
+ ["topic-c", "page-d"],
58
+ ["topic-c", "page-d"],
59
+ ["topic-c", "page-d"],
60
+ ];
61
+ const graph = buildCoretrievalGraph(turns, opts);
62
+ const neighbors = graph.get("page-a")!;
63
+ expect(neighbors[0].target).toBe("page-b");
64
+ const bScore = neighbors.find((n) => n.target === "page-b")!.score;
65
+ const cScore = neighbors.find((n) => n.target === "topic-c")?.score ?? -1;
66
+ expect(bScore).toBeGreaterThan(cScore);
67
+ });
68
+
69
+ test("drops pairs below the min-count floor", () => {
70
+ const turns = [
71
+ ["page-a", "page-b"],
72
+ ["page-a", "page-b"], // page-a↔page-b co-occur twice
73
+ ["page-a", "page-c"], // page-a↔page-c co-occur once
74
+ ];
75
+ const strict = buildCoretrievalGraph(turns, { ...opts, minCount: 2 });
76
+ const targets = (strict.get("page-a") ?? []).map((n) => n.target);
77
+ expect(targets).toContain("page-b");
78
+ expect(targets).not.toContain("page-c");
79
+ });
80
+
81
+ test("excludes always-on neighbors above the frequency ratio", () => {
82
+ // topic-hub appears on every turn (always-on); page-b appears on few. With a
83
+ // 0.5 ratio cap, topic-hub is excluded as a neighbor even though it co-occurs.
84
+ const turns = [
85
+ ["page-a", "page-b", "topic-hub"],
86
+ ["page-a", "page-b", "topic-hub"],
87
+ ["page-x", "topic-hub"],
88
+ ["page-y", "topic-hub"],
89
+ ];
90
+ const graph = buildCoretrievalGraph(turns, {
91
+ minCount: 2,
92
+ maxNeighborFreqRatio: 0.5,
93
+ topK: 10,
94
+ });
95
+ const targets = (graph.get("page-a") ?? []).map((n) => n.target);
96
+ expect(targets).toContain("page-b");
97
+ expect(targets).not.toContain("topic-hub");
98
+ });
99
+
100
+ test("caps neighbors at top-K", () => {
101
+ const turns = [
102
+ ["src", "n1", "n2", "n3", "n4"],
103
+ ["src", "n1", "n2", "n3", "n4"],
104
+ ];
105
+ const graph = buildCoretrievalGraph(turns, { ...opts, topK: 2 });
106
+ expect(graph.get("src")!.length).toBe(2);
107
+ });
108
+ });
109
+
110
+ describe("seedCoretrievalEdges", () => {
111
+ function freshDb(): { db: DrizzleDb; sqlite: Database } {
112
+ const sqlite = new Database(":memory:");
113
+ sqlite.run(
114
+ `CREATE TABLE memory_v2_activation_logs (
115
+ id TEXT PRIMARY KEY, mode TEXT NOT NULL, concepts_json TEXT NOT NULL
116
+ )`,
117
+ );
118
+ sqlite.run(
119
+ `CREATE TABLE memory_v3_auto_edges (
120
+ source_slug TEXT NOT NULL, target_slug TEXT NOT NULL,
121
+ weight REAL NOT NULL, last_reinforced_at INTEGER NOT NULL,
122
+ PRIMARY KEY (source_slug, target_slug)
123
+ )`,
124
+ );
125
+ return { db: drizzle(sqlite) as unknown as DrizzleDb, sqlite };
126
+ }
127
+
128
+ function insertRouterTurn(
129
+ sqlite: Database,
130
+ id: string,
131
+ slugs: string[],
132
+ ): void {
133
+ const concepts = slugs.map((slug) => ({ slug, status: "injected" }));
134
+ sqlite.run(
135
+ `INSERT INTO memory_v2_activation_logs (id, mode, concepts_json) VALUES (?, 'router', ?)`,
136
+ [id, JSON.stringify(concepts)],
137
+ );
138
+ }
139
+
140
+ test("persists the co-retrieval graph in the shape aboveThreshold reads", () => {
141
+ const { db, sqlite } = freshDb();
142
+ for (let i = 0; i < 4; i++) {
143
+ insertRouterTurn(sqlite, `t${i}`, ["page-a", "page-b"]);
144
+ }
145
+ // A non-router row must be ignored.
146
+ sqlite.run(
147
+ `INSERT INTO memory_v2_activation_logs (id, mode, concepts_json) VALUES ('x', 'v3_shadow', ?)`,
148
+ [JSON.stringify([{ slug: "noise-a", status: "injected" }])],
149
+ );
150
+
151
+ const result = seedCoretrievalEdges(db, {
152
+ minCount: 2,
153
+ maxNeighborFreqRatio: 1,
154
+ topK: 10,
155
+ seedWeight: 2,
156
+ });
157
+
158
+ expect(result.turnsScanned).toBe(4);
159
+ expect(result.edgesWritten).toBeGreaterThan(0);
160
+
161
+ // Assert via a direct table read (not auto-edges' aboveThreshold, which a
162
+ // sibling test file mocks) so the seed's persistence is verified in
163
+ // isolation. The edge is symmetric: both directions are written.
164
+ const pairs = (
165
+ sqlite
166
+ .query(
167
+ `SELECT source_slug, target_slug FROM memory_v3_auto_edges WHERE weight >= 1`,
168
+ )
169
+ .all() as Array<{ source_slug: string; target_slug: string }>
170
+ ).map((r) => `${r.source_slug}->${r.target_slug}`);
171
+ expect(pairs).toContain("page-a->page-b");
172
+ expect(pairs).toContain("page-b->page-a");
173
+ });
174
+
175
+ test("is idempotent — re-running does not inflate weights", () => {
176
+ const { db, sqlite } = freshDb();
177
+ for (let i = 0; i < 4; i++) {
178
+ insertRouterTurn(sqlite, `t${i}`, ["page-a", "page-b"]);
179
+ }
180
+ const seedOpts = {
181
+ minCount: 2,
182
+ maxNeighborFreqRatio: 1,
183
+ topK: 10,
184
+ seedWeight: 2,
185
+ };
186
+ seedCoretrievalEdges(db, seedOpts);
187
+ seedCoretrievalEdges(db, seedOpts);
188
+
189
+ const row = sqlite
190
+ .query(
191
+ `SELECT weight FROM memory_v3_auto_edges WHERE source_slug = 'page-a' AND target_slug = 'page-b'`,
192
+ )
193
+ .get() as { weight: number };
194
+ expect(row.weight).toBe(2);
195
+ });
196
+
197
+ test("weights edges by NPMI — a stronger association gets a higher weight", () => {
198
+ const { db, sqlite } = freshDb();
199
+ // page-b co-occurs with page-a more than page-c does (both exclusive to
200
+ // page-a), so NPMI(a,b) > NPMI(a,c) > 0; the filler turns lower base rates.
201
+ for (let i = 0; i < 3; i++)
202
+ insertRouterTurn(sqlite, `ab${i}`, ["page-a", "page-b"]);
203
+ for (let i = 0; i < 2; i++)
204
+ insertRouterTurn(sqlite, `ac${i}`, ["page-a", "page-c"]);
205
+ for (let i = 0; i < 5; i++)
206
+ insertRouterTurn(sqlite, `de${i}`, ["topic-d", "topic-e"]);
207
+
208
+ seedCoretrievalEdges(db, {
209
+ minCount: 2,
210
+ maxNeighborFreqRatio: 1,
211
+ topK: 10,
212
+ seedWeight: 2,
213
+ });
214
+
215
+ const weightOf = (s: string, t: string) =>
216
+ (
217
+ sqlite
218
+ .query(
219
+ `SELECT weight FROM memory_v3_auto_edges WHERE source_slug = ? AND target_slug = ?`,
220
+ )
221
+ .get(s, t) as { weight: number } | null
222
+ )?.weight;
223
+ const wAB = weightOf("page-a", "page-b");
224
+ const wAC = weightOf("page-a", "page-c");
225
+ expect(wAC).toBeGreaterThan(0);
226
+ expect(wAB!).toBeGreaterThan(wAC!);
227
+ // NPMI < 1 for a non-exclusive pair, so weight stays below the perfect-
228
+ // association seedWeight.
229
+ expect(wAB!).toBeLessThan(2);
230
+ });
231
+
232
+ test("skips edges with non-positive NPMI (co-occur at or below chance)", () => {
233
+ const { db, sqlite } = freshDb();
234
+ // page-a and page-b are each frequent but seldom together (below chance) =>
235
+ // negative NPMI => skipped. page-a/topic-x co-occur above chance => kept.
236
+ for (let i = 0; i < 4; i++)
237
+ insertRouterTurn(sqlite, `ax${i}`, ["page-a", "topic-x"]);
238
+ for (let i = 0; i < 4; i++)
239
+ insertRouterTurn(sqlite, `by${i}`, ["page-b", "topic-y"]);
240
+ for (let i = 0; i < 2; i++)
241
+ insertRouterTurn(sqlite, `ab${i}`, ["page-a", "page-b"]);
242
+
243
+ seedCoretrievalEdges(db, {
244
+ minCount: 2,
245
+ maxNeighborFreqRatio: 1,
246
+ topK: 10,
247
+ seedWeight: 2,
248
+ });
249
+
250
+ const has = (s: string, t: string) =>
251
+ sqlite
252
+ .query(
253
+ `SELECT 1 FROM memory_v3_auto_edges WHERE source_slug = ? AND target_slug = ?`,
254
+ )
255
+ .get(s, t) !== null;
256
+ expect(has("page-a", "topic-x")).toBe(true);
257
+ expect(has("page-a", "page-b")).toBe(false);
258
+ });
259
+
260
+ test("returns an empty summary when there are no router turns", () => {
261
+ const { db } = freshDb();
262
+ const result = seedCoretrievalEdges(db);
263
+ expect(result).toEqual({
264
+ turnsScanned: 0,
265
+ nodes: 0,
266
+ edgesWritten: 0,
267
+ avgDegree: 0,
268
+ });
269
+ });
270
+ });
@@ -5,9 +5,12 @@
5
5
  * Coverage matrix:
6
6
  * - 1-hop and 2-hop outgoing expansion from a single seed.
7
7
  * - Default hops (2) when omitted.
8
- * - Seed excluded from its own `pulled`.
8
+ * - Seed excluded from its own `pulled`, and from another seed's `pulled`
9
+ * when one seed is reachable from another (seeds-excluded contract).
9
10
  * - Multiple seeds: top-level `pulled` is the union; per-seed expansions
10
11
  * attribute correctly; duplicate seeds collapse.
12
+ * - Per-seed cap spends slots on unique neighbors, not duplicates an earlier
13
+ * seed already pulled (recall at the cap).
11
14
  * - `extraAdjacency` merges with the curated graph during traversal.
12
15
  * - `extraAdjacency` bridges across hops (curated → extra → curated).
13
16
  * - Cycles in the curated graph (and via extraAdjacency) terminate, bounded
@@ -463,6 +466,46 @@ describe("expandEdges — bounds", () => {
463
466
  }
464
467
  });
465
468
 
469
+ test("maxTotalPulls overrides the default union ceiling", async () => {
470
+ // 20 seeds × 32 disjoint targets = 640 reachable, far past any cap. A low
471
+ // per-call maxTotalPulls must bound the union to that value, not the 400
472
+ // default; an omitted or invalid value falls back to the default.
473
+ const seedCount = 20;
474
+ const graph: Record<string, string[]> = {};
475
+ const seeds: string[] = [];
476
+ for (let s = 0; s < seedCount; s++) {
477
+ const seed = topicSlug("people", s);
478
+ const targets: string[] = [];
479
+ for (let t = 0; t < MAX_PULLS_PER_SEED; t++) {
480
+ const target = topicSlug("targets", s * MAX_PULLS_PER_SEED + t);
481
+ targets.push(target);
482
+ graph[target] = [];
483
+ }
484
+ graph[seed] = targets;
485
+ seeds.push(seed);
486
+ }
487
+ await writeGraph(graph);
488
+
489
+ const capped = await expandEdges({
490
+ workspaceDir,
491
+ seeds,
492
+ hops: 1,
493
+ maxTotalPulls: 40,
494
+ });
495
+ expect(capped.pulled.size).toBeLessThanOrEqual(40);
496
+
497
+ // Omitted → the 400 default; an invalid (negative) value also falls back.
498
+ const dflt = await expandEdges({ workspaceDir, seeds, hops: 1 });
499
+ expect(dflt.pulled.size).toBe(MAX_TOTAL_PULLS);
500
+ const invalid = await expandEdges({
501
+ workspaceDir,
502
+ seeds,
503
+ hops: 1,
504
+ maxTotalPulls: -5,
505
+ });
506
+ expect(invalid.pulled.size).toBe(MAX_TOTAL_PULLS);
507
+ });
508
+
466
509
  test("duplicate slugs across seeds don't waste the total budget", async () => {
467
510
  // Two seeds, both pointing at the same shared target plus one private each.
468
511
  // The shared slug is counted once, so the union is 3 — well under the cap,
@@ -492,6 +535,106 @@ describe("expandEdges — bounds", () => {
492
535
  { from: "people/bob", pulled: ["topics/bob-only", "topics/shared"] },
493
536
  ]);
494
537
  });
538
+
539
+ test("a seed reachable from another seed is excluded from pulled", async () => {
540
+ // alice -> bob, and bob is itself a seed. bob is a confident hit in its own
541
+ // right, not a neighbor, so it must never appear in the pulled union — even
542
+ // though alice's outgoing walk reaches it. bob's own private neighbor is
543
+ // still pulled.
544
+ await writeGraph({
545
+ alice: ["bob"],
546
+ bob: ["carol"],
547
+ carol: [],
548
+ });
549
+
550
+ const { pulled, expansions } = await expandEdges({
551
+ workspaceDir,
552
+ seeds: ["alice", "bob"],
553
+ hops: 1,
554
+ });
555
+
556
+ expect(pulled.has("bob")).toBe(false);
557
+ expect([...pulled].sort()).toEqual(["carol"]);
558
+ // alice reaches only bob, which is a seed, so alice contributes nothing.
559
+ expect(expansions).toEqual([
560
+ { from: "alice", pulled: [] },
561
+ { from: "bob", pulled: ["carol"] },
562
+ ]);
563
+ });
564
+
565
+ test("a seed two hops from another seed is still excluded from pulled", async () => {
566
+ // alice -> mid -> bob. With a 2-hop walk alice reaches bob, but bob is a
567
+ // seed and must not leak into pulled; the intermediate `mid` is a genuine
568
+ // neighbor and is kept.
569
+ await writeGraph({
570
+ alice: ["mid"],
571
+ mid: ["bob"],
572
+ bob: [],
573
+ });
574
+
575
+ const { pulled } = await expandEdges({
576
+ workspaceDir,
577
+ seeds: ["alice", "bob"],
578
+ hops: 2,
579
+ });
580
+
581
+ expect(pulled.has("bob")).toBe(false);
582
+ expect([...pulled].sort()).toEqual(["mid"]);
583
+ });
584
+
585
+ test("per-seed cap is spent on unique neighbors, not duplicates", async () => {
586
+ // alice pulls 16 shared `dup/*` neighbors. bob reaches those same 16 dups
587
+ // plus 32 unique `fresh/*` neighbors. bob's per-seed budget is
588
+ // MAX_PULLS_PER_SEED (32) because the union is nowhere near the total cap.
589
+ //
590
+ // The old slice took bob's lexicographically-first 32 reached slugs — the
591
+ // 16 already-pulled dups plus only the first 16 fresh — wasting 16 budget
592
+ // slots on duplicates and dropping the other 16 unique neighbors. Filtering
593
+ // already-pulled slugs before the slice spends all 32 slots on fresh
594
+ // neighbors, so every unique one is retained (recall north star).
595
+ const dupCount = 16;
596
+ const freshCount = MAX_PULLS_PER_SEED; // 32, so old code drops 16 unique.
597
+ const dups: string[] = [];
598
+ const fresh: string[] = [];
599
+ const graph: Record<string, string[]> = {};
600
+ for (let i = 0; i < dupCount; i++) {
601
+ const slug = topicSlug("dup", i);
602
+ dups.push(slug);
603
+ graph[slug] = [];
604
+ }
605
+ for (let i = 0; i < freshCount; i++) {
606
+ const slug = topicSlug("fresh", i);
607
+ fresh.push(slug);
608
+ graph[slug] = [];
609
+ }
610
+ // "dup/*" sorts before "fresh/*", so the old front-of-list slice is exactly
611
+ // the 16 dups + first 16 fresh.
612
+ graph["alice"] = [...dups];
613
+ graph["bob"] = [...dups, ...fresh];
614
+ await writeGraph(graph);
615
+
616
+ const { pulled, expansions } = await expandEdges({
617
+ workspaceDir,
618
+ seeds: ["alice", "bob"],
619
+ hops: 1,
620
+ });
621
+
622
+ // All 32 unique fresh neighbors survive — none dropped to a duplicate slot.
623
+ for (const slug of fresh) expect(pulled.has(slug)).toBe(true);
624
+ // A unique neighbor the old code would have dropped (past the first 16) is
625
+ // now retained.
626
+ expect(pulled.has(topicSlug("fresh", freshCount - 1))).toBe(true);
627
+ // Union is the 16 dups + 32 fresh, all distinct.
628
+ expect(pulled.size).toBe(dupCount + freshCount);
629
+
630
+ // bob's budget (32 fresh slots) is fully spent on unique neighbors; the
631
+ // dups it also reaches stay in its trace for faithful attribution but did
632
+ // not consume the budget.
633
+ const bobExpansion = expansions.find((e) => e.from === "bob")!;
634
+ expect(bobExpansion.pulled).toEqual([...dups, ...fresh].sort());
635
+ // Every trace slug is genuinely in the union.
636
+ for (const slug of bobExpansion.pulled) expect(pulled.has(slug)).toBe(true);
637
+ });
495
638
  });
496
639
 
497
640
  // ---------------------------------------------------------------------------
@@ -171,6 +171,36 @@ describe("filterDenseHits — judged keep/drop", () => {
171
171
  expect(userText).not.toContain("x");
172
172
  });
173
173
 
174
+ test("excludes the full sticky set from judgment, not just bypass", async () => {
175
+ const calls: ProviderCall[] = [];
176
+ // Dense surfaces a, b, plus sticky-but-not-bypass slug `s`. `s` must not be
177
+ // judged (the gate force-injects every sticky slug regardless), so the model
178
+ // only sees a, b. Model keeps a.
179
+ const provider = makeProvider(
180
+ filterToolResponse({ keep_slugs: ["a"] }),
181
+ calls,
182
+ );
183
+
184
+ const result = await filterDenseHits({
185
+ input: makeInput(),
186
+ dense: denseResult(["a", "b", "s"]),
187
+ // sticky is a strict superset of bypass here: `s` is sticky but not bypass.
188
+ sticky: new Set(["s"]),
189
+ bypass: new Set(),
190
+ provider,
191
+ });
192
+
193
+ // `s` is excluded from the judged set even though it is not in bypass.
194
+ expect(result.trace.judged).toEqual(["a", "b"]);
195
+ expect(result.trace.dropped).toEqual(["b"]);
196
+ // The sticky slug `s` was never shown to the model.
197
+ const slugLines = calls[0].messages[0].content
198
+ .map((b) => (b.type === "text" ? b.text : ""))
199
+ .join("\n")
200
+ .split("\n");
201
+ expect(slugLines).not.toContain("s");
202
+ });
203
+
174
204
  test("forces tool_choice on filter_dense_hits and surfaces judged candidates", async () => {
175
205
  const calls: ProviderCall[] = [];
176
206
  const provider = makeProvider(
@@ -306,6 +336,24 @@ describe("filterDenseHits — no LLM call", () => {
306
336
  expect([...result.kept].sort()).toEqual(["x", "y"]);
307
337
  expect(result.trace).toEqual({ judged: [], dropped: [] });
308
338
  });
339
+
340
+ test("dense fully covered by sticky (not bypass) → no call", async () => {
341
+ const provider = makeNeverCalledProvider();
342
+
343
+ // Every dense slug is sticky but none is bypass: there is nothing to judge,
344
+ // so no LLM round-trip happens. kept is the (empty) bypass set — the sticky
345
+ // slugs are force-selected downstream by the gate, not via the filter.
346
+ const result = await filterDenseHits({
347
+ input: makeInput(),
348
+ dense: denseResult(["x", "y"]),
349
+ sticky: new Set(["x", "y"]),
350
+ bypass: new Set(),
351
+ provider,
352
+ });
353
+
354
+ expect(result.kept).toEqual([]);
355
+ expect(result.trace).toEqual({ judged: [], dropped: [] });
356
+ });
309
357
  });
310
358
 
311
359
  describe("filterDenseHits — system prompt", () => {