@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091926.ebb2d62

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 (490) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/docs/activation-funnel-telemetry.md +310 -0
  4. package/examples/plugins/echo/README.md +61 -60
  5. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  6. package/examples/plugins/echo/hooks/stop.ts +16 -0
  7. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  8. package/examples/plugins/echo/package.json +1 -2
  9. package/examples/plugins/echo/src/emit.ts +19 -0
  10. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  11. package/openapi.yaml +273 -121
  12. package/package.json +2 -2
  13. package/scripts/sync-llm-catalog.ts +2 -0
  14. package/src/__tests__/activation-early-marking.test.ts +120 -0
  15. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  16. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  17. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  18. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  19. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  20. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  21. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  22. package/src/__tests__/agent-loop.test.ts +441 -96
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  24. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  25. package/src/__tests__/anthropic-provider.test.ts +23 -8
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  27. package/src/__tests__/app-control-flow.test.ts +1 -1
  28. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  29. package/src/__tests__/app-executors.test.ts +132 -0
  30. package/src/__tests__/approval-cascade.test.ts +6 -5
  31. package/src/__tests__/approval-routes-http.test.ts +4 -1
  32. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  33. package/src/__tests__/build-persisted-content.test.ts +75 -1
  34. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  35. package/src/__tests__/channel-approvals.test.ts +1 -1
  36. package/src/__tests__/compaction-circuit.test.ts +258 -0
  37. package/src/__tests__/compaction-direct.test.ts +146 -0
  38. package/src/__tests__/compaction-events.test.ts +7 -7
  39. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  40. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  41. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  42. package/src/__tests__/context-overflow-reducer.test.ts +265 -125
  43. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  44. package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
  45. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  46. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  47. package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
  48. package/src/__tests__/conversation-agent-loop.test.ts +342 -623
  49. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  50. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
  51. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  52. package/src/__tests__/conversation-error.test.ts +15 -1
  53. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  54. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  55. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
  56. package/src/__tests__/conversation-process-callsite.test.ts +15 -15
  57. package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
  58. package/src/__tests__/conversation-queue.test.ts +10 -10
  59. package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
  60. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  61. package/src/__tests__/conversation-slash-queue.test.ts +7 -5
  62. package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
  63. package/src/__tests__/conversation-speed-override.test.ts +11 -10
  64. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  65. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  66. package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
  67. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  68. package/src/__tests__/conversation-undo.test.ts +2 -2
  69. package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
  70. package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
  71. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
  72. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  73. package/src/__tests__/corrected-target.test.ts +93 -0
  74. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -4
  75. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  76. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  77. package/src/__tests__/db-acp-history.test.ts +101 -0
  78. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  79. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  80. package/src/__tests__/empty-response-hook.test.ts +1 -1
  81. package/src/__tests__/file-write-tool.test.ts +63 -0
  82. package/src/__tests__/gateway-only-guard.test.ts +17 -2
  83. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  84. package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
  85. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  86. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  87. package/src/__tests__/heartbeat-service.test.ts +1 -0
  88. package/src/__tests__/history-repair-hook.test.ts +5 -1
  89. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  90. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  91. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  92. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  93. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  94. package/src/__tests__/injector-background-turn.test.ts +13 -23
  95. package/src/__tests__/injector-chain.test.ts +268 -44
  96. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  97. package/src/__tests__/injector-document-comments.test.ts +97 -114
  98. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  99. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  100. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  101. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  102. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  103. package/src/__tests__/llm-resolver.test.ts +73 -0
  104. package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
  105. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  106. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  107. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  108. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  109. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  110. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  111. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  112. package/src/__tests__/plugin-bootstrap.test.ts +91 -81
  113. package/src/__tests__/plugin-registry.test.ts +3 -49
  114. package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
  115. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  116. package/src/__tests__/plugin-types.test.ts +0 -70
  117. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  118. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  119. package/src/__tests__/published-app-updater.test.ts +138 -0
  120. package/src/__tests__/reaction-persistence.test.ts +1 -1
  121. package/src/__tests__/retry-thinking-adaptive-only.test.ts +245 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  123. package/src/__tests__/server-history-render.test.ts +39 -0
  124. package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
  125. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  126. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  127. package/src/__tests__/subagent-detail.test.ts +25 -7
  128. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  129. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  130. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  131. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  132. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  133. package/src/__tests__/title-generate-hook.test.ts +5 -1
  134. package/src/__tests__/tool-error-hook.test.ts +1 -1
  135. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  136. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  137. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  138. package/src/__tests__/web-fetch.test.ts +45 -0
  139. package/src/acp/__tests__/agent-process.test.ts +161 -0
  140. package/src/acp/__tests__/client-handler.test.ts +40 -0
  141. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  142. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  143. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  144. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  145. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  146. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  147. package/src/acp/agent-process.ts +61 -1
  148. package/src/acp/auto-install.test.ts +280 -0
  149. package/src/acp/auto-install.ts +232 -0
  150. package/src/acp/client-handler.ts +31 -0
  151. package/src/acp/prepare-agent-env.ts +80 -27
  152. package/src/acp/resolve-agent.test.ts +188 -28
  153. package/src/acp/resolve-agent.ts +119 -42
  154. package/src/acp/resume-hint.ts +23 -0
  155. package/src/acp/session-manager.ts +507 -73
  156. package/src/agent/compaction-circuit.ts +60 -102
  157. package/src/agent/loop.ts +403 -251
  158. package/src/api/events/tool-result.ts +9 -0
  159. package/src/api/events/tool-use-start.ts +7 -0
  160. package/src/api/responses/conversation-message.ts +72 -13
  161. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  162. package/src/approvals/guardian-request-resolvers.ts +27 -1
  163. package/src/background-wake/next-wake.ts +1 -0
  164. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  165. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  166. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  167. package/src/cli/commands/memory-v3.ts +57 -199
  168. package/src/cli/commands/plugins.ts +43 -37
  169. package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
  170. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  171. package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
  172. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
  173. package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
  174. package/src/cli/lib/install-from-github.ts +797 -60
  175. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  176. package/src/cli/lib/plugin-details.ts +416 -0
  177. package/src/cli/lib/plugin-marketplace.ts +215 -0
  178. package/src/cli/lib/search-plugins.ts +201 -29
  179. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  180. package/src/config/acp-defaults.test.ts +10 -0
  181. package/src/config/acp-defaults.ts +9 -3
  182. package/src/config/acp-schema.ts +1 -7
  183. package/src/config/bundled-skills/acp/SKILL.md +80 -41
  184. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  185. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  186. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  187. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  188. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  189. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  190. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  191. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  192. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  193. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  194. package/src/config/bundled-tool-registry.ts +4 -0
  195. package/src/config/call-site-defaults.ts +0 -3
  196. package/src/config/feature-flag-registry.json +10 -56
  197. package/src/config/llm-resolver.ts +39 -7
  198. package/src/config/loader.ts +5 -22
  199. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  200. package/src/config/schemas/call-site-catalog.ts +0 -21
  201. package/src/config/schemas/heartbeat.ts +9 -0
  202. package/src/config/schemas/llm.ts +17 -3
  203. package/src/config/schemas/memory-v3.ts +58 -8
  204. package/src/config/seed-inference-profiles.ts +18 -0
  205. package/src/context/compactor.ts +22 -4
  206. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  207. package/src/context/strip-injections.ts +8 -2
  208. package/src/daemon/conversation-agent-loop-handlers.ts +120 -42
  209. package/src/daemon/conversation-agent-loop.ts +308 -1297
  210. package/src/daemon/conversation-error.ts +31 -4
  211. package/src/daemon/conversation-history.ts +1 -1
  212. package/src/daemon/conversation-lifecycle.ts +11 -255
  213. package/src/daemon/conversation-media-retry.ts +19 -6
  214. package/src/daemon/conversation-messaging.ts +17 -0
  215. package/src/daemon/conversation-process.ts +22 -141
  216. package/src/daemon/conversation-queue-manager.ts +8 -0
  217. package/src/daemon/conversation-registry.ts +159 -0
  218. package/src/daemon/conversation-runtime-assembly.ts +361 -393
  219. package/src/daemon/conversation-store.ts +9 -90
  220. package/src/daemon/conversation-surfaces.ts +165 -11
  221. package/src/daemon/conversation-workspace.ts +17 -0
  222. package/src/daemon/conversation.ts +444 -62
  223. package/src/daemon/disk-pressure-policy.ts +0 -1
  224. package/src/daemon/external-plugins-bootstrap.ts +36 -46
  225. package/src/daemon/handlers/config-channels.ts +11 -2
  226. package/src/daemon/handlers/conversations.ts +3 -1
  227. package/src/daemon/handlers/shared.ts +9 -1
  228. package/src/daemon/handlers/skills.ts +4 -1
  229. package/src/daemon/host-app-control-proxy.ts +72 -57
  230. package/src/daemon/host-proxy-preactivation.ts +1 -3
  231. package/src/daemon/lifecycle.ts +21 -7
  232. package/src/daemon/persist-unsendable-image.ts +62 -25
  233. package/src/daemon/process-message.ts +1 -1
  234. package/src/daemon/server.ts +2 -0
  235. package/src/daemon/tool-side-effects.ts +15 -0
  236. package/src/daemon/wake-conversation-ops.ts +269 -0
  237. package/src/embedded/plugin-api.ts +2 -2
  238. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  239. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  240. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  241. package/src/heartbeat/heartbeat-service.ts +26 -0
  242. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  243. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  244. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  245. package/src/ipc/skill-routes/memory.ts +4 -2
  246. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  247. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  248. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  249. package/src/memory/activation-session-store.ts +43 -0
  250. package/src/memory/conversation-crud.ts +29 -19
  251. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  252. package/src/memory/db-init.ts +6 -0
  253. package/src/memory/embedding-backend.ts +5 -22
  254. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  255. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  256. package/src/memory/jobs-worker.ts +25 -1
  257. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  258. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  259. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  260. package/src/memory/migrations/index.ts +3 -0
  261. package/src/memory/onboarding-events-store.ts +66 -18
  262. package/src/memory/schema/acp.ts +4 -0
  263. package/src/memory/schema/infrastructure.ts +13 -0
  264. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  265. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  266. package/src/memory/v2/consolidation-job.ts +2 -63
  267. package/src/memory/v2/types.ts +5 -0
  268. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  269. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  270. package/src/notifications/access-request-copy.ts +298 -0
  271. package/src/notifications/adapters/slack.ts +3 -3
  272. package/src/notifications/adapters/telegram.ts +9 -2
  273. package/src/notifications/copy-composer.ts +49 -267
  274. package/src/notifications/decision-engine.ts +16 -35
  275. package/src/notifications/home-feed-side-effect.ts +1 -6
  276. package/src/plugin-api/constants.ts +4 -0
  277. package/src/plugin-api/index.ts +40 -11
  278. package/src/plugin-api/types.ts +108 -0
  279. package/src/plugins/defaults/compaction/compact.ts +106 -0
  280. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
  281. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  282. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  283. package/src/plugins/defaults/compaction/package.json +1 -2
  284. package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
  285. package/src/plugins/defaults/empty-response/package.json +0 -1
  286. package/src/plugins/defaults/history-repair/package.json +0 -1
  287. package/src/plugins/defaults/index.ts +156 -26
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  291. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  297. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  304. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  305. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
  306. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  307. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
  308. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
  309. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
  310. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  311. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  312. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  313. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  314. package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
  315. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
  316. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
  317. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  318. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
  319. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  320. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
  321. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  322. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  323. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  324. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
  325. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
  326. package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
  327. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
  328. package/src/plugins/defaults/title-generate/package.json +0 -1
  329. package/src/plugins/defaults/tool-error/package.json +0 -1
  330. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  331. package/src/plugins/external-api.ts +12 -2
  332. package/src/plugins/pipeline.ts +6 -293
  333. package/src/plugins/registry.ts +9 -37
  334. package/src/plugins/types.ts +76 -381
  335. package/src/plugins/user-loader.ts +30 -127
  336. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  337. package/src/prompts/system-prompt.ts +61 -10
  338. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
  339. package/src/providers/anthropic/client.ts +9 -10
  340. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  341. package/src/providers/inference/logit-bias.ts +55 -0
  342. package/src/providers/model-catalog.ts +57 -0
  343. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  344. package/src/providers/openai/chat-completions-provider.ts +44 -1
  345. package/src/providers/retry.ts +57 -7
  346. package/src/providers/types.ts +6 -0
  347. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  348. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  349. package/src/runtime/agent-wake.ts +108 -209
  350. package/src/runtime/assistant-event-hub.ts +1 -1
  351. package/src/runtime/channel-approvals.ts +1 -1
  352. package/src/runtime/interactive-ui.ts +1 -1
  353. package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
  354. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  355. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  356. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  357. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  358. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  359. package/src/runtime/routes/acp-routes.test.ts +185 -32
  360. package/src/runtime/routes/acp-routes.ts +328 -30
  361. package/src/runtime/routes/app-management-routes.ts +3 -0
  362. package/src/runtime/routes/approval-routes.ts +1 -1
  363. package/src/runtime/routes/browser-routes.ts +1 -1
  364. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  365. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  366. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  367. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  368. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  369. package/src/runtime/routes/conversation-routes.ts +30 -2
  370. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  371. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  372. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  373. package/src/runtime/routes/host-cu-routes.ts +1 -1
  374. package/src/runtime/routes/identity-routes.ts +18 -3
  375. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  376. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  377. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  378. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  379. package/src/runtime/routes/memory-v3-routes.ts +65 -305
  380. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  381. package/src/runtime/routes/playground/helpers.ts +1 -1
  382. package/src/runtime/routes/plugins-routes.ts +337 -35
  383. package/src/runtime/routes/stt-routes.ts +45 -12
  384. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  385. package/src/runtime/routes/work-items-routes.ts +2 -4
  386. package/src/runtime/routes/workspace-routes.ts +50 -15
  387. package/src/runtime/services/conversation-serializer.ts +1 -1
  388. package/src/runtime/verification-outbound-actions.ts +147 -2
  389. package/src/runtime/verification-templates.ts +29 -3
  390. package/src/services/published-app-updater.ts +30 -8
  391. package/src/signals/cancel.ts +2 -4
  392. package/src/subagent/manager.ts +21 -5
  393. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  394. package/src/telemetry/activation-funnel.ts +167 -0
  395. package/src/telemetry/types.ts +13 -0
  396. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  397. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  398. package/src/tools/acp/context.ts +20 -0
  399. package/src/tools/acp/list-agents.test.ts +9 -19
  400. package/src/tools/acp/list-agents.ts +3 -15
  401. package/src/tools/acp/spawn.test.ts +173 -202
  402. package/src/tools/acp/spawn.ts +37 -172
  403. package/src/tools/acp/steer.test.ts +105 -8
  404. package/src/tools/acp/steer.ts +48 -17
  405. package/src/tools/apps/executors.ts +166 -50
  406. package/src/tools/browser/browser-execution.ts +12 -2
  407. package/src/tools/filesystem/write.ts +34 -0
  408. package/src/tools/network/web-fetch.ts +65 -24
  409. package/src/tools/skills/load.ts +1 -1
  410. package/src/tools/subagent/spawn.ts +2 -4
  411. package/src/tools/ui-surface/definitions.ts +32 -5
  412. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  413. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  414. package/docs/plugins.md +0 -832
  415. package/examples/plugins/echo/register.ts +0 -143
  416. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  417. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  418. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  419. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  420. package/src/__tests__/pipeline-runner.test.ts +0 -554
  421. package/src/daemon/wake-target-adapter.ts +0 -253
  422. package/src/memory/v3/__tests__/assign.test.ts +0 -242
  423. package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
  424. package/src/memory/v3/__tests__/core.test.ts +0 -39
  425. package/src/memory/v3/__tests__/health.test.ts +0 -219
  426. package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
  427. package/src/memory/v3/__tests__/needle.test.ts +0 -107
  428. package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
  429. package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
  430. package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
  431. package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
  432. package/src/memory/v3/__tests__/router.test.ts +0 -332
  433. package/src/memory/v3/__tests__/selector.test.ts +0 -470
  434. package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
  435. package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
  436. package/src/memory/v3/__tests__/tree.test.ts +0 -192
  437. package/src/memory/v3/assign.ts +0 -268
  438. package/src/memory/v3/capabilities.ts +0 -124
  439. package/src/memory/v3/core.ts +0 -26
  440. package/src/memory/v3/health.ts +0 -0
  441. package/src/memory/v3/maintain-job.ts +0 -314
  442. package/src/memory/v3/needle.ts +0 -115
  443. package/src/memory/v3/orchestrate.ts +0 -126
  444. package/src/memory/v3/page-content.ts +0 -34
  445. package/src/memory/v3/provider-blocks.ts +0 -26
  446. package/src/memory/v3/reconcile.ts +0 -523
  447. package/src/memory/v3/router.ts +0 -190
  448. package/src/memory/v3/selection-log-store.ts +0 -84
  449. package/src/memory/v3/selector.ts +0 -226
  450. package/src/memory/v3/shadow-plugin.ts +0 -411
  451. package/src/memory/v3/snapshot.ts +0 -209
  452. package/src/memory/v3/tree.ts +0 -174
  453. package/src/memory/v3/types.ts +0 -59
  454. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  455. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  456. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  457. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  458. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  459. package/src/plugins/defaults/compaction/register.ts +0 -35
  460. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  461. package/src/plugins/defaults/empty-response/register.ts +0 -23
  462. package/src/plugins/defaults/history-repair/register.ts +0 -24
  463. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
  464. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  465. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  466. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  467. package/src/plugins/defaults/title-generate/register.ts +0 -35
  468. package/src/plugins/defaults/tool-error/register.ts +0 -23
  469. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  470. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  471. package/src/proactive-artifact/decision.test.ts +0 -226
  472. package/src/proactive-artifact/decision.ts +0 -165
  473. package/src/proactive-artifact/index.ts +0 -7
  474. package/src/proactive-artifact/job.test.ts +0 -962
  475. package/src/proactive-artifact/job.ts +0 -372
  476. package/src/proactive-artifact/message-copy.ts +0 -58
  477. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  478. package/src/proactive-artifact/trigger-state.ts +0 -123
  479. package/src/util/map-limit.ts +0 -27
  480. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  481. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  482. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  483. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  484. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  485. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  486. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  487. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  488. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  489. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  490. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
