@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091853.fbaa2ae

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 (475) 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 +263 -121
  12. package/package.json +2 -2
  13. package/src/__tests__/activation-early-marking.test.ts +120 -0
  14. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  15. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  16. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  17. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  18. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  19. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  20. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  21. package/src/__tests__/agent-loop.test.ts +441 -96
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  23. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  24. package/src/__tests__/anthropic-provider.test.ts +23 -8
  25. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  26. package/src/__tests__/app-control-flow.test.ts +1 -1
  27. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  28. package/src/__tests__/app-executors.test.ts +132 -0
  29. package/src/__tests__/approval-cascade.test.ts +6 -5
  30. package/src/__tests__/approval-routes-http.test.ts +4 -1
  31. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  32. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  33. package/src/__tests__/channel-approvals.test.ts +1 -1
  34. package/src/__tests__/compaction-circuit.test.ts +258 -0
  35. package/src/__tests__/compaction-direct.test.ts +146 -0
  36. package/src/__tests__/compaction-events.test.ts +7 -7
  37. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  38. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  39. package/src/__tests__/context-overflow-reducer.test.ts +265 -125
  40. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  41. package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
  42. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  43. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  44. package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
  45. package/src/__tests__/conversation-agent-loop.test.ts +342 -623
  46. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  47. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
  48. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  49. package/src/__tests__/conversation-error.test.ts +15 -1
  50. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  51. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  52. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
  53. package/src/__tests__/conversation-process-callsite.test.ts +15 -15
  54. package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
  55. package/src/__tests__/conversation-queue.test.ts +10 -10
  56. package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
  57. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  58. package/src/__tests__/conversation-slash-queue.test.ts +7 -5
  59. package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
  60. package/src/__tests__/conversation-speed-override.test.ts +11 -10
  61. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  62. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  65. package/src/__tests__/conversation-undo.test.ts +2 -2
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
  67. package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
  69. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  70. package/src/__tests__/corrected-target.test.ts +93 -0
  71. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  72. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  73. package/src/__tests__/db-acp-history.test.ts +101 -0
  74. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  75. package/src/__tests__/empty-response-hook.test.ts +1 -1
  76. package/src/__tests__/file-write-tool.test.ts +63 -0
  77. package/src/__tests__/gateway-only-guard.test.ts +17 -2
  78. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  79. package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
  80. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  82. package/src/__tests__/heartbeat-service.test.ts +1 -0
  83. package/src/__tests__/history-repair-hook.test.ts +5 -1
  84. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  85. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  86. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  87. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  88. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  89. package/src/__tests__/injector-background-turn.test.ts +13 -23
  90. package/src/__tests__/injector-chain.test.ts +268 -44
  91. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  92. package/src/__tests__/injector-document-comments.test.ts +97 -114
  93. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  94. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  95. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  96. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  97. package/src/__tests__/llm-resolver.test.ts +73 -0
  98. package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
  99. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  100. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  101. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  102. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  103. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  104. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  105. package/src/__tests__/plugin-bootstrap.test.ts +91 -81
  106. package/src/__tests__/plugin-registry.test.ts +3 -49
  107. package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
  108. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  109. package/src/__tests__/plugin-types.test.ts +0 -70
  110. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  111. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  112. package/src/__tests__/published-app-updater.test.ts +138 -0
  113. package/src/__tests__/reaction-persistence.test.ts +1 -1
  114. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  115. package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
  116. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  117. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  118. package/src/__tests__/subagent-detail.test.ts +25 -7
  119. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  120. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  121. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  122. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  123. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  124. package/src/__tests__/title-generate-hook.test.ts +5 -1
  125. package/src/__tests__/tool-error-hook.test.ts +1 -1
  126. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  127. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  128. package/src/__tests__/web-fetch.test.ts +45 -0
  129. package/src/acp/__tests__/agent-process.test.ts +161 -0
  130. package/src/acp/__tests__/client-handler.test.ts +40 -0
  131. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  132. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  133. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  134. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  135. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  136. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  137. package/src/acp/agent-process.ts +61 -1
  138. package/src/acp/auto-install.test.ts +280 -0
  139. package/src/acp/auto-install.ts +232 -0
  140. package/src/acp/client-handler.ts +31 -0
  141. package/src/acp/prepare-agent-env.ts +80 -27
  142. package/src/acp/resolve-agent.test.ts +188 -28
  143. package/src/acp/resolve-agent.ts +119 -42
  144. package/src/acp/resume-hint.ts +23 -0
  145. package/src/acp/session-manager.ts +507 -73
  146. package/src/agent/compaction-circuit.ts +60 -102
  147. package/src/agent/loop.ts +403 -251
  148. package/src/api/responses/conversation-message.ts +22 -2
  149. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  150. package/src/approvals/guardian-request-resolvers.ts +27 -1
  151. package/src/background-wake/next-wake.ts +1 -0
  152. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  153. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  154. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  155. package/src/cli/commands/memory-v3.ts +57 -199
  156. package/src/cli/commands/plugins.ts +43 -37
  157. package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
  158. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  159. package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
  160. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
  161. package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
  162. package/src/cli/lib/install-from-github.ts +797 -60
  163. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  164. package/src/cli/lib/plugin-details.ts +416 -0
  165. package/src/cli/lib/plugin-marketplace.ts +215 -0
  166. package/src/cli/lib/search-plugins.ts +201 -29
  167. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  168. package/src/config/acp-defaults.test.ts +10 -0
  169. package/src/config/acp-defaults.ts +9 -3
  170. package/src/config/acp-schema.ts +1 -7
  171. package/src/config/bundled-skills/acp/SKILL.md +80 -41
  172. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  173. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  174. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  175. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  176. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  177. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  178. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  179. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  180. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  181. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  182. package/src/config/bundled-tool-registry.ts +4 -0
  183. package/src/config/call-site-defaults.ts +0 -3
  184. package/src/config/feature-flag-registry.json +10 -16
  185. package/src/config/llm-resolver.ts +39 -7
  186. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  187. package/src/config/schemas/call-site-catalog.ts +0 -21
  188. package/src/config/schemas/heartbeat.ts +9 -0
  189. package/src/config/schemas/llm.ts +17 -3
  190. package/src/config/schemas/memory-v3.ts +58 -8
  191. package/src/config/seed-inference-profiles.ts +18 -0
  192. package/src/context/compactor.ts +22 -4
  193. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  194. package/src/context/strip-injections.ts +8 -2
  195. package/src/daemon/conversation-agent-loop-handlers.ts +18 -36
  196. package/src/daemon/conversation-agent-loop.ts +308 -1297
  197. package/src/daemon/conversation-error.ts +31 -4
  198. package/src/daemon/conversation-history.ts +1 -1
  199. package/src/daemon/conversation-lifecycle.ts +11 -255
  200. package/src/daemon/conversation-media-retry.ts +19 -6
  201. package/src/daemon/conversation-messaging.ts +17 -0
  202. package/src/daemon/conversation-process.ts +22 -141
  203. package/src/daemon/conversation-queue-manager.ts +8 -0
  204. package/src/daemon/conversation-registry.ts +159 -0
  205. package/src/daemon/conversation-runtime-assembly.ts +361 -393
  206. package/src/daemon/conversation-store.ts +9 -90
  207. package/src/daemon/conversation-surfaces.ts +165 -11
  208. package/src/daemon/conversation-workspace.ts +17 -0
  209. package/src/daemon/conversation.ts +444 -62
  210. package/src/daemon/disk-pressure-policy.ts +0 -1
  211. package/src/daemon/external-plugins-bootstrap.ts +36 -46
  212. package/src/daemon/handlers/config-channels.ts +11 -2
  213. package/src/daemon/handlers/conversations.ts +3 -1
  214. package/src/daemon/handlers/skills.ts +4 -1
  215. package/src/daemon/host-app-control-proxy.ts +72 -57
  216. package/src/daemon/host-proxy-preactivation.ts +1 -3
  217. package/src/daemon/lifecycle.ts +21 -7
  218. package/src/daemon/persist-unsendable-image.ts +62 -25
  219. package/src/daemon/process-message.ts +1 -1
  220. package/src/daemon/server.ts +2 -0
  221. package/src/daemon/tool-side-effects.ts +15 -0
  222. package/src/daemon/wake-conversation-ops.ts +269 -0
  223. package/src/embedded/plugin-api.ts +2 -2
  224. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  225. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  226. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  227. package/src/heartbeat/heartbeat-service.ts +26 -0
  228. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  229. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  230. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  231. package/src/ipc/skill-routes/memory.ts +4 -2
  232. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  233. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  234. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  235. package/src/memory/activation-session-store.ts +43 -0
  236. package/src/memory/conversation-crud.ts +29 -19
  237. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  238. package/src/memory/db-init.ts +6 -0
  239. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  240. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  241. package/src/memory/jobs-worker.ts +25 -1
  242. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  243. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  244. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  245. package/src/memory/migrations/index.ts +3 -0
  246. package/src/memory/onboarding-events-store.ts +66 -18
  247. package/src/memory/schema/acp.ts +4 -0
  248. package/src/memory/schema/infrastructure.ts +13 -0
  249. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  250. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  251. package/src/memory/v2/consolidation-job.ts +2 -63
  252. package/src/memory/v2/types.ts +5 -0
  253. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  254. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  255. package/src/notifications/access-request-copy.ts +298 -0
  256. package/src/notifications/adapters/slack.ts +3 -3
  257. package/src/notifications/adapters/telegram.ts +9 -2
  258. package/src/notifications/copy-composer.ts +49 -267
  259. package/src/notifications/decision-engine.ts +16 -35
  260. package/src/notifications/home-feed-side-effect.ts +1 -6
  261. package/src/plugin-api/constants.ts +4 -0
  262. package/src/plugin-api/index.ts +40 -11
  263. package/src/plugin-api/types.ts +108 -0
  264. package/src/plugins/defaults/compaction/compact.ts +106 -0
  265. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
  266. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  267. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  268. package/src/plugins/defaults/compaction/package.json +1 -2
  269. package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
  270. package/src/plugins/defaults/empty-response/package.json +0 -1
  271. package/src/plugins/defaults/history-repair/package.json +0 -1
  272. package/src/plugins/defaults/index.ts +156 -26
  273. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  274. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
  275. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  276. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  277. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  278. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  282. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
  283. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
  284. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
  285. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  286. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
  287. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  288. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  289. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  290. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
  291. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
  293. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
  294. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
  295. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  296. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  297. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  298. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  299. package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
  300. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
  301. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
  302. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  303. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
  304. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  305. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
  306. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  307. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  308. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  309. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
  310. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
  311. package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
  312. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
  313. package/src/plugins/defaults/title-generate/package.json +0 -1
  314. package/src/plugins/defaults/tool-error/package.json +0 -1
  315. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  316. package/src/plugins/external-api.ts +12 -2
  317. package/src/plugins/pipeline.ts +6 -293
  318. package/src/plugins/registry.ts +9 -37
  319. package/src/plugins/types.ts +76 -381
  320. package/src/plugins/user-loader.ts +30 -127
  321. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  322. package/src/prompts/system-prompt.ts +61 -10
  323. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
  324. package/src/providers/anthropic/client.ts +9 -10
  325. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  326. package/src/providers/inference/logit-bias.ts +55 -0
  327. package/src/providers/model-catalog.ts +34 -0
  328. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  329. package/src/providers/openai/chat-completions-provider.ts +44 -1
  330. package/src/providers/retry.ts +22 -0
  331. package/src/providers/types.ts +6 -0
  332. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  333. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  334. package/src/runtime/agent-wake.ts +108 -209
  335. package/src/runtime/assistant-event-hub.ts +1 -1
  336. package/src/runtime/channel-approvals.ts +1 -1
  337. package/src/runtime/interactive-ui.ts +1 -1
  338. package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
  339. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  340. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  341. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  342. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  343. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  344. package/src/runtime/routes/acp-routes.test.ts +185 -32
  345. package/src/runtime/routes/acp-routes.ts +328 -30
  346. package/src/runtime/routes/app-management-routes.ts +3 -0
  347. package/src/runtime/routes/approval-routes.ts +1 -1
  348. package/src/runtime/routes/browser-routes.ts +1 -1
  349. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  350. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  351. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  352. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  353. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  354. package/src/runtime/routes/conversation-routes.ts +30 -2
  355. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  356. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  357. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  358. package/src/runtime/routes/host-cu-routes.ts +1 -1
  359. package/src/runtime/routes/identity-routes.ts +18 -3
  360. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  361. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  362. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  363. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  364. package/src/runtime/routes/memory-v3-routes.ts +65 -305
  365. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  366. package/src/runtime/routes/playground/helpers.ts +1 -1
  367. package/src/runtime/routes/plugins-routes.ts +337 -35
  368. package/src/runtime/routes/stt-routes.ts +45 -12
  369. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  370. package/src/runtime/routes/work-items-routes.ts +2 -4
  371. package/src/runtime/routes/workspace-routes.ts +50 -15
  372. package/src/runtime/services/conversation-serializer.ts +1 -1
  373. package/src/runtime/verification-outbound-actions.ts +147 -2
  374. package/src/runtime/verification-templates.ts +29 -3
  375. package/src/services/published-app-updater.ts +30 -8
  376. package/src/signals/cancel.ts +2 -4
  377. package/src/subagent/manager.ts +21 -5
  378. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  379. package/src/telemetry/activation-funnel.ts +167 -0
  380. package/src/telemetry/types.ts +13 -0
  381. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  382. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  383. package/src/tools/acp/context.ts +20 -0
  384. package/src/tools/acp/list-agents.test.ts +9 -19
  385. package/src/tools/acp/list-agents.ts +3 -15
  386. package/src/tools/acp/spawn.test.ts +173 -202
  387. package/src/tools/acp/spawn.ts +37 -172
  388. package/src/tools/acp/steer.test.ts +105 -8
  389. package/src/tools/acp/steer.ts +48 -17
  390. package/src/tools/apps/executors.ts +166 -50
  391. package/src/tools/browser/browser-execution.ts +12 -2
  392. package/src/tools/filesystem/write.ts +34 -0
  393. package/src/tools/network/web-fetch.ts +65 -24
  394. package/src/tools/skills/load.ts +1 -1
  395. package/src/tools/subagent/spawn.ts +2 -4
  396. package/src/tools/ui-surface/definitions.ts +32 -5
  397. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  398. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  399. package/docs/plugins.md +0 -832
  400. package/examples/plugins/echo/register.ts +0 -143
  401. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  402. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  403. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  404. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  405. package/src/__tests__/pipeline-runner.test.ts +0 -554
  406. package/src/daemon/wake-target-adapter.ts +0 -253
  407. package/src/memory/v3/__tests__/assign.test.ts +0 -242
  408. package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
  409. package/src/memory/v3/__tests__/core.test.ts +0 -39
  410. package/src/memory/v3/__tests__/health.test.ts +0 -219
  411. package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
  412. package/src/memory/v3/__tests__/needle.test.ts +0 -107
  413. package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
  414. package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
  415. package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
  416. package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
  417. package/src/memory/v3/__tests__/router.test.ts +0 -332
  418. package/src/memory/v3/__tests__/selector.test.ts +0 -470
  419. package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
  420. package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
  421. package/src/memory/v3/__tests__/tree.test.ts +0 -192
  422. package/src/memory/v3/assign.ts +0 -268
  423. package/src/memory/v3/capabilities.ts +0 -124
  424. package/src/memory/v3/core.ts +0 -26
  425. package/src/memory/v3/health.ts +0 -0
  426. package/src/memory/v3/maintain-job.ts +0 -314
  427. package/src/memory/v3/needle.ts +0 -115
  428. package/src/memory/v3/orchestrate.ts +0 -126
  429. package/src/memory/v3/page-content.ts +0 -34
  430. package/src/memory/v3/provider-blocks.ts +0 -26
  431. package/src/memory/v3/reconcile.ts +0 -523
  432. package/src/memory/v3/router.ts +0 -190
  433. package/src/memory/v3/selection-log-store.ts +0 -84
  434. package/src/memory/v3/selector.ts +0 -226
  435. package/src/memory/v3/shadow-plugin.ts +0 -411
  436. package/src/memory/v3/snapshot.ts +0 -209
  437. package/src/memory/v3/tree.ts +0 -174
  438. package/src/memory/v3/types.ts +0 -59
  439. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  440. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  441. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  442. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  443. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  444. package/src/plugins/defaults/compaction/register.ts +0 -35
  445. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  446. package/src/plugins/defaults/empty-response/register.ts +0 -23
  447. package/src/plugins/defaults/history-repair/register.ts +0 -24
  448. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
  449. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  450. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  451. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  452. package/src/plugins/defaults/title-generate/register.ts +0 -35
  453. package/src/plugins/defaults/tool-error/register.ts +0 -23
  454. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  455. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  456. package/src/proactive-artifact/decision.test.ts +0 -226
  457. package/src/proactive-artifact/decision.ts +0 -165
  458. package/src/proactive-artifact/index.ts +0 -7
  459. package/src/proactive-artifact/job.test.ts +0 -962
  460. package/src/proactive-artifact/job.ts +0 -372
  461. package/src/proactive-artifact/message-copy.ts +0 -58
  462. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  463. package/src/proactive-artifact/trigger-state.ts +0 -123
  464. package/src/util/map-limit.ts +0 -27
  465. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  466. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  467. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  468. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  469. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  470. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  471. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  472. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  473. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  474. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  475. /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
+ });