@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
@@ -35,6 +35,16 @@ import type { TreeNode } from "./types.js";
35
35
  /** Trailer label introducing a node's own routing hints. */
36
36
  const ROUTING_HINTS_LABEL = "Routing hints:";
37
37
 
38
+ /**
39
+ * Flatten a summary to a single line. The index is parsed by the descender as
40
+ * one child per line, so any embedded newline (or other whitespace run) in a
41
+ * summary would corrupt that format — collapse every whitespace run to a single
42
+ * space and trim the ends.
43
+ */
44
+ function collapseToSingleLine(text: string): string {
45
+ return text.replace(/\s+/g, " ").trim();
46
+ }
47
+
38
48
  /**
39
49
  * Resolve a node's display summary: its frontmatter `summary` if non-empty,
40
50
  * otherwise the first non-empty line of its body, otherwise the empty string.
@@ -54,9 +64,11 @@ function nodeSummary(node: TreeNode): string {
54
64
  * Render one child ref into its index line, or `null` when the ref's target is
55
65
  * absent from the supplied indices (validation owns reporting those).
56
66
  *
57
- * A resolvable `node:` child always yields a line — its header (`[node:<id>]`)
58
- * with a trailing summary when one exists. A `page:` child yields
59
- * `[page:<slug>] <summary>`; the v2 page index already truncates `summary`.
67
+ * A resolvable child always yields a line — its header (`[node:<id>]` /
68
+ * `[page:<slug>]`) with a trailing summary when one exists. Each summary is
69
+ * collapsed to a single line so an embedded newline can't break the one-child-
70
+ * per-line format the descender parses; the v2 page index already truncates
71
+ * `page:` summaries.
60
72
  */
