@vellumai/assistant 0.8.1 → 0.8.2

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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -879,17 +879,17 @@ describe("injectMemoryV2Block", () => {
879
879
  // Unified pool — skills as `skills/<id>` slugs
880
880
  // ---------------------------------------------------------------------------
881
881
 
882
- test("renders a skill-only block via the skills/ slug prefix", async () => {
882
+ test("renders a retrieved skills/<id> slug under Skills You Can Use", async () => {
883
883
  // No concept-page candidates this turn — the only ANN hit is a skill
884
884
  // slug. The render path branches on `skills/` prefix: it pulls the
885
885
  // entry from the skill-store cache (mocked) and emits the bullet under
886
886
  // the `### Skills You Can Use` subsection.
887
- stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
887
+ stageTurn([{ slug: "skills/retrieved-skill", denseScore: 0.9 }]);
888
888
  stageSkills([
889
889
  {
890
- id: "example-skill-a",
890
+ id: "retrieved-skill",
891
891
  content:
892
- 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
892
+ 'The "Retrieved Skill" skill (retrieved-skill) is available. Helps with retrieved skills.',
893
893
  },
894
894
  ]);
895
895
 
@@ -904,16 +904,18 @@ describe("injectMemoryV2Block", () => {
904
904
  config: makeConfig(),
905
905
  });
906
906
 
907
- expect(result.toInject).toEqual(["skills/example-skill-a"]);
907
+ expect(result.toInject).toEqual(["skills/retrieved-skill"]);
908
908
  expect(result.block).not.toBeNull();
909
909
  expect(result.block).not.toContain("<memory>");
910
910
  expect(result.block).not.toContain("</memory>");
911
911
  expect(result.block).not.toContain("## What I Remember Right Now");
912
912
  expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
913
- expect(result.block).toContain("### Skills You Can Use");
914
- expect(result.block).toContain(
915
- '- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
913
+ const headerIdx = result.block!.indexOf("### Skills You Can Use");
914
+ const skillIdx = result.block!.indexOf(
915
+ '- The "Retrieved Skill" skill (retrieved-skill) is available. Helps with retrieved skills. → use skill_load to activate',
916
916
  );
917
+ expect(headerIdx).toBeGreaterThan(-1);
918
+ expect(skillIdx).toBeGreaterThan(headerIdx);
917
919
  });
918
920
 
919
921
  test("renders concept-page sections before the skills subsection in mixed blocks", async () => {
@@ -1679,6 +1681,56 @@ describe("injectMemoryV2Block", () => {
1679
1681
  expect(row.concepts).toEqual([]);
1680
1682
  });
1681
1683
 
1684
+ test("flag-on: router-failure path swallows a save() error and returns block:null instead of throwing", async () => {
1685
+ // PR 30176 refactored router-failure handling to delegate to
1686
+ // `finalizeInjection`. That regressed the prior inline log-and-continue
1687
+ // semantics on the router-failure path: a transient SQLite write
1688
+ // throwing during the stub-state save now aborted the whole turn
1689
+ // because `finalizeInjection`'s try/catch re-threw caughtErr at the end.
1690
+ //
1691
+ // This test stages exactly that scenario — router returns
1692
+ // `failureReason: api_error` AND `save()` throws — and asserts the
1693
+ // turn completes with `{ block: null, toInject: [] }` rather than
1694
+ // propagating the SQLite error to `prepareMemory`.
1695
+ routerState.nextResult = {
1696
+ selectedSlugs: [],
1697
+ failureReason: "api_error",
1698
+ };
1699
+ activationStoreState.saveShouldThrow = true;
1700
+
1701
+ let threw: unknown = undefined;
1702
+ let result: Awaited<ReturnType<typeof injectMemoryV2Block>> | undefined;
1703
+ try {
1704
+ result = await injectMemoryV2Block({
1705
+ database: db,
1706
+ conversationId: "conv-router-fail-save-throws",
1707
+ currentTurn: 5,
1708
+ userMessage: "anything",
1709
+ assistantMessage: "ok",
1710
+ nowText: "Now",
1711
+ messageId: "msg-fail-save",
1712
+ config: makeConfig({ router: { enabled: true } }),
1713
+ });
1714
+ } catch (err) {
1715
+ threw = err;
1716
+ }
1717
+
1718
+ expect(threw).toBeUndefined();
1719
+ expect(result).toBeDefined();
1720
+ expect(result!.block).toBeNull();
1721
+ expect(result!.toInject).toEqual([]);
1722
+
1723
+ // Telemetry still flushes with `mode: "errored"` so the failure stays
1724
+ // observable — the same row the inline pre-refactor path emitted.
1725
+ expect(telemetryState.recordCalls.length).toBe(1);
1726
+ const row = telemetryState.recordCalls[0] as {
1727
+ mode: string;
1728
+ concepts: unknown[];
1729
+ };
1730
+ expect(row.mode).toBe("errored");
1731
+ expect(row.concepts).toEqual([]);
1732
+ });
1733
+
1682
1734
  test("flag-on: router abstention (empty selectedSlugs, no failure) writes mode:`router` row with no injected pages", async () => {
1683
1735
  routerState.nextResult = {
1684
1736
  selectedSlugs: [],
@@ -1904,5 +1956,43 @@ describe("injectMemoryV2Block", () => {
1904
1956
  const row = telemetryState.recordCalls[0] as { mode: string };
1905
1957
  expect(row.mode).toBe("per-turn");
1906
1958
  });
1959
+
1960
+ test("flag-on + mode='context-load': router runs (everInjected was cleared by onCompacted so dedupe is a no-op; abstention is accepted as the trade-off)", async () => {
1961
+ // Context-load is the full top-K bootstrap fired after compaction or
1962
+ // a fresh conversation reload. The earlier worry about the router's
1963
+ // `everInjected` dedupe filtering out post-compaction restorations
1964
+ // doesn't apply: `ConversationGraphMemory.onCompacted` calls
1965
+ // `evictCompactedTurnsV2` which empties the list before this code
1966
+ // runs. Router abstention here means no v2 pages this turn — that's
1967
+ // preferable to letting the activation graph pick something arbitrary.
1968
+ routerState.nextResult = {
1969
+ selectedSlugs: ["alice-vscode"],
1970
+ failureReason: null,
1971
+ };
1972
+
1973
+ const result = await injectMemoryV2Block({
1974
+ database: db,
1975
+ conversationId: "conv-context-load-router-on",
1976
+ currentTurn: 1,
1977
+ userMessage: "Tell me about Alice",
1978
+ assistantMessage: "",
1979
+ nowText: "Now",
1980
+ messageId: "msg-1",
1981
+ mode: "context-load",
1982
+ config: makeConfig({ router: { enabled: true } }),
1983
+ });
1984
+
1985
+ // Router was called on context-load too.
1986
+ expect(routerState.callCount).toBe(1);
1987
+
1988
+ // Router's picks were rendered.
1989
+ expect(result.toInject).toEqual(["alice-vscode"]);
1990
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
1991
+
1992
+ // Telemetry row reflects the router mode, not the activation mode.
1993
+ expect(telemetryState.recordCalls.length).toBe(1);
1994
+ const row = telemetryState.recordCalls[0] as { mode: string };
1995
+ expect(row.mode).toBe("router");
1996
+ });
1907
1997
  });
1908
1998
  });
@@ -754,6 +754,93 @@ describe("runMemoryV2Migration", () => {
754
754
  expect(body).toContain("second body");
755
755
  });
756
756
 
757
+ test("force=true cleanly strips the prior migration block from essentials.md", async () => {
758
+ insertNode(database, {
759
+ content: "Alice prefers VS Code.",
760
+ significance: 0.95,
761
+ });
762
+ await runMemoryV2Migration({
763
+ workspaceDir,
764
+ database,
765
+ provider: buildStubProvider("body"),
766
+ });
767
+
768
+ const essentialsPath = join(workspaceDir, "memory", "essentials.md");
769
+ const afterFirst = readFileSync(essentialsPath, "utf-8");
770
+ expect(afterFirst).toContain("<!-- migration:v1-to-v2 -->");
771
+ expect(afterFirst).toContain("<!-- /migration:v1-to-v2 -->");
772
+
773
+ await runMemoryV2Migration({
774
+ workspaceDir,
775
+ database,
776
+ provider: buildStubProvider("body"),
777
+ force: true,
778
+ });
779
+
780
+ const afterRerun = readFileSync(essentialsPath, "utf-8");
781
+ // Exactly one migration block — no leftover/duplicated markers.
782
+ expect(afterRerun.match(/<!-- migration:v1-to-v2 -->/g)?.length ?? 0).toBe(
783
+ 1,
784
+ );
785
+ expect(
786
+ afterRerun.match(/<!-- \/migration:v1-to-v2 -->/g)?.length ?? 0,
787
+ ).toBe(1);
788
+ });
789
+
790
+ test("force=true preserves user-appended content after the migration block", async () => {
791
+ insertNode(database, {
792
+ content: "Alice prefers VS Code.",
793
+ significance: 0.95,
794
+ });
795
+ await runMemoryV2Migration({
796
+ workspaceDir,
797
+ database,
798
+ provider: buildStubProvider("body"),
799
+ });
800
+
801
+ const essentialsPath = join(workspaceDir, "memory", "essentials.md");
802
+ const beforeAppend = readFileSync(essentialsPath, "utf-8");
803
+ const userAppended = "\nUser added this after the migration ran.\n";
804
+ writeFileSync(essentialsPath, beforeAppend + userAppended, "utf-8");
805
+
806
+ await runMemoryV2Migration({
807
+ workspaceDir,
808
+ database,
809
+ provider: buildStubProvider("body"),
810
+ force: true,
811
+ });
812
+
813
+ const after = readFileSync(essentialsPath, "utf-8");
814
+ expect(after).toContain("User added this after the migration ran.");
815
+ // And exactly one migration envelope remains.
816
+ expect(after.match(/<!-- migration:v1-to-v2 -->/g)?.length ?? 0).toBe(1);
817
+ expect(after.match(/<!-- \/migration:v1-to-v2 -->/g)?.length ?? 0).toBe(1);
818
+ });
819
+
820
+ test("force=true on legacy (close-marker-less) file strips only up to the next blank line", async () => {
821
+ // Simulate a file written by the prior migration format: opening marker
822
+ // with no closing sentinel, with user-appended content separated by a
823
+ // blank line. The strip should preserve the user content.
824
+ insertNode(database, { content: "Alice prefers VS Code." });
825
+ const essentialsPath = join(workspaceDir, "memory", "essentials.md");
826
+ writeFileSync(
827
+ essentialsPath,
828
+ "<!-- migration:v1-to-v2 -->\nlegacy migrated line\n\nUser-appended legacy note.\n",
829
+ "utf-8",
830
+ );
831
+
832
+ await runMemoryV2Migration({
833
+ workspaceDir,
834
+ database,
835
+ provider: buildStubProvider("body"),
836
+ force: true,
837
+ });
838
+
839
+ const after = readFileSync(essentialsPath, "utf-8");
840
+ expect(after).toContain("User-appended legacy note.");
841
+ expect(after).not.toContain("legacy migrated line");
842
+ });
843
+
757
844
  test("clearing the sentinel allows a non-force re-run", async () => {
758
845
  insertNode(database, { content: "fact" });
759
846
  const provider = buildStubProvider("body");
@@ -245,6 +245,89 @@ describe("getPageIndex", () => {
245
245
  const idx = await getPageIndex(workspaceDir);
246
246
  expect(idx.bySlug.get("alice")?.summary.length).toBe(200);
247
247
  });
248
+
249
+ test("collapses embedded newlines in frontmatter.summary to single spaces", async () => {
250
+ await writePage(
251
+ workspaceDir,
252
+ makePage("alice", { summary: "First line.\nSecond line.\nThird line." }),
253
+ );
254
+ const idx = await getPageIndex(workspaceDir);
255
+ expect(idx.bySlug.get("alice")?.summary).toBe(
256
+ "First line. Second line. Third line.",
257
+ );
258
+ });
259
+
260
+ test("collapses embedded newlines and runs of whitespace in body fallback", async () => {
261
+ await writePage(
262
+ workspaceDir,
263
+ makePage("alice", {
264
+ body: " Body with\n\nmultiple\tlines\n and spaces. ",
265
+ }),
266
+ );
267
+ const idx = await getPageIndex(workspaceDir);
268
+ expect(idx.bySlug.get("alice")?.summary).toBe(
269
+ "Body with multiple lines and spaces.",
270
+ );
271
+ });
272
+
273
+ test("normalizes skill-entry content with embedded newlines", async () => {
274
+ skillState.entries = [
275
+ { id: "browser", content: "Drive a browser.\nSupports multiple tabs." },
276
+ ];
277
+ const idx = await getPageIndex(workspaceDir);
278
+ expect(idx.bySlug.get("skills/browser")?.summary).toBe(
279
+ "Drive a browser. Supports multiple tabs.",
280
+ );
281
+ });
282
+
283
+ test("renders a single line per entry even when summaries contain newlines", async () => {
284
+ await writePage(
285
+ workspaceDir,
286
+ makePage("alice", { summary: "line one\nline two" }),
287
+ );
288
+ const idx = await getPageIndex(workspaceDir);
289
+ // Exactly one trailing newline — the entry itself must not split.
290
+ expect(idx.rendered.split("\n").filter(Boolean).length).toBe(1);
291
+ });
292
+
293
+ test("drops a user concept page whose slug collides with a seeded skill entry", async () => {
294
+ await writePage(
295
+ workspaceDir,
296
+ makePage("skills/browser", {
297
+ summary: "User-authored page that shadows the skill.",
298
+ }),
299
+ );
300
+ skillState.entries = [{ id: "browser", content: "Seeded skill content." }];
301
+
302
+ const idx = await getPageIndex(workspaceDir);
303
+ // Only the skill entry survives under skills/browser.
304
+ expect(idx.entries.filter((e) => e.slug === "skills/browser").length).toBe(
305
+ 1,
306
+ );
307
+ expect(idx.bySlug.get("skills/browser")?.summary).toBe(
308
+ "Seeded skill content.",
309
+ );
310
+ });
311
+
312
+ test("collision dedupe leaves non-colliding pages and skills intact", async () => {
313
+ await writePage(workspaceDir, makePage("alice", { summary: "Alice" }));
314
+ await writePage(
315
+ workspaceDir,
316
+ makePage("skills/browser", { summary: "Shadow page." }),
317
+ );
318
+ skillState.entries = [
319
+ { id: "browser", content: "Seeded browser." },
320
+ { id: "calendar", content: "Seeded calendar." },
321
+ ];
322
+
323
+ const idx = await getPageIndex(workspaceDir);
324
+ expect(idx.entries.map((e) => e.slug)).toEqual([
325
+ "alice",
326
+ "skills/browser",
327
+ "skills/calendar",
328
+ ]);
329
+ expect(idx.bySlug.get("skills/browser")?.summary).toBe("Seeded browser.");
330
+ });
248
331
  });
249
332
 
250
333
  // ---------------------------------------------------------------------------
@@ -123,6 +123,39 @@ describe("renderRouterPrompt — page index handling", () => {
123
123
  });
124
124
  });
125
125
 
126
+ describe("renderRouterPrompt — replacement-pattern specials", () => {
127
+ // String.prototype.replaceAll interprets `$&`, `$'`, `` $` ``, `$$`, and
128
+ // `$n` in the replacement string as backreferences. LLM-generated page
129
+ // index content can contain literal `$` runs, so the substituter must
130
+ // pass values through unchanged.
131
+ const SPECIALS = "$& and $' and $` and $$ and $1";
132
+
133
+ test.each([
134
+ [
135
+ "pageIndexBlock",
136
+ { assistantName: "Aria", userName: "Alice", pageIndexBlock: SPECIALS },
137
+ ],
138
+ [
139
+ "assistantName",
140
+ {
141
+ assistantName: SPECIALS,
142
+ userName: "Alice",
143
+ pageIndexBlock: SAMPLE_INDEX,
144
+ },
145
+ ],
146
+ [
147
+ "userName",
148
+ {
149
+ assistantName: "Aria",
150
+ userName: SPECIALS,
151
+ pageIndexBlock: SAMPLE_INDEX,
152
+ },
153
+ ],
154
+ ])("renders %s with $ specials verbatim", (_, opts) => {
155
+ expect(renderRouterPrompt(opts)).toContain(SPECIALS);
156
+ });
157
+ });
158
+
126
159
  describe("renderRouterPrompt — determinism & snapshot stability", () => {
127
160
  test("returns the same string for the same inputs", () => {
128
161
  const opts = {
@@ -196,7 +229,7 @@ describe("resolveRouterPrompt — override path", () => {
196
229
  };
197
230
 
198
231
  test("null overridePath returns the bundled prompt verbatim", () => {
199
- expect(resolveRouterPrompt(null, STD_OPTS)).toEqual(
232
+ expect(resolveRouterPrompt(null, tmpDir, STD_OPTS)).toEqual(
200
233
  renderRouterPrompt(STD_OPTS),
201
234
  );
202
235
  });
@@ -209,16 +242,35 @@ describe("resolveRouterPrompt — override path", () => {
209
242
  "utf-8",
210
243
  );
211
244
 
212
- const out = resolveRouterPrompt(overridePath, STD_OPTS);
245
+ const out = resolveRouterPrompt(overridePath, tmpDir, STD_OPTS);
213
246
  expect(out).toContain("Hi Aria, you are routing for Alice.");
214
247
  expect(out).toContain(SAMPLE_INDEX);
215
248
  expect(out).not.toContain("{{ASSISTANT_NAME}}");
216
249
  expect(out).not.toContain("{{PAGE_INDEX}}");
217
250
  });
218
251
 
252
+ test("relative override path is resolved under the passed workspaceDir, not the default workspace", () => {
253
+ // Write the override into the per-test temp dir, which acts as a
254
+ // non-default workspace. The configured path is purely relative so the
255
+ // loader must resolve it against the supplied workspaceDir — if it
256
+ // resolved against the process-wide default workspace instead, the file
257
+ // wouldn't be found and the bundled prompt would be returned.
258
+ const relativeName = "router-override.md";
259
+ writeFileSync(
260
+ join(tmpDir, relativeName),
261
+ "Routed via {{ASSISTANT_NAME}} for {{USER_NAME}} :: {{PAGE_INDEX}}",
262
+ "utf-8",
263
+ );
264
+
265
+ const out = resolveRouterPrompt(relativeName, tmpDir, STD_OPTS);
266
+ expect(out).toContain("Routed via Aria for Alice");
267
+ expect(out).toContain(SAMPLE_INDEX);
268
+ expect(out).not.toEqual(renderRouterPrompt(STD_OPTS));
269
+ });
270
+
219
271
  test("missing override file falls back to bundled prompt", () => {
220
272
  const overridePath = join(tmpDir, "does-not-exist.md");
221
- expect(resolveRouterPrompt(overridePath, STD_OPTS)).toEqual(
273
+ expect(resolveRouterPrompt(overridePath, tmpDir, STD_OPTS)).toEqual(
222
274
  renderRouterPrompt(STD_OPTS),
223
275
  );
224
276
  });
@@ -226,7 +278,7 @@ describe("resolveRouterPrompt — override path", () => {
226
278
  test("empty override file falls back to bundled prompt", () => {
227
279
  const overridePath = join(tmpDir, "empty.md");
228
280
  writeFileSync(overridePath, " \n\t\n", "utf-8");
229
- expect(resolveRouterPrompt(overridePath, STD_OPTS)).toEqual(
281
+ expect(resolveRouterPrompt(overridePath, tmpDir, STD_OPTS)).toEqual(
230
282
  renderRouterPrompt(STD_OPTS),
231
283
  );
232
284
  });
@@ -234,7 +286,7 @@ describe("resolveRouterPrompt — override path", () => {
234
286
  test("override that is a directory falls back to bundled prompt", () => {
235
287
  // Pass the temp directory itself as the override path — lstat sees a
236
288
  // directory, not a regular file, so the loader bails to bundled.
237
- expect(resolveRouterPrompt(tmpDir, STD_OPTS)).toEqual(
289
+ expect(resolveRouterPrompt(tmpDir, tmpDir, STD_OPTS)).toEqual(
238
290
  renderRouterPrompt(STD_OPTS),
239
291
  );
240
292
  });
@@ -247,7 +299,7 @@ describe("resolveRouterPrompt — override path", () => {
247
299
  "utf-8",
248
300
  );
249
301
 
250
- const out = resolveRouterPrompt(overridePath, {
302
+ const out = resolveRouterPrompt(overridePath, tmpDir, {
251
303
  assistantName: null,
252
304
  userName: null,
253
305
  pageIndexBlock: "",
@@ -121,6 +121,9 @@ const state = {
121
121
  // Throw queue for upsert: first call shifts and throws if non-null;
122
122
  // subsequent calls succeed once the queue is exhausted.
123
123
  upsertThrowQueue: [] as Array<Error | null>,
124
+ // Throw queue for createPayloadIndex: each entry maps to the next call,
125
+ // so tests can simulate index-creation failures (strict-mode, network).
126
+ createIndexThrowQueue: [] as Array<Error | null>,
124
127
  };
125
128
 
126
129
  class MockQdrantClient {
@@ -157,6 +160,10 @@ class MockQdrantClient {
157
160
  params: { field_name: string; field_schema: string },
158
161
  ) {
159
162
  state.createIndexCalls.push(params);
163
+ if (state.createIndexThrowQueue.length > 0) {
164
+ const next = state.createIndexThrowQueue.shift();
165
+ if (next) throw next;
166
+ }
160
167
  return {};
161
168
  }
162
169
  async upsert(_name: string, params: { wait: boolean; points: MockPoint[] }) {
@@ -228,6 +235,7 @@ function resetState(): void {
228
235
  state.countThrows = null;
229
236
  state.countCalls = 0;
230
237
  state.upsertThrowQueue.length = 0;
238
+ state.createIndexThrowQueue.length = 0;
231
239
  _resetMemoryV2QdrantForTests();
232
240
  // Drop any sentinel a prior test left behind so the no-drift default path
233
241
  // doesn't accidentally report `migrated: true`.
@@ -278,9 +286,10 @@ describe("memory v2 qdrant — collection lifecycle", () => {
278
286
  });
279
287
  expect(params.on_disk_payload).toBe(true);
280
288
 
281
- // Slug payload index is created up front.
289
+ // Slug + kind payload indexes are created up front.
282
290
  expect(state.createIndexCalls).toEqual([
283
291
  { field_name: "slug", field_schema: "keyword" },
292
+ { field_name: "kind", field_schema: "keyword" },
284
293
  ]);
285
294
  });
286
295
 
@@ -295,9 +304,14 @@ describe("memory v2 qdrant — collection lifecycle", () => {
295
304
  await ensureConceptPageCollection();
296
305
 
297
306
  // Existence check fired exactly once thanks to the in-memory readiness
298
- // cache; createCollection / createPayloadIndex never ran.
307
+ // cache; createCollection never ran. Payload indexes are (idempotently)
308
+ // ensured on the existing-collection path to backfill long-lived installs
309
+ // that predate the `kind` index.
299
310
  expect(state.createCollectionCalls).toBe(0);
300
- expect(state.createIndexCalls).toEqual([]);
311
+ expect(state.createIndexCalls).toEqual([
312
+ { field_name: "slug", field_schema: "keyword" },
313
+ { field_name: "kind", field_schema: "keyword" },
314
+ ]);
301
315
  expect(state.collectionExistsCalls).toBe(1);
302
316
  });
303
317
 
@@ -314,6 +328,7 @@ describe("memory v2 qdrant — collection lifecycle", () => {
314
328
  expect(state.createCollectionCalls).toBe(1);
315
329
  expect(state.createIndexCalls).toEqual([
316
330
  { field_name: "slug", field_schema: "keyword" },
331
+ { field_name: "kind", field_schema: "keyword" },
317
332
  ]);
318
333
  });
319
334
 
@@ -433,6 +448,54 @@ describe("memory v2 qdrant — collection lifecycle", () => {
433
448
  expect(existsSync(REEMBED_SENTINEL_PATH)).toBe(false);
434
449
  });
435
450
 
451
+ test("swallows 'already exists' on createPayloadIndex but propagates other failures without latching readiness", async () => {
452
+ // Existing collection that already has the full schema — the ensure
453
+ // path goes through `ensurePayloadIndexes` to backfill long-lived
454
+ // installs. The first index call hits an "already exists" race
455
+ // (benign; swallow); the second hits a strict-mode rejection (must
456
+ // propagate so readiness is not latched).
457
+ state.collectionExistsBeforeCreate = true;
458
+ state.createIndexThrowQueue.push(
459
+ Object.assign(
460
+ new Error("Wrong input: Payload field 'slug' already exists"),
461
+ { status: 400 },
462
+ ),
463
+ Object.assign(
464
+ new Error(
465
+ "Strict mode prohibits creating payload indexes on this deployment",
466
+ ),
467
+ { status: 400 },
468
+ ),
469
+ );
470
+
471
+ let caught: unknown = null;
472
+ try {
473
+ await ensureConceptPageCollection();
474
+ } catch (err) {
475
+ caught = err;
476
+ }
477
+ expect((caught as Error | null)?.message).toMatch(/strict mode/i);
478
+
479
+ // Both attempts ran; the strict-mode failure was not swallowed.
480
+ expect(state.createIndexCalls).toEqual([
481
+ { field_name: "slug", field_schema: "keyword" },
482
+ { field_name: "kind", field_schema: "keyword" },
483
+ ]);
484
+
485
+ // Readiness must NOT be latched after a non-benign failure — otherwise
486
+ // later slug/kind-filtered queries (e.g. skill backfill) would keep
487
+ // failing until a daemon restart. A follow-up ensure must retry.
488
+ const result = await ensureConceptPageCollection();
489
+ expect(result).toEqual({ migrated: false });
490
+ // Indexes attempted again on the retry (no throws queued this time).
491
+ expect(state.createIndexCalls).toEqual([
492
+ { field_name: "slug", field_schema: "keyword" },
493
+ { field_name: "kind", field_schema: "keyword" },
494
+ { field_name: "slug", field_schema: "keyword" },
495
+ { field_name: "kind", field_schema: "keyword" },
496
+ ]);
497
+ });
498
+
436
499
  test("concurrent ensure during a schema rebuild only deletes/creates once", async () => {
437
500
  state.collectionExistsBeforeCreate = true;
438
501
  state.getCollectionInfo = {
@@ -460,6 +460,21 @@ describe("runRouter — failure modes", () => {
460
460
  expect(warnSeen).toBe(true);
461
461
  });
462
462
 
463
+ test("duplicate-heavy IDs are deduped before the cap is applied", async () => {
464
+ // [1, 1, 2] with max=2 must yield two distinct slugs, not collapse to one
465
+ // after a pre-dedupe slice trims away the only other unique ID.
466
+ providerStub = makeProvider(toolUseResponse([1, 1, 2]));
467
+
468
+ const result = await runRouter({
469
+ workspaceDir,
470
+ ...COMMON_PARAMS,
471
+ config: makeConfig({ maxPageIds: 2 }),
472
+ });
473
+
474
+ expect(result.failureReason).toBeNull();
475
+ expect(result.selectedSlugs).toEqual(["alpha", "bravo"]);
476
+ });
477
+
463
478
  test("more than max_page_ids → truncated with warn", async () => {
464
479
  providerStub = makeProvider(toolUseResponse([1, 2, 3]));
465
480