@vellumai/assistant 0.8.1 → 0.8.3

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 (630) hide show
  1. package/ARCHITECTURE.md +13 -19
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +17 -0
  5. package/docker-init-apt-root.sh +167 -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 +642 -5
  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-loop-exit-reason.test.ts +272 -0
  21. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  23. package/src/__tests__/anthropic-provider.test.ts +45 -0
  24. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  25. package/src/__tests__/app-executors.test.ts +220 -4
  26. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  27. package/src/__tests__/bundled-asset.test.ts +6 -6
  28. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  29. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  30. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  31. package/src/__tests__/clawhub.test.ts +75 -16
  32. package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
  33. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  34. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  35. package/src/__tests__/config-schema.test.ts +21 -0
  36. package/src/__tests__/config-set-route.test.ts +80 -0
  37. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  38. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  39. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  40. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  41. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  42. package/src/__tests__/context-token-estimator.test.ts +31 -65
  43. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  44. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  45. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  46. package/src/__tests__/conversation-agent-loop.test.ts +59 -1
  47. package/src/__tests__/conversation-error.test.ts +42 -3
  48. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  49. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  50. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  51. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  52. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  53. package/src/__tests__/conversation-pairing.test.ts +54 -0
  54. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  55. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  56. package/src/__tests__/conversation-queue.test.ts +4 -1
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
  58. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  59. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  60. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  61. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  64. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  65. package/src/__tests__/date-context.test.ts +45 -0
  66. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  67. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  68. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  69. package/src/__tests__/dm-backfill.test.ts +121 -10
  70. package/src/__tests__/document-tool-security.test.ts +258 -0
  71. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  72. package/src/__tests__/edit-propagation.test.ts +33 -0
  73. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  74. package/src/__tests__/external-plugin-loader.test.ts +151 -55
  75. package/src/__tests__/filing-service.test.ts +140 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  77. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  78. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  79. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  80. package/src/__tests__/heartbeat-service.test.ts +24 -164
  81. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  82. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  83. package/src/__tests__/helpers/wait-for.ts +21 -0
  84. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  85. package/src/__tests__/history-repair.test.ts +73 -0
  86. package/src/__tests__/host-app-control-proxy.test.ts +507 -10
  87. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  88. package/src/__tests__/image-credentials.test.ts +1 -1
  89. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  90. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  91. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  92. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  93. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  94. package/src/__tests__/injector-background-turn.test.ts +153 -0
  95. package/src/__tests__/injector-chain.test.ts +15 -8
  96. package/src/__tests__/install-skill-routing.test.ts +155 -37
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
  98. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  99. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  100. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  101. package/src/__tests__/llm-catalog-parity.test.ts +58 -13
  102. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  103. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  104. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
  105. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  106. package/src/__tests__/llm-resolver.test.ts +255 -2
  107. package/src/__tests__/llm-usage-store.test.ts +114 -0
  108. package/src/__tests__/managed-profile-guard.test.ts +41 -29
  109. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  110. package/src/__tests__/managed-store.test.ts +84 -192
  111. package/src/__tests__/media-generate-image.test.ts +1 -1
  112. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  113. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  114. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  115. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  116. package/src/__tests__/notification-deep-link.test.ts +15 -0
  117. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  118. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  119. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  120. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  121. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  122. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  123. package/src/__tests__/openai-provider.test.ts +242 -3
  124. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  125. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  126. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  127. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  128. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  129. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
  130. package/src/__tests__/platform.test.ts +2 -0
  131. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  132. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  133. package/src/__tests__/plugin-external-api.test.ts +68 -0
  134. package/src/__tests__/plugin-registry.test.ts +0 -77
  135. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  136. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  137. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  138. package/src/__tests__/plugin-types.test.ts +3 -13
  139. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  140. package/src/__tests__/process-message-display-content.test.ts +421 -0
  141. package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  143. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
  144. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  145. package/src/__tests__/schedule-routes.test.ts +50 -3
  146. package/src/__tests__/schedule-store.test.ts +94 -0
  147. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  148. package/src/__tests__/schema-transforms.test.ts +20 -0
  149. package/src/__tests__/search-skills-unified.test.ts +0 -5
  150. package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
  151. package/src/__tests__/server-history-render.test.ts +43 -0
  152. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  153. package/src/__tests__/skill-load-tool.test.ts +27 -89
  154. package/src/__tests__/skill-memory.test.ts +23 -3
  155. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  156. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  157. package/src/__tests__/skills-install-extract.test.ts +49 -38
  158. package/src/__tests__/skills-install-staging.test.ts +159 -0
  159. package/src/__tests__/skills-uninstall.test.ts +9 -41
  160. package/src/__tests__/skills.test.ts +51 -58
  161. package/src/__tests__/slack-channel-config.test.ts +9 -0
  162. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  163. package/src/__tests__/system-prompt.test.ts +670 -63
  164. package/src/__tests__/terminal-tools.test.ts +28 -1
  165. package/src/__tests__/thread-backfill.test.ts +557 -27
  166. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  167. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  168. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  169. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  170. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  171. package/src/__tests__/tool-executor.test.ts +16 -4
  172. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  173. package/src/__tests__/turn-events-store.test.ts +256 -0
  174. package/src/__tests__/twilio-routes.test.ts +4 -0
  175. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  176. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  177. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  178. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  179. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  180. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  181. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  182. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  183. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  184. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  185. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  186. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  187. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  188. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  189. package/src/a2a/__tests__/task-store.test.ts +246 -0
  190. package/src/a2a/agent-card.ts +58 -0
  191. package/src/a2a/feature-gate.ts +8 -0
  192. package/src/a2a/protocol-constants.ts +21 -0
  193. package/src/a2a/protocol-errors.ts +50 -0
  194. package/src/a2a/protocol-types.ts +162 -0
  195. package/src/a2a/task-store.ts +168 -0
  196. package/src/acp/resolve-agent.ts +1 -1
  197. package/src/agent/image-optimize.ts +13 -5
  198. package/src/agent/loop.ts +167 -18
  199. package/src/calls/voice-session-bridge.ts +61 -42
  200. package/src/channels/config.ts +9 -0
  201. package/src/channels/types.ts +122 -0
  202. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  203. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  204. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  205. package/src/cli/commands/__tests__/schedules.test.ts +960 -0
  206. package/src/cli/commands/changelog.ts +106 -42
  207. package/src/cli/commands/conversations.ts +102 -17
  208. package/src/cli/commands/default-action.ts +10 -53
  209. package/src/cli/commands/notifications.ts +388 -346
  210. package/src/cli/commands/plugins.ts +252 -0
  211. package/src/cli/commands/schedules.ts +683 -0
  212. package/src/cli/commands/telemetry.ts +40 -0
  213. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  214. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  215. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  216. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  217. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  218. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  219. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  220. package/src/cli/lib/cli-colors.ts +12 -0
  221. package/src/cli/lib/confirm-prompt.ts +79 -0
  222. package/src/cli/lib/install-from-github.ts +303 -0
  223. package/src/cli/lib/list-installed-plugins.ts +137 -0
  224. package/src/cli/lib/search-plugins.ts +163 -0
  225. package/src/cli/lib/uninstall-plugin.ts +82 -0
  226. package/src/cli/lib/unknown-command.ts +111 -0
  227. package/src/cli/program.ts +52 -2
  228. package/src/config/assistant-feature-flags.ts +24 -54
  229. package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
  230. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  231. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  232. package/src/config/bundled-skills/document/SKILL.md +23 -3
  233. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  234. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  235. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  236. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  237. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  238. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  239. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  240. package/src/config/bundled-tool-registry.ts +6 -0
  241. package/src/config/call-site-defaults.ts +105 -0
  242. package/src/config/feature-flag-registry.json +41 -9
  243. package/src/config/llm-resolver.ts +52 -1
  244. package/src/config/loader.ts +64 -38
  245. package/src/config/schema.ts +9 -10
  246. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  247. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  248. package/src/config/schemas/channels.ts +17 -0
  249. package/src/config/schemas/compaction.ts +28 -0
  250. package/src/config/schemas/conversations.ts +10 -0
  251. package/src/config/schemas/heartbeat.ts +23 -0
  252. package/src/config/schemas/llm-request-logs.ts +31 -7
  253. package/src/config/schemas/llm.ts +1 -0
  254. package/src/config/schemas/memory-retrieval.ts +18 -0
  255. package/src/config/schemas/memory-retrospective.ts +1 -1
  256. package/src/config/schemas/memory-v2.ts +4 -4
  257. package/src/config/schemas/memory.ts +3 -1
  258. package/src/config/schemas/tools.ts +14 -0
  259. package/src/config/seed-inference-profiles.ts +99 -29
  260. package/src/config/skills.ts +3 -96
  261. package/src/context/compactor.ts +1107 -0
  262. package/src/context/token-estimator.ts +34 -36
  263. package/src/context/window-manager.ts +197 -1520
  264. package/src/credential-execution/managed-catalog.ts +37 -0
  265. package/src/credential-health/credential-health-service.ts +280 -19
  266. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
  267. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  268. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  269. package/src/daemon/approval-generators.ts +8 -6
  270. package/src/daemon/config-watcher.ts +94 -31
  271. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  272. package/src/daemon/conversation-agent-loop.ts +198 -11
  273. package/src/daemon/conversation-error.ts +171 -37
  274. package/src/daemon/conversation-lifecycle.ts +53 -40
  275. package/src/daemon/conversation-messaging.ts +25 -6
  276. package/src/daemon/conversation-process.ts +49 -12
  277. package/src/daemon/conversation-runtime-assembly.ts +25 -1
  278. package/src/daemon/conversation-slash.ts +12 -5
  279. package/src/daemon/conversation-store.ts +11 -4
  280. package/src/daemon/conversation-tool-setup.ts +39 -7
  281. package/src/daemon/conversation.ts +33 -8
  282. package/src/daemon/date-context.ts +40 -0
  283. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  284. package/src/daemon/first-greeting.ts +22 -2
  285. package/src/daemon/guardian-action-generators.ts +1 -125
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  287. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  288. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  289. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  290. package/src/daemon/handlers/config-a2a.ts +289 -0
  291. package/src/daemon/handlers/config-model.ts +6 -5
  292. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  293. package/src/daemon/handlers/conversations.ts +1 -0
  294. package/src/daemon/handlers/shared.ts +14 -5
  295. package/src/daemon/handlers/skills.ts +111 -108
  296. package/src/daemon/history-repair.ts +28 -1
  297. package/src/daemon/host-app-control-proxy.ts +153 -27
  298. package/src/daemon/host-proxy-preactivation.ts +85 -18
  299. package/src/daemon/lifecycle.ts +89 -91
  300. package/src/daemon/meet-host-supervisor.ts +5 -4
  301. package/src/daemon/memory-v2-startup.ts +85 -0
  302. package/src/daemon/message-protocol.ts +1 -0
  303. package/src/daemon/message-types/conversations.ts +25 -0
  304. package/src/daemon/message-types/messages.ts +61 -0
  305. package/src/daemon/message-types/notifications.ts +21 -0
  306. package/src/daemon/message-types/subagents.ts +1 -0
  307. package/src/daemon/message-types/sync.ts +1 -0
  308. package/src/daemon/pkb-reminder-builder.test.ts +11 -54
  309. package/src/daemon/pkb-reminder-builder.ts +5 -20
  310. package/src/daemon/plugin-source-watcher.ts +146 -0
  311. package/src/daemon/process-message.ts +24 -3
  312. package/src/daemon/server.ts +11 -2
  313. package/src/daemon/skill-memory-refresh.ts +33 -0
  314. package/src/daemon/wake-target-adapter.ts +2 -0
  315. package/src/documents/document-store.ts +221 -3
  316. package/src/embedded/plugin-api.ts +40 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  318. package/src/export/transcript-formatter.ts +54 -20
  319. package/src/filing/filing-service.ts +39 -0
  320. package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
  321. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  322. package/src/heartbeat/heartbeat-service.ts +73 -189
  323. package/src/home/__tests__/feed-types.test.ts +80 -0
  324. package/src/home/feed-types.ts +36 -2
  325. package/src/home/post-connect-feed.ts +1 -0
  326. package/src/index.ts +18 -1
  327. package/src/ipc/cli-client.ts +147 -45
  328. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  329. package/src/mcp/client.ts +20 -4
  330. package/src/media/image-credentials.ts +3 -3
  331. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  332. package/src/memory/__tests__/conversation-queries.test.ts +483 -0
  333. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  334. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  335. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  336. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  337. package/src/memory/__tests__/message-content.test.ts +35 -0
  338. package/src/memory/bookmark-crud.ts +42 -10
  339. package/src/memory/context-search/sources/conversations.ts +62 -2
  340. package/src/memory/context-search/sources/workspace.ts +4 -0
  341. package/src/memory/conversation-crud.ts +63 -19
  342. package/src/memory/conversation-queries.ts +197 -11
  343. package/src/memory/conversation-title-service.ts +26 -4
  344. package/src/memory/db-init.ts +12 -0
  345. package/src/memory/delivery-crud.ts +152 -5
  346. package/src/memory/embedding-backend.ts +4 -4
  347. package/src/memory/external-conversation-store.ts +66 -5
  348. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
  349. package/src/memory/graph/conversation-graph-memory.ts +49 -21
  350. package/src/memory/graph/tools.ts +9 -40
  351. package/src/memory/indexer.ts +34 -29
  352. package/src/memory/invite-store.ts +53 -0
  353. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  354. package/src/memory/jobs/embed-concept-page.ts +20 -11
  355. package/src/memory/jobs-worker.ts +6 -1
  356. package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
  357. package/src/memory/llm-request-log-source.ts +19 -52
  358. package/src/memory/llm-request-log-store.ts +92 -1
  359. package/src/memory/llm-usage-store.ts +125 -5
  360. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  361. package/src/memory/memory-retrospective-job.ts +33 -6
  362. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  363. package/src/memory/message-content.ts +1 -1
  364. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  365. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  366. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  367. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  368. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  369. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  370. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  371. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  372. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  373. package/src/memory/migrations/index.ts +9 -0
  374. package/src/memory/migrations/registry.ts +16 -0
  375. package/src/memory/onboarding-events-store.ts +106 -0
  376. package/src/memory/schema/a2a.ts +15 -0
  377. package/src/memory/schema/bookmarks.ts +0 -2
  378. package/src/memory/schema/calls.ts +1 -0
  379. package/src/memory/schema/index.ts +1 -0
  380. package/src/memory/schema/inference.ts +3 -3
  381. package/src/memory/schema/infrastructure.ts +13 -0
  382. package/src/memory/turn-events-store.ts +127 -2
  383. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  384. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  385. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  386. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  387. package/src/memory/v2/__tests__/injection.test.ts +288 -11
  388. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  389. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  390. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  391. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  392. package/src/memory/v2/__tests__/router.test.ts +15 -0
  393. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  394. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  395. package/src/memory/v2/activation-store.ts +14 -16
  396. package/src/memory/v2/cli-command-content.ts +19 -0
  397. package/src/memory/v2/cli-command-store.ts +304 -0
  398. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  399. package/src/memory/v2/injection.ts +81 -26
  400. package/src/memory/v2/migration.ts +49 -19
  401. package/src/memory/v2/page-index.ts +63 -8
  402. package/src/memory/v2/prompts/router.ts +11 -8
  403. package/src/memory/v2/prompts/sweep.ts +2 -2
  404. package/src/memory/v2/qdrant.ts +135 -7
  405. package/src/memory/v2/router.ts +9 -8
  406. package/src/memory/v2/skill-store.ts +120 -35
  407. package/src/memory/v2/static-context.ts +4 -4
  408. package/src/memory/v2/types.ts +23 -0
  409. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  410. package/src/messaging/providers/a2a/deliver.ts +156 -0
  411. package/src/messaging/providers/gmail/client.ts +9 -2
  412. package/src/messaging/providers/index.ts +11 -2
  413. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  414. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  415. package/src/messaging/providers/slack/adapter.ts +43 -5
  416. package/src/messaging/providers/slack/client.ts +27 -0
  417. package/src/messaging/providers/slack/deep-link.ts +65 -0
  418. package/src/messaging/providers/slack/download.ts +104 -0
  419. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  420. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  421. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  422. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  423. package/src/messaging/providers/slack/types.ts +20 -1
  424. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  425. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  426. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  427. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  428. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  429. package/src/notifications/adapters/macos.ts +12 -2
  430. package/src/notifications/broadcaster.ts +29 -4
  431. package/src/notifications/conversation-pairing.ts +2 -1
  432. package/src/notifications/copy-composer.ts +17 -64
  433. package/src/notifications/decision-engine.ts +113 -45
  434. package/src/notifications/deterministic-checks.ts +96 -0
  435. package/src/notifications/emit-signal.ts +21 -1
  436. package/src/notifications/home-feed-side-effect.ts +138 -5
  437. package/src/notifications/signal.ts +3 -5
  438. package/src/notifications/types.ts +8 -0
  439. package/src/oauth/connection-resolver.ts +8 -4
  440. package/src/oauth/platform-connection.test.ts +43 -3
  441. package/src/oauth/platform-connection.ts +19 -6
  442. package/src/oauth/seed-providers.ts +10 -1
  443. package/src/permissions/checker.ts +2 -0
  444. package/src/permissions/ipc-risk-types.ts +1 -0
  445. package/src/permissions/question-prompter.test.ts +416 -0
  446. package/src/permissions/question-prompter.ts +294 -0
  447. package/src/platform/client.test.ts +1 -1
  448. package/src/platform/client.ts +1 -1
  449. package/src/plugin-api/constants.ts +26 -0
  450. package/src/plugin-api/index.ts +34 -1
  451. package/src/plugin-api/types.ts +104 -22
  452. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  453. package/src/plugins/defaults/compaction.ts +0 -4
  454. package/src/plugins/defaults/empty-response.ts +0 -2
  455. package/src/plugins/defaults/history-repair.ts +0 -2
  456. package/src/plugins/defaults/injectors.ts +74 -22
  457. package/src/plugins/defaults/llm-call.ts +0 -2
  458. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  459. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  460. package/src/plugins/defaults/persistence.ts +0 -2
  461. package/src/plugins/defaults/title-generate.ts +0 -5
  462. package/src/plugins/defaults/token-estimate.ts +0 -2
  463. package/src/plugins/defaults/tool-error.ts +0 -7
  464. package/src/plugins/defaults/tool-execute.ts +0 -2
  465. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  466. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  467. package/src/plugins/external-api.ts +104 -0
  468. package/src/plugins/external-plugin-loader.ts +187 -42
  469. package/src/plugins/feature-gate.ts +22 -0
  470. package/src/plugins/pipeline.ts +37 -0
  471. package/src/plugins/registry.ts +48 -80
  472. package/src/plugins/types.ts +40 -26
  473. package/src/plugins/user-loader.ts +21 -2
  474. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  475. package/src/proactive-artifact/job.test.ts +37 -5
  476. package/src/prompts/__tests__/system-prompt.test.ts +10 -43
  477. package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
  478. package/src/prompts/normalize-onboarding.ts +27 -0
  479. package/src/prompts/sections.ts +302 -0
  480. package/src/prompts/system-prompt.ts +63 -174
  481. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  482. package/src/prompts/templates/system-sections.ts +164 -0
  483. package/src/providers/__tests__/inference.test.ts +24 -7
  484. package/src/providers/anthropic/client.ts +28 -28
  485. package/src/providers/call-site-routing.ts +24 -6
  486. package/src/providers/connection-resolution.ts +68 -11
  487. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  488. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  489. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  490. package/src/providers/inference/adapter-factory.ts +32 -6
  491. package/src/providers/inference/auth.ts +12 -0
  492. package/src/providers/inference/backfill.ts +14 -1
  493. package/src/providers/inference/connections.ts +159 -34
  494. package/src/providers/inference/resolve-auth.ts +14 -4
  495. package/src/providers/model-catalog.ts +249 -12
  496. package/src/providers/model-intents.ts +3 -3
  497. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  498. package/src/providers/openai/chat-completions-provider.ts +169 -8
  499. package/src/providers/openrouter/client.ts +49 -4
  500. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
  501. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  502. package/src/providers/provider-availability.ts +17 -2
  503. package/src/providers/provider-catalog-visibility.ts +38 -0
  504. package/src/providers/provider-send-message.ts +27 -12
  505. package/src/providers/registry.ts +52 -15
  506. package/src/providers/retry.ts +47 -1
  507. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  508. package/src/runtime/agent-wake.ts +103 -15
  509. package/src/runtime/auth/route-policy.ts +21 -1
  510. package/src/runtime/btw-sidechain.ts +2 -0
  511. package/src/runtime/http-server.ts +7 -16
  512. package/src/runtime/http-types.ts +19 -47
  513. package/src/runtime/migrations/origin-mode.ts +1 -1
  514. package/src/runtime/pending-interactions.ts +1 -0
  515. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  516. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  517. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  518. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
  519. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  520. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  521. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  522. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  523. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  524. package/src/runtime/routes/acp-routes.ts +5 -3
  525. package/src/runtime/routes/auth-routes.ts +1 -1
  526. package/src/runtime/routes/bookmark-routes.ts +5 -3
  527. package/src/runtime/routes/btw-routes.ts +5 -1
  528. package/src/runtime/routes/channel-availability-routes.ts +126 -0
  529. package/src/runtime/routes/consolidation-routes.ts +100 -0
  530. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  531. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  532. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  533. package/src/runtime/routes/conversation-query-routes.ts +99 -35
  534. package/src/runtime/routes/conversation-routes.ts +97 -11
  535. package/src/runtime/routes/documents-routes.ts +25 -86
  536. package/src/runtime/routes/group-routes.ts +5 -0
  537. package/src/runtime/routes/inbound-conversation.ts +28 -8
  538. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  539. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  540. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  541. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  542. package/src/runtime/routes/index.ts +8 -0
  543. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  544. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  545. package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
  546. package/src/runtime/routes/integrations/a2a.ts +235 -0
  547. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  548. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  549. package/src/runtime/routes/integrations/twilio.ts +6 -13
  550. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  551. package/src/runtime/routes/notification-routes.ts +1 -1
  552. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  553. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  554. package/src/runtime/routes/question-routes.ts +259 -0
  555. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  556. package/src/runtime/routes/schedule-routes.ts +4 -7
  557. package/src/runtime/routes/subagents-routes.ts +98 -18
  558. package/src/runtime/routes/telemetry-routes.ts +27 -0
  559. package/src/runtime/routes/tts-routes.ts +27 -2
  560. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  561. package/src/runtime/routes/workspace-routes.ts +28 -0
  562. package/src/runtime/services/conversation-serializer.ts +39 -7
  563. package/src/runtime/sync/resource-sync-events.ts +93 -1
  564. package/src/schedule/schedule-store.ts +27 -2
  565. package/src/schedule/scheduler.ts +9 -1
  566. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  567. package/src/security/untrusted-content.ts +93 -8
  568. package/src/skills/catalog-files.ts +1 -1
  569. package/src/skills/catalog-install.ts +233 -116
  570. package/src/skills/clawhub.ts +70 -13
  571. package/src/skills/managed-store.ts +4 -119
  572. package/src/skills/skillssh-registry.ts +27 -48
  573. package/src/subagent/manager.ts +17 -7
  574. package/src/telemetry/types.ts +113 -1
  575. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  576. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  577. package/src/tools/apps/executors.ts +58 -7
  578. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  579. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  580. package/src/tools/browser/browser-execution.ts +15 -11
  581. package/src/tools/computer-use/definitions.ts +3 -3
  582. package/src/tools/credentials/vault.ts +1 -1
  583. package/src/tools/document/document-tool.ts +124 -1
  584. package/src/tools/filesystem/edit.ts +1 -1
  585. package/src/tools/filesystem/list.ts +1 -1
  586. package/src/tools/filesystem/read.ts +1 -1
  587. package/src/tools/filesystem/write.ts +5 -2
  588. package/src/tools/host-filesystem/transfer.ts +1 -1
  589. package/src/tools/host-terminal/host-shell.ts +1 -1
  590. package/src/tools/memory/register.ts +1 -9
  591. package/src/tools/permission-checker.ts +1 -1
  592. package/src/tools/registry.ts +17 -7
  593. package/src/tools/schedule/create.ts +2 -2
  594. package/src/tools/schema-transforms.ts +7 -2
  595. package/src/tools/side-effects.ts +1 -0
  596. package/src/tools/skills/delete-managed.ts +4 -4
  597. package/src/tools/skills/execute.ts +1 -1
  598. package/src/tools/skills/scaffold-managed.ts +3 -2
  599. package/src/tools/subagent/notify-parent.ts +1 -1
  600. package/src/tools/system/request-permission.ts +2 -2
  601. package/src/tools/terminal/safe-env.ts +60 -1
  602. package/src/tools/tool-manifest.ts +2 -0
  603. package/src/tools/types.ts +107 -21
  604. package/src/tools/ui-surface/definitions.ts +6 -5
  605. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  606. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  607. package/src/types/onboarding-context.ts +2 -0
  608. package/src/util/errors.ts +17 -0
  609. package/src/util/platform.ts +10 -0
  610. package/src/watcher/__tests__/engine.test.ts +22 -0
  611. package/src/watcher/engine.ts +6 -2
  612. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  613. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  614. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  615. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  616. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  617. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  618. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  619. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  620. package/src/workspace/migrations/registry.ts +10 -0
  621. package/src/workspace/migrations/runner.ts +39 -9
  622. package/src/workspace/migrations/types.ts +4 -0
  623. package/examples/plugins/echo/bun.lock +0 -25
  624. package/src/__tests__/context-window-manager.test.ts +0 -2481
  625. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  626. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  627. package/src/context/prompts/compact.md +0 -26
  628. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  629. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  630. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -643,12 +643,15 @@ export async function runMemoryV2Migration(
643
643
  }