61
73
  function renderChild(
62
74
  kind: "page" | "node",
@@ -67,12 +79,13 @@ function renderChild(
67
79
  if (kind === "node") {
68
80
  const child = tree.nodes.get(ref);
69
81
  if (!child) return null;
70
- const summary = nodeSummary(child);
82
+ const summary = collapseToSingleLine(nodeSummary(child));
71
83
  return summary ? `[node:${ref}] ${summary}` : `[node:${ref}]`;
72
84
  }
73
85
  const entry = pages.bySlug.get(ref);
74
86
  if (!entry) return null;
75
- return `[page:${ref}] ${entry.summary}`;
87
+ const summary = collapseToSingleLine(entry.summary);
88
+ return summary ? `[page:${ref}] ${summary}` : `[page:${ref}]`;
76
89
  }
77
90
 
78
91
  /**
@@ -36,12 +36,14 @@
36
36
  * {@link runScouts}. Toggling a lane off removes its contribution from the
37
37
  * candidate set so the offline harness can measure each lane's marginal recall.
38
38
  *
39
- * Cross-pass accumulation. A `visited` candidate accumulator deduplicates slugs
40
- * across passes by canonical slug, tagging each with the first lane that
41
- * surfaced it (`sourceBySlug`). The full {@link DescentTrace} carries one
42
- * {@link DescentPass} per pass (scouts / treeLevels / edgeExpansions / gate),
43
- * and {@link RetrievalCost} (wall-clock `ms`, the one dimension observable at
44
- * this composition layer) accumulates across every pass.
39
+ * Cross-pass accumulation. The `candidates` pool is unioned across every pass
40
+ * and the gate judges that cumulative pool, so a multi-pass `more` never drops
41
+ * the non-sticky hits earlier passes surfaced. Each slug is tagged with the
42
+ * most trusted lane that surfaced it (`sourceBySlug`). The full
43
+ * {@link DescentTrace}
44
+ * carries one {@link DescentPass} per pass (scouts / treeLevels /
45
+ * edgeExpansions / gate), and {@link RetrievalCost} (wall-clock `ms`, the one
46
+ * dimension observable at this composition layer) accumulates across every pass.
45
47
  */
46
48
 
47
49
  import { getLogger } from "../../util/logger.js";
@@ -57,6 +59,7 @@ import type {
57
59
  GateDecision,
58
60
  } from "../v2/harness/trace.js";
59
61
  import { getPageIndex } from "../v2/page-index.js";
62
+ import { aboveThreshold } from "./auto-edges.js";
60
63
  import {
61
64
  type CoactivationRow,
62
65
  recordCoactivations,
@@ -111,8 +114,22 @@ export async function runRetrievalLoop(
111
114
  const passCap = Math.max(1, v3.passCap);
112
115
  const lanes = v3.lanes;
113
116
 
117
+ // Learned co-retrieval adjacency (memory_v3_auto_edges), read once and merged
118
+ // into the edge lane's curated graph when the threshold is set. At threshold 0
119
+ // (the default) this is undefined and edge behavior is identical to before.
120
+ const learnedAdjacencyThreshold = v3.edges?.learnedAdjacencyThreshold ?? 0;
121
+ const learnedAdjacency =
122
+ learnedAdjacencyThreshold > 0
123
+ ? aboveThreshold(deps.db, learnedAdjacencyThreshold)
124
+ : undefined;
125
+
114
126
  // Cross-pass accumulators.
115
127
  const sourceBySlug = new Map<string, LaneSource>();
128
+ // Candidate pool unioned across every pass. Each pass adds its own surfaced
129
+ // slugs (hot/sparse, dense-filter survivors, tree, edge) and the gate judges
130
+ // the cumulative pool, so a multi-pass `more` never discards earlier passes'
131
+ // non-sticky hits.
132
+ const candidates = new Set<string>();
116
133
  // The first pass each slug entered the candidate set. Drives co-activation
117
134
  // emission below — pass-1 hits (gap source) vs. later-surfaced pages (target).
118
135
  const firstPassBySlug = new Map<string, number>();
@@ -148,18 +165,19 @@ export async function runRetrievalLoop(
148
165
  const scoutResult = await runScouts(passInput, { db: deps.db });
149
166
  for (const slug of scoutResult.sticky) sticky.add(slug);
150
167
 
151
- // Tag hot + sparse scout hits with their lane (first lane wins). Dense
152
- // slugs are tagged only if they survive the filter below — a dropped dense
153
- // near-neighbor never enters the candidate set, so it earns no source tag.
168
+ // Tag hot + sparse scout hits with their lane (most trusted lane wins
169
+ // see tagSlug). Dense slugs are tagged only if they survive the filter
170
+ // below — a dropped dense near-neighbor never enters the candidate set, so
171
+ // it earns no source tag.
154
172
  for (const scout of scoutResult.scouts) {
155
173
  if (scout.lane === "dense") continue;
156
174
  for (const slug of scout.slugs) tagSlug(sourceBySlug, slug, scout.lane);
157
175
  }
158
176
 
159
- // 2. Dense filter — judges only the dense lane (hot/sparse bypass it). The
160
- // surviving dense slugs replace the raw dense candidates in the running set.
177
+ // 2. Dense filter — judges only the dense lane (hot/sparse bypass it). Only
178
+ // the surviving dense slugs enter the candidate pool; a dropped dense
179
+ // near-neighbor never joins it (and so never reaches the gate).
161
180
  const denseScout = scoutResult.scouts.find((s) => s.lane === "dense");
162
- const candidates = new Set<string>();
163
181
 
164
182
  // Hot + sparse lane hits enter the candidate set directly.
165
183
  for (const scout of scoutResult.scouts) {
@@ -218,6 +236,11 @@ export async function runRetrievalLoop(
218
236
  // hot) so the seed cap spends its budget on query-relevant seeds, not
219
237
  // recency. `sourceBySlug` holds each candidate's first-seen lane.
220
238
  laneBySlug: sourceBySlug,
239
+ // Merge the learned co-retrieval graph with the curated edges when
240
+ // enabled (undefined = curated-only, the default).
241
+ ...(learnedAdjacency ? { extraAdjacency: learnedAdjacency } : {}),
242
+ // Cap the lane's contribution to the gate pool (default 400).
243
+ maxTotalPulls: v3.edges?.maxPulls,
221
244
  });
222
245
  edgeExpansions = expansion.expansions;
223
246
  for (const slug of expansion.pulled) {
@@ -354,17 +377,42 @@ function emitCoactivations(args: {
354
377
  }
355
378
 
356
379
  /**
357
- * Tag `slug`'s provenance with `lane`, keeping the first lane that surfaced it.
358
- * The pass order (scouts tree edge) gives a deterministic precedence: a
359
- * slug first seen by a scout lane keeps that label even when the tree or edge
360
- * lane re-surfaces it.
380
+ * Lane-trust order for `sourceBySlug` provenance (lower = more trusted). A slug
381
+ * surfaced by several lanes is tagged with the most trusted one. Mirrors
382
+ * `SEED_LANE_RANK` in {@link expandEdges}'s module, the downstream consumer that
383
+ * ranks seeds by this tag before the candidate cap: LLM-vetted tree/dense seeds
384
+ * rank above lexical sparse, recency-only hot, and associative edge pulls.
385
+ * Keeping the two orders aligned ensures the upgrade picks the lane the cap
386
+ * actually trusts. Any lane absent here (or an edge pull) ranks last.
387
+ */
388
+ const LANE_TRUST_RANK: Readonly<Record<LaneSource, number>> = {
389
+ tree: 0,
390
+ dense: 1,
391
+ sparse: 2,
392
+ hot: 3,
393
+ edge: 4,
394
+ };
395
+
396
+ /**
397
+ * Tag `slug`'s provenance with `lane`, upgrading to the most trusted lane that
398
+ * surfaces it (see {@link LANE_TRUST_RANK}). A slug first seen via a low-trust
399
+ * lane (e.g. `edge`) is relabeled when a higher-trust lane (e.g. `dense`) also
400
+ * surfaces it, so the downstream seed cap ranks it by its strongest signal
401
+ * rather than a stale first-seen lane. Pass provenance (`firstPassBySlug`) is
402
+ * tracked separately and keeps earliest-pass semantics — only the lane upgrades.
361
403
  */
362
404
  function tagSlug(
363
405
  sourceBySlug: Map<string, LaneSource>,
364
406
  slug: string,
365
407
  lane: LaneSource,
366
408
  ): void {
367
- if (!sourceBySlug.has(slug)) sourceBySlug.set(slug, lane);
409
+ const current = sourceBySlug.get(slug);
410
+ if (
411
+ current === undefined ||
412
+ LANE_TRUST_RANK[lane] < LANE_TRUST_RANK[current]
413
+ ) {
414
+ sourceBySlug.set(slug, lane);
415
+ }
368
416
  }
369
417
 
370
418
  /**
@@ -361,11 +361,22 @@ function applyQuotaAndMmr(
361
361
  function mmrReorder(pool: readonly ScoredSlug[], lambda: number): ScoredSlug[] {
362
362
  if (pool.length <= 1) return [...pool];
363
363
 
364
- // Normalize relevance to [0, 1] by the pool max so it shares a scale with the
365
- // redundancy term (also [0, 1]). All-zero scores collapse to pure diversity.
364
+ // Normalize relevance to [0, 1] so it shares a scale with the redundancy term
365
+ // (also [0, 1]). The pool is score-descending, so `pool[0]` is the max and the
366
+ // last entry is the min. When the max is positive we divide by it — the
367
+ // healthy case, kept exactly as-is. When every cosine is <= 0 (all candidates
368
+ // weakly/negatively similar) dividing by the max would collapse relevance to a
369
+ // single value and degrade ranking to diversity-only; instead normalize from
370
+ // the observed min..max range so the relevance ordering is preserved. A
371
+ // zero-width range (all scores equal) has no gradient to preserve, so it
372
+ // collapses to pure diversity.
366
373
  const maxScore = pool[0].score;
367
- const relevance = (hit: ScoredSlug): number =>
368
- maxScore > 0 ? hit.score / maxScore : 0;
374
+ const minScore = pool[pool.length - 1].score;
375
+ const range = maxScore - minScore;
376
+ const relevance = (hit: ScoredSlug): number => {
377
+ if (maxScore > 0) return hit.score / maxScore;
378
+ return range > 0 ? (hit.score - minScore) / range : 0;
379
+ };
369
380
 
370
381
  const remaining = [...pool];
371
382
  const selected: ScoredSlug[] = [];
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Memory v3 shadow-diff — compare the v3 shadow selection against the live v2
3
+ * router selection, turn-for-turn, from the `memory_v2_activation_logs` table.
4
+ *
5
+ * When v3 runs in shadow mode it logs its per-turn selection as a `v3_shadow`
6
+ * row while the live v2 router logs its own selection as a `router` row on the
7
+ * same turn. This module pairs the two and reports where they agree, what v3
8
+ * surfaced that v2 did not, and what v2 had that v3 dropped — broken down by the
9
+ * v3 provenance lane so a shadow run is analyzable by where its recall comes
10
+ * from.
11
+ *
12
+ * Pairing is by timestamp, NOT by the `turn` column: the shadow middleware logs
13
+ * the orchestrator's per-runtime turn counter while v2 logs the cumulative
14
+ * conversation turn, so the two numbers diverge. The shadow row and its sibling
15
+ * router row are written within the same turn (a second or two apart), so each
16
+ * shadow row is matched to the nearest router row in the same conversation
17
+ * within a tolerance window.
18
+ *
19
+ * The v2 comparand is the router's FRESH per-turn pick (`status: "injected"`),
20
+ * not its full in-context set. v2 accumulates pages across turns (`in_context`
21
+ * carry-over can reach the hundreds on a long conversation) whereas v3 selects
22
+ * fresh each turn; comparing fresh-against-fresh keeps the diff meaningful. The
23
+ * carried-over count is surfaced per turn as context, not folded into the sets.
24
+ *
25
+ * Pure and DB-free: the route handler reads the rows and hands them here.
26
+ */
27
+
28
+ import type { MemoryV2ConceptRowRecord } from "../memory-v2-activation-log-store.js";
29
+
30
+ /** An activation-log row reduced to what the diff needs. */
31
+ export interface ShadowDiffLogRow {
32
+ conversationId: string;
33
+ /** Epoch milliseconds. */
34
+ createdAt: number;
35
+ concepts: MemoryV2ConceptRowRecord[];
36
+ }
37
+
38
+ /** One paired (v2 router ↔ v3 shadow) turn. */
39
+ export interface ShadowDiffTurn {
40
+ conversationId: string;
41
+ /** Epoch ms of the v3 shadow row. */
42
+ shadowAt: number;
43
+ /** Epoch ms of the paired v2 router row. */
44
+ routerAt: number;
45
+ /** `routerAt - shadowAt`; small (within tolerance) by construction. */
46
+ deltaMs: number;
47
+ /** Size of the v2 fresh pick (`status: "injected"`). */
48
+ v2Count: number;
49
+ /** Size of the v3 shadow selection. */
50
+ v3Count: number;
51
+ /** v2 pages carried over from prior turns (`in_context`); annotation only. */
52
+ v2CachedCount: number;
53
+ /** `|overlap| / |v2 ∪ v3|`; 0 when both sets are empty. */
54
+ jaccard: number;
55
+ /** Slugs both systems picked, sorted. */
56
+ overlap: string[];
57
+ /** Slugs v3 surfaced but v2 did not freshly inject, sorted. */
58
+ v3Only: string[];
59
+ /** Slugs v2 freshly injected but v3 missed, sorted. */
60
+ v2Only: string[];
61
+ /** Provenance lane for each v3 slug (overlap + v3-only). */
62
+ laneBySlug: Record<string, string>;
63
+ }
64
+
65
+ /** A shadow row with no router row inside the tolerance window. */
66
+ export interface UnpairedShadowTurn {
67
+ conversationId: string;
68
+ shadowAt: number;
69
+ v3Count: number;
70
+ }
71
+
72
+ /** A slug with how many paired turns it appeared in. */
73
+ export interface SlugFrequency {
74
+ slug: string;
75
+ count: number;
76
+ }
77
+
78
+ export interface ShadowDiffResult {
79
+ /** Pairing tolerance actually used (ms). */
80
+ toleranceMs: number;
81
+ /** Total v3 shadow rows in the read window. */
82
+ shadowRows: number;
83
+ /** Shadow rows that paired to a router row. */
84
+ turnsCompared: number;
85
+ /** Shadow rows that did not pair. */
86
+ unpaired: UnpairedShadowTurn[];
87
+ agg: {
88
+ meanV2: number;
89
+ meanV3: number;
90
+ meanOverlap: number;
91
+ meanJaccard: number;
92
+ totalOverlap: number;
93
+ totalV3Only: number;
94
+ totalV2Only: number;
95
+ /** v3-only slug count by the lane that surfaced it — v3's extra reach. */
96
+ v3OnlyByLane: Record<string, number>;
97
+ /** overlap slug count by the v3 lane that recovered v2's pick. */
98
+ overlapByLane: Record<string, number>;
99
+ /** Most frequently dropped v2 pages (recall-regression watchlist). */
100
+ v2OnlyTop: SlugFrequency[];
101
+ /** Most frequent v3 extras (associative reach beyond v2). */
102
+ v3OnlyTop: SlugFrequency[];
103
+ };
104
+ /** Per-turn detail, newest first, capped at the requested limit. */
105
+ turns: ShadowDiffTurn[];
106
+ }
107
+
108
+ /** Status on a v2 router row that counts as a fresh per-turn selection. */
109
+ const V2_PICKED_STATUS = "injected";
110
+ /** Status on a v2 router row that means carried-over from a prior turn. */
111
+ const V2_CACHED_STATUS = "in_context";
112
+ /** How many slugs to list in the top-frequency aggregates. */
113
+ const TOP_FREQUENCY_LIMIT = 15;
114
+
115
+ function selectedV2Slugs(concepts: MemoryV2ConceptRowRecord[]): Set<string> {
116
+ const slugs = new Set<string>();
117
+ for (const c of concepts) {
118
+ if (c.status === V2_PICKED_STATUS) slugs.add(c.slug);
119
+ }
120
+ return slugs;
121
+ }
122
+
123
+ function cachedV2Count(concepts: MemoryV2ConceptRowRecord[]): number {
124
+ let n = 0;
125
+ for (const c of concepts) {
126
+ if (c.status === V2_CACHED_STATUS) n += 1;
127
+ }
128
+ return n;
129
+ }
130
+
131
+ function v3LaneBySlug(
132
+ concepts: MemoryV2ConceptRowRecord[],
133
+ ): Map<string, string> {
134
+ const bySlug = new Map<string, string>();
135
+ for (const c of concepts) {
136
+ bySlug.set(c.slug, c.lane ?? "unknown");
137
+ }
138
+ return bySlug;
139
+ }
140
+
141
+ /** Increment a string-keyed tally in place. */
142
+ function bump(tally: Record<string, number>, key: string): void {
143
+ tally[key] = (tally[key] ?? 0) + 1;
144
+ }
145
+
146
+ /** Sort a frequency map into the top-N slugs, ties broken by slug name. */
147
+ function topSlugs(freq: Map<string, number>, limit: number): SlugFrequency[] {
148
+ return [...freq.entries()]
149
+ .map(([slug, count]) => ({ slug, count }))
150
+ .sort((a, b) => b.count - a.count || a.slug.localeCompare(b.slug))
151
+ .slice(0, limit);
152
+ }
153
+
154
+ /**
155
+ * Pair each shadow row to the nearest unconsumed router row in the same
156
+ * conversation within `toleranceMs`, then diff the two selections. Pairing is
157
+ * greedy by absolute time delta; since real turns are minutes apart while a
158
+ * shadow/router sibling pair lands a second or two apart, the greedy match is a
159
+ * clean bijection in practice.
160
+ */
161
+ export function computeShadowDiff(
162
+ shadow: readonly ShadowDiffLogRow[],
163
+ router: readonly ShadowDiffLogRow[],
164
+ opts: { toleranceMs: number; detailLimit: number },
165
+ ): ShadowDiffResult {
166
+ const { toleranceMs, detailLimit } = opts;
167
+
168
+ // Index router rows by conversation, time-sorted, with a consumed flag so a
169
+ // router row pairs to at most one shadow row.
170
+ const routerByConv = new Map<
171
+ string,
172
+ Array<{ row: ShadowDiffLogRow; consumed: boolean }>
173
+ >();
174
+ for (const row of router) {
175
+ const bucket = routerByConv.get(row.conversationId) ?? [];
176
+ bucket.push({ row, consumed: false });
177
+ routerByConv.set(row.conversationId, bucket);
178
+ }
179
+ for (const bucket of routerByConv.values()) {
180
+ bucket.sort((a, b) => a.row.createdAt - b.row.createdAt);
181
+ }
182
+
183
+ const sortedShadow = [...shadow].sort((a, b) => a.createdAt - b.createdAt);
184
+
185
+ const turns: ShadowDiffTurn[] = [];
186
+ const unpaired: UnpairedShadowTurn[] = [];
187
+ const v3OnlyByLane: Record<string, number> = {};
188
+ const overlapByLane: Record<string, number> = {};
189
+ const v2OnlyFreq = new Map<string, number>();
190
+ const v3OnlyFreq = new Map<string, number>();
191
+
192
+ for (const sh of sortedShadow) {
193
+ const bucket = routerByConv.get(sh.conversationId);
194
+ let best: { row: ShadowDiffLogRow; consumed: boolean } | undefined;
195
+ let bestDelta = Number.POSITIVE_INFINITY;
196
+ for (const candidate of bucket ?? []) {
197
+ if (candidate.consumed) continue;
198
+ const delta = Math.abs(candidate.row.createdAt - sh.createdAt);
199
+ if (delta < bestDelta) {
200
+ bestDelta = delta;
201
+ best = candidate;
202
+ }
203
+ }
204
+
205
+ if (!best || bestDelta > toleranceMs) {
206
+ unpaired.push({
207
+ conversationId: sh.conversationId,
208
+ shadowAt: sh.createdAt,
209
+ v3Count: new Set(sh.concepts.map((c) => c.slug)).size,
210
+ });
211
+ continue;
212
+ }
213
+ best.consumed = true;
214
+
215
+ const v2Set = selectedV2Slugs(best.row.concepts);
216
+ const laneBySlug = v3LaneBySlug(sh.concepts);
217
+ const v3Set = new Set(laneBySlug.keys());
218
+
219
+ const overlap: string[] = [];
220
+ const v3Only: string[] = [];
221
+ for (const slug of v3Set) {
222
+ if (v2Set.has(slug)) {
223
+ overlap.push(slug);
224
+ bump(overlapByLane, laneBySlug.get(slug)!);
225
+ } else {
226
+ v3Only.push(slug);
227
+ bump(v3OnlyByLane, laneBySlug.get(slug)!);
228
+ v3OnlyFreq.set(slug, (v3OnlyFreq.get(slug) ?? 0) + 1);
229
+ }
230
+ }
231
+ const v2Only: string[] = [];
232
+ for (const slug of v2Set) {
233
+ if (!v3Set.has(slug)) {
234
+ v2Only.push(slug);
235
+ v2OnlyFreq.set(slug, (v2OnlyFreq.get(slug) ?? 0) + 1);
236
+ }
237
+ }
238
+
239
+ const unionSize = new Set([...v2Set, ...v3Set]).size;
240
+ turns.push({
241
+ conversationId: sh.conversationId,
242
+ shadowAt: sh.createdAt,
243
+ routerAt: best.row.createdAt,
244
+ deltaMs: best.row.createdAt - sh.createdAt,
245
+ v2Count: v2Set.size,
246
+ v3Count: v3Set.size,
247
+ v2CachedCount: cachedV2Count(best.row.concepts),
248
+ jaccard: unionSize === 0 ? 0 : overlap.length / unionSize,
249
+ overlap: overlap.sort(),
250
+ v3Only: v3Only.sort(),
251
+ v2Only: v2Only.sort(),
252
+ laneBySlug: Object.fromEntries(laneBySlug),
253
+ });
254
+ }
255
+
256
+ const n = turns.length;
257
+ const sum = (pick: (t: ShadowDiffTurn) => number): number =>
258
+ turns.reduce((acc, t) => acc + pick(t), 0);
259
+ const mean = (pick: (t: ShadowDiffTurn) => number): number =>
260
+ n === 0 ? 0 : sum(pick) / n;
261
+
262
+ // Newest-first for the detail listing; aggregates are order-independent.
263
+ const detail = [...turns]
264
+ .sort((a, b) => b.shadowAt - a.shadowAt)
265
+ .slice(0, detailLimit);
266
+
267
+ return {
268
+ toleranceMs,
269
+ shadowRows: shadow.length,
270
+ turnsCompared: n,
271
+ unpaired,
272
+ agg: {
273
+ meanV2: mean((t) => t.v2Count),
274
+ meanV3: mean((t) => t.v3Count),
275
+ meanOverlap: mean((t) => t.overlap.length),
276
+ meanJaccard: mean((t) => t.jaccard),
277
+ totalOverlap: sum((t) => t.overlap.length),
278
+ totalV3Only: sum((t) => t.v3Only.length),
279
+ totalV2Only: sum((t) => t.v2Only.length),
280
+ v3OnlyByLane,
281
+ overlapByLane,
282
+ v2OnlyTop: topSlugs(v2OnlyFreq, TOP_FREQUENCY_LIMIT),
283
+ v3OnlyTop: topSlugs(v3OnlyFreq, TOP_FREQUENCY_LIMIT),
284
+ },
285
+ turns: detail,
286
+ };
287
+ }
@@ -34,6 +34,7 @@ import {
34
34
  PluginExecutionError,
35
35
  } from "../../plugins/types.js";
36
36
  import type { ContentBlock } from "../../providers/types.js";
37
+ import { isUntrustedTrustClass } from "../../runtime/actor-trust-resolver.js";
37
38
  import { getLogger } from "../../util/logger.js";
38
39
  import { getWorkspaceDir } from "../../util/platform.js";
39
40
  import type { DrizzleDb } from "../db-connection.js";
@@ -151,10 +152,13 @@ const SHADOW_CONFIG_SNAPSHOT: MemoryV2ConfigSnapshot = {
151
152
  * becomes a zeroed concept row tagged `source: "router"` and
152
153
  * `status: "injected"` — the shadow has no activation scores to record, and the
153
154
  * `mode='v3_shadow'` row tag (not the concept source) is what distinguishes
154
- * shadow telemetry from live router selections.
155
+ * shadow telemetry from live router selections. Each row also carries the
156
+ * `lane` that surfaced the slug (from `sourceBySlug`) so a shadow run can be
157
+ * analyzed by provenance.
155
158
  */
156
159
  function buildShadowConceptRows(
157
160
  selectedSlugs: readonly string[],
161
+ sourceBySlug: ReadonlyMap<string, string>,
158
162
  ): MemoryV2ConceptRowRecord[] {
159
163
  return selectedSlugs.map((slug) => ({
160
164
  slug,
@@ -170,6 +174,7 @@ function buildShadowConceptRows(
170
174
  spreadContribution: 0,
171
175
  source: "router",
172
176
  status: "injected",
177
+ ...(sourceBySlug.get(slug) ? { lane: sourceBySlug.get(slug) } : {}),
173
178
  }));
174
179
  }
175
180
 
@@ -232,11 +237,37 @@ async function runShadowAndLog(
232
237
 
233
238
  if (signal.aborted) return;
234
239
 
240
+ // Per-turn summary so a shadow run is analyzable from the logs — the pool
241
+ // shape, how many passes ran, and the gate's verdict + rationale. The
242
+ // selection set + per-slug lane land in the activation log below.
243
+ const passes = output.trace?.passes ?? [];
244
+ const lastGate = passes[passes.length - 1]?.gate;
245
+ const laneTally: Record<string, number> = {};
246
+ for (const lane of output.sourceBySlug.values()) {
247
+ laneTally[lane] = (laneTally[lane] ?? 0) + 1;
248
+ }
249
+ log.info(
250
+ {
251
+ conversationId: args.conversationId,
252
+ turn: args.turnIndex,
253
+ selected: output.selectedSlugs.length,
254
+ poolSize: output.sourceBySlug.size,
255
+ laneTally,
256
+ passes: passes.length,
257
+ gateDecision: lastGate?.decision,
258
+ gateReasoning: lastGate?.reasoning,
259
+ },
260
+ "v3 shadow selection",
261
+ );
262
+
235
263
  recordMemoryV2ActivationLog({
236
264
  conversationId: args.conversationId,
237
265
  turn: args.turnIndex,
238
266
  mode: "v3_shadow",
239
- concepts: buildShadowConceptRows(output.selectedSlugs),
267
+ concepts: buildShadowConceptRows(
268
+ output.selectedSlugs,
269
+ output.sourceBySlug,
270
+ ),
240
271
  config: SHADOW_CONFIG_SNAPSHOT,
241
272
  });
242
273
  } catch (err) {
@@ -253,6 +284,11 @@ async function runShadowAndLog(
253
284
  * Flag-gated INSIDE the middleware (per-turn, live-toggle): when v3 shadow is
254
285
  * off it is a pure pass-through. When on, it fires the v3 loop detached and
255
286
  * returns the unchanged downstream (v2) result immediately.
287
+ *
288
+ * The shadow loop spends filter + gate LLM calls, so — like the other
289
+ * guardian-trust background memory loops (`enqueueAutoAnalysisOnCompaction`,
290
+ * `enqueueMemoryRetrospectiveOnCompaction`) — it is gated on actor trust: an
291
+ * untrusted turn passes through without kicking off the v3 loop.
256
292
  */
257
293
  export const memoryV3ShadowMiddleware: Middleware<MemoryArgs, MemoryResult> =
258
294
  async function memoryV3Shadow(args, next) {
@@ -262,6 +298,12 @@ export const memoryV3ShadowMiddleware: Middleware<MemoryArgs, MemoryResult> =
262
298
  return next(args);
263
299
  }
264
300
 
301
+ if (isUntrustedTrustClass(args.trustContext?.trustClass)) {
302
+ // Untrusted actor: don't spend shadow retrieval LLM calls — mirrors the
303
+ // live path's trust gate. Pure pass-through, no detached work.
304
+ return next(args);
305
+ }
306
+
265
307
  // Detached — never awaited on the path that returns the result, so the
266
308
  // shadow can neither block nor slow the live turn. Errors are swallowed
267
309
  // inside `runShadowAndLog`.
@@ -165,13 +165,18 @@ export async function walkTree(
165
165
  const offeredRefs = new Set(offeredNodes.map((c) => c.ref));
166
166
 
167
167
  // Honor the descend pick in the order it was returned, dedup'd, filtered
168
- // to genuinely-offered node children, and capped by `breadthBudget`.
168
+ // to genuinely-offered node children, skipping nodes already visited
169
+ // elsewhere in the DAG, and capped by `breadthBudget`. Filtering visited
170
+ // nodes *before* the budget check ensures the budget is spent only on
171
+ // nodes the walk will actually descend — an already-visited pick must not
172
+ // consume a slot that an unvisited sibling could use.
169
173
  const descended: string[] = [];
170
174
  const descendedSet = new Set<string>();
171
175
  for (const choice of result.descend) {
172
176
  if (choice.kind !== "node") continue;
173
177
  if (!offeredRefs.has(choice.ref)) continue;
174
178
  if (descendedSet.has(choice.ref)) continue;
179
+ if (visited.has(nodeKey(choice.ref))) continue;
175
180
  if (descended.length >= breadthBudget) break;
176
181
  descendedSet.add(choice.ref);
177
182
  descended.push(choice.ref);
@@ -320,8 +320,13 @@ export function createDescender(args: CreateDescenderArgs): DescendDecision {
320
320
  parsed.data.descend,
321
321
  new Map(offeredNodes.map((c) => [c.ref, c])),
322
322
  );
323
+ // Recall-safe fallback: an *omitted* `keep_pages` means the model gave no
324
+ // instruction at this node, so keep every offered page — dropping all pages
325
+ // a node presented on a silent omission is the worse failure. An *explicit*
326
+ // `[]` is the model genuinely keeping nothing here and is honored as-is.
327
+ const keepSlugs = parsed.data.keep_pages ?? offeredPageSlugs;
323
328
  const keep = resolveOffered(
324
- parsed.data.keep_pages ?? [],
329
+ keepSlugs,
325
330
  new Map(offeredPages.map((c) => [c.ref, c])),
326
331
  );
327
332
  return { descend, keep, reasoning: parsed.data.reasoning ?? "" };