@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,71 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ capabilityOrDiskBody,
5
+ type CapabilityResolvers,
6
+ isCapabilitySlug,
7
+ renderCapabilityContent,
8
+ } from "../capabilities.js";
9
+
10
+ describe("isCapabilitySlug", () => {
11
+ test("true for skill and CLI-command slugs, false otherwise", () => {
12
+ expect(isCapabilitySlug("skills/meet-join")).toBe(true);
13
+ expect(isCapabilitySlug("cli-commands/schedules")).toBe(true);
14
+ expect(isCapabilitySlug("relationship/vows")).toBe(false);
15
+ expect(isCapabilitySlug("some-page")).toBe(false);
16
+ });
17
+ });
18
+
19
+ describe("renderCapabilityContent", () => {
20
+ const resolvers: CapabilityResolvers = {
21
+ skill: (slug) =>
22
+ slug === "skills/foo" ? { id: "foo", content: "foo capability" } : null,
23
+ cli: (slug) =>
24
+ slug === "cli-commands/bar"
25
+ ? { id: "bar", content: "bar capability" }
26
+ : null,
27
+ };
28
+
29
+ test("renders a skill slug with a Skill header", () => {
30
+ expect(renderCapabilityContent("skills/foo", resolvers)).toBe(
31
+ "# Skill: foo\nfoo capability",
32
+ );
33
+ });
34
+
35
+ test("renders a CLI-command slug with a CLI header", () => {
36
+ expect(renderCapabilityContent("cli-commands/bar", resolvers)).toBe(
37
+ "# CLI command: bar\nbar capability",
38
+ );
39
+ });
40
+
41
+ test("degrades to '' for a capability slug the cache cannot resolve", () => {
42
+ expect(renderCapabilityContent("skills/missing", resolvers)).toBe("");
43
+ });
44
+
45
+ test("returns null for a non-capability slug so the caller reads the on-disk page", () => {
46
+ expect(renderCapabilityContent("relationship/vows", resolvers)).toBeNull();
47
+ });
48
+ });
49
+
50
+ describe("capabilityOrDiskBody", () => {
51
+ test("a capability slug routes through renderCapabilityContent and never reads disk", async () => {
52
+ let diskReads = 0;
53
+ const readDiskBody = async (): Promise<string> => {
54
+ diskReads += 1;
55
+ return "disk body — should not be read";
56
+ };
57
+ // A capability slug whose production cache has no entry degrades to "" (the
58
+ // `renderCapabilityContent` contract) rather than falling through to the
59
+ // injected disk reader, which must stay untouched.
60
+ expect(await capabilityOrDiskBody("skills/example", readDiskBody)).toBe("");
61
+ expect(diskReads).toBe(0);
62
+ });
63
+
64
+ test("a non-capability slug returns the injected disk reader's result", async () => {
65
+ const readDiskBody = async (slug: string): Promise<string> =>
66
+ `disk body for ${slug}`;
67
+ expect(await capabilityOrDiskBody("relationship/vows", readDiskBody)).toBe(
68
+ "disk body for relationship/vows",
69
+ );
70
+ });
71
+ });
@@ -0,0 +1,181 @@
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
+
6
+ mock.module("../../../../util/logger.js", () => ({
7
+ getLogger: () => makeMockLogger(),
8
+ }));
9
+
10
+ mock.module("../../../../memory/qdrant-client.js", () => ({
11
+ resolveQdrantUrl: () => "http://127.0.0.1:6333",
12
+ }));
13
+
14
+ // Stub the shared embedding backend. Records the queries it was asked to embed
15
+ // and returns one deterministic vector so `denseLane` can issue the search.
16
+ const embedState = {
17
+ calls: [] as string[][],
18
+ throws: null as Error | null,
19
+ };
20
+ mock.module("../../../../memory/embedding-backend.js", () => ({
21
+ embedWithBackend: async (_config: unknown, inputs: string[]) => {
22
+ embedState.calls.push(inputs);
23
+ if (embedState.throws) throw embedState.throws;
24
+ return {
25
+ provider: "local",
26
+ model: "test-model",
27
+ vectors: inputs.map(() => [0.1, 0.2, 0.3, 0.4]),
28
+ };
29
+ },
30
+ }));
31
+
32
+ // Mock the @qdrant/js-client-rest client. The mock records the query params and
33
+ // returns whatever section points the test programmed (in descending score
34
+ // order, matching Qdrant's behavior).
35
+ type MockPoint = {
36
+ payload: { article: string; ordinal: number };
37
+ score: number;
38
+ };
39
+
40
+ const state = {
41
+ points: [] as MockPoint[],
42
+ queryThrows: null as Error | null,
43
+ queryCalls: [] as Array<{
44
+ collection: string;
45
+ query: unknown;
46
+ limit: number;
47
+ }>,
48
+ };
49
+
50
+ class MockQdrantClient {
51
+ constructor(_opts: unknown) {}
52
+ async query(name: string, params: { query: unknown; limit: number }) {
53
+ state.queryCalls.push({
54
+ collection: name,
55
+ query: params.query,
56
+ limit: params.limit,
57
+ });
58
+ if (state.queryThrows) throw state.queryThrows;
59
+ return { points: state.points };
60
+ }
61
+ }
62
+
63
+ mock.module("@qdrant/js-client-rest", () => ({
64
+ QdrantClient: MockQdrantClient,
65
+ }));
66
+
67
+ const { denseLane, OVERSAMPLE } = await import("../dense.js");
68
+ const { SECTION_COLLECTION, _resetSectionDenseStoreForTests } =
69
+ await import("../section-dense-store.js");
70
+
71
+ const CONFIG = {
72
+ memory: { qdrant: { vectorSize: 4, onDisk: true } },
73
+ } as unknown as AssistantConfig;
74
+
75
+ function point(article: string, ordinal: number, score: number): MockPoint {
76
+ return { payload: { article, ordinal }, score };
77
+ }
78
+
79
+ function resetState(): void {
80
+ embedState.calls.length = 0;
81
+ embedState.throws = null;
82
+ state.points = [];
83
+ state.queryThrows = null;
84
+ state.queryCalls.length = 0;
85
+ _resetSectionDenseStoreForTests();
86
+ }
87
+
88
+ describe("memory v3 dense lane", () => {
89
+ beforeEach(resetState);
90
+ afterEach(resetState);
91
+
92
+ test("targets the section collection at k * OVERSAMPLE depth", async () => {
93
+ state.points = [point("page-a", 0, 0.9)];
94
+
95
+ await denseLane(CONFIG, "some query", 5);
96
+
97
+ // Embedded the query once.
98
+ expect(embedState.calls).toEqual([["some query"]]);
99
+ // Oversampled the section search so dedupe still yields k articles.
100
+ expect(state.queryCalls).toHaveLength(1);
101
+ expect(state.queryCalls[0]!.collection).toBe(SECTION_COLLECTION);
102
+ expect(state.queryCalls[0]!.limit).toBe(5 * OVERSAMPLE);
103
+ expect(state.queryCalls[0]!.query).toEqual([0.1, 0.2, 0.3, 0.4]);
104
+ });
105
+
106
+ test("dedupes section points to distinct articles with their best section", async () => {
107
+ // page-a's best section is ordinal 2 (highest score); the later page-a
108
+ // section is ignored once the article is already represented.
109
+ state.points = [
110
+ point("page-a", 2, 0.95),
111
+ point("topic-x", 0, 0.9),
112
+ point("page-a", 1, 0.5),
113
+ ];
114
+
115
+ const hits = await denseLane(CONFIG, "query", 5);
116
+
117
+ expect(hits).toEqual([
118
+ { article: "page-a", section: 2 },
119
+ { article: "topic-x", section: 0 },
120
+ ]);
121
+ });
122
+
123
+ test("oversamples then truncates to k distinct articles", async () => {
124
+ state.points = [
125
+ point("page-a", 0, 0.99),
126
+ point("page-b", 0, 0.98),
127
+ point("page-c", 0, 0.97),
128
+ point("page-d", 0, 0.96),
129
+ ];
130
+
131
+ const hits = await denseLane(CONFIG, "query", 2);
132
+
133
+ expect(hits).toEqual([
134
+ { article: "page-a", section: 0 },
135
+ { article: "page-b", section: 0 },
136
+ ]);
137
+ });
138
+
139
+ test("ignores malformed payloads", async () => {
140
+ state.points = [
141
+ {
142
+ payload: {
143
+ article: 123,
144
+ ordinal: 0,
145
+ } as unknown as MockPoint["payload"],
146
+ score: 0.9,
147
+ },
148
+ point("topic-x", 1, 0.8),
149
+ ];
150
+
151
+ const hits = await denseLane(CONFIG, "query", 5);
152
+
153
+ expect(hits).toEqual([{ article: "topic-x", section: 1 }]);
154
+ });
155
+
156
+ test("a thrown Qdrant search degrades to []", async () => {
157
+ state.queryThrows = new Error("qdrant unreachable");
158
+
159
+ const hits = await denseLane(CONFIG, "query", 5);
160
+
161
+ expect(hits).toEqual([]);
162
+ });
163
+
164
+ test("a failed embedding degrades to []", async () => {
165
+ embedState.throws = new Error("embed backend down");
166
+
167
+ const hits = await denseLane(CONFIG, "query", 5);
168
+
169
+ expect(hits).toEqual([]);
170
+ // No Qdrant round-trip when embedding fails.
171
+ expect(state.queryCalls).toHaveLength(0);
172
+ });
173
+
174
+ test("non-positive k short-circuits without a search", async () => {
175
+ const hits = await denseLane(CONFIG, "query", 0);
176
+
177
+ expect(hits).toEqual([]);
178
+ expect(embedState.calls).toEqual([]);
179
+ expect(state.queryCalls).toHaveLength(0);
180
+ });
181
+ });
@@ -0,0 +1,247 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { PageIndexEntry } from "../../../../memory/v2/page-index.js";
4
+ import { buildEdgeGraph, edgeExpand } from "../edge.js";
5
+ import type { Slug } from "../types.js";
6
+
7
+ /** Minimal page-index entry factory — only the fields the edge lane reads. */
8
+ function entry(id: number, slug: Slug, edges: number[] = []): PageIndexEntry {
9
+ return { id, slug, summary: "", edges, leaves: [], modifiedAt: 0 };
10
+ }
11
+
12
+ /** Build a raw-page reader from a fixture map; missing slugs read as "". */
13
+ function rawReader(pages: Record<string, string>) {
14
+ return async (slug: Slug): Promise<string> => pages[slug] ?? "";
15
+ }
16
+
17
+ /** Frontmatter helper: wrap a `links:` list around an optional body. */
18
+ function withLinks(links: string[], body = "body"): string {
19
+ const block = links.map((l) => ` - "${l}"`).join("\n");
20
+ return `---\ntitle: x\nlinks:\n${block}\n---\n\n${body}`;
21
+ }
22
+
23
+ describe("buildEdgeGraph — links: frontmatter", () => {
24
+ test("parses target + description split on the first ' — '", async () => {
25
+ const articles = [entry(1, "page-a"), entry(2, "page-b")];
26
+ const graph = await buildEdgeGraph(
27
+ articles,
28
+ rawReader({
29
+ "page-a": withLinks(["page-b — the b page — with an extra dash"]),
30
+ "page-b": "no frontmatter",
31
+ }),
32
+ );
33
+ const out = graph.adjacency.get("page-a")!;
34
+ // Split on the FIRST ' — ' only: description keeps the trailing dash text.
35
+ expect(out.get("page-b")).toBe("the b page — with an extra dash");
36
+ });
37
+
38
+ test("drops unknown/dangling link targets", async () => {
39
+ const articles = [entry(1, "page-a"), entry(2, "page-b")];
40
+ const graph = await buildEdgeGraph(
41
+ articles,
42
+ rawReader({
43
+ "page-a": withLinks([
44
+ "page-b — known target",
45
+ "page-ghost — dangling target not in corpus",
46
+ ]),
47
+ }),
48
+ );
49
+ const out = graph.adjacency.get("page-a")!;
50
+ expect(out.has("page-b")).toBe(true);
51
+ expect(out.has("page-ghost")).toBe(false);
52
+ expect([...out.keys()]).toEqual(["page-b"]);
53
+ });
54
+
55
+ test("a page with no links: falls back to wikilinks and numeric edges", async () => {
56
+ const articles = [
57
+ entry(1, "page-a", [2]), // numeric edge → page-b
58
+ entry(2, "page-b"),
59
+ entry(3, "topic-x"),
60
+ ];
61
+ const graph = await buildEdgeGraph(
62
+ articles,
63
+ rawReader({
64
+ // No links: frontmatter; body has an inline wikilink to topic-x.
65
+ "page-a": "plain body referencing [[topic-x]] inline",
66
+ }),
67
+ );
68
+ const out = graph.adjacency.get("page-a")!;
69
+ expect(out.has("page-b")).toBe(true); // from numeric edge
70
+ expect(out.has("topic-x")).toBe(true); // from wikilink
71
+ // No curated descriptions on wikilink/numeric edges.
72
+ expect(out.get("page-b")).toBeUndefined();
73
+ expect(out.get("topic-x")).toBeUndefined();
74
+ });
75
+ });
76
+
77
+ describe("buildEdgeGraph — three-source union", () => {
78
+ test("links ∪ wikilinks ∪ numeric edges merge into one adjacency map", async () => {
79
+ const articles = [
80
+ entry(1, "page-a", [4]), // numeric edge → topic-y
81
+ entry(2, "page-b"),
82
+ entry(3, "topic-x"),
83
+ entry(4, "topic-y"),
84
+ ];
85
+ const graph = await buildEdgeGraph(
86
+ articles,
87
+ rawReader({
88
+ "page-a": withLinks(
89
+ ["page-b — authored neighbour"],
90
+ "and a [[topic-x]] wikilink in the body",
91
+ ),
92
+ }),
93
+ );
94
+ const out = graph.adjacency.get("page-a")!;
95
+ expect(new Set(out.keys())).toEqual(
96
+ new Set(["page-b", "topic-x", "topic-y"]),
97
+ );
98
+ expect(out.get("page-b")).toBe("authored neighbour"); // from links
99
+ expect(out.get("topic-x")).toBeUndefined(); // from wikilink
100
+ expect(out.get("topic-y")).toBeUndefined(); // from numeric edge
101
+ });
102
+
103
+ test("read failure still keeps numeric-edge fallback", async () => {
104
+ const articles = [entry(1, "page-a", [2]), entry(2, "page-b")];
105
+ const graph = await buildEdgeGraph(articles, async () => {
106
+ throw new Error("read failed");
107
+ });
108
+ const out = graph.adjacency.get("page-a")!;
109
+ expect(out.has("page-b")).toBe(true);
110
+ });
111
+ });
112
+
113
+ describe("buildEdgeGraph — hubs", () => {
114
+ test("marks articles with in-degree > hubDegree as hubs", async () => {
115
+ // hub receives 3 inbound edges; with hubDegree=2 it is a hub.
116
+ const articles = [
117
+ entry(1, "page-a"),
118
+ entry(2, "page-b"),
119
+ entry(3, "page-c"),
120
+ entry(4, "hub"),
121
+ ];
122
+ const graph = await buildEdgeGraph(
123
+ articles,
124
+ rawReader({
125
+ "page-a": withLinks(["hub — to hub"]),
126
+ "page-b": withLinks(["hub — to hub"]),
127
+ "page-c": withLinks(["hub — to hub"]),
128
+ }),
129
+ { hubDegree: 2 },
130
+ );
131
+ expect(graph.hubs.has("hub")).toBe(true);
132
+ expect(graph.hubs.has("page-a")).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe("edgeExpand", () => {
137
+ /** Build a star/chain graph: page-a → b,c,d,e,f,g ; topic-x → b. */
138
+ async function fixtureGraph(hubDegree = 30) {
139
+ const articles = [
140
+ entry(1, "page-a"),
141
+ entry(2, "page-b"),
142
+ entry(3, "page-c"),
143
+ entry(4, "page-d"),
144
+ entry(5, "page-e"),
145
+ entry(6, "page-f"),
146
+ entry(7, "page-g"),
147
+ entry(8, "topic-x"),
148
+ ];
149
+ return buildEdgeGraph(
150
+ articles,
151
+ rawReader({
152
+ "page-a": withLinks([
153
+ "page-b — neighbour b",
154
+ "page-c — neighbour c",
155
+ "page-d — neighbour d",
156
+ "page-e — neighbour e",
157
+ "page-f — neighbour f",
158
+ "page-g — neighbour g",
159
+ ]),
160
+ "topic-x": withLinks(["page-b — also to b"]),
161
+ }),
162
+ { hubDegree },
163
+ );
164
+ }
165
+
166
+ test("carries the curated links description through expansion", async () => {
167
+ const graph = await fixtureGraph();
168
+ const out = edgeExpand(graph, ["page-a"]);
169
+ const b = out.find((n) => n.article === "page-b");
170
+ expect(b?.description).toBe("neighbour b");
171
+ });
172
+
173
+ test("respects perSeed", async () => {
174
+ const graph = await fixtureGraph();
175
+ const out = edgeExpand(graph, ["page-a"], { perSeed: 2 });
176
+ expect(out).toHaveLength(2);
177
+ expect(out.map((n) => n.article)).toEqual(["page-b", "page-c"]);
178
+ });
179
+
180
+ test("respects cap across seeds", async () => {
181
+ const graph = await fixtureGraph();
182
+ // Two seeds each could add many; cap=3 truncates the total.
183
+ const out = edgeExpand(graph, ["page-a", "topic-x"], {
184
+ perSeed: 6,
185
+ cap: 3,
186
+ });
187
+ expect(out).toHaveLength(3);
188
+ });
189
+
190
+ test("respects seedCount (only top-N seeds expanded)", async () => {
191
+ // seedCount=1: only the first seed expands; the second seed's unique
192
+ // neighbour is never reached.
193
+ const articles = [
194
+ entry(1, "seed-1"),
195
+ entry(2, "seed-2"),
196
+ entry(3, "only-via-2"),
197
+ entry(4, "via-1"),
198
+ ];
199
+ const g = await buildEdgeGraph(
200
+ articles,
201
+ rawReader({
202
+ "seed-1": withLinks(["via-1 — x"]),
203
+ "seed-2": withLinks(["only-via-2 — y"]),
204
+ }),
205
+ );
206
+ const out = edgeExpand(g, ["seed-1", "seed-2"], { seedCount: 1 });
207
+ expect(out.map((n) => n.article)).toEqual(["via-1"]);
208
+ expect(out.find((n) => n.article === "only-via-2")).toBeUndefined();
209
+ });
210
+
211
+ test("excludes hub neighbours from expansion", async () => {
212
+ // page-b has in-degree 2 (page-a + topic-x). At the default threshold it
213
+ // is not a hub; lowering hubDegree below 2 makes it one.
214
+ const nonHubGraph = await fixtureGraph();
215
+ expect(nonHubGraph.hubs.has("page-b")).toBe(false);
216
+
217
+ const hubGraph = await fixtureGraph(1); // page-b in-degree 2 > 1 → hub
218
+ expect(hubGraph.hubs.has("page-b")).toBe(true);
219
+ const out = edgeExpand(hubGraph, ["page-a"]);
220
+ expect(out.find((n) => n.article === "page-b")).toBeUndefined();
221
+ // non-hub neighbours still surface
222
+ expect(out.find((n) => n.article === "page-c")).toBeDefined();
223
+ });
224
+
225
+ test("respects the alive predicate", async () => {
226
+ const graph = await fixtureGraph();
227
+ const out = edgeExpand(graph, ["page-a"], {
228
+ alive: (slug) => slug !== "page-c",
229
+ });
230
+ expect(out.find((n) => n.article === "page-c")).toBeUndefined();
231
+ expect(out.find((n) => n.article === "page-b")).toBeDefined();
232
+ });
233
+
234
+ test("does not surface the seeds themselves", async () => {
235
+ const articles = [entry(1, "page-a"), entry(2, "page-b")];
236
+ const graph = await buildEdgeGraph(
237
+ articles,
238
+ rawReader({
239
+ "page-a": withLinks(["page-b — to b"]),
240
+ "page-b": withLinks(["page-a — back to a"]),
241
+ }),
242
+ );
243
+ const out = edgeExpand(graph, ["page-a", "page-b"]);
244
+ // page-a and page-b are both seeds; neither should be surfaced.
245
+ expect(out).toHaveLength(0);
246
+ });
247
+ });