@@ -0,0 +1,662 @@
1
+ /**
2
+ * Tests for `orchestrate.ts` (section-lane pipeline).
3
+ *
4
+ * The orchestrator composes three deterministic candidate lanes — the
5
+ * section-grain BM25 needle, the dense lane, and link-graph edge expansion —
6
+ * into ONE unified pool, runs a SINGLE forced-tool select over the pool, then
7
+ * carries forward the working set. Synthetic capability pages (skills / CLI
8
+ * commands) are indexed like any other page, so they enter the pool through the
9
+ * lanes (e.g. the needle) rather than being always-added.
10
+ *
11
+ * The select provider is stubbed (no network); a single stub answers the one
12
+ * `select_pages` call per turn by reading the numbered `<candidates>` block.
13
+ * The dense lane is stubbed at the module boundary. The needle and edge graph
14
+ * are real, built from tiny inline fixtures so the pool union is exercised
15
+ * end-to-end.
16
+ */
17
+
18
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
19
+
20
+ import type { PageIndexEntry } from "../../../../memory/v2/page-index.js";
21
+ import type {
22
+ Message,
23
+ Provider,
24
+ ProviderResponse,
25
+ } from "../../../../providers/types.js";
26
+ import type { EdgeGraph } from "../edge.js";
27
+ import { buildEdgeGraph } from "../edge.js";
28
+ import { buildSectionNeedle } from "../section-needle.js";
29
+ import { buildSectionIndex } from "../sections.js";
30
+ import type { MemoryRoutingTurn, SectionIndex, Slug } from "../types.js";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Module stubs installed BEFORE the orchestrator import so pool-select and the
34
+ // dense lane observe them at load time.
35
+ // ---------------------------------------------------------------------------
36
+
37
+ let providerStub: Provider | null = null;
38
+
39
+ mock.module("../../../../providers/provider-send-message.js", () => ({
40
+ getConfiguredProvider: async () => providerStub,
41
+ extractToolUse: (response: ProviderResponse) =>
42
+ response.content.find((b) => b.type === "tool_use"),
43
+ }));
44
+
45
+ mock.module("../../../../util/logger.js", () => ({
46
+ getLogger: () =>
47
+ new Proxy({} as Record<string, unknown>, {
48
+ get: (_t, prop) => (prop === "child" ? () => ({}) : () => {}),
49
+ }),
50
+ }));
51
+
52
+ // The dense lane is stubbed: each test sets `denseHits` to control which
53
+ // articles (+ matched ordinals) the dense lane returns. The stub DELEGATES to
54
+ // the real `denseLane` unless this file's tests are running (`denseMockActive`),
55
+ // so the process-global `mock.module` cannot leak fake behavior into
56
+ // dense.test.ts (which exercises the real lane). Spread the real module so
57
+ // every other export (`OVERSAMPLE`) stays present.
58
+ const realDense = { ...(await import("../dense.js")) };
59
+ let denseMockActive = false;
60
+ let denseHits: Array<{ article: Slug; section: number }> = [];
61
+ mock.module("../dense.js", () => ({
62
+ ...realDense,
63
+ denseLane: async (...args: Parameters<typeof realDense.denseLane>) =>
64
+ denseMockActive ? denseHits : realDense.denseLane(...args),
65
+ }));
66
+
67
+ const { orchestrate, DEFAULT_NEEDLE_K, DEFAULT_DENSE_K } =
68
+ await import("../orchestrate.js");
69
+ const { WorkingSet } = await import("../working-set.js");
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Fixtures: a tiny corpus of pages with bodies + `links:` frontmatter.
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const PAGES: Record<Slug, string> = {
76
+ "topic-a": "lead for topic a\n## Details\napple banana about topic a",
77
+ "topic-b": "lead for topic b\n## More\ncherry date about topic b",
78
+ "topic-c": "lead for topic c\n## Notes\nelderberry fig about topic c",
79
+ "topic-d": "lead for topic d\n## Notes\ngrape about topic d",
80
+ };
81
+
82
+ /** Raw page = frontmatter (with `links:`) + body. */
83
+ const RAW: Record<Slug, string> = {
84
+ "topic-a": `---\nlinks:\n - "topic-d — the curated edge from a to d"\n---\n${PAGES["topic-a"]}`,
85
+ "topic-b": `---\nedges: []\n---\n${PAGES["topic-b"]}`,
86
+ "topic-c": `---\nedges: []\n---\n${PAGES["topic-c"]}`,
87
+ "topic-d": `---\nedges: []\n---\n${PAGES["topic-d"]}`,
88
+ };
89
+
90
+ const SLUGS = Object.keys(PAGES);
91
+
92
+ function makeEntries(): PageIndexEntry[] {
93
+ return SLUGS.map((slug, i) => ({
94
+ id: i + 1,
95
+ slug,
96
+ summary: `summary of ${slug}`,
97
+ edges: [],
98
+ leaves: [],
99
+ modifiedAt: 0,
100
+ }));
101
+ }
102
+
103
+ async function buildLanes(): Promise<{
104
+ sectionIndex: SectionIndex;
105
+ needle: ReturnType<typeof buildSectionNeedle>;
106
+ edgeGraph: EdgeGraph;
107
+ }> {
108
+ const sectionIndex = await buildSectionIndex(SLUGS, async (s) => PAGES[s]!);
109
+ const needle = buildSectionNeedle(sectionIndex);
110
+ const edgeGraph = await buildEdgeGraph(makeEntries(), async (s) => RAW[s]!);
111
+ return { sectionIndex, needle, edgeGraph };
112
+ }
113
+
114
+ const config = {} as never;
115
+
116
+ function makeTurn(
117
+ turnNumber: number,
118
+ currentMessage: string,
119
+ ): MemoryRoutingTurn {
120
+ return {
121
+ conversationId: "conv-xyz",
122
+ turnNumber,
123
+ currentMessage,
124
+ recentContext: "prior context",
125
+ };
126
+ }
127
+
128
+ function toolUseResponse(input: Record<string, unknown>): ProviderResponse {
129
+ return {
130
+ model: "stub-model",
131
+ stopReason: "tool_use",
132
+ usage: { inputTokens: 0, outputTokens: 0 },
133
+ content: [{ type: "tool_use", id: "tu-1", name: "select_pages", input }],
134
+ };
135
+ }
136
+
137
+ /** Parse the numbered `<candidates>` block back into an ordered slug list. */
138
+ function candidateSlugs(messages: Message[]): Slug[] {
139
+ for (const msg of messages) {
140
+ for (const block of msg.content) {
141
+ if (block.type !== "text") continue;
142
+ const m = /<candidates>\n([\s\S]*?)\n<\/candidates>/.exec(block.text);
143
+ if (!m) continue;
144
+ return m[1]
145
+ .split("\n")
146
+ .map((line) => /^\[\d+\] (\S+) —/.exec(line)?.[1])
147
+ .filter((s): s is string => !!s);
148
+ }
149
+ }
150
+ return [];
151
+ }
152
+
153
+ /**
154
+ * Provider that selects the pool candidates whose slug is in `keep` (mapping
155
+ * each back to its 1-based id), pinning those in `pin`. Captures the rendered
156
+ * candidate list for pool assertions.
157
+ */
158
+ let lastPool: Slug[] = [];
159
+ let selectCalls = 0;
160
+ function selectProvider(keep: Slug[], pin: Slug[] = []): Provider {
161
+ return {
162
+ name: "stub",
163
+ sendMessage: async (messages) => {
164
+ selectCalls++;
165
+ const pool = candidateSlugs(messages);
166
+ lastPool = pool;
167
+ const ids: number[] = [];
168
+ const pinned_ids: number[] = [];
169
+ pool.forEach((slug, i) => {
170
+ if (keep.includes(slug)) ids.push(i + 1);
171
+ if (pin.includes(slug)) pinned_ids.push(i + 1);
172
+ });
173
+ return toolUseResponse({ ids, pinned_ids });
174
+ },
175
+ };
176
+ }
177
+
178
+ beforeEach(() => {
179
+ denseMockActive = true;
180
+ providerStub = null;
181
+ denseHits = [];
182
+ lastPool = [];
183
+ selectCalls = 0;
184
+ });
185
+
186
+ afterAll(() => {
187
+ denseMockActive = false;
188
+ });
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Pool composition: the candidate pool is the union of the lanes. Synthetic
192
+ // capability pages are no longer always-added — they enter through a lane (see
193
+ // the dedicated test below).
194
+ // ---------------------------------------------------------------------------
195
+
196
+ describe("orchestrate — candidate pool composition", () => {
197
+ test("pool unions needle ∪ dense ∪ edge; one select runs", async () => {
198
+ const lanes = await buildLanes();
199
+ // "apple" hits topic-a (needle). Dense returns topic-b. topic-a links to
200
+ // topic-d (edge).
201
+ denseHits = [{ article: "topic-b", section: 0 }];
202
+ providerStub = selectProvider([]); // selection is irrelevant to pool union
203
+
204
+ await orchestrate(makeTurn(1, "apple"), {
205
+ sectionIndex: lanes.sectionIndex,
206
+ needle: lanes.needle,
207
+ denseConfig: config,
208
+ edgeGraph: lanes.edgeGraph,
209
+ workingSet: new WorkingSet(),
210
+ });
211
+
212
+ expect(selectCalls).toBe(1);
213
+ expect(new Set(lastPool)).toEqual(
214
+ new Set(["topic-a", "topic-b", "topic-d"]),
215
+ );
216
+ });
217
+
218
+ test("a synthetic capability page enters the pool via the needle lane", async () => {
219
+ // A capability slug indexed with body content (the section index treats it
220
+ // like any other page) is ranked by the real needle when the query matches
221
+ // its text — this is how synthetic pages reach the pool now that they are no
222
+ // longer always-added.
223
+ const CAP: Slug = "skills/example";
224
+ const sectionIndex = await buildSectionIndex([...SLUGS, CAP], async (s) =>
225
+ s === CAP
226
+ ? "# Skill: example\nuse the kumquat skill to do the thing"
227
+ : PAGES[s]!,
228
+ );
229
+ const needle = buildSectionNeedle(sectionIndex);
230
+ const edgeGraph = await buildEdgeGraph(makeEntries(), async (s) => RAW[s]!);
231
+
232
+ providerStub = selectProvider([]); // selection irrelevant to pool union
233
+ await orchestrate(makeTurn(1, "kumquat"), {
234
+ sectionIndex,
235
+ needle,
236
+ denseConfig: config,
237
+ edgeGraph,
238
+ workingSet: new WorkingSet(),
239
+ });
240
+
241
+ // The needle ranked the capability page on the "kumquat" term, so it is in
242
+ // the candidate pool.
243
+ expect(lastPool).toContain(CAP);
244
+ });
245
+
246
+ test("edge curated link description becomes the edge candidate's descriptor", async () => {
247
+ const lanes = await buildLanes();
248
+ let descriptorLine = "";
249
+ providerStub = {
250
+ name: "stub",
251
+ sendMessage: async (messages) => {
252
+ for (const msg of messages) {
253
+ for (const block of msg.content) {
254
+ if (block.type !== "text") continue;
255
+ const m = /\[\d+\] topic-d — ([^\n]+)/.exec(block.text);
256
+ if (m) descriptorLine = m[1]!;
257
+ }
258
+ }
259
+ return toolUseResponse({ ids: [] });
260
+ },
261
+ };
262
+
263
+ await orchestrate(makeTurn(1, "apple"), {
264
+ sectionIndex: lanes.sectionIndex,
265
+ needle: lanes.needle,
266
+ denseConfig: config,
267
+ edgeGraph: lanes.edgeGraph,
268
+ workingSet: new WorkingSet(),
269
+ });
270
+
271
+ expect(descriptorLine).toContain("the curated edge from a to d");
272
+ });
273
+
274
+ test("needleK and denseK default to their constants", async () => {
275
+ const lanes = await buildLanes();
276
+ let needleK = -1;
277
+ const needle = {
278
+ query: (_t: string, k: number) => {
279
+ needleK = k;
280
+ return [];
281
+ },
282
+ bestSection: () => -1,
283
+ };
284
+ providerStub = selectProvider([]);
285
+ await orchestrate(makeTurn(1, "x"), {
286
+ sectionIndex: lanes.sectionIndex,
287
+ needle,
288
+ denseConfig: config,
289
+ edgeGraph: lanes.edgeGraph,
290
+ workingSet: new WorkingSet(),
291
+ });
292
+ expect(needleK).toBe(DEFAULT_NEEDLE_K);
293
+ expect(DEFAULT_DENSE_K).toBe(100);
294
+ });
295
+
296
+ test("sectionBySlug is populated from matched lane sections", async () => {
297
+ const lanes = await buildLanes();
298
+ denseHits = [];
299
+ providerStub = selectProvider(["topic-a"]);
300
+ const result = await orchestrate(makeTurn(1, "apple"), {
301
+ sectionIndex: lanes.sectionIndex,
302
+ needle: lanes.needle,
303
+ denseConfig: config,
304
+ edgeGraph: lanes.edgeGraph,
305
+ workingSet: new WorkingSet(),
306
+ });
307
+ // topic-a matched "apple" in its `## Details` section.
308
+ expect(result.sectionBySlug.get("topic-a")?.article).toBe("topic-a");
309
+ expect(result.sectionBySlug.get("topic-a")?.text).toContain("apple");
310
+ });
311
+ });
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // Edge-only injection: a page surfaced ONLY by the edge lane (the query did not
315
+ // lexically hit it) records NO matched section, so injection falls back to the
316
+ // FULL page (the curated `links` description — not the often-empty lead the
317
+ // query never hit — is what made the candidate relevant). The best-section text
318
+ // is kept only as the select-pool descriptor fallback.
319
+ // ---------------------------------------------------------------------------
320
+
321
+ describe("orchestrate — edge-only injection", () => {
322
+ test("an edge-only page records NO sectionBySlug entry (→ full-page inject)", async () => {
323
+ const lanes = await buildLanes();
324
+ // "apple" hits topic-a (needle); topic-a links to topic-d (edge-only — the
325
+ // query never hits topic-d). Select topic-d so it is in the result.
326
+ denseHits = [];
327
+ providerStub = selectProvider(["topic-d"]);
328
+ const result = await orchestrate(makeTurn(1, "apple"), {
329
+ sectionIndex: lanes.sectionIndex,
330
+ needle: lanes.needle,
331
+ denseConfig: config,
332
+ edgeGraph: lanes.edgeGraph,
333
+ workingSet: new WorkingSet(),
334
+ });
335
+
336
+ // topic-d was selected and injected, but with NO matched section — so
337
+ // `renderV3SectionContent(slug, undefined)` falls back to the full page.
338
+ expect(result.finalInjection).toContain("topic-d");
339
+ expect(result.sectionBySlug.has("topic-d")).toBe(false);
340
+ });
341
+
342
+ test("an edge-only page still carries the curated links description as its descriptor", async () => {
343
+ const lanes = await buildLanes();
344
+ let descriptorLine = "";
345
+ providerStub = {
346
+ name: "stub",
347
+ sendMessage: async (messages) => {
348
+ for (const msg of messages) {
349
+ for (const block of msg.content) {
350
+ if (block.type !== "text") continue;
351
+ const m = /\[\d+\] topic-d — ([^\n]+)/.exec(block.text);
352
+ if (m) descriptorLine = m[1]!;
353
+ }
354
+ }
355
+ return toolUseResponse({ ids: [] });
356
+ },
357
+ };
358
+
359
+ await orchestrate(makeTurn(1, "apple"), {
360
+ sectionIndex: lanes.sectionIndex,
361
+ needle: lanes.needle,
362
+ denseConfig: config,
363
+ edgeGraph: lanes.edgeGraph,
364
+ workingSet: new WorkingSet(),
365
+ });
366
+
367
+ // The curated `links` description (not the lead text) is the descriptor.
368
+ expect(descriptorLine).toContain("the curated edge from a to d");
369
+ });
370
+
371
+ test("an edge-only page with no curated description falls back to bestSection text as the descriptor", async () => {
372
+ // A bare `links:` entry (no ` — `) carries NO description, so the edge
373
+ // candidate's descriptor falls back to the page's best section against the
374
+ // query. The query never hits dst-page, so bestSection returns its lead;
375
+ // that lead text becomes the descriptor (and the page is still injected in
376
+ // full, with no sectionBySlug entry).
377
+ const pages: Record<Slug, string> = {
378
+ "src-page": "lead for src\n## Body\nalpha bravo about src",
379
+ "dst-page": "lead content for dst page\n## Extra\nnothing relevant here",
380
+ };
381
+ const raw: Record<Slug, string> = {
382
+ // bare slug — no ` — ` separator → undefined curated description.
383
+ "src-page": `---\nlinks:\n - "dst-page"\n---\n${pages["src-page"]}`,
384
+ "dst-page": `---\nedges: []\n---\n${pages["dst-page"]}`,
385
+ };
386
+ const slugs = Object.keys(pages);
387
+ const entries: PageIndexEntry[] = slugs.map((slug, i) => ({
388
+ id: i + 1,
389
+ slug,
390
+ summary: `summary of ${slug}`,
391
+ edges: [],
392
+ leaves: [],
393
+ modifiedAt: 0,
394
+ }));
395
+ const sectionIndex = await buildSectionIndex(slugs, async (s) => pages[s]!);
396
+ const needle = buildSectionNeedle(sectionIndex);
397
+ const edgeGraph = await buildEdgeGraph(entries, async (s) => raw[s]!);
398
+
399
+ let descriptorLine = "";
400
+ providerStub = {
401
+ name: "stub",
402
+ sendMessage: async (messages) => {
403
+ for (const msg of messages) {
404
+ for (const block of msg.content) {
405
+ if (block.type !== "text") continue;
406
+ const m = /\[\d+\] dst-page — ([^\n]+)/.exec(block.text);
407
+ if (m) descriptorLine = m[1]!;
408
+ }
409
+ }
410
+ return toolUseResponse({ ids: [] });
411
+ },
412
+ };
413
+
414
+ // "alpha" hits src-page (needle); src-page links to dst-page (edge-only, no
415
+ // curated description).
416
+ const result = await orchestrate(makeTurn(1, "alpha"), {
417
+ sectionIndex,
418
+ needle,
419
+ denseConfig: config,
420
+ edgeGraph,
421
+ workingSet: new WorkingSet(),
422
+ });
423
+
424
+ // Descriptor fell back to dst-page's lead text; still no matched section.
425
+ expect(descriptorLine).toContain("lead content for dst page");
426
+ expect(result.sectionBySlug.has("dst-page")).toBe(false);
427
+ });
428
+ });
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // Dense liveness: a dense hit whose article is no longer in the live section
432
+ // index (its page was deleted; its Qdrant points linger) is dropped from the
433
+ // pool, so a deleted page can never be surfaced via the dense lane.
434
+ // ---------------------------------------------------------------------------
435
+
436
+ describe("orchestrate — dense liveness filter", () => {
437
+ test("a dense hit absent from sectionIndex.byArticle is dropped from the pool", async () => {
438
+ const lanes = await buildLanes();
439
+ // Dense returns a live page (topic-b) AND a deleted page (gone-page) whose
440
+ // points still linger in Qdrant but which is absent from the section index.
441
+ denseHits = [
442
+ { article: "topic-b", section: 0 },
443
+ { article: "gone-page", section: 0 },
444
+ ];
445
+ providerStub = selectProvider([]); // selection irrelevant to pool membership
446
+ const result = await orchestrate(makeTurn(1, "apple"), {
447
+ sectionIndex: lanes.sectionIndex,
448
+ needle: lanes.needle,
449
+ denseConfig: config,
450
+ edgeGraph: lanes.edgeGraph,
451
+ workingSet: new WorkingSet(),
452
+ });
453
+
454
+ // The live dense hit is pooled; the deleted page is dropped entirely.
455
+ expect(lastPool).toContain("topic-b");
456
+ expect(lastPool).not.toContain("gone-page");
457
+ expect(result.laneBySlug.has("gone-page")).toBe(false);
458
+ });
459
+ });
460
+
461
+ // ---------------------------------------------------------------------------
462
+ // Lane provenance: each pooled slug records the candgen lane that FIRST
463
+ // surfaced it (needle → dense → edge precedence), exposed via
464
+ // `result.laneBySlug` so the selection telemetry can attribute true sources.
465
+ // ---------------------------------------------------------------------------
466
+
467
+ describe("orchestrate — lane provenance (laneBySlug)", () => {
468
+ test("laneBySlug tags each pooled slug with its surfacing lane", async () => {
469
+ const lanes = await buildLanes();
470
+ // "apple" hits topic-a (needle). Dense returns topic-b. topic-a links to
471
+ // topic-d (edge). So each lane contributes exactly one distinct slug.
472
+ denseHits = [{ article: "topic-b", section: 0 }];
473
+ providerStub = selectProvider([]); // selection irrelevant to pool provenance
474
+ const result = await orchestrate(makeTurn(1, "apple"), {
475
+ sectionIndex: lanes.sectionIndex,
476
+ needle: lanes.needle,
477
+ denseConfig: config,
478
+ edgeGraph: lanes.edgeGraph,
479
+ workingSet: new WorkingSet(),
480
+ });
481
+
482
+ expect(result.laneBySlug.get("topic-a")).toBe("needle");
483
+ expect(result.laneBySlug.get("topic-b")).toBe("dense");
484
+ expect(result.laneBySlug.get("topic-d")).toBe("edge");
485
+ });
486
+
487
+ test("a slug surfaced by needle AND dense keeps the needle lane (first wins)", async () => {
488
+ const lanes = await buildLanes();
489
+ // topic-a is surfaced by the needle on "apple"; dense ALSO returns topic-a.
490
+ // Needle runs first (step 2a before 2b), so the recorded lane stays needle.
491
+ denseHits = [{ article: "topic-a", section: 0 }];
492
+ providerStub = selectProvider([]);
493
+ const result = await orchestrate(makeTurn(1, "apple"), {
494
+ sectionIndex: lanes.sectionIndex,
495
+ needle: lanes.needle,
496
+ denseConfig: config,
497
+ edgeGraph: lanes.edgeGraph,
498
+ workingSet: new WorkingSet(),
499
+ });
500
+
501
+ expect(result.laneBySlug.get("topic-a")).toBe("needle");
502
+ });
503
+ });
504
+
505
+ // ---------------------------------------------------------------------------
506
+ // Carry-forward: same working-set semantics as the prior tree pipeline.
507
+ // ---------------------------------------------------------------------------
508
+
509
+ describe("orchestrate — carry-forward", () => {
510
+ test("a page selected+pinned in turn 1 carries into turn 2 without re-selection", async () => {
511
+ const lanes = await buildLanes();
512
+ const workingSet = new WorkingSet();
513
+
514
+ // Turn 1 selects+pins topic-a.
515
+ providerStub = selectProvider(["topic-a"], ["topic-a"]);
516
+ await orchestrate(makeTurn(1, "apple"), {
517
+ sectionIndex: lanes.sectionIndex,
518
+ needle: lanes.needle,
519
+ denseConfig: config,
520
+ edgeGraph: lanes.edgeGraph,
521
+ workingSet,
522
+ });
523
+
524
+ // Turn 2 selects a DIFFERENT page and never re-selects topic-a.
525
+ denseHits = [{ article: "topic-b", section: 0 }];
526
+ providerStub = selectProvider(["topic-b"]);
527
+ const t2 = await orchestrate(makeTurn(2, "cherry"), {
528
+ sectionIndex: lanes.sectionIndex,
529
+ needle: lanes.needle,
530
+ denseConfig: config,
531
+ edgeGraph: lanes.edgeGraph,
532
+ workingSet,
533
+ });
534
+
535
+ expect(t2.currentSelections.map((s) => s.slug)).not.toContain("topic-a");
536
+ expect(t2.finalInjection).toContain("topic-a");
537
+ });
538
+
539
+ test("carry-forward survives a turn whose selections fill the cap", async () => {
540
+ const lanes = await buildLanes();
541
+ // Cap of 1: under a naive record-then-cap order this turn's own selection
542
+ // would evict the carried page before injection. Snapshotting the carry
543
+ // BEFORE recording this turn keeps the earlier page in the injection.
544
+ const workingSet = new WorkingSet(1);
545
+
546
+ providerStub = selectProvider(["topic-a"]); // turn 1 → topic-a
547
+ await orchestrate(makeTurn(1, "apple"), {
548
+ sectionIndex: lanes.sectionIndex,
549
+ needle: lanes.needle,
550
+ denseConfig: config,
551
+ edgeGraph: lanes.edgeGraph,
552
+ workingSet,
553
+ });
554
+
555
+ denseHits = [{ article: "topic-b", section: 0 }];
556
+ providerStub = selectProvider(["topic-b"]); // turn 2 → topic-b
557
+ const t2 = await orchestrate(makeTurn(2, "cherry"), {
558
+ sectionIndex: lanes.sectionIndex,
559
+ needle: lanes.needle,
560
+ denseConfig: config,
561
+ edgeGraph: lanes.edgeGraph,
562
+ workingSet,
563
+ });
564
+
565
+ expect(t2.currentSelections.map((s) => s.slug)).toEqual(["topic-b"]);
566
+ expect(t2.finalInjection).toContain("topic-a"); // carried despite the cap
567
+ });
568
+
569
+ test("a stale non-pinned page ages out of the carry-forward window", async () => {
570
+ const lanes = await buildLanes();
571
+ // window 2: a non-pinned entry unseen for >2 turns evicts.
572
+ const workingSet = new WorkingSet(150, 2);
573
+
574
+ providerStub = selectProvider(["topic-a"]); // turn 1 selects topic-a
575
+ const t1 = await orchestrate(makeTurn(1, "apple"), {
576
+ sectionIndex: lanes.sectionIndex,
577
+ needle: lanes.needle,
578
+ denseConfig: config,
579
+ edgeGraph: lanes.edgeGraph,
580
+ workingSet,
581
+ });
582
+ expect(t1.finalInjection).toContain("topic-a");
583
+
584
+ // Turns 2–4 never re-select topic-a. By turn 4 (4-1=3 > 2) it ages out.
585
+ providerStub = selectProvider([]);
586
+ await orchestrate(makeTurn(2, "x"), {
587
+ sectionIndex: lanes.sectionIndex,
588
+ needle: lanes.needle,
589
+ denseConfig: config,
590
+ edgeGraph: lanes.edgeGraph,
591
+ workingSet,
592
+ });
593
+ await orchestrate(makeTurn(3, "x"), {
594
+ sectionIndex: lanes.sectionIndex,
595
+ needle: lanes.needle,
596
+ denseConfig: config,
597
+ edgeGraph: lanes.edgeGraph,
598
+ workingSet,
599
+ });
600
+ const t4 = await orchestrate(makeTurn(4, "x"), {
601
+ sectionIndex: lanes.sectionIndex,
602
+ needle: lanes.needle,
603
+ denseConfig: config,
604
+ edgeGraph: lanes.edgeGraph,
605
+ workingSet,
606
+ });
607
+ expect(t4.finalInjection).not.toContain("topic-a");
608
+ });
609
+
610
+ test("pinned current-turn selections land in the working set", async () => {
611
+ const lanes = await buildLanes();
612
+ const ws = new WorkingSet();
613
+ providerStub = selectProvider(["topic-a"], ["topic-a"]);
614
+ await orchestrate(makeTurn(1, "apple"), {
615
+ sectionIndex: lanes.sectionIndex,
616
+ needle: lanes.needle,
617
+ denseConfig: config,
618
+ edgeGraph: lanes.edgeGraph,
619
+ workingSet: ws,
620
+ });
621
+ expect(ws.union().has("topic-a")).toBe(true);
622
+ });
623
+ });
624
+
625
+ // ---------------------------------------------------------------------------
626
+ // Degradation: an empty pool and a provider-unavailable path are recall-safe.
627
+ // ---------------------------------------------------------------------------
628
+
629
+ describe("orchestrate — degradation", () => {
630
+ test("an empty pool yields no selections and an empty injection", async () => {
631
+ const lanes = await buildLanes();
632
+ providerStub = selectProvider([]);
633
+ const result = await orchestrate(makeTurn(1, "zzzzz no-match"), {
634
+ sectionIndex: lanes.sectionIndex,
635
+ needle: { query: () => [], bestSection: () => -1 },
636
+ denseConfig: config,
637
+ edgeGraph: lanes.edgeGraph,
638
+ workingSet: new WorkingSet(),
639
+ });
640
+ expect(result.currentSelections).toEqual([]);
641
+ expect(result.finalInjection).toEqual([]);
642
+ });
643
+
644
+ test("omitted ids keeps ALL pooled candidates (recall-safe)", async () => {
645
+ const lanes = await buildLanes();
646
+ denseHits = [{ article: "topic-b", section: 0 }];
647
+ providerStub = {
648
+ name: "stub",
649
+ sendMessage: async () => toolUseResponse({}), // omitted ids → keep all
650
+ };
651
+ const result = await orchestrate(makeTurn(1, "apple"), {
652
+ sectionIndex: lanes.sectionIndex,
653
+ needle: lanes.needle,
654
+ denseConfig: config,
655
+ edgeGraph: lanes.edgeGraph,
656
+ workingSet: new WorkingSet(),
657
+ });
658
+ expect(new Set(result.finalInjection)).toEqual(
659
+ new Set(["topic-a", "topic-b", "topic-d"]),
660
+ );
661
+ });
662
+ });