@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,236 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Memory v3 — section-grain dense Qdrant collection
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Owns a dedicated Qdrant collection holding one dense embedding per page
6
+ // *section* (the lead block plus each `## `-delimited heading section, chunked
7
+ // to fit the embedding window — see `sections.ts`). This is the dense lane for
8
+ // the section-grain retrieval design: where the v2 `memory_v2_concept_pages`
9
+ // collection embeds whole pages, this one embeds sub-page sections so a query
10
+ // can match the single relevant block of a long article.
11
+ //
12
+ // Reuses the v2 Qdrant URL/config (`resolveQdrantUrl` + `config.memory.qdrant.*`)
13
+ // and the shared embedding backend (`embedWithBackend`) rather than standing up
14
+ // new infrastructure. The collection carries a single dense named vector sized
15
+ // to the configured embedding backend (`config.memory.qdrant.vectorSize`) — no
16
+ // sparse channel, since the section dense lane is dense-only.
17
+ //
18
+ // This module owns the write side (collection lifecycle + upserts) and the
19
+ // shared Qdrant client; the read side (`dense.ts`) reuses that client to query
20
+ // this collection.
21
+
22
+ import { QdrantClient as QdrantRestClient } from "@qdrant/js-client-rest";
23
+ import { v5 as uuidv5 } from "uuid";
24
+
25
+ import type { AssistantConfig } from "../../../config/types.js";
26
+ import { embedWithBackend } from "../../../memory/embedding-backend.js";
27
+ import { resolveQdrantUrl } from "../../../memory/qdrant-client.js";
28
+ import { getLogger } from "../../../util/logger.js";
29
+ import type { Section } from "./types.js";
30
+
31
+ const log = getLogger("memory-v3-section-dense-store");
32
+
33
+ /** Name of the dedicated Qdrant collection holding section-grain embeddings. */
34
+ export const SECTION_COLLECTION = "memory_v3_sections";
35
+
36
+ /**
37
+ * Stable UUIDv5 namespace used to derive a deterministic Qdrant point ID from a
38
+ * section's `(article, ordinal)` pair. The namespace itself is an arbitrary
39
+ * fixed UUID; what matters is that the same section always maps to the same
40
+ * point ID so re-upserts replace the prior point in place instead of
41
+ * accumulating duplicates.
42
+ */
43
+ const SECTION_NAMESPACE = "1d2c3b4a-5e6f-4a7b-8c9d-0e1f2a3b4c5d";
44
+
45
+ let _client: QdrantRestClient | null = null;
46
+ let _collectionReady = false;
47
+
48
+ /**
49
+ * Lazily create the shared Qdrant REST client bound to the resolved URL.
50
+ * Shared by both section-dense lanes (this store and the dense read lane in
51
+ * `dense.ts`) so they reuse one client instance and one reset hook.
52
+ */
53
+ export function getSectionDenseClient(
54
+ config: AssistantConfig,
55
+ ): QdrantRestClient {
56
+ if (_client) return _client;
57
+ _client = new QdrantRestClient({
58
+ url: resolveQdrantUrl(config),
59
+ checkCompatibility: false,
60
+ });
61
+ return _client;
62
+ }
63
+
64
+ /**
65
+ * Derive the deterministic Qdrant point ID for a section. Qdrant requires
66
+ * UUID/integer IDs; UUIDv5 over `${article}#${ordinal}` keeps the mapping
67
+ * stable across processes so upserts replace in place.
68
+ */
69
+ function pointIdForSection(article: string, ordinal: number): string {
70
+ return uuidv5(`${article}#${ordinal}`, SECTION_NAMESPACE);
71
+ }
72
+
73
+ /**
74
+ * Create the section-grain collection if it does not already exist, with a
75
+ * single dense vector sized to the configured embedding backend. Idempotent:
76
+ * an already-ready collection is a no-op, and a concurrent 409-on-create is
77
+ * treated as success.
78
+ */
79
+ export async function ensureSectionCollection(
80
+ config: AssistantConfig,
81
+ ): Promise<void> {
82
+ if (_collectionReady) return;
83
+
84
+ const client = getSectionDenseClient(config);
85
+ const vectorSize = config.memory.qdrant.vectorSize;
86
+ const onDisk = config.memory.qdrant.onDisk;
87
+
88
+ const exists = await client.collectionExists(SECTION_COLLECTION);
89
+ if (!exists.exists) {
90
+ log.info(
91
+ { collection: SECTION_COLLECTION, vectorSize },
92
+ "Creating Qdrant collection for memory v3 sections",
93
+ );
94
+ try {
95
+ await client.createCollection(SECTION_COLLECTION, {
96
+ vectors: {
97
+ size: vectorSize,
98
+ distance: "Cosine",
99
+ on_disk: onDisk,
100
+ },
101
+ hnsw_config: {
102
+ on_disk: onDisk,
103
+ m: 16,
104
+ ef_construct: 100,
105
+ },
106
+ optimizers_config: {
107
+ default_segment_number: 2,
108
+ },
109
+ on_disk_payload: onDisk,
110
+ });
111
+ } catch (err) {
112
+ // 409 = a concurrent caller created the collection — fall through to
113
+ // ensure the payload index below rather than returning early.
114
+ const status =
115
+ err instanceof Error && "status" in err
116
+ ? (err as { status: number }).status
117
+ : undefined;
118
+ if (status !== 409) throw err;
119
+ }
120
+ }
121
+
122
+ // Always ensure the `article` payload index — on EVERY path (fresh create,
123
+ // concurrent 409, and a pre-existing collection whose index creation was
124
+ // interrupted by an earlier crash). `deleteSectionsForArticle`/pruning filter
125
+ // on `article`, and strict-mode Qdrant rejects filters on unindexed fields, so
126
+ // a missing index would make them fail until manual repair. createPayloadIndex
127
+ // is idempotent, so re-running it against an existing index is a no-op.
128
+ await client.createPayloadIndex(SECTION_COLLECTION, {
129
+ field_name: "article",
130
+ field_schema: "keyword",
131
+ });
132
+
133
+ _collectionReady = true;
134
+ }
135
+
136
+ /**
137
+ * Embed each section's `text` and upsert one point per section, keyed by a
138
+ * deterministic `(article, ordinal)`-derived ID. Stable IDs mean re-upserting
139
+ * the same sections overwrites in place rather than accumulating duplicates,
140
+ * so the operation is idempotent. Payload carries `{ article, ordinal, title }`
141
+ * for downstream filtering and rendering.
142
+ *
143
+ * An empty `sections` array is a no-op (no embedding round-trip).
144
+ */
145
+ export async function upsertSections(
146
+ config: AssistantConfig,
147
+ sections: Section[],
148
+ ): Promise<void> {
149
+ if (sections.length === 0) return;
150
+
151
+ await ensureSectionCollection(config);
152
+
153
+ const { vectors } = await embedWithBackend(
154
+ config,
155
+ sections.map((s) => s.text),
156
+ );
157
+
158
+ const points = sections.map((section, i) => ({
159
+ id: pointIdForSection(section.article, section.ordinal),
160
+ vector: vectors[i]!,
161
+ payload: {
162
+ article: section.article,
163
+ ordinal: section.ordinal,
164
+ title: section.title,
165
+ },
166
+ }));
167
+
168
+ await getSectionDenseClient(config).upsert(SECTION_COLLECTION, {
169
+ wait: true,
170
+ points,
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Delete every section point belonging to an article. Used by incremental
176
+ * rebuilds (in a later PR) to clear an article's stale sections before
177
+ * re-upserting its current ones. Idempotent: deleting an absent article's
178
+ * sections is a no-op server-side.
179
+ */
180
+ export async function deleteSectionsForArticle(
181
+ config: AssistantConfig,
182
+ article: string,
183
+ ): Promise<void> {
184
+ await ensureSectionCollection(config);
185
+
186
+ await getSectionDenseClient(config).delete(SECTION_COLLECTION, {
187
+ wait: true,
188
+ filter: { must: [{ key: "article", match: { value: article } }] },
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Return every distinct `article` slug that currently has at least one section
194
+ * point in the collection. Scrolls the whole collection (payload only, no
195
+ * vectors) in bounded batches and collects the distinct `article` values.
196
+ *
197
+ * Used by the maintain job's prune stage to find articles whose points linger
198
+ * after the page was deleted from the index — the change-delta selector never
199
+ * names a slug that is gone, so a deleted page's stale points are only
200
+ * observable by reading the collection back. An empty/absent collection yields
201
+ * an empty list.
202
+ */
203
+ export async function listSectionArticles(
204
+ config: AssistantConfig,
205
+ ): Promise<string[]> {
206
+ await ensureSectionCollection(config);
207
+
208
+ const client = getSectionDenseClient(config);
209
+ const articles = new Set<string>();
210
+ let offset: string | number | undefined = undefined;
211
+ const maxIterations = 10_000;
212
+ const batchSize = 256;
213
+ for (let i = 0; i < maxIterations; i++) {
214
+ const result = await client.scroll(SECTION_COLLECTION, {
215
+ limit: batchSize,
216
+ with_payload: true,
217
+ with_vector: false,
218
+ ...(offset !== undefined ? { offset } : {}),
219
+ });
220
+ for (const point of result.points) {
221
+ const article = (point.payload as { article?: unknown } | null)?.article;
222
+ if (typeof article === "string") articles.add(article);
223
+ }
224
+ const next = result.next_page_offset;
225
+ if (next == null) break;
226
+ offset = typeof next === "string" ? next : (next as number);
227
+ }
228
+
229
+ return [...articles];
230
+ }
231
+
232
+ /** @internal Test-only: reset module-level singletons. */
233
+ export function _resetSectionDenseStoreForTests(): void {
234
+ _client = null;
235
+ _collectionReady = false;
236
+ }
@@ -0,0 +1,200 @@
1
+ import type { SectionIndex, Slug } from "./types.js";
2
+
3
+ /**
4
+ * Section-grain "needle" lane for memory-v3 retrieval: a lexical BM25F search
5
+ * over a {@link SectionIndex}. Each section is a tiny two-field document — a
6
+ * weighted `head` line (`${lastSlugSegment} — ${title}`) and the remaining
7
+ * `body` text — so a literal term in a heading outranks the same term buried in
8
+ * prose. Scoring happens at section grain; results are deduped to distinct
9
+ * articles, each tagged with its best-scoring section.
10
+ *
11
+ * Implementation notes:
12
+ * - Hand-rolled Okapi BM25F (no dependency). The corpus is bounded (one doc per
13
+ * section), so a plain inverted index is plenty.
14
+ * - To match the validated harness the term stream includes adjacent-token
15
+ * bigrams (`token[i] + "_" + token[i+1]`) in addition to unigrams.
16
+ */
17
+
18
+ /** Okapi BM25 term-frequency saturation parameter. */
19
+ const k1 = 1.5;
20
+ /** Okapi BM25 length-normalization parameter. */
21
+ const b = 0.75;
22
+
23
+ /** `head`-field weight in the BM25F term-frequency blend. */
24
+ const HEAD_WEIGHT = 2.5;
25
+ /** `body`-field weight in the BM25F term-frequency blend. */
26
+ const BODY_WEIGHT = 1;
27
+
28
+ export interface SectionNeedle {
29
+ /**
30
+ * Returns up to `k` distinct articles ranked by BM25F score (descending),
31
+ * each tagged with its best-scoring section index (into the underlying
32
+ * `SectionIndex.sections`). Ties break by `(article, ordinal)`.
33
+ */
34
+ query(text: string, k: number): { article: Slug; section: number }[];
35
+ /**
36
+ * Highest-scoring section index (into `SectionIndex.sections`) for `article`
37
+ * against `queryText`. Returns the article's first section index when no term
38
+ * matches (or `-1` if the article has no sections). Used by other lanes to
39
+ * attach a matched-section descriptor to an article they surface.
40
+ */
41
+ bestSection(article: Slug, queryText: string): number;
42
+ }
43
+
44
+ /** Lowercase, split on non-alphanumeric, drop empties. */
45
+ function tokenizeUnigrams(text: string): string[] {
46
+ return text
47
+ .toLowerCase()
48
+ .split(/[^a-z0-9]+/)
49
+ .filter((token) => token.length > 0);
50
+ }
51
+
52
+ /** Unigrams plus adjacent-token bigrams (`a_b`), matching the harness. */
53
+ function tokenize(text: string): string[] {
54
+ const unigrams = tokenizeUnigrams(text);
55
+ const terms = [...unigrams];
56
+ for (let i = 0; i + 1 < unigrams.length; i++) {
57
+ terms.push(`${unigrams[i]}_${unigrams[i + 1]}`);
58
+ }
59
+ return terms;
60
+ }
61
+
62
+ /** Split a section's text into its head line and the remaining body. */
63
+ function splitHeadBody(text: string): { head: string; body: string } {
64
+ const newline = text.indexOf("\n");
65
+ if (newline === -1) return { head: text, body: "" };
66
+ return { head: text.slice(0, newline), body: text.slice(newline + 1) };
67
+ }
68
+
69
+ interface Posting {
70
+ doc: number;
71
+ /** Field-weighted term frequency: HEAD_WEIGHT*tf_head + BODY_WEIGHT*tf_body. */
72
+ weightedTf: number;
73
+ }
74
+
75
+ export function buildSectionNeedle(index: SectionIndex): SectionNeedle {
76
+ const { sections } = index;
77
+ const docCount = sections.length;
78
+
79
+ // term -> postings (section index + weighted term frequency).
80
+ const postings = new Map<string, Posting[]>();
81
+ // BM25F effective document length = weighted token count per section.
82
+ const docLengths: number[] = new Array(docCount).fill(0);
83
+ let totalLength = 0;
84
+
85
+ for (let doc = 0; doc < docCount; doc++) {
86
+ const { head, body } = splitHeadBody(sections[doc]!.text);
87
+ const headTerms = tokenize(head);
88
+ const bodyTerms = tokenize(body);
89
+
90
+ const length =
91
+ HEAD_WEIGHT * headTerms.length + BODY_WEIGHT * bodyTerms.length;
92
+ docLengths[doc] = length;
93
+ totalLength += length;
94
+
95
+ const weightedTf = new Map<string, number>();
96
+ for (const term of headTerms) {
97
+ weightedTf.set(term, (weightedTf.get(term) ?? 0) + HEAD_WEIGHT);
98
+ }
99
+ for (const term of bodyTerms) {
100
+ weightedTf.set(term, (weightedTf.get(term) ?? 0) + BODY_WEIGHT);
101
+ }
102
+
103
+ for (const [term, tf] of weightedTf) {
104
+ let list = postings.get(term);
105
+ if (!list) {
106
+ list = [];
107
+ postings.set(term, list);
108
+ }
109
+ list.push({ doc, weightedTf: tf });
110
+ }
111
+ }
112
+
113
+ const avgDocLength = docCount > 0 ? totalLength / docCount : 0;
114
+
115
+ /** BM25F score per section for the given query terms. */
116
+ function scoreSections(queryTerms: Set<string>): Map<number, number> {
117
+ const scores = new Map<number, number>();
118
+ if (docCount === 0) return scores;
119
+
120
+ for (const term of queryTerms) {
121
+ const list = postings.get(term);
122
+ if (!list) continue;
123
+
124
+ const idf = Math.log(
125
+ 1 + (docCount - list.length + 0.5) / (list.length + 0.5),
126
+ );
127
+
128
+ for (const { doc, weightedTf } of list) {
129
+ const norm = weightedTf * (k1 + 1);
130
+ const denom =
131
+ weightedTf + k1 * (1 - b + b * (docLengths[doc]! / avgDocLength));
132
+ scores.set(doc, (scores.get(doc) ?? 0) + idf * (norm / denom));
133
+ }
134
+ }
135
+
136
+ return scores;
137
+ }
138
+
139
+ /** Deterministic order: score desc, then (article, ordinal) asc. */
140
+ function rankSection(
141
+ a: number,
142
+ c: number,
143
+ scores: Map<number, number>,
144
+ ): number {
145
+ const byScore = (scores.get(c) ?? 0) - (scores.get(a) ?? 0);
146
+ if (byScore !== 0) return byScore;
147
+ const sa = sections[a]!;
148
+ const sc = sections[c]!;
149
+ return sa.article.localeCompare(sc.article) || sa.ordinal - sc.ordinal;
150
+ }
151
+
152
+ function query(
153
+ text: string,
154
+ k: number,
155
+ ): { article: Slug; section: number }[] {
156
+ if (k <= 0 || docCount === 0) return [];
157
+
158
+ const queryTerms = new Set(tokenize(text));
159
+ const scores = scoreSections(queryTerms);
160
+ if (scores.size === 0) return [];
161
+
162
+ const ranked = [...scores.keys()].sort((a, c) => rankSection(a, c, scores));
163
+
164
+ // Dedupe to distinct articles, keeping each article's best (first-ranked)
165
+ // section, until we have `k` articles.
166
+ const seen = new Set<Slug>();
167
+ const result: { article: Slug; section: number }[] = [];
168
+ for (const doc of ranked) {
169
+ const article = sections[doc]!.article;
170
+ if (seen.has(article)) continue;
171
+ seen.add(article);
172
+ result.push({ article, section: doc });
173
+ if (result.length >= k) break;
174
+ }
175
+ return result;
176
+ }
177
+
178
+ function bestSection(article: Slug, queryText: string): number {
179
+ const docs = index.byArticle.get(article);
180
+ if (!docs || docs.length === 0) return -1;
181
+
182
+ const queryTerms = new Set(tokenize(queryText));
183
+ const scores = scoreSections(queryTerms);
184
+
185
+ let best = docs[0]!;
186
+ let bestScore = scores.get(best) ?? 0;
187
+ for (const doc of docs) {
188
+ const score = scores.get(doc) ?? 0;
189
+ // Strictly-greater keeps the earliest (lowest ordinal) section on ties,
190
+ // since `docs` is in ascending section order.
191
+ if (score > bestScore) {
192
+ best = doc;
193
+ bestScore = score;
194
+ }
195
+ }
196
+ return best;
197
+ }
198
+
199
+ return { query, bestSection };
200
+ }
@@ -0,0 +1,115 @@
1
+ import type { Section, SectionIndex, Slug } from "./types.js";
2
+
3
+ /**
4
+ * Section-grain index builder for memory-v3 lane retrieval.
5
+ *
6
+ * Splits each page body into sections at `## ` headings — text before the
7
+ * first heading is the page lead (ordinal 0) — and emits a flat, deterministic
8
+ * `SectionIndex`. Over-long sections are chunked so each `Section.text` fits a
9
+ * typical embedding window. The function is pure: it does no I/O of its own
10
+ * (the caller supplies `pageBody`), no LLM calls, and no Qdrant/daemon wiring.
11
+ */
12
+
13
+ /**
14
+ * Max `Section.text` length in characters. Sections longer than this are split
15
+ * into multiple ordered `Section`s sharing the same `(article, ordinal)` so the
16
+ * embedding backend never receives an over-window input.
17
+ */
18
+ export const SECTION_CHUNK_CHARS = 6000;
19
+
20
+ /** Last `/`- or `.`-delimited segment of a slug (used for the head line). */
21
+ function lastSlugSegment(slug: Slug): string {
22
+ const segments = slug.split(/[/.]/);
23
+ return segments[segments.length - 1] ?? slug;
24
+ }
25
+
26
+ interface RawSection {
27
+ title: string;
28
+ body: string;
29
+ }
30
+
31
+ /**
32
+ * Split a frontmatter-stripped markdown body into raw sections. The text before
33
+ * the first `## ` heading is the lead (title `""`); each subsequent `## `
34
+ * heading starts a new section whose title is the heading text.
35
+ */
36
+ function splitIntoRawSections(body: string): RawSection[] {
37
+ // Always seed a lead section (title `""`). The lead may stay empty, so a
38
+ // headingless or empty page still yields a single ordinal-0 section.
39
+ const sections: { title: string; lines: string[] }[] = [
40
+ { title: "", lines: [] },
41
+ ];
42
+
43
+ for (const line of body.split("\n")) {
44
+ const heading = /^## (.*)$/.exec(line);
45
+ if (heading) {
46
+ sections.push({ title: heading[1]!.trim(), lines: [] });
47
+ } else {
48
+ sections[sections.length - 1]!.lines.push(line);
49
+ }
50
+ }
51
+
52
+ return sections.map((s) => ({ title: s.title, body: s.lines.join("\n") }));
53
+ }
54
+
55
+ /**
56
+ * Split `text` into chunks no longer than `SECTION_CHUNK_CHARS`, preferring to
57
+ * break on newlines so chunks stay readable. Order is preserved.
58
+ */
59
+ function chunkText(text: string): string[] {
60
+ if (text.length <= SECTION_CHUNK_CHARS) return [text];
61
+
62
+ const chunks: string[] = [];
63
+ let remaining = text;
64
+ while (remaining.length > SECTION_CHUNK_CHARS) {
65
+ const window = remaining.slice(0, SECTION_CHUNK_CHARS);
66
+ const newlineBreak = window.lastIndexOf("\n");
67
+ // Only break on a newline if it leaves a non-trivial chunk; otherwise hard
68
+ // split at the window boundary to guarantee forward progress.
69
+ const breakAt = newlineBreak > 0 ? newlineBreak : SECTION_CHUNK_CHARS;
70
+ chunks.push(remaining.slice(0, breakAt));
71
+ remaining = remaining.slice(breakAt).replace(/^\n/, "");
72
+ }
73
+ if (remaining.length > 0) chunks.push(remaining);
74
+ return chunks;
75
+ }
76
+
77
+ export async function buildSectionIndex(
78
+ slugs: Slug[],
79
+ pageBody: (slug: Slug) => Promise<string>,
80
+ ): Promise<SectionIndex> {
81
+ const sections: Section[] = [];
82
+
83
+ // Sort slugs so the flat `sections` array is deterministic across runs.
84
+ for (const article of [...slugs].sort((a, b) => a.localeCompare(b))) {
85
+ const body = await pageBody(article);
86
+ const segment = lastSlugSegment(article);
87
+
88
+ let ordinal = 0;
89
+ for (const raw of splitIntoRawSections(body)) {
90
+ const head = `${segment} — ${raw.title}`;
91
+ const fullText = `${head}\n${raw.body}`;
92
+ for (const chunk of chunkText(fullText)) {
93
+ sections.push({
94
+ article,
95
+ title: raw.title,
96
+ text: chunk,
97
+ ordinal: ordinal++,
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ const byArticle = new Map<Slug, number[]>();
104
+ for (let i = 0; i < sections.length; i++) {
105
+ const article = sections[i]!.article;
106
+ let indices = byArticle.get(article);
107
+ if (!indices) {
108
+ indices = [];
109
+ byArticle.set(article, indices);
110
+ }
111
+ indices.push(i);
112
+ }
113
+
114
+ return { sections, byArticle };
115
+ }