@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,402 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { makeMockLogger } from "../../../../__tests__/helpers/mock-logger.js";
4
+ import type { AssistantConfig } from "../../../../config/types.js";
5
+ import type { Section } from "../types.js";
6
+
7
+ mock.module("../../../../util/logger.js", () => ({
8
+ getLogger: () => makeMockLogger(),
9
+ }));
10
+
11
+ mock.module("../../../../memory/qdrant-client.js", () => ({
12
+ resolveQdrantUrl: () => "http://127.0.0.1:6333",
13
+ }));
14
+
15
+ // Stub the shared embedding backend. Records inputs and returns one
16
+ // deterministic vector per input so `upsertSections` can map vectors → points.
17
+ const embedState = {
18
+ calls: [] as string[][],
19
+ dim: 4,
20
+ };
21
+ mock.module("../../../../memory/embedding-backend.js", () => ({
22
+ embedWithBackend: async (_config: unknown, inputs: string[]) => {
23
+ embedState.calls.push(inputs);
24
+ return {
25
+ provider: "local",
26
+ model: "test-model",
27
+ vectors: inputs.map((_input, i) =>
28
+ Array.from({ length: embedState.dim }, (_v, j) => (i + 1) * (j + 1)),
29
+ ),
30
+ };
31
+ },
32
+ }));
33
+
34
+ // Mock the underlying @qdrant/js-client-rest package. The mock client records
35
+ // every call and lets each test program collection existence.
36
+ type MockPoint = {
37
+ id: string;
38
+ vector: number[];
39
+ payload: { article: string; ordinal: number; title: string };
40
+ };
41
+
42
+ /** One programmed `scroll` page: the points to return and the next offset. */
43
+ type ScrollPage = {
44
+ points: Array<{ id: string; payload: { article?: unknown } }>;
45
+ next_page_offset: string | number | null;
46
+ };
47
+
48
+ const state = {
49
+ collectionExists: false,
50
+ collectionExistsCalls: 0,
51
+ createCollectionCalls: 0,
52
+ createCollectionParams: null as unknown,
53
+ createIndexCalls: [] as Array<{ field_name: string; field_schema: string }>,
54
+ upsertCalls: [] as Array<{ wait: boolean; points: MockPoint[] }>,
55
+ deleteCalls: [] as Array<{ wait: boolean; filter: unknown }>,
56
+ createCollectionThrows: null as Error | null,
57
+ // Programmed `scroll` pages, consumed in order; each `scroll` call shifts one.
58
+ scrollPages: [] as ScrollPage[],
59
+ scrollCalls: [] as Array<{ limit: number; offset: unknown }>,
60
+ };
61
+
62
+ class MockQdrantClient {
63
+ constructor(_opts: unknown) {}
64
+ async collectionExists(_name: string) {
65
+ state.collectionExistsCalls++;
66
+ return { exists: state.collectionExists };
67
+ }
68
+ async createCollection(_name: string, params: unknown) {
69
+ state.createCollectionCalls++;
70
+ state.createCollectionParams = params;
71
+ if (state.createCollectionThrows) throw state.createCollectionThrows;
72
+ state.collectionExists = true;
73
+ return {};
74
+ }
75
+ async createPayloadIndex(
76
+ _name: string,
77
+ params: { field_name: string; field_schema: string },
78
+ ) {
79
+ state.createIndexCalls.push(params);
80
+ return {};
81
+ }
82
+ async upsert(_name: string, params: { wait: boolean; points: MockPoint[] }) {
83
+ state.upsertCalls.push(params);
84
+ return {};
85
+ }
86
+ async delete(_name: string, params: { wait: boolean; filter: unknown }) {
87
+ state.deleteCalls.push(params);
88
+ return {};
89
+ }
90
+ async scroll(
91
+ _name: string,
92
+ params: { limit: number; offset?: unknown },
93
+ ): Promise<ScrollPage> {
94
+ state.scrollCalls.push({ limit: params.limit, offset: params.offset });
95
+ return state.scrollPages.shift() ?? { points: [], next_page_offset: null };
96
+ }
97
+ }
98
+
99
+ mock.module("@qdrant/js-client-rest", () => ({
100
+ QdrantClient: MockQdrantClient,
101
+ }));
102
+
103
+ const {
104
+ ensureSectionCollection,
105
+ upsertSections,
106
+ deleteSectionsForArticle,
107
+ listSectionArticles,
108
+ SECTION_COLLECTION,
109
+ _resetSectionDenseStoreForTests,
110
+ } = await import("../section-dense-store.js");
111
+
112
+ const CONFIG = {
113
+ memory: { qdrant: { vectorSize: 4, onDisk: true } },
114
+ } as unknown as AssistantConfig;
115
+
116
+ function section(
117
+ article: string,
118
+ ordinal: number,
119
+ text: string,
120
+ title = "",
121
+ ): Section {
122
+ return { article, ordinal, text, title };
123
+ }
124
+
125
+ function resetState(): void {
126
+ state.collectionExists = false;
127
+ state.collectionExistsCalls = 0;
128
+ state.createCollectionCalls = 0;
129
+ state.createCollectionParams = null;
130
+ state.createIndexCalls.length = 0;
131
+ state.upsertCalls.length = 0;
132
+ state.deleteCalls.length = 0;
133
+ state.createCollectionThrows = null;
134
+ state.scrollPages.length = 0;
135
+ state.scrollCalls.length = 0;
136
+ embedState.calls.length = 0;
137
+ embedState.dim = 4;
138
+ _resetSectionDenseStoreForTests();
139
+ }
140
+
141
+ const UUID_RE =
142
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
143
+
144
+ describe("memory v3 section-dense-store — collection lifecycle", () => {
145
+ beforeEach(resetState);
146
+ afterEach(resetState);
147
+
148
+ test("uses the documented collection name", () => {
149
+ expect(SECTION_COLLECTION).toBe("memory_v3_sections");
150
+ });
151
+
152
+ test("creates the collection with a single dense vector at the configured dimension", async () => {
153
+ state.collectionExists = false;
154
+
155
+ await ensureSectionCollection(CONFIG);
156
+
157
+ expect(state.createCollectionCalls).toBe(1);
158
+ const params = state.createCollectionParams as {
159
+ vectors: { size: number; distance: string; on_disk: boolean };
160
+ };
161
+ // Dimension comes from config.memory.qdrant.vectorSize, never hard-coded.
162
+ expect(params.vectors).toEqual({
163
+ size: 4,
164
+ distance: "Cosine",
165
+ on_disk: true,
166
+ });
167
+ // `article` payload index is created so delete-by-filter works in strict mode.
168
+ expect(state.createIndexCalls).toEqual([
169
+ { field_name: "article", field_schema: "keyword" },
170
+ ]);
171
+ });
172
+
173
+ test("creates at whatever dimension the backend is configured for", async () => {
174
+ const config = {
175
+ memory: { qdrant: { vectorSize: 1536, onDisk: false } },
176
+ } as unknown as AssistantConfig;
177
+
178
+ await ensureSectionCollection(config);
179
+
180
+ const params = state.createCollectionParams as {
181
+ vectors: { size: number };
182
+ };
183
+ expect(params.vectors.size).toBe(1536);
184
+ });
185
+
186
+ test("ensures the article payload index when the collection already exists", async () => {
187
+ state.collectionExists = true;
188
+
189
+ await ensureSectionCollection(CONFIG);
190
+
191
+ // No collection create, but the article payload index is ensured idempotently
192
+ // so a collection left without it (e.g. an earlier crash between create and
193
+ // index, under strict-mode Qdrant) self-heals on the next start.
194
+ expect(state.createCollectionCalls).toBe(0);
195
+ expect(state.createIndexCalls).toEqual([
196
+ { field_name: "article", field_schema: "keyword" },
197
+ ]);
198
+ });
199
+
200
+ test("re-running ensure latches readiness (single existence probe)", async () => {
201
+ state.collectionExists = false;
202
+
203
+ await ensureSectionCollection(CONFIG);
204
+ await ensureSectionCollection(CONFIG);
205
+
206
+ expect(state.collectionExistsCalls).toBe(1);
207
+ expect(state.createCollectionCalls).toBe(1);
208
+ });
209
+
210
+ test("treats a 409-on-create as success and still ensures the index", async () => {
211
+ state.collectionExists = false;
212
+ state.createCollectionThrows = Object.assign(new Error("Conflict"), {
213
+ status: 409,
214
+ });
215
+
216
+ await ensureSectionCollection(CONFIG);
217
+
218
+ // No throw; after the racing-peer 409 we fall through and still ensure the
219
+ // article payload index (idempotent) rather than returning early.
220
+ expect(state.createCollectionCalls).toBe(1);
221
+ expect(state.createIndexCalls).toEqual([
222
+ { field_name: "article", field_schema: "keyword" },
223
+ ]);
224
+ });
225
+ });
226
+
227
+ describe("memory v3 section-dense-store — upsert", () => {
228
+ beforeEach(resetState);
229
+ afterEach(resetState);
230
+
231
+ test("embeds each section's text and upserts one point per section", async () => {
232
+ state.collectionExists = true;
233
+ const sections = [
234
+ section("people/alice", 0, "alice lead text"),
235
+ section("people/alice", 1, "alice section one", "History"),
236
+ section("people/bob", 0, "bob lead text"),
237
+ ];
238
+
239
+ await upsertSections(CONFIG, sections);
240
+
241
+ // Embedded exactly the section texts, in order.
242
+ expect(embedState.calls).toEqual([
243
+ ["alice lead text", "alice section one", "bob lead text"],
244
+ ]);
245
+
246
+ expect(state.upsertCalls).toHaveLength(1);
247
+ const call = state.upsertCalls[0]!;
248
+ expect(call.wait).toBe(true);
249
+ expect(call.points).toHaveLength(3);
250
+
251
+ expect(call.points.map((p) => p.payload)).toEqual([
252
+ { article: "people/alice", ordinal: 0, title: "" },
253
+ { article: "people/alice", ordinal: 1, title: "History" },
254
+ { article: "people/bob", ordinal: 0, title: "" },
255
+ ]);
256
+
257
+ // Each point carries its matching embedding vector and a UUID-shaped id.
258
+ for (const point of call.points) {
259
+ expect(point.id).toMatch(UUID_RE);
260
+ expect(point.vector).toHaveLength(4);
261
+ }
262
+ // Vectors are positional: section i gets backend vector i.
263
+ expect(call.points[0]!.vector).toEqual([1, 2, 3, 4]);
264
+ expect(call.points[1]!.vector).toEqual([2, 4, 6, 8]);
265
+ });
266
+
267
+ test("re-upserting the same sections is idempotent (stable point ids)", async () => {
268
+ state.collectionExists = true;
269
+ const sections = [section("people/alice", 0, "alice lead text")];
270
+
271
+ await upsertSections(CONFIG, sections);
272
+ await upsertSections(CONFIG, sections);
273
+
274
+ expect(state.upsertCalls).toHaveLength(2);
275
+ expect(state.upsertCalls[0]!.points[0]!.id).toBe(
276
+ state.upsertCalls[1]!.points[0]!.id,
277
+ );
278
+ });
279
+
280
+ test("distinct (article, ordinal) pairs map to distinct point ids", async () => {
281
+ state.collectionExists = true;
282
+
283
+ await upsertSections(CONFIG, [
284
+ section("people/alice", 0, "x"),
285
+ section("people/alice", 1, "y"),
286
+ section("people/bob", 0, "z"),
287
+ ]);
288
+
289
+ const ids = state.upsertCalls[0]!.points.map((p) => p.id);
290
+ expect(new Set(ids).size).toBe(3);
291
+ });
292
+
293
+ test("empty sections array is a no-op (no embedding, no upsert)", async () => {
294
+ state.collectionExists = true;
295
+
296
+ await upsertSections(CONFIG, []);
297
+
298
+ expect(embedState.calls).toEqual([]);
299
+ expect(state.upsertCalls).toHaveLength(0);
300
+ });
301
+
302
+ test("ensures the collection before upserting", async () => {
303
+ state.collectionExists = false;
304
+
305
+ await upsertSections(CONFIG, [section("people/alice", 0, "x")]);
306
+
307
+ expect(state.createCollectionCalls).toBe(1);
308
+ expect(state.upsertCalls).toHaveLength(1);
309
+ });
310
+ });
311
+
312
+ describe("memory v3 section-dense-store — delete", () => {
313
+ beforeEach(resetState);
314
+ afterEach(resetState);
315
+
316
+ test("deletes section points filtered by the article payload", async () => {
317
+ state.collectionExists = true;
318
+
319
+ await deleteSectionsForArticle(CONFIG, "people/alice");
320
+
321
+ expect(state.deleteCalls).toHaveLength(1);
322
+ const call = state.deleteCalls[0]!;
323
+ expect(call.wait).toBe(true);
324
+ expect(call.filter).toEqual({
325
+ must: [{ key: "article", match: { value: "people/alice" } }],
326
+ });
327
+ });
328
+
329
+ test("delete is idempotent across repeated calls", async () => {
330
+ state.collectionExists = true;
331
+
332
+ await deleteSectionsForArticle(CONFIG, "people/alice");
333
+ await deleteSectionsForArticle(CONFIG, "people/alice");
334
+
335
+ expect(state.deleteCalls).toHaveLength(2);
336
+ });
337
+ });
338
+
339
+ describe("memory v3 section-dense-store — listSectionArticles", () => {
340
+ beforeEach(resetState);
341
+ afterEach(resetState);
342
+
343
+ test("returns the distinct articles across all scrolled points", async () => {
344
+ state.collectionExists = true;
345
+ // Two scroll pages; `page-a` repeats within and across pages (one article,
346
+ // many section points) — the result must be the DISTINCT article set.
347
+ state.scrollPages = [
348
+ {
349
+ points: [
350
+ { id: "1", payload: { article: "page-a" } },
351
+ { id: "2", payload: { article: "page-a" } },
352
+ { id: "3", payload: { article: "topic-x" } },
353
+ ],
354
+ next_page_offset: "cursor-1",
355
+ },
356
+ {
357
+ points: [
358
+ { id: "4", payload: { article: "page-a" } },
359
+ { id: "5", payload: { article: "skills/example" } },
360
+ ],
361
+ next_page_offset: null,
362
+ },
363
+ ];
364
+
365
+ const articles = await listSectionArticles(CONFIG);
366
+
367
+ expect(new Set(articles)).toEqual(
368
+ new Set(["page-a", "topic-x", "skills/example"]),
369
+ );
370
+ // Both pages were scrolled, and the second carried the prior page's cursor.
371
+ expect(state.scrollCalls).toHaveLength(2);
372
+ expect(state.scrollCalls[0]!.offset).toBeUndefined();
373
+ expect(state.scrollCalls[1]!.offset).toBe("cursor-1");
374
+ });
375
+
376
+ test("skips points whose payload has no string article", async () => {
377
+ state.collectionExists = true;
378
+ state.scrollPages = [
379
+ {
380
+ points: [
381
+ { id: "1", payload: { article: "page-a" } },
382
+ { id: "2", payload: {} },
383
+ { id: "3", payload: { article: 42 } },
384
+ ],
385
+ next_page_offset: null,
386
+ },
387
+ ];
388
+
389
+ const articles = await listSectionArticles(CONFIG);
390
+
391
+ expect(articles).toEqual(["page-a"]);
392
+ });
393
+
394
+ test("returns an empty list for an empty collection", async () => {
395
+ state.collectionExists = true;
396
+ state.scrollPages = [{ points: [], next_page_offset: null }];
397
+
398
+ const articles = await listSectionArticles(CONFIG);
399
+
400
+ expect(articles).toEqual([]);
401
+ });
402
+ });
@@ -0,0 +1,135 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { buildSectionNeedle } from "../section-needle.js";
4
+ import { buildSectionIndex } from "../sections.js";
5
+ import type { SectionIndex, Slug } from "../types.js";
6
+
7
+ /**
8
+ * Build a `SectionIndex` from a fixture page-body map. All slugs and content
9
+ * are invented placeholders — no real content.
10
+ */
11
+ async function index(pages: Record<string, string>): Promise<SectionIndex> {
12
+ const reader = async (slug: Slug): Promise<string> => pages[slug] ?? "";
13
+ return buildSectionIndex(Object.keys(pages), reader);
14
+ }
15
+
16
+ describe("buildSectionNeedle", () => {
17
+ test("a term present only in one section ranks that section's article", async () => {
18
+ const idx = await index({
19
+ "page-a": [
20
+ "## Intro",
21
+ "general background prose",
22
+ "",
23
+ "## Details",
24
+ "the elephant appears only here",
25
+ ].join("\n"),
26
+ "topic-x": "## Notes\nunrelated material about gardens",
27
+ });
28
+
29
+ const needle = buildSectionNeedle(idx);
30
+ const results = needle.query("elephant", 5);
31
+
32
+ expect(results.length).toBeGreaterThan(0);
33
+ expect(results[0]!.article).toBe("page-a");
34
+ // The matched section is the "Details" section, not the lead/intro.
35
+ expect(idx.sections[results[0]!.section]!.title).toBe("Details");
36
+ });
37
+
38
+ test("dedupe keeps one entry per article with its top section", async () => {
39
+ const idx = await index({
40
+ "page-a": [
41
+ "## First",
42
+ "zebra zebra zebra mention",
43
+ "",
44
+ "## Second",
45
+ "single zebra mention",
46
+ ].join("\n"),
47
+ });
48
+
49
+ const needle = buildSectionNeedle(idx);
50
+ const results = needle.query("zebra", 5);
51
+
52
+ // Only one entry for page-a despite two matching sections.
53
+ expect(results).toHaveLength(1);
54
+ expect(results[0]!.article).toBe("page-a");
55
+ // The denser "First" section wins.
56
+ expect(idx.sections[results[0]!.section]!.title).toBe("First");
57
+ });
58
+
59
+ test("bestSection returns the highest-scoring section for an article", async () => {
60
+ const idx = await index({
61
+ "page-a": [
62
+ "## Alpha",
63
+ "background filler text",
64
+ "",
65
+ "## Beta",
66
+ "the keyword giraffe lives here",
67
+ ].join("\n"),
68
+ });
69
+
70
+ const needle = buildSectionNeedle(idx);
71
+ const best = needle.bestSection("page-a", "giraffe");
72
+
73
+ expect(idx.sections[best]!.title).toBe("Beta");
74
+ });
75
+
76
+ test("bestSection falls back to first section when no term matches", async () => {
77
+ const idx = await index({
78
+ "page-a": "## Alpha\nfiller\n\n## Beta\nmore filler",
79
+ });
80
+
81
+ const needle = buildSectionNeedle(idx);
82
+ const best = needle.bestSection("page-a", "nonexistentterm");
83
+
84
+ // First section index for the article (the lead, ordinal 0).
85
+ expect(best).toBe(idx.byArticle.get("page-a")![0]!);
86
+ expect(idx.sections[best]!.ordinal).toBe(0);
87
+ });
88
+
89
+ test("bestSection returns -1 for an unknown article", async () => {
90
+ const idx = await index({ "page-a": "## Alpha\nfiller" });
91
+ const needle = buildSectionNeedle(idx);
92
+ expect(needle.bestSection("missing-page", "anything")).toBe(-1);
93
+ });
94
+
95
+ test("k truncation is respected", async () => {
96
+ const idx = await index({
97
+ "page-a": "## S\nshared keyword apple here",
98
+ "page-b": "## S\nshared keyword apple here too",
99
+ "page-c": "## S\nshared keyword apple again",
100
+ });
101
+
102
+ const needle = buildSectionNeedle(idx);
103
+ expect(needle.query("apple", 2)).toHaveLength(2);
104
+ expect(needle.query("apple", 0)).toHaveLength(0);
105
+ });
106
+
107
+ test("head-field term outranks the same term in body", async () => {
108
+ // Same term in the head line (title) of page-a vs in the body of topic-x.
109
+ // Bodies are padded to comparable length so the only edge is field weight.
110
+ const idx = await index({
111
+ "page-a": "## Mango\nfiller words to balance length across the docs here",
112
+ "topic-x":
113
+ "## Notes\nfiller words mango plus more padding to balance length",
114
+ });
115
+
116
+ const needle = buildSectionNeedle(idx);
117
+ const results = needle.query("mango", 5);
118
+
119
+ expect(results[0]!.article).toBe("page-a");
120
+ });
121
+
122
+ test("deterministic tie-breaking by (article, ordinal)", async () => {
123
+ // Identical content across two articles → identical scores; the lexically
124
+ // smaller article wins the tie.
125
+ const idx = await index({
126
+ "page-b": "## S\nidentical pineapple content",
127
+ "page-a": "## S\nidentical pineapple content",
128
+ });
129
+
130
+ const needle = buildSectionNeedle(idx);
131
+ const results = needle.query("pineapple", 5);
132
+
133
+ expect(results.map((r) => r.article)).toEqual(["page-a", "page-b"]);
134
+ });
135
+ });
@@ -0,0 +1,125 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { buildSectionIndex, SECTION_CHUNK_CHARS } from "../sections.js";
4
+ import type { Slug } from "../types.js";
5
+
6
+ /** Build a page-body reader from a fixture map. */
7
+ function reader(pages: Record<string, string>) {
8
+ return async (slug: Slug): Promise<string> => pages[slug] ?? "";
9
+ }
10
+
11
+ describe("buildSectionIndex", () => {
12
+ test("lead + two ## sections yield 3 ordered sections with titles", async () => {
13
+ const body = [
14
+ "Lead paragraph before any heading.",
15
+ "",
16
+ "## First Heading",
17
+ "first body line",
18
+ "",
19
+ "## Second Heading",
20
+ "second body line",
21
+ ].join("\n");
22
+
23
+ const index = await buildSectionIndex(
24
+ ["page-a"],
25
+ reader({ "page-a": body }),
26
+ );
27
+
28
+ expect(index.sections).toHaveLength(3);
29
+
30
+ expect(index.sections[0]!.ordinal).toBe(0);
31
+ expect(index.sections[0]!.title).toBe("");
32
+ expect(index.sections[0]!.text).toContain("page-a — ");
33
+ expect(index.sections[0]!.text).toContain("Lead paragraph");
34
+
35
+ expect(index.sections[1]!.ordinal).toBe(1);
36
+ expect(index.sections[1]!.title).toBe("First Heading");
37
+ expect(index.sections[1]!.text).toContain("page-a — First Heading");
38
+ expect(index.sections[1]!.text).toContain("first body line");
39
+
40
+ expect(index.sections[2]!.ordinal).toBe(2);
41
+ expect(index.sections[2]!.title).toBe("Second Heading");
42
+ expect(index.sections[2]!.text).toContain("second body line");
43
+ });
44
+
45
+ test("section over the chunk cap splits into ordered chunks", async () => {
46
+ const big = "x".repeat(SECTION_CHUNK_CHARS * 2 + 100);
47
+ const body = `## Big Section\n${big}`;
48
+
49
+ const index = await buildSectionIndex(
50
+ ["topic-x"],
51
+ reader({ "topic-x": body }),
52
+ );
53
+
54
+ // Lead (empty) + the chunked big section.
55
+ const bigChunks = index.sections.filter((s) => s.title === "Big Section");
56
+ expect(bigChunks.length).toBeGreaterThan(1);
57
+
58
+ // Every chunk fits the embedding window.
59
+ for (const section of index.sections) {
60
+ expect(section.text.length).toBeLessThanOrEqual(SECTION_CHUNK_CHARS);
61
+ }
62
+
63
+ // Ordinals are strictly increasing in array order (chunks ordered).
64
+ for (let i = 1; i < index.sections.length; i++) {
65
+ expect(index.sections[i]!.ordinal).toBe(
66
+ index.sections[i - 1]!.ordinal + 1,
67
+ );
68
+ }
69
+ });
70
+
71
+ test("byArticle maps each article to its section indices", async () => {
72
+ const index = await buildSectionIndex(
73
+ ["page-a", "topic-x"],
74
+ reader({
75
+ "page-a": "lead a\n\n## Sec A\nbody a",
76
+ "topic-x": "lead x",
77
+ }),
78
+ );
79
+
80
+ expect([...index.byArticle.keys()].sort()).toEqual(["page-a", "topic-x"]);
81
+
82
+ for (const [article, indices] of index.byArticle) {
83
+ for (const i of indices) {
84
+ expect(index.sections[i]!.article).toBe(article);
85
+ }
86
+ }
87
+
88
+ // page-a has lead + one heading section; topic-x is headingless (lead only).
89
+ expect(index.byArticle.get("page-a")).toHaveLength(2);
90
+ expect(index.byArticle.get("topic-x")).toHaveLength(1);
91
+ });
92
+
93
+ test("empty / headingless page yields a single ordinal-0 section", async () => {
94
+ const empty = await buildSectionIndex(["page-a"], reader({ "page-a": "" }));
95
+ expect(empty.sections).toHaveLength(1);
96
+ expect(empty.sections[0]!.ordinal).toBe(0);
97
+ expect(empty.sections[0]!.title).toBe("");
98
+
99
+ const headingless = await buildSectionIndex(
100
+ ["topic-x"],
101
+ reader({ "topic-x": "just a paragraph\nand another line" }),
102
+ );
103
+ expect(headingless.sections).toHaveLength(1);
104
+ expect(headingless.sections[0]!.ordinal).toBe(0);
105
+ expect(headingless.sections[0]!.text).toContain("just a paragraph");
106
+ });
107
+
108
+ test("sections are deterministic, sorted by (article, ordinal)", async () => {
109
+ const pages = {
110
+ "topic-x": "lead x\n\n## X1\nbody",
111
+ "page-a": "lead a\n\n## A1\nbody",
112
+ };
113
+ const first = await buildSectionIndex(["topic-x", "page-a"], reader(pages));
114
+ const second = await buildSectionIndex(
115
+ ["page-a", "topic-x"],
116
+ reader(pages),
117
+ );
118
+
119
+ const shape = (s: { article: Slug; ordinal: number }) =>
120
+ `${s.article}#${s.ordinal}`;
121
+ expect(first.sections.map(shape)).toEqual(second.sections.map(shape));
122
+ // page-a sorts before topic-x regardless of input order.
123
+ expect(first.sections[0]!.article).toBe("page-a");
124
+ });
125
+ });