@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
@@ -2,8 +2,8 @@
2
2
  * Tests for `assistant/src/memory/v3/gate.ts`.
3
3
  *
4
4
  * Coverage matrix:
5
- * - ready + selection → selection maps from candidates, in model order, and
6
- * includes sticky slugs even when the model omits them.
5
+ * - ready + selection → numbered `[N]` candidate ids map back to slugs in
6
+ * model order, and includes sticky slugs even when the model omits them.
7
7
  * - more + questions → `decision.questions` surfaced; selection still returned.
8
8
  * - more with no/blank questions → decision is `{ decision: "more" }` (no
9
9
  * empty `questions` array).
@@ -12,7 +12,7 @@
12
12
  * - provider throws → fail-safe (ready, all candidates).
13
13
  * - missing tool_use block → fail-safe (ready, all candidates).
14
14
  * - tool input failing schema → fail-safe (ready, all candidates).
15
- * - model selecting a slug outside the candidate set → dropped.
15
+ * - model returning an out-of-range id (0 / past the end) → dropped.
16
16
  * - request shape: forced tool_choice on `decide_selection`, candidate set in
17
17
  * the user message, abort signal forwarded.
18
18
  *
@@ -132,7 +132,7 @@ describe("runGate — ready decision", () => {
132
132
  const provider = makeProvider(
133
133
  // Model selects b, a (its own order). Sticky `c` is omitted by the
134
134
  // model but must survive in the final selection.
135
- gateToolResponse({ decision: "ready", selected_slugs: ["b", "a"] }),
135
+ gateToolResponse({ decision: "ready", selected_ids: [2, 1] }),
136
136
  calls,
137
137
  );
138
138
 
@@ -153,7 +153,7 @@ describe("runGate — ready decision", () => {
153
153
  test("forces tool_choice on decide_selection and surfaces candidates", async () => {
154
154
  const calls: ProviderCall[] = [];
155
155
  const provider = makeProvider(
156
- gateToolResponse({ decision: "ready", selected_slugs: ["a"] }),
156
+ gateToolResponse({ decision: "ready", selected_ids: [1] }),
157
157
  calls,
158
158
  );
159
159
 
@@ -172,18 +172,30 @@ describe("runGate — ready decision", () => {
172
172
  });
173
173
  expect(call.options?.config?.callSite).toBe("memoryV3Gate");
174
174
  expect(call.tools?.[0].name).toBe("decide_selection");
175
+ // The output schema is candidate-independent: selected_ids is a plain
176
+ // integer array (no per-candidate enum), so it stays byte-identical per turn.
177
+ const schema = call.tools![0].input_schema as {
178
+ properties: Record<
179
+ string,
180
+ { type?: string; items?: { type?: string; enum?: unknown } }
181
+ >;
182
+ };
183
+ expect(schema.properties.selected_ids?.type).toBe("array");
184
+ expect(schema.properties.selected_ids?.items?.type).toBe("integer");
185
+ expect(schema.properties.selected_ids?.items?.enum).toBeUndefined();
175
186
  const userText = call.messages[0].content
176
187
  .map((b) => (b.type === "text" ? b.text : ""))
177
188
  .join("\n");
178
189
  expect(userText).toContain("NOW-MARKER");
179
- expect(userText).toContain("a");
180
- expect(userText).toContain("b");
190
+ // Candidates are numbered from 1 in candidate order.
191
+ expect(userText).toContain("[1] a");
192
+ expect(userText).toContain("[2] b");
181
193
  });
182
194
 
183
195
  test("includes the just-arrived turn so the selection is query-aware", async () => {
184
196
  const calls: ProviderCall[] = [];
185
197
  const provider = makeProvider(
186
- gateToolResponse({ decision: "ready", selected_slugs: ["a"] }),
198
+ gateToolResponse({ decision: "ready", selected_ids: [1] }),
187
199
  calls,
188
200
  );
189
201
 
@@ -211,10 +223,58 @@ describe("runGate — ready decision", () => {
211
223
  expect(userText).toContain("an earlier reply");
212
224
  });
213
225
 
214
- test("drops a model-selected slug outside the candidate set", async () => {
226
+ test("omitted selected_ids falls back to all candidates (recall-safe)", async () => {
227
+ const calls: ProviderCall[] = [];
228
+ // `ready` verdict with no `selected_ids` field at all. A silent omission
229
+ // must keep everything surfaced, not drop all non-sticky context.
230
+ const provider = makeProvider(
231
+ gateToolResponse({ decision: "ready" }),
232
+ calls,
233
+ );
234
+
235
+ const result = await runGate({
236
+ input: makeInput(),
237
+ candidates: new Set(["frames/example-a", "people/alice", "people/bob"]),
238
+ sticky: new Set(["people/bob"]),
239
+ passNumber: 1,
240
+ provider,
241
+ });
242
+
243
+ expect(result.decision).toEqual({ decision: "ready" });
244
+ expect([...result.selectedSlugs].sort()).toEqual([
245
+ "frames/example-a",
246
+ "people/alice",
247
+ "people/bob",
248
+ ]);
249
+ });
250
+
251
+ test("explicit empty selected_ids selects nothing but sticky", async () => {
252
+ const calls: ProviderCall[] = [];
253
+ // An *explicit* `[]` is the model genuinely choosing nothing — honored as-is
254
+ // (only sticky survives), unlike an omitted field.
255
+ const provider = makeProvider(
256
+ gateToolResponse({ decision: "ready", selected_ids: [] }),
257
+ calls,
258
+ );
259
+
260
+ const result = await runGate({
261
+ input: makeInput(),
262
+ candidates: new Set(["frames/example-a", "people/alice", "people/bob"]),
263
+ sticky: new Set(["people/bob"]),
264
+ passNumber: 1,
265
+ provider,
266
+ });
267
+
268
+ expect(result.decision).toEqual({ decision: "ready" });
269
+ // Only sticky carries over; the model dropped everything else.
270
+ expect(result.selectedSlugs).toEqual(["people/bob"]);
271
+ });
272
+
273
+ test("drops out-of-range ids (0, past the end) without throwing", async () => {
215
274
  const calls: ProviderCall[] = [];
216
275
  const provider = makeProvider(
217
- gateToolResponse({ decision: "ready", selected_slugs: ["a", "ghost"] }),
276
+ // id 1 -> "a"; 0 and 99 are out of range (1-based, only 2 candidates).
277
+ gateToolResponse({ decision: "ready", selected_ids: [1, 0, 99] }),
218
278
  calls,
219
279
  );
220
280
 
@@ -234,7 +294,7 @@ describe("runGate — ready decision", () => {
234
294
  const controller = new AbortController();
235
295
  controller.abort();
236
296
  const provider = makeProvider(
237
- gateToolResponse({ decision: "ready", selected_slugs: ["a"] }),
297
+ gateToolResponse({ decision: "ready", selected_ids: [1] }),
238
298
  calls,
239
299
  );
240
300
 
@@ -257,7 +317,8 @@ describe("runGate — candidate summaries", () => {
257
317
  test("renders candidates as `slug — summary` when summaryBySlug is provided", async () => {
258
318
  const calls: ProviderCall[] = [];
259
319
  const provider = makeProvider(
260
- gateToolResponse({ decision: "ready", selected_slugs: ["a/one"] }),
320
+ // id 1 -> "a/one".
321
+ gateToolResponse({ decision: "ready", selected_ids: [1] }),
261
322
  calls,
262
323
  );
263
324
 
@@ -273,19 +334,20 @@ describe("runGate — candidate summaries", () => {
273
334
  provider,
274
335
  });
275
336
 
276
- // The model still answers in bare slugs (the enum is slug-only).
337
+ // The model answers with the [N] numbers, mapped back to slugs by the gate.
277
338
  expect(result.selectedSlugs).toEqual(["a/one"]);
278
339
  const userText = calls[0].messages[0].content
279
340
  .map((b) => (b.type === "text" ? b.text : ""))
280
341
  .join("\n");
281
- expect(userText).toContain("a/onefirst summary");
282
- expect(userText).toContain("b/twosecond summary");
342
+ // Candidates are numbered `[N] slug summary`, in candidate order.
343
+ expect(userText).toContain("[1] a/onefirst summary");
344
+ expect(userText).toContain("[2] b/two — second summary");
283
345
  });
284
346
 
285
347
  test("falls back to the bare slug when no summary is available", async () => {
286
348
  const calls: ProviderCall[] = [];
287
349
  const provider = makeProvider(
288
- gateToolResponse({ decision: "ready", selected_slugs: [] }),
350
+ gateToolResponse({ decision: "ready", selected_ids: [] }),
289
351
  calls,
290
352
  );
291
353
 
@@ -300,7 +362,7 @@ describe("runGate — candidate summaries", () => {
300
362
  const userText = calls[0].messages[0].content
301
363
  .map((b) => (b.type === "text" ? b.text : ""))
302
364
  .join("\n");
303
- expect(userText).toContain("a/one");
365
+ expect(userText).toContain("[1] a/one");
304
366
  expect(userText).not.toContain("a/one —");
305
367
  });
306
368
  });
@@ -311,7 +373,7 @@ describe("runGate — more decision", () => {
311
373
  const provider = makeProvider(
312
374
  gateToolResponse({
313
375
  decision: "more",
314
- selected_slugs: ["a"],
376
+ selected_ids: [1],
315
377
  questions: ["What is the user's deadline?", "Who else is involved?"],
316
378
  }),
317
379
  calls,
@@ -338,7 +400,7 @@ describe("runGate — more decision", () => {
338
400
  const provider = makeProvider(
339
401
  gateToolResponse({
340
402
  decision: "more",
341
- selected_slugs: ["a"],
403
+ selected_ids: [1],
342
404
  questions: [" ", ""],
343
405
  }),
344
406
  calls,
@@ -360,7 +422,7 @@ describe("runGate — more decision", () => {
360
422
  const provider = makeProvider(
361
423
  gateToolResponse({
362
424
  decision: "more",
363
- selected_slugs: ["a"],
425
+ selected_ids: [1],
364
426
  questions: ["follow-up?"],
365
427
  }),
366
428
  calls,
@@ -382,7 +444,7 @@ describe("runGate — system prompt", () => {
382
444
  test("uses the bundled default when no override is configured", async () => {
383
445
  const calls: ProviderCall[] = [];
384
446
  const provider = makeProvider(
385
- gateToolResponse({ decision: "ready", selected_slugs: ["a"] }),
447
+ gateToolResponse({ decision: "ready", selected_ids: [1] }),
386
448
  calls,
387
449
  );
388
450
 
@@ -400,7 +462,7 @@ describe("runGate — system prompt", () => {
400
462
  test("uses the configured inline override as the system prompt", async () => {
401
463
  const calls: ProviderCall[] = [];
402
464
  const provider = makeProvider(
403
- gateToolResponse({ decision: "ready", selected_slugs: ["a"] }),
465
+ gateToolResponse({ decision: "ready", selected_ids: [1] }),
404
466
  calls,
405
467
  );
406
468
 
@@ -468,10 +530,7 @@ describe("runGate — fail-safe", () => {
468
530
  sticky: new Set(),
469
531
  passNumber: 1,
470
532
  // `decision` is required; missing it fails the Zod schema.
471
- provider: makeProvider(
472
- gateToolResponse({ selected_slugs: ["a"] }),
473
- calls,
474
- ),
533
+ provider: makeProvider(gateToolResponse({ selected_ids: [1] }), calls),
475
534
  });
476
535
 
477
536
  expect(result.decision).toEqual({ decision: "ready" });
@@ -484,7 +543,7 @@ describe("runGate — capture", () => {
484
543
  const calls: ProviderCall[] = [];
485
544
  const captured: Omit<LlmCallRecord, "pass">[] = [];
486
545
  const provider = makeProvider(
487
- gateToolResponse({ decision: "ready", selected_slugs: ["a"] }),
546
+ gateToolResponse({ decision: "ready", selected_ids: [1] }),
488
547
  calls,
489
548
  );
490
549
 
@@ -526,7 +585,7 @@ describe("runGate — reasoning field", () => {
526
585
  test("exposes an optional reasoning property in the forced tool schema", async () => {
527
586
  const calls: ProviderCall[] = [];
528
587
  const provider = makeProvider(
529
- gateToolResponse({ decision: "ready", selected_slugs: ["a"] }),
588
+ gateToolResponse({ decision: "ready", selected_ids: [1] }),
530
589
  calls,
531
590
  );
532
591
 
@@ -547,12 +606,12 @@ describe("runGate — reasoning field", () => {
547
606
  expect(schema.required ?? []).not.toContain("reasoning");
548
607
  });
549
608
 
550
- test("accepts model-supplied reasoning without altering the decision", async () => {
609
+ test("surfaces model-supplied reasoning on the decision without altering the verdict", async () => {
551
610
  const calls: ProviderCall[] = [];
552
611
  const provider = makeProvider(
553
612
  gateToolResponse({
554
613
  decision: "ready",
555
- selected_slugs: ["b", "a"],
614
+ selected_ids: [2, 1],
556
615
  reasoning: "kept the two query-relevant pages, dropped the rest",
557
616
  }),
558
617
  calls,
@@ -566,9 +625,13 @@ describe("runGate — reasoning field", () => {
566
625
  provider,
567
626
  });
568
627
 
569
- // Reasoning is ignored by control flow: decision + ordered selection
570
- // (model order, omitted sticky appended) are unchanged.
571
- expect(result.decision).toEqual({ decision: "ready" });
628
+ // Reasoning does not alter control flow the verdict and ordered selection
629
+ // (model order, omitted sticky appended) are unchanged — but it IS carried
630
+ // on the decision so a run can be analyzed (trace + shadow telemetry).
631
+ expect(result.decision).toEqual({
632
+ decision: "ready",
633
+ reasoning: "kept the two query-relevant pages, dropped the rest",
634
+ });
572
635
  expect(result.selectedSlugs).toEqual(["b", "a", "c"]);
573
636
  });
574
637
  });
@@ -230,4 +230,62 @@ describe("composeNodeIndex", () => {
230
230
 
231
231
  expect(composeNodeIndex("root", tree, pageIndex([]))).toBe("[node:empty]");
232
232
  });
233
+
234
+ test("collapses a multi-line node summary to a single index line", () => {
235
+ const tree = treeIndex(
236
+ [
237
+ treeNode("root", {}),
238
+ treeNode("multiline", {
239
+ summary: "First sentence.\nSecond sentence.\n Third.",
240
+ }),
241
+ ],
242
+ { root: [{ kind: "node", ref: "multiline" }] },
243
+ );
244
+
245
+ const block = composeNodeIndex("root", tree, pageIndex([]));
246
+
247
+ expect(block).toBe(
248
+ "[node:multiline] First sentence. Second sentence. Third.",
249
+ );
250
+ expect(block.split("\n")).toHaveLength(1);
251
+ });
252
+
253
+ test("collapses a multi-line page summary to a single index line", () => {
254
+ const tree = treeIndex([treeNode("root", {})], {
255
+ root: [{ kind: "page", ref: "notes" }],
256
+ });
257
+ const pages = pageIndex([
258
+ pageEntry("notes", "Line one\nLine two\n\nLine three"),
259
+ ]);
260
+
261
+ const block = composeNodeIndex("root", tree, pages);
262
+
263
+ expect(block).toBe("[page:notes] Line one Line two Line three");
264
+ expect(block.split("\n")).toHaveLength(1);
265
+ });
266
+
267
+ test("a multi-line summary cannot inject extra index lines", () => {
268
+ // A summary crafted to look like another child entry must not become its
269
+ // own parsed line once collapsed.
270
+ const tree = treeIndex(
271
+ [
272
+ treeNode("root", {}),
273
+ treeNode("a", { summary: "Real summary\n[node:injected] fake entry" }),
274
+ treeNode("b", { summary: "Sibling" }),
275
+ ],
276
+ {
277
+ root: [
278
+ { kind: "node", ref: "a" },
279
+ { kind: "node", ref: "b" },
280
+ ],
281
+ },
282
+ );
283
+
284
+ const block = composeNodeIndex("root", tree, pageIndex([]));
285
+
286
+ expect(block.split("\n")).toEqual([
287
+ "[node:a] Real summary [node:injected] fake entry",
288
+ "[node:b] Sibling",
289
+ ]);
290
+ });
233
291
  });
@@ -86,12 +86,20 @@ const lane = {
86
86
  /** Records the args the loop passed each lane, one entry per call. */
87
87
  const laneCalls = {
88
88
  scouts: [] as Array<{ nowText: string }>,
89
- filter: [] as Array<{ nowText: string; dense: ScoutResult }>,
89
+ filter: [] as Array<{
90
+ nowText: string;
91
+ dense: ScoutResult;
92
+ sticky: string[];
93
+ bypass: string[];
94
+ }>,
90
95
  walk: [] as Array<{
91
96
  nowText: string;
92
97
  scouts: ScoutResult[];
93
98
  }>,
94
- edges: [] as Array<{ seeds: string[] }>,
99
+ edges: [] as Array<{
100
+ seeds: string[];
101
+ extraAdjacency?: ReadonlyMap<string, ReadonlySet<string>>;
102
+ }>,
95
103
  gate: [] as Array<{
96
104
  nowText: string;
97
105
  passNumber: number;
@@ -110,6 +118,14 @@ let walkCallCount = 0;
110
118
  let edgeCallCount = 0;
111
119
  let gateCallCount = 0;
112
120
 
121
+ // Learned-adjacency mock state: what `aboveThreshold` returns, and a recorder of
122
+ // the thresholds the loop read it at (empty when the loop never reads it).
123
+ let aboveThresholdCalls: Array<{ threshold: number }> = [];
124
+ let learnedAdjacencyResult: ReadonlyMap<
125
+ string,
126
+ ReadonlySet<string>
127
+ > = new Map();
128
+
113
129
  mock.module("../scouts.js", () => ({
114
130
  runScouts: async (input: RetrievalInput): Promise<RunScoutsResult> => {
115
131
  laneCalls.scouts.push({ nowText: input.nowText });
@@ -121,8 +137,15 @@ mock.module("../filter.js", () => ({
121
137
  filterDenseHits: async (args: {
122
138
  input: RetrievalInput;
123
139
  dense: ScoutResult;
140
+ sticky: Set<string>;
141
+ bypass: Set<string>;
124
142
  }): Promise<FilterResult> => {
125
- laneCalls.filter.push({ nowText: args.input.nowText, dense: args.dense });
143
+ laneCalls.filter.push({
144
+ nowText: args.input.nowText,
145
+ dense: args.dense,
146
+ sticky: [...args.sticky],
147
+ bypass: [...args.bypass],
148
+ });
126
149
  // Filter calls share the scout pass index (one filter call per dense pass).
127
150
  return nextOf(lane.filter, laneCalls.filter.length - 1);
128
151
  },
@@ -144,12 +167,23 @@ mock.module("../tree-walk.js", () => ({
144
167
  mock.module("../edges.js", () => ({
145
168
  expandEdges: async (args: {
146
169
  seeds: Iterable<string>;
170
+ extraAdjacency?: ReadonlyMap<string, ReadonlySet<string>>;
147
171
  }): Promise<ExpandResult> => {
148
- laneCalls.edges.push({ seeds: [...args.seeds] });
172
+ laneCalls.edges.push({
173
+ seeds: [...args.seeds],
174
+ extraAdjacency: args.extraAdjacency,
175
+ });
149
176
  return nextOf(lane.edges, edgeCallCount++);
150
177
  },
151
178
  }));
152
179
 
180
+ mock.module("../auto-edges.js", () => ({
181
+ aboveThreshold: (_db: unknown, threshold: number) => {
182
+ aboveThresholdCalls.push({ threshold });
183
+ return learnedAdjacencyResult;
184
+ },
185
+ }));
186
+
153
187
  mock.module("../gate.js", () => ({
154
188
  runGate: async (args: {
155
189
  input: RetrievalInput;
@@ -213,6 +247,7 @@ function makeInput(opts?: {
213
247
  nowText?: string;
214
248
  passCap?: number;
215
249
  lanes?: LaneConfig;
250
+ edgesThreshold?: number;
216
251
  }): RetrievalInput {
217
252
  const lanes = {
218
253
  hot: true,
@@ -228,7 +263,13 @@ function makeInput(opts?: {
228
263
  nowText: opts?.nowText ?? "NOW",
229
264
  priorEverInjected: [],
230
265
  config: {
231
- memory: { v3: { passCap: opts?.passCap ?? 3, lanes } },
266
+ memory: {
267
+ v3: {
268
+ passCap: opts?.passCap ?? 3,
269
+ lanes,
270
+ edges: { learnedAdjacencyThreshold: opts?.edgesThreshold ?? 0 },
271
+ },
272
+ },
232
273
  } as unknown as RetrievalInput["config"],
233
274
  };
234
275
  }
@@ -260,6 +301,8 @@ function reset(): void {
260
301
  walkCallCount = 0;
261
302
  edgeCallCount = 0;
262
303
  gateCallCount = 0;
304
+ aboveThresholdCalls = [];
305
+ learnedAdjacencyResult = new Map();
263
306
  }
264
307
 
265
308
  beforeEach(reset);
@@ -405,6 +448,46 @@ describe("runRetrievalLoop — multi pass", () => {
405
448
  expect(out.selectedSlugs).toEqual(["p1", "p2"]);
406
449
  });
407
450
 
451
+ test("candidates accumulate across passes so the final gate sees pass-1 hits", async () => {
452
+ // Each pass surfaces a distinct dense hit. Without cross-pass accumulation
453
+ // the pass-2 gate would only see "p2"; with it, the cumulative pool carries
454
+ // pass-1's "p1" into the final gate input.
455
+ lane.scouts = [
456
+ {
457
+ scouts: [scout("dense", ["p1"])],
458
+ sticky: new Set(),
459
+ bypass: new Set(),
460
+ },
461
+ {
462
+ scouts: [scout("dense", ["p2"])],
463
+ sticky: new Set(),
464
+ bypass: new Set(),
465
+ },
466
+ ];
467
+ lane.filter = [
468
+ { kept: ["p1"], trace: { judged: ["p1"], dropped: [] } },
469
+ { kept: ["p2"], trace: { judged: ["p2"], dropped: [] } },
470
+ ];
471
+ lane.walk = [
472
+ { pages: new Set(), levels: [] },
473
+ { pages: new Set(), levels: [] },
474
+ ];
475
+ lane.edges = [
476
+ { pulled: new Set(), expansions: [] },
477
+ { pulled: new Set(), expansions: [] },
478
+ ];
479
+ lane.gate = [moreGate(["p1"], ["more?"]), readyGate(["p1", "p2"])];
480
+
481
+ const out = await runRetrievalLoop(makeInput({ passCap: 3 }), { db });
482
+
483
+ // Pass 1's gate saw only p1; pass 2's gate saw the cumulative pool.
484
+ expect(laneCalls.gate[0].candidates).toEqual(["p1"]);
485
+ expect(laneCalls.gate[1].candidates).toEqual(
486
+ expect.arrayContaining(["p1", "p2"]),
487
+ );
488
+ expect(out.selectedSlugs).toEqual(["p1", "p2"]);
489
+ });
490
+
408
491
  test("passCap force-exits with the current selection when the gate keeps asking for more", async () => {
409
492
  lane.scouts = [
410
493
  { scouts: [scout("dense", ["p"])], sticky: new Set(), bypass: new Set() },
@@ -492,6 +575,125 @@ describe("runRetrievalLoop — lane toggles", () => {
492
575
  });
493
576
  });
494
577
 
578
+ describe("runRetrievalLoop — sourceBySlug lane trust", () => {
579
+ test("upgrades a slug's lane when a higher-trust lane re-surfaces it in one pass", async () => {
580
+ // `shared` is surfaced by the low-trust hot lane AND survives the
581
+ // higher-trust dense filter in the same pass. Provenance must end on the
582
+ // more trusted lane (dense) so the downstream seed cap ranks it by its
583
+ // strongest signal, not the stale first-seen hot lane.
584
+ lane.scouts = [
585
+ {
586
+ scouts: [scout("hot", ["shared"]), scout("dense", ["shared"])],
587
+ sticky: new Set(),
588
+ bypass: new Set(),
589
+ },
590
+ ];
591
+ lane.filter = [
592
+ { kept: ["shared"], trace: { judged: ["shared"], dropped: [] } },
593
+ ];
594
+ lane.walk = [{ pages: new Set(), levels: [] }];
595
+ lane.edges = [{ pulled: new Set(), expansions: [] }];
596
+ lane.gate = [readyGate(["shared"])];
597
+
598
+ const out = await runRetrievalLoop(makeInput(), { db });
599
+
600
+ // hot tagged first, then upgraded to the more trusted dense lane.
601
+ expect(out.sourceBySlug.get("shared")).toBe("dense");
602
+ });
603
+
604
+ test("does not downgrade a slug's lane when a lower-trust lane re-surfaces it", async () => {
605
+ // `s` is a dense survivor (trusted) in pass 1, then edge-pulled (lowest
606
+ // trust) in pass 2. The lane must stay dense — only upgrades apply.
607
+ lane.scouts = [
608
+ {
609
+ scouts: [scout("dense", ["s"])],
610
+ sticky: new Set(),
611
+ bypass: new Set(),
612
+ },
613
+ { scouts: [], sticky: new Set(), bypass: new Set() },
614
+ ];
615
+ lane.filter = [
616
+ { kept: ["s"], trace: { judged: ["s"], dropped: [] } },
617
+ { kept: [], trace: { judged: [], dropped: [] } },
618
+ ];
619
+ lane.walk = [
620
+ { pages: new Set(), levels: [] },
621
+ { pages: new Set(), levels: [] },
622
+ ];
623
+ // Pass 2's edge expansion re-pulls the already-dense `s`.
624
+ lane.edges = [
625
+ { pulled: new Set(), expansions: [] },
626
+ { pulled: new Set(["s"]), expansions: [{ from: "s", pulled: ["s"] }] },
627
+ ];
628
+ lane.gate = [moreGate(["s"], ["more?"]), readyGate(["s"])];
629
+
630
+ const out = await runRetrievalLoop(makeInput({ passCap: 2 }), { db });
631
+
632
+ expect(out.sourceBySlug.get("s")).toBe("dense");
633
+ });
634
+
635
+ test("upgrades the lane across passes while preserving earliest-pass provenance", async () => {
636
+ // `s` first enters via the low-trust edge lane on pass 1, then is surfaced
637
+ // by the higher-trust dense lane on pass 2. The lane upgrades to dense, but
638
+ // the co-activation provenance must still treat it as a pass-1 hit: the lane
639
+ // upgrades, the first-seen pass does not.
640
+ lane.scouts = [
641
+ { scouts: [], sticky: new Set(), bypass: new Set() },
642
+ {
643
+ scouts: [scout("dense", ["s"])],
644
+ sticky: new Set(),
645
+ bypass: new Set(),
646
+ },
647
+ ];
648
+ // Pass 1 has no dense scout, so the filter is called only once (pass 2);
649
+ // the mock indexes filter results by call count, so the single entry keeps `s`.
650
+ lane.filter = [{ kept: ["s"], trace: { judged: ["s"], dropped: [] } }];
651
+ lane.walk = [
652
+ { pages: new Set(), levels: [] },
653
+ { pages: new Set(), levels: [] },
654
+ ];
655
+ lane.edges = [
656
+ { pulled: new Set(["s"]), expansions: [{ from: "x", pulled: ["s"] }] },
657
+ { pulled: new Set(), expansions: [] },
658
+ ];
659
+ lane.gate = [moreGate([], ["more?"]), readyGate(["s"])];
660
+
661
+ const out = await runRetrievalLoop(makeInput({ passCap: 2 }), { db });
662
+
663
+ // Lane upgraded from edge (pass 1) to dense (pass 2).
664
+ expect(out.sourceBySlug.get("s")).toBe("dense");
665
+ });
666
+ });
667
+
668
+ describe("runRetrievalLoop — sticky excluded from dense filter", () => {
669
+ test("forwards the full sticky set to the filter while keeping sticky in the selection", async () => {
670
+ // `sk` is sticky (and a dense hit); `keep` is a plain dense candidate. The
671
+ // loop must hand the filter the full sticky set so it can exclude `sk` from
672
+ // judgment — `sk` is force-selected by the gate regardless. `keep` is
673
+ // judged and kept normally.
674
+ lane.scouts = [
675
+ {
676
+ scouts: [scout("dense", ["sk", "keep"])],
677
+ sticky: new Set(["sk"]),
678
+ bypass: new Set(),
679
+ },
680
+ ];
681
+ lane.filter = [
682
+ { kept: ["keep"], trace: { judged: ["keep"], dropped: [] } },
683
+ ];
684
+ lane.walk = [{ pages: new Set(), levels: [] }];
685
+ lane.edges = [{ pulled: new Set(), expansions: [] }];
686
+ lane.gate = [readyGate(["sk", "keep"])];
687
+
688
+ const out = await runRetrievalLoop(makeInput(), { db });
689
+
690
+ // The loop forwarded the full sticky set to the filter so it can subtract it.
691
+ expect(laneCalls.filter[0].sticky).toEqual(["sk"]);
692
+ // The sticky slug is still in the final selection (gate force-injects it).
693
+ expect(out.selectedSlugs).toEqual(expect.arrayContaining(["sk", "keep"]));
694
+ });
695
+ });
696
+
495
697
  describe("runRetrievalLoop — failure + cost", () => {
496
698
  test("surfaces a filter failureReason on the output", async () => {
497
699
  lane.scouts = [
@@ -528,3 +730,46 @@ describe("runRetrievalLoop — failure + cost", () => {
528
730
  expect(out.cost?.ms).toBeGreaterThanOrEqual(0);
529
731
  });
530
732
  });
733
+
734
+ describe("runRetrievalLoop — learned edge adjacency", () => {
735
+ /** Minimal single-pass fixture; the edge lane is the subject under test. */
736
+ function singlePassFixture(): void {
737
+ lane.scouts = [
738
+ {
739
+ scouts: [scout("hot", ["seed-x"])],
740
+ sticky: new Set(),
741
+ bypass: new Set(),
742
+ },
743
+ ];
744
+ lane.filter = [{ kept: [], trace: { judged: [], dropped: [] } }];
745
+ lane.walk = [{ pages: new Set(), levels: [] }];
746
+ lane.edges = [
747
+ {
748
+ pulled: new Set(["learned-y"]),
749
+ expansions: [{ from: "seed-x", pulled: ["learned-y"] }],
750
+ },
751
+ ];
752
+ lane.gate = [readyGate(["seed-x", "learned-y"])];
753
+ }
754
+
755
+ test("reads and merges learned adjacency into edge expansion when threshold > 0", async () => {
756
+ singlePassFixture();
757
+ learnedAdjacencyResult = new Map([["seed-x", new Set(["learned-y"])]]);
758
+
759
+ await runRetrievalLoop(makeInput({ edgesThreshold: 1.5 }), { db });
760
+
761
+ // The loop reads the learned graph once at the configured threshold and
762
+ // hands it to the edge lane as extraAdjacency.
763
+ expect(aboveThresholdCalls).toEqual([{ threshold: 1.5 }]);
764
+ expect(laneCalls.edges[0].extraAdjacency).toBe(learnedAdjacencyResult);
765
+ });
766
+
767
+ test("does not read or pass learned adjacency when the threshold is 0", async () => {
768
+ singlePassFixture();
769
+
770
+ await runRetrievalLoop(makeInput({ edgesThreshold: 0 }), { db });
771
+
772
+ expect(aboveThresholdCalls).toHaveLength(0);
773
+ expect(laneCalls.edges[0].extraAdjacency).toBeUndefined();
774
+ });
775
+ });