644
644
 
645
645
  /**
646
- * HTML marker embedded in each appended block so a crash-recovery rerun can
647
- * detect already-applied promotions and skip the append. `appendLines` is a
648
- * read-modify-write without this, a crash between `appendPromotions` and
649
- * `writeSentinel` would let the next boot duplicate every promotion line.
646
+ * Paired HTML markers wrapped around each appended block. The opening marker
647
+ * also serves as the idempotency guard: `appendLines` is a read-modify-write,
648
+ * and without it a crash between `appendPromotions` and `writeSentinel` would
649
+ * let the next boot duplicate every promotion line. The closing marker
650
+ * delimits the migration-inserted region so a force-rerun strip can remove
651
+ * exactly that block without touching user/assistant edits appended below.
650
652
  */
651
- const PROMOTION_MARKER = "<!-- migration:v1-to-v2 -->";
653
+ const PROMOTION_MARKER_OPEN = "<!-- migration:v1-to-v2 -->";
654
+ const PROMOTION_MARKER_CLOSE = "<!-- /migration:v1-to-v2 -->";
652
655
 
653
656
  /**
654
657
  * Append each promotion bucket to its target file. Files are created if
@@ -680,8 +683,8 @@ async function appendPromotions(
680
683
 
681
684
  /**
682
685
  * Append `lines` to `path`, creating it (with a trailing newline) if absent.
683
- * If the file already contains `PROMOTION_MARKER`, the append is skipped — a
684
- * prior partially-completed migration already wrote this block.
686
+ * If the file already contains `PROMOTION_MARKER_OPEN`, the append is skipped
687
+ * — a prior partially-completed migration already wrote this block.
685
688
  */
