@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
@@ -358,6 +358,55 @@ describe("runScouts — dense lane", () => {
358
358
  expect(slugs.indexOf("people/x")).toBeLessThan(slugs.length - 1);
359
359
  });
360
360
 
361
+ test("MMR stays relevance-aware when every cosine is non-positive", async () => {
362
+ // All dense scores are <= 0 (weakly/negatively similar). Dividing by the
363
+ // pool max would collapse every relevance to a single value and degrade
364
+ // ranking to diversity-only, which would pull the lowest-scoring off-domain
365
+ // hit ahead of a higher-scoring active-domain hit. Range-normalizing keeps
366
+ // relevance ordering: work/b (higher score) stays ahead of people/x.
367
+ hybridHits = [
368
+ hit("work/a", { denseScore: -0.1 }),
369
+ hit("work/b", { denseScore: -0.5 }),
370
+ hit("people/x", { denseScore: -0.9 }),
371
+ ];
372
+ const { scouts } = await runScouts(
373
+ makeInput({
374
+ lanes: { hot: false, sparse: false },
375
+ denseQuota: { activeDomain: 30, offDomain: 8 },
376
+ }),
377
+ DEPS,
378
+ );
379
+ const slugs = scouts.find((s) => s.lane === "dense")?.slugs ?? [];
380
+ // Relevance-aware order: the higher-scoring work/b outranks people/x.
381
+ // Diversity-only (the bug) would interleave people/x ahead of work/b.
382
+ expect(slugs.indexOf("work/b")).toBeLessThan(slugs.indexOf("people/x"));
383
+ });
384
+
385
+ test("MMR positive-cosine ordering is unchanged by the range fix", async () => {
386
+ // A healthy positive range must behave exactly as before: relevance is
387
+ // score/max, so a strong active-domain run still gets diversified by the
388
+ // redundancy term once the subtree is over-represented.
389
+ hybridHits = [
390
+ hit("work/a", { denseScore: 0.95 }),
391
+ hit("work/b", { denseScore: 0.94 }),
392
+ hit("work/c", { denseScore: 0.93 }),
393
+ hit("work/d", { denseScore: 0.92 }),
394
+ hit("people/x", { denseScore: 0.9 }),
395
+ ];
396
+ const { scouts } = await runScouts(
397
+ makeInput({
398
+ lanes: { hot: false, sparse: false },
399
+ denseQuota: { activeDomain: 30, offDomain: 8 },
400
+ }),
401
+ DEPS,
402
+ );
403
+ const slugs = scouts.find((s) => s.lane === "dense")?.slugs ?? [];
404
+ // The strongest hit still leads; the lower-scoring off-domain hit is pulled
405
+ // forward (not stranded last) exactly as in the unmodified positive case.
406
+ expect(slugs[0]).toBe("work/a");
407
+ expect(slugs.indexOf("people/x")).toBeLessThan(slugs.length - 1);
408
+ });
409
+
361
410
  test("no dense hits yields no dense ScoutResult", async () => {
362
411
  hybridHits = [hit("sparse/only", { sparseScore: 2.0 })];
363
412
  const { scouts } = await runScouts(
@@ -0,0 +1,225 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { MemoryV2ConceptRowRecord } from "../../memory-v2-activation-log-store.js";
4
+ import { computeShadowDiff, type ShadowDiffLogRow } from "../shadow-diff.js";
5
+
6
+ const ZERO = {
7
+ finalActivation: 0,
8
+ ownActivation: 0,
9
+ priorActivation: 0,
10
+ simUser: 0,
11
+ simAssistant: 0,
12
+ simNow: 0,
13
+ simUserRerankBoost: 0,
14
+ simAssistantRerankBoost: 0,
15
+ inRerankPool: false,
16
+ spreadContribution: 0,
17
+ } as const;
18
+
19
+ /** A v2 router concept row with the given selection status. */
20
+ function v2concept(
21
+ slug: string,
22
+ status: MemoryV2ConceptRowRecord["status"],
23
+ ): MemoryV2ConceptRowRecord {
24
+ return { slug, status, source: "tier3:0", ...ZERO };
25
+ }
26
+
27
+ /** A v3 shadow concept row (always status 'injected') tagged with its lane. */
28
+ function v3concept(slug: string, lane: string): MemoryV2ConceptRowRecord {
29
+ return { slug, status: "injected", source: "router", lane, ...ZERO };
30
+ }
31
+
32
+ function shadowRow(
33
+ conversationId: string,
34
+ createdAt: number,
35
+ slugLanes: Array<[string, string]>,
36
+ ): ShadowDiffLogRow {
37
+ return {
38
+ conversationId,
39
+ createdAt,
40
+ concepts: slugLanes.map(([slug, lane]) => v3concept(slug, lane)),
41
+ };
42
+ }
43
+
44
+ function routerRow(
45
+ conversationId: string,
46
+ createdAt: number,
47
+ opts: { injected?: string[]; cached?: string[]; rejected?: string[] },
48
+ ): ShadowDiffLogRow {
49
+ return {
50
+ conversationId,
51
+ createdAt,
52
+ concepts: [
53
+ ...(opts.injected ?? []).map((s) => v2concept(s, "injected")),
54
+ ...(opts.cached ?? []).map((s) => v2concept(s, "in_context")),
55
+ ...(opts.rejected ?? []).map((s) => v2concept(s, "not_injected")),
56
+ ],
57
+ };
58
+ }
59
+
60
+ const OPTS = { toleranceMs: 10_000, detailLimit: 50 };
61
+
62
+ describe("computeShadowDiff", () => {
63
+ test("pairs nearest router row and diffs fresh-injected vs shadow sets", () => {
64
+ const shadow = [
65
+ shadowRow("conv1", 1000, [
66
+ ["a", "tree"],
67
+ ["b", "edge"],
68
+ ["c", "hot"],
69
+ ]),
70
+ ];
71
+ const router = [
72
+ routerRow("conv1", 1500, {
73
+ injected: ["a", "b", "x", "y"],
74
+ cached: ["cached1", "cached2"],
75
+ rejected: ["rej1"],
76
+ }),
77
+ ];
78
+
79
+ const result = computeShadowDiff(shadow, router, OPTS);
80
+
81
+ expect(result.turnsCompared).toBe(1);
82
+ expect(result.unpaired).toHaveLength(0);
83
+
84
+ const turn = result.turns[0]!;
85
+ expect(turn.deltaMs).toBe(500);
86
+ expect(turn.v2Count).toBe(4);
87
+ expect(turn.v3Count).toBe(3);
88
+ expect(turn.v2CachedCount).toBe(2);
89
+ expect(turn.overlap).toEqual(["a", "b"]);
90
+ expect(turn.v3Only).toEqual(["c"]);
91
+ expect(turn.v2Only).toEqual(["x", "y"]);
92
+ // cached / rejected v2 rows are NOT counted as dropped picks.
93
+ expect(turn.v2Only).not.toContain("cached1");
94
+ expect(turn.v2Only).not.toContain("rej1");
95
+ expect(turn.jaccard).toBeCloseTo(2 / 5, 10);
96
+ });
97
+
98
+ test("attributes overlap and v3-only to the v3 provenance lane", () => {
99
+ const shadow = [
100
+ shadowRow("conv1", 1000, [
101
+ ["a", "tree"],
102
+ ["b", "edge"],
103
+ ["c", "hot"],
104
+ ]),
105
+ ];
106
+ const router = [routerRow("conv1", 1100, { injected: ["a", "b"] })];
107
+
108
+ const { agg } = computeShadowDiff(shadow, router, OPTS);
109
+
110
+ expect(agg.overlapByLane).toEqual({ tree: 1, edge: 1 });
111
+ expect(agg.v3OnlyByLane).toEqual({ hot: 1 });
112
+ });
113
+
114
+ test("defaults a missing lane to 'unknown'", () => {
115
+ const shadow: ShadowDiffLogRow[] = [
116
+ {
117
+ conversationId: "conv1",
118
+ createdAt: 1000,
119
+ concepts: [
120
+ { slug: "a", status: "injected", source: "router", ...ZERO },
121
+ ],
122
+ },
123
+ ];
124
+ const router = [routerRow("conv1", 1100, { injected: [] })];
125
+
126
+ const { agg } = computeShadowDiff(shadow, router, OPTS);
127
+
128
+ expect(agg.v3OnlyByLane).toEqual({ unknown: 1 });
129
+ });
130
+
131
+ test("sends a shadow row with no router row within tolerance to unpaired", () => {
132
+ const shadow = [shadowRow("conv1", 1000, [["a", "tree"]])];
133
+ const router = [routerRow("conv1", 1000 + 20_000, { injected: ["a"] })];
134
+
135
+ const result = computeShadowDiff(shadow, router, OPTS);
136
+
137
+ expect(result.turnsCompared).toBe(0);
138
+ expect(result.unpaired).toEqual([
139
+ { conversationId: "conv1", shadowAt: 1000, v3Count: 1 },
140
+ ]);
141
+ });
142
+
143
+ test("greedily pairs each router row to at most one shadow row", () => {
144
+ const shadow = [
145
+ shadowRow("conv1", 1000, [["a", "tree"]]),
146
+ shadowRow("conv1", 5000, [["b", "edge"]]),
147
+ ];
148
+ const router = [
149
+ routerRow("conv1", 1200, { injected: ["a", "z"] }),
150
+ routerRow("conv1", 5200, { injected: ["b"] }),
151
+ ];
152
+
153
+ const result = computeShadowDiff(shadow, router, OPTS);
154
+
155
+ expect(result.turnsCompared).toBe(2);
156
+ const byShadow = new Map(result.turns.map((t) => [t.shadowAt, t]));
157
+ expect(byShadow.get(1000)!.routerAt).toBe(1200);
158
+ expect(byShadow.get(1000)!.v2Only).toEqual(["z"]);
159
+ expect(byShadow.get(5000)!.routerAt).toBe(5200);
160
+ expect(byShadow.get(5000)!.overlap).toEqual(["b"]);
161
+ });
162
+
163
+ test("caps per-turn detail newest-first but aggregates over all turns", () => {
164
+ const shadow = [
165
+ shadowRow("c1", 1000, [["a", "tree"]]),
166
+ shadowRow("c2", 2000, [["b", "tree"]]),
167
+ shadowRow("c3", 3000, [["c", "tree"]]),
168
+ ];
169
+ const router = [
170
+ routerRow("c1", 1000, { injected: ["a"] }),
171
+ routerRow("c2", 2000, { injected: ["b"] }),
172
+ routerRow("c3", 3000, { injected: ["c"] }),
173
+ ];
174
+
175
+ const result = computeShadowDiff(shadow, router, {
176
+ toleranceMs: 10_000,
177
+ detailLimit: 2,
178
+ });
179
+
180
+ expect(result.turnsCompared).toBe(3);
181
+ expect(result.turns.map((t) => t.shadowAt)).toEqual([3000, 2000]);
182
+ });
183
+
184
+ test("ranks the most-dropped and most-added slugs across turns", () => {
185
+ const shadow = [
186
+ shadowRow("c1", 1000, [["extra", "edge"]]),
187
+ shadowRow("c2", 2000, [["extra", "edge"]]),
188
+ ];
189
+ const router = [
190
+ routerRow("c1", 1000, { injected: ["dropped", "kept"] }),
191
+ routerRow("c2", 2000, { injected: ["dropped"] }),
192
+ ];
193
+
194
+ const { agg } = computeShadowDiff(shadow, router, OPTS);
195
+
196
+ expect(agg.totalV3Only).toBe(2);
197
+ expect(agg.totalV2Only).toBe(3);
198
+ expect(agg.v3OnlyTop[0]).toEqual({ slug: "extra", count: 2 });
199
+ expect(agg.v2OnlyTop[0]).toEqual({ slug: "dropped", count: 2 });
200
+ expect(agg.meanV3).toBeCloseTo(1, 10);
201
+ expect(agg.meanV2).toBeCloseTo(1.5, 10);
202
+ });
203
+
204
+ test("returns zeroed aggregates with no rows", () => {
205
+ const result = computeShadowDiff([], [], OPTS);
206
+ expect(result.turnsCompared).toBe(0);
207
+ expect(result.shadowRows).toBe(0);
208
+ expect(result.agg.meanJaccard).toBe(0);
209
+ expect(result.agg.v2OnlyTop).toEqual([]);
210
+ expect(result.turns).toEqual([]);
211
+ });
212
+
213
+ test("treats two empty selections as jaccard 0, not NaN", () => {
214
+ const shadow: ShadowDiffLogRow[] = [
215
+ { conversationId: "c1", createdAt: 1000, concepts: [] },
216
+ ];
217
+ const router = [routerRow("c1", 1000, { cached: ["only-cached"] })];
218
+
219
+ const result = computeShadowDiff(shadow, router, OPTS);
220
+
221
+ expect(result.turnsCompared).toBe(1);
222
+ expect(result.turns[0]!.jaccard).toBe(0);
223
+ expect(result.turns[0]!.v2CachedCount).toBe(1);
224
+ });
225
+ });
@@ -35,6 +35,16 @@ mock.module("../../../util/logger.js", () => ({
35
35
  getLogger: () => makeMockLogger(),
36
36
  }));
37
37
 
38
+ // Mirror the real `isUntrustedTrustClass` predicate (anything that is not the
39
+ // guardian is untrusted) without pulling the resolver's contact-store graph
40
+ // into the test module set.
41
+ mock.module("../../../runtime/actor-trust-resolver.js", () => ({
42
+ isUntrustedTrustClass: (trustClass: string | undefined) =>
43
+ trustClass === "trusted_contact" ||
44
+ trustClass === "unknown" ||
45
+ trustClass === undefined,
46
+ }));
47
+
38
48
  // ── Mutable test doubles, rewired per test ───────────────────────────────
39
49
 
40
50
  /** Drives `config.memory.v3.{enabled,shadow}` and `historical_pairs`. */
@@ -131,10 +141,13 @@ function makeCtx(): TurnContext {
131
141
  };
132
142
  }
133
143
 
134
- function makeArgs(signal?: AbortSignal): MemoryArgs {
144
+ function makeArgs(
145
+ signal?: AbortSignal,
146
+ trustContext: TrustContext | undefined = trust,
147
+ ): MemoryArgs {
135
148
  return {
136
149
  conversationId: "conv-shadow",
137
- trustContext: trust,
150
+ trustContext,
138
151
  turnIndex: 3,
139
152
  signal: signal ?? new AbortController().signal,
140
153
  };
@@ -268,6 +281,12 @@ describe("memory-v3 shadow middleware", () => {
268
281
  expect(logged.conversationId).toBe("conv-shadow");
269
282
  expect(logged.turn).toBe(3);
270
283
  expect(logged.concepts.map((c) => c.slug)).toEqual(["topic/a", "topic/b"]);
284
+
285
+ // Per-slug lane provenance is carried from sourceBySlug; a slug absent from
286
+ // the map (topic/b) gets no lane rather than a bogus one.
287
+ const bySlug = new Map(logged.concepts.map((c) => [c.slug, c]));
288
+ expect(bySlug.get("topic/a")?.lane).toBe("dense");
289
+ expect(bySlug.get("topic/b")?.lane).toBeUndefined();
271
290
  });
272
291
 
273
292
  test("v3 error → logged/swallowed, turn result unaffected, no log row", async () => {
@@ -309,4 +328,71 @@ describe("memory-v3 shadow middleware", () => {
309
328
  expect(loopCalls.length).toBe(0);
310
329
  expect(logCalls.length).toBe(0);
311
330
  });
331
+
332
+ test("untrusted actor → no v3 work, even with shadow on", async () => {
333
+ v3Enabled = true;
334
+ v3Shadow = true;
335
+
336
+ for (const trustClass of ["trusted_contact", "unknown"] as const) {
337
+ loopCalls.length = 0;
338
+ logCalls.length = 0;
339
+ const untrusted: TrustContext = { sourceChannel: "vellum", trustClass };
340
+ const result = await memoryV3ShadowMiddleware(
341
+ makeArgs(undefined, untrusted),
342
+ async () => DOWNSTREAM_RESULT,
343
+ makeCtx(),
344
+ );
345
+ expect(result).toBe(DOWNSTREAM_RESULT);
346
+
347
+ await flush();
348
+ // Untrusted turn never spends shadow retrieval LLM calls.
349
+ expect(loopCalls.length).toBe(0);
350
+ expect(logCalls.length).toBe(0);
351
+ }
352
+ });
353
+
354
+ test("absent trust context → treated as untrusted, no v3 work", async () => {
355
+ v3Enabled = true;
356
+ v3Shadow = true;
357
+
358
+ // Build args with an explicitly-undefined trustContext (the `makeArgs`
359
+ // default would substitute the guardian fixture, so construct directly).
360
+ const args: MemoryArgs = {
361
+ conversationId: "conv-shadow",
362
+ trustContext: undefined,
363
+ turnIndex: 3,
364
+ signal: new AbortController().signal,
365
+ };
366
+ const result = await memoryV3ShadowMiddleware(
367
+ args,
368
+ async () => DOWNSTREAM_RESULT,
369
+ makeCtx(),
370
+ );
371
+ expect(result).toBe(DOWNSTREAM_RESULT);
372
+
373
+ await flush();
374
+ expect(loopCalls.length).toBe(0);
375
+ expect(logCalls.length).toBe(0);
376
+ });
377
+
378
+ test("guardian actor → shadow runs (trust gate lets trusted turns through)", async () => {
379
+ v3Enabled = true;
380
+ v3Shadow = true;
381
+ const guardian: TrustContext = {
382
+ sourceChannel: "vellum",
383
+ trustClass: "guardian",
384
+ };
385
+
386
+ const result = await memoryV3ShadowMiddleware(
387
+ makeArgs(undefined, guardian),
388
+ async () => DOWNSTREAM_RESULT,
389
+ makeCtx(),
390
+ );
391
+ expect(result).toBe(DOWNSTREAM_RESULT);
392
+
393
+ await flush();
394
+ // Trusted turn fires the v3 loop and logs its selection.
395
+ expect(loopCalls.length).toBe(1);
396
+ expect(logCalls.length).toBe(1);
397
+ });
312
398
  });
@@ -228,6 +228,45 @@ describe("walkTree — breadthBudget", () => {
228
228
  expect([...pages].sort()).toEqual(["pa", "pb"]);
229
229
  expect(levels.map((l) => l.node).sort()).toEqual(["_root", "a", "b"]);
230
230
  });
231
+
232
+ test("spends the budget only on unvisited siblings, not already-visited picks", async () => {
233
+ // `_root` → {p1, p2}. `p1` descends `shared`, marking it visited. `p2`
234
+ // then offers `[shared, x, y]` under a budget of 2. If the budget were
235
+ // applied before filtering visited nodes, `shared` would consume a slot
236
+ // and `y` would be skipped. Filtering visited first spends the budget on
237
+ // `x` and `y`, so both unvisited siblings are descended.
238
+ const tree = makeTree("_root", {
239
+ _root: [node("p1"), node("p2")],
240
+ p1: [node("shared")],
241
+ p2: [node("shared"), node("x"), node("y")],
242
+ shared: [page("ps")],
243
+ x: [page("px")],
244
+ y: [page("py")],
245
+ });
246
+
247
+ const { pages, levels } = await walkTree(tree, {
248
+ breadthBudget: 2,
249
+ maxDepth: 8,
250
+ descend: descendAll,
251
+ });
252
+
253
+ const p2Level = levels.find((l) => l.node === "p2")!;
254
+ // `shared` is offered but already visited; the budget goes to x and y.
255
+ expect(p2Level.considered).toEqual(["shared", "x", "y"]);
256
+ expect(p2Level.descended).toEqual(["x", "y"]);
257
+ expect(p2Level.skipped).toEqual(["shared"]);
258
+
259
+ // The previously-skipped sibling `y` (and its page) is now reached.
260
+ expect([...pages].sort()).toEqual(["ps", "px", "py"]);
261
+ expect(levels.map((l) => l.node).sort()).toEqual([
262
+ "_root",
263
+ "p1",
264
+ "p2",
265
+ "shared",
266
+ "x",
267
+ "y",
268
+ ]);
269
+ });
231
270
  });
232
271
 
233
272
  // ---------------------------------------------------------------------------
@@ -279,6 +279,83 @@ describe("runTreeWalk — scripted descent", () => {
279
279
  expect(levels.map((l) => l.node).sort()).toEqual(["_root", "a"]);
280
280
  });
281
281
 
282
+ test("omitted keep_pages keeps every offered page at the node (recall-safe)", async () => {
283
+ // _root → a, a leaf bucket of two pages. The model descends but returns NO
284
+ // keep_pages field at all — a silent omission must keep every offered page,
285
+ // not drop them all.
286
+ const tree = makeTree("_root", {
287
+ _root: [node("a")],
288
+ a: [page("frames/example-a"), page("people/alice")],
289
+ });
290
+ const pages = makePages(["frames/example-a", "people/alice"]);
291
+ const calls: ProviderCall[] = [];
292
+ // Bespoke stub: emits tool input WITHOUT a `keep_pages` key for node "a".
293
+ const provider: Provider = {
294
+ name: "omit-keep-pages",
295
+ sendMessage: async (messages, tools, systemPrompt, options) => {
296
+ calls.push({ messages, tools, systemPrompt, options });
297
+ const nodeId =
298
+ nodeIdFromCall({ messages, tools, systemPrompt, options }) ?? "";
299
+ const input: Record<string, unknown> =
300
+ nodeId === "_root"
301
+ ? { descend: ["a"], keep_pages: [] }
302
+ : { descend: [] }; // node "a": keep_pages omitted entirely
303
+ return {
304
+ model: "stub-model",
305
+ stopReason: "tool_use",
306
+ usage: { inputTokens: 0, outputTokens: 0 },
307
+ content: [
308
+ {
309
+ type: "tool_use",
310
+ id: `tu-${nodeId}`,
311
+ name: "choose_branches",
312
+ input,
313
+ },
314
+ ],
315
+ };
316
+ },
317
+ };
318
+
319
+ const { pages: collected } = await runTreeWalk({
320
+ input: makeInput(),
321
+ tree,
322
+ pages,
323
+ scouts: [],
324
+ provider,
325
+ });
326
+
327
+ // Both offered pages survive the omission.
328
+ expect([...collected].sort()).toEqual(["frames/example-a", "people/alice"]);
329
+ });
330
+
331
+ test("explicit empty keep_pages keeps no pages at the node", async () => {
332
+ // Same tree, but node "a" returns an *explicit* empty keep_pages — honored
333
+ // as the model genuinely keeping nothing here.
334
+ const tree = makeTree("_root", {
335
+ _root: [node("a")],
336
+ a: [page("frames/example-a"), page("people/alice")],
337
+ });
338
+ const pages = makePages(["frames/example-a", "people/alice"]);
339
+ const calls: ProviderCall[] = [];
340
+ const provider = makeProvider(
341
+ {
342
+ _root: { descend: ["a"], keep: [] },
343
+ a: { descend: [], keep: [] },
344
+ },
345
+ calls,
346
+ );
347
+
348
+ const { pages: collected } = await runTreeWalk({
349
+ input: makeInput(),
350
+ tree,
351
+ pages,
352
+ scouts: [],
353
+ provider,
354
+ });
355
+
356
+ expect([...collected]).toEqual([]);
357
+ });
358
+
282
359
  test("keeps only the pages the model selects at a node", async () => {
283
360
  // _root → a, a leaf bucket of two pages. The model keeps only one.
284
361
  const tree = makeTree("_root", {
@@ -178,6 +178,38 @@ describe("validateTree — cycles", () => {
178
178
 
179
179
  expect(report.cycles).toEqual([]);
180
180
  });
181
+
182
+ test("detects a cycle in a component unreachable from the root", async () => {
183
+ // _root is a leaf with no edges; the x ↔ y cycle lives in a disconnected
184
+ // component the root can never reach. The sweep over unvisited nodes must
185
+ // still surface it.
186
+ await writeNode(workspaceDir, node(ROOT_NODE_ID, []));
187
+ await writeNode(workspaceDir, node("x", ["node:y"]));
188
+ await writeNode(workspaceDir, node("y", ["node:x"]));
189
+ resetCaches();
190
+
191
+ const report = await validateTree(workspaceDir);
192
+
193
+ expect(report.cycleCount).toBe(1);
194
+ // The back-edge closes on whichever of the pair the sweep enters second.
195
+ const [{ from, to }] = report.cycles;
196
+ expect(new Set([from, to])).toEqual(new Set(["x", "y"]));
197
+ });
198
+
199
+ test("a page off an unreachable cyclic node stays an orphan", async () => {
200
+ // The disconnected-component sweep covers x/y for cycle detection but must
201
+ // not mark them root-reachable — `detached` therefore remains an orphan.
202
+ await writeNode(workspaceDir, node(ROOT_NODE_ID, []));
203
+ await writeNode(workspaceDir, node("x", ["node:y", "page:detached"]));
204
+ await writeNode(workspaceDir, node("y", ["node:x"]));
205
+ await writePage(workspaceDir, page("detached"));
206
+ resetCaches();
207
+
208
+ const report = await validateTree(workspaceDir);
209
+
210
+ expect(report.cycleCount).toBe(1);
211
+ expect(report.orphanPages).toEqual(["detached"]);
212
+ });
181
213
  });
182
214
 
183
215
  describe("validateTree — staleIndex", () => {