686
689
  async function appendLines(path: string, lines: string[]): Promise<void> {
687
690
  let existing = "";
@@ -690,16 +693,17 @@ async function appendLines(path: string, lines: string[]): Promise<void> {
690
693
  } catch (err) {
691
694
  if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
692
695
  }
693
- if (existing.includes(PROMOTION_MARKER)) return;
696
+ if (existing.includes(PROMOTION_MARKER_OPEN)) return;
694
697
  const trailing = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
695
- const next = `${existing}${trailing}${PROMOTION_MARKER}\n${lines.join("\n")}\n`;
698
+ const block = `${PROMOTION_MARKER_OPEN}\n${lines.join("\n")}\n${PROMOTION_MARKER_CLOSE}\n`;
699
+ const next = `${existing}${trailing}${block}`;
696
700
  await writeFile(path, next, "utf-8");
697
701
  }
698
702
 
699
703
  /**
700
- * Strip any prior migration-block (everything from the marker line through
701
- * end of file) from each promotion target. Called on force-reruns so the
702
- * marker guard in `appendLines` doesn't skip the new promotions.
704
+ * Strip any prior migration-block from each promotion target. Called on
705
+ * force-reruns so the marker guard in `appendLines` doesn't skip the new
706
+ * promotions.
703
707
  */
704
708
  async function stripPromotionMarkerBlocks(workspaceDir: string): Promise<void> {
705
709
  const memoryDir = join(workspaceDir, "memory");
@@ -718,6 +722,16 @@ async function stripPromotionMarkerBlocks(workspaceDir: string): Promise<void> {
718
722
  await Promise.all(candidates.map(stripMarkerBlock));
719
723
  }
720
724
 
725
+ /**
726
+ * Remove the migration-inserted block from `path` while preserving content
727
+ * outside it. The block is identified by the
728
+ * `PROMOTION_MARKER_OPEN ... PROMOTION_MARKER_CLOSE` envelope.
729
+ *
730
+ * If an opening marker is present without a matching close, strip from the
731
+ * opening marker to the next blank line, or to EOF if none is found. Content
732
+ * appended after such an unclosed block without a blank-line separator can
733
+ * be dropped on that fallback path.
734
+ */
721
735
  async function stripMarkerBlock(path: string): Promise<void> {
722
736
  let existing: string;
723
737
  try {
@@ -726,13 +740,29 @@ async function stripMarkerBlock(path: string): Promise<void> {
726
740
  if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
727
741
  throw err;
728
742
  }
729
- const idx = existing.indexOf(PROMOTION_MARKER);
730
- if (idx === -1) return;
731
- // Cut from the start of the marker line. `idx` already points at the marker,
732
- // which `appendLines` always wrote at the start of its own line, so a plain
733
- // slice here also drops the leading newline that preceded it (if any).
734
- const trimmed = existing.slice(0, idx).replace(/\n+$/, "");
735
- const next = trimmed.length === 0 ? "" : `${trimmed}\n`;
743
+ const openIdx = existing.indexOf(PROMOTION_MARKER_OPEN);
744
+ if (openIdx === -1) return;
745
+
746
+ let endIdx: number;
747
+ const closeIdx = existing.indexOf(PROMOTION_MARKER_CLOSE, openIdx);
748
+ if (closeIdx !== -1) {
749
+ endIdx = closeIdx + PROMOTION_MARKER_CLOSE.length;
750
+ if (existing[endIdx] === "\n") endIdx += 1;
751
+ } else {
752
+ const blankIdx = existing.indexOf("\n\n", openIdx);
753
+ endIdx = blankIdx === -1 ? existing.length : blankIdx + 2;
754
+ }
755
+
756
+ const head = existing.slice(0, openIdx).replace(/\n+$/, "");
757
+ const tail = existing.slice(endIdx);
758
+ let next: string;
759
+ if (head.length === 0) {
760
+ next = tail;
761
+ } else if (tail.length === 0) {
762
+ next = `${head}\n`;
763
+ } else {
764
+ next = `${head}\n${tail}`;
765
+ }
736
766
  await writeFile(path, next, "utf-8");
737
767
  }
738
768
 
@@ -30,6 +30,16 @@ const log = getLogger("memory-v2-page-index");
30
30
 
31
31
  const SUMMARY_MAX_LENGTH = 200;
32
32
 
33
+ /**
34
+ * Collapse every run of whitespace (including embedded newlines and tabs) to a
35
+ * single space and trim. The router prompt renders one entry per line, so an
36
+ * embedded newline anywhere in `summary` would split that entry across lines
37
+ * and corrupt the format the router parses.
38
+ */
39
+ function normalizeSummary(raw: string): string {
40
+ return raw.replace(/\s+/g, " ").trim().slice(0, SUMMARY_MAX_LENGTH);
41
+ }
42
+
33
43
  /**
34
44
  * One row in the rendered page index. `id` is a 1-based dense integer that is
35
45
  * stable within a single index version (i.e. a single build). It changes when
@@ -70,11 +80,12 @@ let cache: CachedIndex | null = null;
70
80
  /**
71
81
  * Return a `PageIndex` for `workspaceDir`. Cached module-locally; the cache
72
82
  * is invalidated by `invalidatePageIndex` (called by daemon-side hooks when
73
- * concept pages or skill entries change).
83
+ * concept pages, skill entries, or CLI-command entries change).
74
84
  *
75
85
  * Cold builds list every concept page in parallel, drop pages whose read
76
- * rejects, append seeded skill entries from `listSkillEntries()`, sort by
77
- * slug for deterministic IDs, then resolve outgoing edges to numeric IDs.
86
+ * rejects, append seeded skill entries from `listSkillEntries()` and CLI
87
+ * command entries from `listCliCommandEntries()`, sort by slug for
88
+ * deterministic IDs, then resolve outgoing edges to numeric IDs.
78
89
  */
79
90
  export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
80
91
  if (cache && cache.workspaceDir === workspaceDir) {
@@ -97,6 +108,30 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
97
108
  outgoingSlugs: string[];
98
109
  }
99
110
 
111
+ const [
112
+ { listSkillEntries, SKILL_SLUG_PREFIX },
113
+ { listCliCommandEntries, CLI_COMMAND_SLUG_PREFIX },
114
+ ] = await Promise.all([
115
+ import("./skill-store.js"),
116
+ import("./cli-command-store.js"),
117
+ ]);
118
+
119
+ // Build the synthetic-slug sets first so we can drop colliding concept
120
+ // pages. Collision policy: **synthetic entries win**. Skill and CLI rows
121
+ // are seeded from authoritative in-process catalogs; a hand-authored page
122
+ // sitting under `skills/<id>` or `cli-commands/<name>` is either a stale
123
+ // leftover from a prior write or a user mistake. `bySlug` is last-writer-
124
+ // wins, so without explicit dedupe one side would silently shadow the
125
+ // other depending on iteration order.
126
+ const skillEntries = listSkillEntries();
127
+ const skillSlugs = new Set(
128
+ skillEntries.map((entry) => `${SKILL_SLUG_PREFIX}${entry.id}`),
129
+ );
130
+ const cliCommandEntries = listCliCommandEntries();
131
+ const cliCommandSlugs = new Set(
132
+ cliCommandEntries.map((entry) => `${CLI_COMMAND_SLUG_PREFIX}${entry.id}`),
133
+ );
134
+
100
135
  const drafts: DraftEntry[] = [];
101
136
  for (let i = 0; i < settled.length; i++) {
102
137
  const result = settled[i];
@@ -110,20 +145,40 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
110
145
  }
111
146
  const page = result.value;
112
147
  if (!page) continue;
148
+ if (skillSlugs.has(slug)) {
149
+ log.warn(
150
+ { slug },
151
+ "Dropping concept page from index — slug collides with a seeded skill entry; skill wins",
152
+ );
153
+ continue;
154
+ }
155
+ if (cliCommandSlugs.has(slug)) {
156
+ log.warn(
157
+ { slug },
158
+ "Dropping concept page from index — slug collides with a seeded CLI-command entry; CLI command wins",
159
+ );
160
+ continue;
161
+ }
113
162
  const summarySource = page.frontmatter.summary?.trim() || page.body.trim();
114
163
  drafts.push({
115
164
  slug,
116
- summary: summarySource.slice(0, SUMMARY_MAX_LENGTH),
165
+ summary: normalizeSummary(summarySource),
117
166
  outgoingSlugs: page.frontmatter.edges,
118
167
  });
119
168
  }
120
169
 
121
- const { listSkillEntries, SKILL_SLUG_PREFIX } =
122
- await import("./skill-store.js");
123
- for (const entry of listSkillEntries()) {
170
+ for (const entry of skillEntries) {
124
171
  drafts.push({
125
172
  slug: `${SKILL_SLUG_PREFIX}${entry.id}`,
126
- summary: entry.content.trim().slice(0, SUMMARY_MAX_LENGTH),
173
+ summary: normalizeSummary(entry.content),
174
+ outgoingSlugs: [],
175
+ });
176
+ }
177
+
178
+ for (const entry of cliCommandEntries) {
179
+ drafts.push({
180
+ slug: `${CLI_COMMAND_SLUG_PREFIX}${entry.id}`,
181
+ summary: normalizeSummary(entry.description),
127
182
  outgoingSlugs: [],
128
183
  });
129
184
  }
@@ -27,7 +27,6 @@ import { homedir } from "node:os";
27
27
  import { isAbsolute, join } from "node:path";
28
28
 
29
29
  import { getLogger } from "../../../util/logger.js";
30
- import { getWorkspaceDir } from "../../../util/platform.js";
31
30
 
32
31
  const log = getLogger("memory-v2-router-prompt");
33
32
 
@@ -102,7 +101,7 @@ export function renderRouterPrompt(opts: RenderRouterPromptOpts): string {
102
101
  * referenced by `memory.v2.router.router_prompt_path`, then substitute the
103
102
  * standard placeholders. Path-resolution rules mirror the consolidation
104
103
  * prompt override: absolute paths used as-is, leading `~/` expanded to home,
105
- * relative paths resolved under the workspace root.
104
+ * relative paths resolved under `workspaceDir`.
106
105
  *
107
106
  * Failure handling is intentionally permissive — missing file, read error,
108
107
  * oversized file, or empty/whitespace-only body all log a warning and fall
@@ -111,11 +110,12 @@ export function renderRouterPrompt(opts: RenderRouterPromptOpts): string {
111
110
  */
112
111
  export function resolveRouterPrompt(
113
112
  overridePath: string | null,
113
+ workspaceDir: string,
114
114
  opts: RenderRouterPromptOpts,
115
115
  ): string {
116
116
  if (overridePath === null) return renderRouterPrompt(opts);
117
117
 
118
- const resolvedPath = resolveOverridePath(overridePath);
118
+ const resolvedPath = resolveOverridePath(overridePath, workspaceDir);
119
119
  let contents: string;
120
120
  try {
121
121
  const stat = lstatSync(resolvedPath);
@@ -178,15 +178,18 @@ function substitutePlaceholders(
178
178
  const assistant = opts.assistantName?.trim() || "the assistant";
179
179
  const user = opts.userName?.trim() || "the user";
180
180
  return template
181
- .replaceAll(ASSISTANT_NAME_PLACEHOLDER, assistant)
182
- .replaceAll(USER_NAME_PLACEHOLDER, user)
183
- .replaceAll(PAGE_INDEX_PLACEHOLDER, opts.pageIndexBlock);
181
+ .replaceAll(ASSISTANT_NAME_PLACEHOLDER, () => assistant)
182
+ .replaceAll(USER_NAME_PLACEHOLDER, () => user)
183
+ .replaceAll(PAGE_INDEX_PLACEHOLDER, () => opts.pageIndexBlock);
184
184
  }
185
185
 
186
- function resolveOverridePath(overridePath: string): string {
186
+ function resolveOverridePath(
187
+ overridePath: string,
188
+ workspaceDir: string,
189
+ ): string {
187
190
  if (overridePath.startsWith("~/")) {
188
191
  return join(homedir(), overridePath.slice(2));
189
192
  }
190
193
  if (isAbsolute(overridePath)) return overridePath;
191
- return join(getWorkspaceDir(), overridePath);
194
+ return join(workspaceDir, overridePath);
192
195
  }
@@ -51,6 +51,6 @@ export function renderSweepPrompt(opts: {
51
51
  const user = opts.userName?.trim() || "the user";
52
52
  return SWEEP_PROMPT.replaceAll(
53
53
  ASSISTANT_NAME_PLACEHOLDER,
54
- assistant,
55
- ).replaceAll(USER_NAME_PLACEHOLDER, user);
54
+ () => assistant,
55
+ ).replaceAll(USER_NAME_PLACEHOLDER, () => user);
56
56
  }
@@ -200,6 +200,9 @@ async function ensureConceptPageCollectionOnce(): Promise<{
200
200
 
201
201
  const missing = missingNamedVectors(info);
202
202
  if (missing.length === 0) {
203
+ // Long-lived installs may predate the `kind` payload index; ensure
204
+ // every required index exists before declaring the collection ready.
205
+ await ensurePayloadIndexes();
203
206
  _collectionReady = true;
204
207
  return { migrated: false };
205
208
  }
@@ -271,17 +274,62 @@ async function ensureConceptPageCollectionOnce(): Promise<{
271
274
  throw err;
272
275
  }
273
276
 
274
- // Slug is the only payload field we filter on; index it once at create-time
275
- // so upserts and slug-restricted queries don't pay a per-call indexing cost.
276
- await client.createPayloadIndex(MEMORY_V2_COLLECTION, {
277
- field_name: "slug",
278
- field_schema: "keyword",
279
- });
277
+ await ensurePayloadIndexes();
280
278
 
281
279
  _collectionReady = true;
282
280
  return { migrated };
283
281
  }
284
282
 
283
+ /**
284
+ * Idempotently create the payload indexes the collection's query and
285
+ * filter paths rely on:
286
+ *
287
+ * - `slug` (keyword): every slug-restricted query and prefix scan filters on it.
288
+ * - `kind` (keyword): the skill-backfill scroll filters with `is_empty` on
289
+ * `kind`. Strict-mode Qdrant deployments reject filters on unindexed
290
+ * payload fields, so without this the backfill consistently fails and
291
+ * legacy skill points remain untagged.
292
+ *
293
+ * Same-schema `createPayloadIndex` calls are idempotent server-side in
294
+ * Qdrant (200 OK), so the only "already exists" failures we expect are
295
+ * narrow races where a concurrent caller created the same index a moment
296
+ * earlier. Those are benign and swallowed. Every other failure — strict-mode
297
+ * rejection, index-limit, transient network blip — must propagate so the
298
+ * caller does not latch readiness on a collection whose `slug`/`kind`
299
+ * filters will keep rejecting queries until the next daemon restart.
300
+ */
301
+ async function ensurePayloadIndexes(): Promise<void> {
302
+ const client = getClient();
303
+ const indexes = [
304
+ { field_name: "slug", field_schema: "keyword" as const },
305
+ { field_name: "kind", field_schema: "keyword" as const },
306
+ ];
307
+ // Parallel so one "already exists" race on a single index doesn't stall
308
+ // the other create round-trip. v1's `qdrant-client.ts` uses the same
309
+ // Promise.all shape.
310
+ await Promise.all(
311
+ indexes.map(async (index) => {
312
+ try {
313
+ await client.createPayloadIndex(MEMORY_V2_COLLECTION, index);
314
+ } catch (err) {
315
+ if (isPayloadIndexAlreadyExists(err)) return;
316
+ throw err;
317
+ }
318
+ }),
319
+ );
320
+ }
321
+
322
+ /**
323
+ * True when a `createPayloadIndex` error indicates the index already
324
+ * exists with matching parameters — the only failure shape it is safe to
325
+ * swallow. Qdrant returns 4xx with messages like
326
+ * `"Wrong input: Payload field 'kind' already exists ..."`.
327
+ */
328
+ function isPayloadIndexAlreadyExists(err: unknown): boolean {
329
+ const msg = err instanceof Error ? err.message : String(err);
330
+ return /already exists/i.test(msg);
331
+ }
332
+
285
333
  /**
286
334
  * Return the names of required named vectors absent from the collection's
287
335
  * current schema. An empty array means the collection is fully migrated.
@@ -422,7 +470,10 @@ export async function deleteConceptPageEmbedding(slug: string): Promise<void> {
422
470
  * `payload.kind` matches are eligible for deletion. This is critical because
423
471
  * `validateSlug` permits user-authored concept pages slugged like
424
472
  * `skills/foo`; without a kind filter they would collide with the skill
425
- * namespace and be repeatedly pruned every seed run.
473
+ * namespace and be repeatedly pruned every seed run. The companion
474
+ * {@link backfillKindOnPointsWithPrefix} preserves this invariant for legacy
475
+ * untagged rows by tagging only suffixes the caller knows are skills —
476
+ * user-authored `skills/<slug>` rows stay kindless and outside this prune.
426
477
  *
427
478
  * Idempotent: when the live `<prefix>*` slugs already match `activeSuffixes`,
428
479
  * the function performs a single scroll and no deletes.
@@ -491,6 +542,83 @@ export async function pruneSlugsWithPrefixExcept(
491
542
  }
492
543
  }
493
544
 
545
+ /**
546
+ * Set `payload.kind` on every point whose slug starts with `prefix`, whose
547
+ * suffix is in `allowedSuffixes`, and is currently missing the `kind`
548
+ * discriminator. Used to tag legacy rows that predate the kind field so the
549
+ * kind-scoped {@link pruneSlugsWithPrefixExcept} no longer leaves them as
550
+ * orphans.
551
+ *
552
+ * `allowedSuffixes` is required because `validateSlug` permits user-authored
553
+ * concept pages slugged like `skills/my-notes` — those rows also lack `kind`
554
+ * and would otherwise be tagged here and then deleted by the kind-scoped
555
+ * prune. Callers must pass the closed set of legitimate suffixes (e.g. the
556
+ * union of installed + remote-catalog skill IDs) so user pages stay untagged.
557
+ *
558
+ * The "missing kind" predicate is pushed to Qdrant via `is_empty`, so once
559
+ * every legacy row has been tagged the scroll returns the bounded set of
560
+ * other kindless concept pages without ever touching the already-tagged
561
+ * rows. Idempotent across retries: a row tagged by an earlier partial run
562
+ * no longer matches the filter and is silently skipped.
563
+ */
564
+ export async function backfillKindOnPointsWithPrefix(
565
+ prefix: string,
566
+ kind: string,
567
+ allowedSuffixes: ReadonlySet<string>,
568
+ ): Promise<number> {
569
+ if (allowedSuffixes.size === 0) return 0;
570
+ await ensureConceptPageCollection();
571
+
572
+ const client = getClient();
573
+
574
+ const doBackfill = async (): Promise<number> => {
575
+ const pointIds: Array<string | number> = [];
576
+ let offset: string | number | undefined = undefined;
577
+ const maxIterations = 10_000;
578
+ const batchSize = 256;
579
+ for (let i = 0; i < maxIterations; i++) {
580
+ const result = await client.scroll(MEMORY_V2_COLLECTION, {
581
+ limit: batchSize,
582
+ with_payload: true,
583
+ with_vector: false,
584
+ filter: { must: [{ is_empty: { key: "kind" } }] },
585
+ ...(offset !== undefined ? { offset } : {}),
586
+ });
587
+ for (const point of result.points) {
588
+ const slug = (point.payload as { slug?: unknown } | null)?.slug;
589
+ if (typeof slug !== "string") continue;
590
+ if (!slug.startsWith(prefix)) continue;
591
+ const suffix = slug.slice(prefix.length);
592
+ if (!allowedSuffixes.has(suffix)) continue;
593
+ pointIds.push(point.id);
594
+ }
595
+ const next = result.next_page_offset;
596
+ if (next == null) break;
597
+ offset = typeof next === "string" ? next : (next as number);
598
+ }
599
+
600
+ if (pointIds.length === 0) return 0;
601
+
602
+ await client.setPayload(MEMORY_V2_COLLECTION, {
603
+ payload: { kind },
604
+ points: pointIds,
605
+ wait: true,
606
+ });
607
+ return pointIds.length;
608
+ };
609
+
610
+ try {
611
+ return await doBackfill();
612
+ } catch (err) {
613
+ if (isCollectionMissing(err)) {
614
+ _collectionReady = false;
615
+ await ensureConceptPageCollection();
616
+ return await doBackfill();
617
+ }
618
+ throw err;
619
+ }
620
+ }
621
+
494
622
  /**
495
623
  * Approximate count of points in the v2 concept-page collection. Used by the
496
624
  * daemon-startup rebuild hook to detect "collection exists but empty" — the
@@ -181,6 +181,7 @@ export async function runRouter(
181
181
 
182
182
  const systemPrompt = resolveRouterPrompt(
183
183
  config.memory?.v2?.router?.router_prompt_path ?? null,
184
+ workspaceDir,
184
185
  {
185
186
  assistantName: getAssistantName(),
186
187
  userName: resolveUserName(workspaceDir),
@@ -274,22 +275,22 @@ export async function runRouter(
274
275
  );
275
276
  }
276
277
 
277
- const truncated = inRangeIds.length > maxPageIds;
278
- const finalIds = truncated ? inRangeIds.slice(0, maxPageIds) : inRangeIds;
278
+ // De-duplicate BEFORE applying the cap — otherwise a duplicate-heavy
279
+ // model output like `[1, 1, 2]` with `max=2` slices to `[1, 1]` and
280
+ // dedupes to `[1]`, under-filling the cap.
281
+ const dedupedIds = Array.from(new Set(inRangeIds));
282
+
283
+ const truncated = dedupedIds.length > maxPageIds;
284
+ const finalIds = truncated ? dedupedIds.slice(0, maxPageIds) : dedupedIds;
279
285
  if (truncated) {
280
286
  log.warn(
281
- { returned: inRangeIds.length, max: maxPageIds },
287
+ { returned: dedupedIds.length, max: maxPageIds },
282
288
  "Router returned more page IDs than max_page_ids; truncating",
283
289
  );
284
290
  }
285
291
 
286
- // De-duplicate while preserving order — the index lookup alone wouldn't
287
- // catch repeats from the model.
288
- const seen = new Set<number>();
289
292
  const selectedSlugs: string[] = [];
290
293
  for (const id of finalIds) {
291
- if (seen.has(id)) continue;
292
- seen.add(id);
293
294
  const entry = pageIndex.byId.get(id);
294
295
  if (!entry) continue;
295
296
  selectedSlugs.push(entry.slug);