@vellumai/assistant 0.8.2 → 0.8.4

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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -47,7 +47,15 @@ import type {
47
47
  ToolDefinition,
48
48
  } from "../../providers/types.js";
49
49
  import { getLogger } from "../../util/logger.js";
50
- import { getPageIndex } from "./page-index.js";
50
+ import type { DrizzleDb } from "../db-connection.js";
51
+ import { computeInjectionScores } from "./injection-events.js";
52
+ import type { PageIndex } from "./page-index.js";
53
+ import {
54
+ getPageIndex,
55
+ partitionPageIndex,
56
+ splitTier1,
57
+ splitTier2,
58
+ } from "./page-index.js";
51
59
  import { resolveRouterPrompt } from "./prompts/router.js";
52
60
  import type { EverInjectedEntry } from "./types.js";
53
61
 
@@ -66,14 +74,28 @@ export type RouterFailureReason =
66
74
  | "api_error"
67
75
  | "empty_index";
68
76
 
77
+ /**
78
+ * Tags which batch a router-selected slug came from. Tier 3 carries the
79
+ * batch index so the inspector can distinguish e.g. `tier3:0` from
80
+ * `tier3:3` — useful for debugging hash bucketing and batch-quality
81
+ * regressions per tier 3 bucket.
82
+ */
83
+ export type RouterSource = "tier1" | "tier2" | `tier3:${number}`;
84
+
69
85
  /**
70
86
  * Result of a single router call. `selectedSlugs` preserves the order the
71
87
  * model returned and is already capped at `config.memory.v2.router.max_page_ids`
72
- * with out-of-range IDs dropped.
88
+ * with out-of-range IDs dropped. `sourceBySlug` attributes each selection
89
+ * to the batch it came from for inspector display.
73
90
  */
74
91
  export interface RouterResult {
75
92
  /** Selected page slugs in the order the model returned them. */
76
93
  selectedSlugs: string[];
94
+ /**
95
+ * Per-slug provenance covering every entry in `selectedSlugs`. Empty when
96
+ * `failureReason !== null` or no batch returned any selections.
97
+ */
98
+ sourceBySlug: ReadonlyMap<string, RouterSource>;
77
99
  /** `null` on success; one of the failure reasons above otherwise. */
78
100
  failureReason: RouterFailureReason | null;
79
101
  }
@@ -112,8 +134,24 @@ const RouterResultSchema = z.object({
112
134
  page_ids: z.array(z.number().int()),
113
135
  });
114
136
 
115
- /** Empty-result helper so call sites don't reconstruct the shape inline. */
137
+ /**
138
+ * Per-batch internal result. The orchestrator stamps provenance during the
139
+ * union so individual batches never need to know their own tier tag.
140
+ */
141
+ interface RouterBatchResult {
142
+ selectedSlugs: string[];
143
+ failureReason: RouterFailureReason | null;
144
+ }
145
+
146
+ /** Empty orchestrator result. */
116
147
  function emptyResult(reason: RouterFailureReason | null): RouterResult {
148
+ return { selectedSlugs: [], sourceBySlug: new Map(), failureReason: reason };
149
+ }
150
+
151
+ /** Empty batch result — slimmer shape; orchestrator builds provenance. */
152
+ function emptyBatchResult(
153
+ reason: RouterFailureReason | null,
154
+ ): RouterBatchResult {
117
155
  return { selectedSlugs: [], failureReason: reason };
118
156
  }
119
157
 
@@ -127,46 +165,42 @@ interface RunRouterParams {
127
165
  priorEverInjected: readonly EverInjectedEntry[];
128
166
  config: AssistantConfig;
129
167
  signal?: AbortSignal;
168
+ /**
169
+ * Database handle for reading EMA scores when `tier2_size` is set. When
170
+ * absent, tier 2 is silently skipped (pages flow tier 1 → tier 3). The
171
+ * production caller (`injectViaRouter`) always passes it; tests that
172
+ * only exercise tier 1 / tier 3 paths can omit it.
173
+ */
174
+ database?: DrizzleDb;
130
175
  }
131
176
 
132
177
  /**
133
- * Run the router for one turn. The implementation steps (mirroring
134
- * `sweep-job.ts` end-to-end):
178
+ * Run the router for one turn.
179
+ *
180
+ * Top-level orchestration. When `config.memory.v2.router.batch_size` is
181
+ * `null` (default), the entire page index is sent in one call — bit-
182
+ * identical to the pre-batching code path so v3's KV cache is preserved.
183
+ * When set, `partitionPageIndex` splits the index into stable hash-bucketed
184
+ * batches and we fire one provider call per batch in parallel; the selected
185
+ * slugs are unioned across batches.
135
186
  *
136
- * 1. Build the page index. If the workspace has no concept pages and no
137
- * seeded skill entries, abstain immediately with `empty_index`.
138
- * 2. Resolve the configured provider for the `memoryRouter` call site.
139
- * Missing `no_provider` so the caller can fall back to spreading
140
- * activation or an empty injection.
141
- * 3. Build system + user prompts. The system prompt is the rendered
142
- * router template with the page index inlined and gets one ephemeral
143
- * breakpoint at the end (the page-index block). The user message is
144
- * *two* text blocks: the cached `<now>` block and the uncached
145
- * already-injected/last-turn block.
146
- * 4. Force `tool_choice` so the model can only emit `select_pages_to_inject`.
147
- * 5. Parse the tool input via Zod. Anything off-shape collapses to
148
- * `schema_mismatch`.
149
- * 6. Map IDs to slugs through the page index, dropping IDs outside
150
- * `[1, N]` and truncating at `max_page_ids`.
187
+ * Per-batch failure does not abort the turn as long as at least one batch
188
+ * returns a usable selection, the union is returned with `failureReason:
189
+ * null`. Only when EVERY batch fails do we surface a failure; in that case
190
+ * the first batch's reason is returned for parity with the single-batch
191
+ * v3 behavior.
151
192
  *
152
- * Any uncaught throw inside the call (network, provider SDK error, abort)
153
- * collapses to `api_error` and is logged at warn so callers can keep going
154
- * without crashing the daemon. `AbortSignal.aborted` errors are *not*
155
- * special-cased; they propagate as `api_error` because the caller treats
156
- * "router didn't finish" the same regardless of cause.
193
+ * Single batch error semantics, preserved from v3:
194
+ * - `empty_index` workspace has no concept pages or skill entries.
195
+ * - `no_provider` `getConfiguredProvider("memoryRouter")` returned null.
196
+ * - `api_error` any uncaught throw during the provider call (incl. abort).
197
+ * - `tool_use_missing` the model returned no `select_pages_to_inject` tool_use.
198
+ * - `schema_mismatch` — tool input failed Zod validation.
157
199
  */
158
200
  export async function runRouter(
159
201
  params: RunRouterParams,
160
202
  ): Promise<RouterResult> {
161
- const {
162
- workspaceDir,
163
- userMessage,
164
- assistantMessage,
165
- nowText,
166
- priorEverInjected,
167
- config,
168
- signal,
169
- } = params;
203
+ const { workspaceDir, priorEverInjected, config } = params;
170
204
 
171
205
  const pageIndex = await getPageIndex(workspaceDir);
172
206
  if (pageIndex.entries.length === 0) {
@@ -179,31 +213,153 @@ export async function runRouter(
179
213
  return emptyResult("no_provider");
180
214
  }
181
215
 
216
+ const batchSize = config.memory?.v2?.router?.batch_size ?? null;
217
+ const tier1Size = config.memory?.v2?.router?.tier1_size ?? null;
218
+ const tier2Size = config.memory?.v2?.router?.tier2_size ?? null;
219
+
220
+ // Carve in tier order so each later tier sees only what's left. With
221
+ // every tier disabled (defaults) we hit the bit-identical single-batch
222
+ // path that preserves v3's KV cache.
223
+ const { tier1, rest: afterTier1 } = splitTier1(pageIndex, tier1Size);
224
+
225
+ let tier2: PageIndex | null = null;
226
+ let afterTier2: PageIndex = afterTier1;
227
+ if (tier2Size !== null && params.database && afterTier1.entries.length > 0) {
228
+ const slugs = afterTier1.entries.map((e) => e.slug);
229
+ const scores = computeInjectionScores(params.database, slugs, Date.now());
230
+ const split = splitTier2(afterTier1, tier2Size, scores);
231
+ tier2 = split.tier2;
232
+ afterTier2 = split.rest;
233
+ } else if (tier2Size !== null && !params.database) {
234
+ log.warn(
235
+ "tier2_size set but no database passed to runRouter; skipping tier 2",
236
+ );
237
+ }
238
+
239
+ const tier3Batches = partitionPageIndex(afterTier2, batchSize).filter(
240
+ (b) => b.entries.length > 0,
241
+ );
242
+
243
+ // Tag each batch with its provenance string. Tier 3 batches carry their
244
+ // bucket index so the inspector can attribute selections per-bucket.
245
+ const taggedBatches: Array<{ source: RouterSource; index: PageIndex }> = [];
246
+ if (tier1) taggedBatches.push({ source: "tier1", index: tier1 });
247
+ if (tier2) taggedBatches.push({ source: "tier2", index: tier2 });
248
+ tier3Batches.forEach((index, i) => {
249
+ taggedBatches.push({ source: `tier3:${i}` as const, index });
250
+ });
251
+ if (taggedBatches.length === 0) {
252
+ return emptyResult("empty_index");
253
+ }
254
+
255
+ const batchResults = await Promise.all(
256
+ taggedBatches.map(({ index }) =>
257
+ runRouterBatch({
258
+ ...params,
259
+ batchIndex: index,
260
+ priorEverInjected,
261
+ provider,
262
+ }),
263
+ ),
264
+ );
265
+
266
+ const successes = batchResults.filter((r) => r.failureReason === null);
267
+ if (successes.length === 0) {
268
+ // For the single-batch (K=null) path this preserves v3's behavior:
269
+ // one batch, one failure reason surfaces directly.
270
+ return emptyResult(batchResults[0].failureReason);
271
+ }
272
+
273
+ // Union selected slugs preserving first-seen order across batches; batch
274
+ // ordering is deterministic so the union and provenance map are stable.
275
+ // First-seen wins if a slug somehow appears in multiple batches (shouldn't
276
+ // happen — tier 1/2/3 partition is disjoint — but be defensive).
277
+ const sourceBySlug = new Map<string, RouterSource>();
278
+ const selectedSlugs: string[] = [];
279
+ for (let i = 0; i < batchResults.length; i++) {
280
+ const result = batchResults[i];
281
+ const source = taggedBatches[i].source;
282
+ for (const slug of result.selectedSlugs) {
283
+ if (sourceBySlug.has(slug)) continue;
284
+ sourceBySlug.set(slug, source);
285
+ selectedSlugs.push(slug);
286
+ }
287
+ }
288
+ if (successes.length < batchResults.length) {
289
+ log.warn(
290
+ {
291
+ totalBatches: batchResults.length,
292
+ failedBatches: batchResults.length - successes.length,
293
+ failureReasons: batchResults
294
+ .filter((r) => r.failureReason !== null)
295
+ .map((r) => r.failureReason),
296
+ },
297
+ "Some router batches failed; returning union of successful batches",
298
+ );
299
+ }
300
+
301
+ // Each per-batch call caps at max_page_ids, but the union across batches can
302
+ // exceed it (e.g. 10 batches × 10 selections each ≫ 25 cap). Apply a final
303
+ // truncation so RouterResult honors the contract that injection.ts trusts.
304
+ // Iteration order above is tier 1 → tier 2 → tier 3:0 → … so earlier-tier
305
+ // slugs win the truncation.
306
+ const maxPageIds = config.memory?.v2?.router?.max_page_ids ?? 25;
307
+ if (selectedSlugs.length > maxPageIds) {
308
+ log.warn(
309
+ { unionSize: selectedSlugs.length, max: maxPageIds },
310
+ "Router union across batches exceeded max_page_ids; truncating",
311
+ );
312
+ const dropped = selectedSlugs.splice(maxPageIds);
313
+ for (const slug of dropped) sourceBySlug.delete(slug);
314
+ }
315
+ return { selectedSlugs, sourceBySlug, failureReason: null };
316
+ }
317
+
318
+ interface RunRouterBatchParams extends RunRouterParams {
319
+ batchIndex: PageIndex;
320
+ provider: NonNullable<Awaited<ReturnType<typeof getConfiguredProvider>>>;
321
+ }
322
+
323
+ /**
324
+ * Route one batch of the page index. Uses batch-local IDs everywhere
325
+ * (including `<already_injected_ids>`, which is filtered to slugs present
326
+ * in this batch). Provider is passed in by the orchestrator so we don't
327
+ * re-resolve it N times for an N-batch turn.
328
+ */
329
+ async function runRouterBatch(
330
+ params: RunRouterBatchParams,
331
+ ): Promise<RouterBatchResult> {
332
+ const {
333
+ workspaceDir,
334
+ userMessage,
335
+ assistantMessage,
336
+ nowText,
337
+ priorEverInjected,
338
+ config,
339
+ signal,
340
+ batchIndex,
341
+ provider,
342
+ } = params;
343
+
182
344
  const systemPrompt = resolveRouterPrompt(
183
345
  config.memory?.v2?.router?.router_prompt_path ?? null,
184
346
  workspaceDir,
185
347
  {
186
348
  assistantName: getAssistantName(),
187
349
  userName: resolveUserName(workspaceDir),
188
- pageIndexBlock: pageIndex.rendered,
350
+ pageIndexBlock: batchIndex.rendered,
189
351
  },
190
352
  );
191
353
 
192
- // Already-injected slugs that map back to a current index ID. Slugs whose
193
- // page has been deleted since the prior turn drop out silently — the model
194
- // only sees IDs that still resolve.
354
+ // Filter prior-injected to slugs present in THIS batch and map to
355
+ // batch-local IDs. The model in batch B can't reference global IDs that
356
+ // aren't in its prompt, so listing them would just be noise.
195
357
  const priorIds: number[] = [];
196
358
  for (const entry of priorEverInjected) {
197
- const idx = pageIndex.bySlug.get(entry.slug);
198
- if (idx) priorIds.push(idx.id);
359
+ const local = batchIndex.bySlug.get(entry.slug);
360
+ if (local) priorIds.push(local.id);
199
361
  }
200
362
 
201
- // Cache breakpoint 2 — `<now>` is stable across most turns (NOW.md only
202
- // changes when the model rewrites it), so the bulk of the user message
203
- // rides the cache. We use a 1h TTL to match the system-prompt breakpoint
204
- // and the provider's auto-applied breakpoints. The trailing block has no
205
- // `cache_control`; the Anthropic provider auto-applies a 1h breakpoint on
206
- // the last text block of a turn-starting user message, which covers it.
207
363
  const userMsg: Message = {
208
364
  role: "user",
209
365
  content: [
@@ -236,7 +392,7 @@ export async function runRouter(
236
392
  );
237
393
  } catch (err) {
238
394
  log.warn({ err }, "Router provider call threw; treating as api_error");
239
- return emptyResult("api_error");
395
+ return emptyBatchResult("api_error");
240
396
  }
241
397
 
242
398
  const toolBlock = extractToolUse(response);
@@ -245,7 +401,7 @@ export async function runRouter(
245
401
  { stopReason: response.stopReason },
246
402
  "Router model returned no select_pages_to_inject tool_use block",
247
403
  );
248
- return emptyResult("tool_use_missing");
404
+ return emptyBatchResult("tool_use_missing");
249
405
  }
250
406
 
251
407
  const parsed = RouterResultSchema.safeParse(toolBlock.input);
@@ -254,11 +410,10 @@ export async function runRouter(
254
410
  { error: parsed.error.message },
255
411
  "Router tool input did not match schema",
256
412
  );
257
- return emptyResult("schema_mismatch");
413
+ return emptyBatchResult("schema_mismatch");
258
414
  }
259
415
 
260
- const N = pageIndex.entries.length;
261
-
416
+ const N = batchIndex.entries.length;
262
417
  const inRangeIds: number[] = [];
263
418
  const droppedIds: number[] = [];
264
419
  for (const id of parsed.data.page_ids) {
@@ -275,9 +430,8 @@ export async function runRouter(
275
430
  );
276
431
  }
277
432
 
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.
433
+ // De-duplicate BEFORE applying the cap — `[1, 1, 2]` with max=2 must
434
+ // yield 2 distinct slugs, not collapse to 1 after slicing duplicates.
281
435
  const dedupedIds = Array.from(new Set(inRangeIds));
282
436
 
283
437
  const truncated = dedupedIds.length > maxPageIds;
@@ -291,7 +445,7 @@ export async function runRouter(
291
445
 
292
446
  const selectedSlugs: string[] = [];
293
447
  for (const id of finalIds) {
294
- const entry = pageIndex.byId.get(id);
448
+ const entry = batchIndex.byId.get(id);
295
449
  if (!entry) continue;
296
450
  selectedSlugs.push(entry.slug);
297
451
  }
@@ -35,9 +35,9 @@ const MEMORY_V2_STATIC_BLOCKS: readonly MemoryV2StaticBlock[] = [
35
35
  ];
36
36
 
37
37
  /**
38
- * Build the v2 static memory block, gated on `config.memory.v2.enabled`.
39
- * Empty/missing files are skipped; returns `null` when the gate is off or
40
- * every file is empty.
38
+ * Build the v2 static memory block, gated on `config.memory.enabled` and
39
+ * `config.memory.v2.enabled`. Empty/missing files are skipped; returns `null`
40
+ * when either gate is off or every file is empty.
41
41
  */
42
42
  export function readMemoryV2StaticContent(): string | null {
43
43
  let config;
@@ -46,7 +46,7 @@ export function readMemoryV2StaticContent(): string | null {
46
46
  } catch {
47
47
  return null;
48
48
  }
49
- if (!config.memory.v2.enabled) {
49
+ if (!config.memory.enabled || !config.memory.v2.enabled) {
50
50
  return null;
51
51
  }
52
52
 
@@ -115,3 +115,26 @@ export interface SkillEntry {
115
115
  id: string;
116
116
  content: string;
117
117
  }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // CLI-command entries (synthetic concept-collection rows, not on-disk pages)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * Per-CLI-subcommand capability snapshot held in-process and embedded into the
125
+ * unified `memory_v2_concept_pages` Qdrant collection under the slug
126
+ * `cli-commands/<name>`. `content` is the full `helpInformation()` output for
127
+ * the top-level subcommand — the embedding target, intentionally uncapped so
128
+ * activation hints in flag descriptions and examples carry semantic weight.
129
+ * `description` is the one-line Commander description, rendered terse in
130
+ * `### CLI Commands You Can Use` so the injection block stays compact even
131
+ * for verbose `--help` outputs.
132
+ *
133
+ * Plain interface (no Zod) — same in-process-only justification as
134
+ * `SkillEntry`.
135
+ */
136
+ export interface CliCommandEntry {
137
+ id: string;
138
+ description: string;
139
+ content: string;
140
+ }
@@ -0,0 +1,274 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { ChannelReplyPayload } from "@vellumai/gateway-client";
4
+
5
+ import type { A2ATask, Artifact } from "../../../../a2a/protocol-types.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mock state
9
+ // ---------------------------------------------------------------------------
10
+
11
+ let completedTask: A2ATask | null = null;
12
+ let completeWithArtifactsCalls: Array<{
13
+ taskId: string;
14
+ artifacts: Artifact[];
15
+ }> = [];
16
+ let pushUrlByTaskId: Record<string, string | null> = {};
17
+ let completeError: Error | null = null;
18
+
19
+ const fetchCalls: Array<{
20
+ url: string;
21
+ init: RequestInit;
22
+ }> = [];
23
+ let fetchResponses: Array<{ ok: boolean; status: number; body: string }> = [];
24
+ let fetchCallIndex = 0;
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Mocks
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const defaultTask: A2ATask = {
31
+ id: "task-123",
32
+ status: { state: "completed", timestamp: new Date().toISOString() },
33
+ artifacts: [
34
+ {
35
+ artifact_id: "art-1",
36
+ parts: [{ kind: "text", text: "Hello from assistant" }],
37
+ },
38
+ ],
39
+ };
40
+
41
+ mock.module("../../../../a2a/task-store.js", () => ({
42
+ completeWithArtifacts: (taskId: string, artifacts: Artifact[]): A2ATask => {
43
+ completeWithArtifactsCalls.push({ taskId, artifacts });
44
+ if (completeError) throw completeError;
45
+ return completedTask ?? defaultTask;
46
+ },
47
+ getPushUrl: (taskId: string): string | null => {
48
+ return pushUrlByTaskId[taskId] ?? null;
49
+ },
50
+ }));
51
+
52
+ mock.module("../../../../util/logger.js", () => ({
53
+ getLogger: () => ({
54
+ debug: () => {},
55
+ info: () => {},
56
+ warn: () => {},
57
+ error: () => {},
58
+ }),
59
+ }));
60
+
61
+ // Intercept global fetch for push notification testing
62
+ const originalFetch = globalThis.fetch;
63
+
64
+ // Import the module under test AFTER mocks are set up
65
+ const { deliverA2AReply } = await import("../deliver.js");
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Setup / teardown
69
+ // ---------------------------------------------------------------------------
70
+
71
+ beforeEach(() => {
72
+ completedTask = null;
73
+ completeWithArtifactsCalls = [];
74
+ pushUrlByTaskId = {};
75
+ completeError = null;
76
+ fetchCalls.length = 0;
77
+ fetchResponses = [];
78
+ fetchCallIndex = 0;
79
+
80
+ globalThis.fetch = (async (
81
+ input: string | URL | Request,
82
+ init?: RequestInit,
83
+ ) => {
84
+ const url = typeof input === "string" ? input : input.toString();
85
+ fetchCalls.push({ url, init: init ?? {} });
86
+ const responseSpec = fetchResponses[fetchCallIndex++] ?? {
87
+ ok: true,
88
+ status: 200,
89
+ body: "{}",
90
+ };
91
+ return new Response(responseSpec.body, {
92
+ status: responseSpec.status,
93
+ statusText: responseSpec.ok ? "OK" : "Error",
94
+ });
95
+ }) as typeof fetch;
96
+ });
97
+
98
+ afterEach(() => {
99
+ globalThis.fetch = originalFetch;
100
+ });
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Tests
104
+ // ---------------------------------------------------------------------------
105
+
106
+ describe("deliverA2AReply", () => {
107
+ const baseCallbackUrl = "https://example.com/deliver/a2a?taskId=task-123";
108
+
109
+ test("completes task with text artifact", async () => {
110
+ const payload: ChannelReplyPayload = {
111
+ chatId: "chat-1",
112
+ text: "Hello from the assistant",
113
+ };
114
+
115
+ const result = await deliverA2AReply(baseCallbackUrl, payload);
116
+
117
+ expect(result.ok).toBe(true);
118
+ expect(completeWithArtifactsCalls).toHaveLength(1);
119
+ expect(completeWithArtifactsCalls[0].taskId).toBe("task-123");
120
+ expect(completeWithArtifactsCalls[0].artifacts).toHaveLength(1);
121
+ expect(completeWithArtifactsCalls[0].artifacts[0].parts).toEqual([
122
+ { kind: "text", text: "Hello from the assistant" },
123
+ ]);
124
+ });
125
+
126
+ test("completes task with file attachments", async () => {
127
+ const payload: ChannelReplyPayload = {
128
+ chatId: "chat-1",
129
+ text: "Here is a file",
130
+ attachments: [
131
+ {
132
+ id: "att-1",
133
+ filename: "report.pdf",
134
+ mimeType: "application/pdf",
135
+ sizeBytes: 1024,
136
+ kind: "file",
137
+ data: "data:application/pdf;base64,abc123",
138
+ },
139
+ ],
140
+ };
141
+
142
+ const result = await deliverA2AReply(baseCallbackUrl, payload);
143
+
144
+ expect(result.ok).toBe(true);
145
+ expect(completeWithArtifactsCalls).toHaveLength(1);
146
+ const parts = completeWithArtifactsCalls[0].artifacts[0].parts;
147
+ expect(parts).toHaveLength(2);
148
+ expect(parts[0]).toEqual({
149
+ kind: "text",
150
+ text: "Here is a file",
151
+ });
152
+ expect(parts[1]).toEqual({
153
+ kind: "file",
154
+ filename: "report.pdf",
155
+ media_type: "application/pdf",
156
+ url: "data:application/pdf;base64,abc123",
157
+ });
158
+ });
159
+
160
+ test("returns ok: false when taskId is missing from URL", async () => {
161
+ const result = await deliverA2AReply("https://example.com/deliver/a2a", {
162
+ chatId: "chat-1",
163
+ text: "Hello",
164
+ });
165
+
166
+ expect(result.ok).toBe(false);
167
+ expect(completeWithArtifactsCalls).toHaveLength(0);
168
+ });
169
+
170
+ test("returns ok: true when payload has no content", async () => {
171
+ const result = await deliverA2AReply(baseCallbackUrl, {
172
+ chatId: "chat-1",
173
+ });
174
+
175
+ expect(result.ok).toBe(true);
176
+ expect(completeWithArtifactsCalls).toHaveLength(0);
177
+ });
178
+
179
+ test("returns ok: false when task completion throws", async () => {
180
+ completeError = new Error("A2A task not found: task-123");
181
+
182
+ const result = await deliverA2AReply(baseCallbackUrl, {
183
+ chatId: "chat-1",
184
+ text: "Hello",
185
+ });
186
+
187
+ expect(result.ok).toBe(false);
188
+ });
189
+
190
+ test("returns ok: false when task is already terminal", async () => {
191
+ completeError = new Error(
192
+ 'Cannot transition task task-123 from terminal state "completed" to "completed"',
193
+ );
194
+
195
+ const result = await deliverA2AReply(baseCallbackUrl, {
196
+ chatId: "chat-1",
197
+ text: "Hello",
198
+ });
199
+
200
+ expect(result.ok).toBe(false);
201
+ });
202
+
203
+ describe("push notifications", () => {
204
+ test("POSTs completed task to push URL", async () => {
205
+ pushUrlByTaskId["task-123"] = "https://requester.example.com/push";
206
+ fetchResponses = [{ ok: true, status: 200, body: "{}" }];
207
+
208
+ const result = await deliverA2AReply(baseCallbackUrl, {
209
+ chatId: "chat-1",
210
+ text: "Done",
211
+ });
212
+
213
+ expect(result.ok).toBe(true);
214
+
215
+ // Wait for the fire-and-forget push to complete
216
+ await new Promise((r) => setTimeout(r, 50));
217
+
218
+ expect(fetchCalls).toHaveLength(1);
219
+ expect(fetchCalls[0].url).toBe("https://requester.example.com/push");
220
+ expect(fetchCalls[0].init.method).toBe("POST");
221
+
222
+ const headers = fetchCalls[0].init.headers as Record<string, string>;
223
+ expect(headers["Content-Type"]).toBe("application/a2a+json");
224
+ expect(headers["A2A-Version"]).toBe("1.0");
225
+ });
226
+
227
+ test("does not push when no push URL configured", async () => {
228
+ const result = await deliverA2AReply(baseCallbackUrl, {
229
+ chatId: "chat-1",
230
+ text: "Done",
231
+ });
232
+
233
+ expect(result.ok).toBe(true);
234
+
235
+ await new Promise((r) => setTimeout(r, 50));
236
+
237
+ expect(fetchCalls).toHaveLength(0);
238
+ });
239
+
240
+ test("push failure does not affect delivery result", async () => {
241
+ pushUrlByTaskId["task-123"] = "https://requester.example.com/push";
242
+ // All retries fail with 500
243
+ fetchResponses = Array(4).fill({
244
+ ok: false,
245
+ status: 500,
246
+ body: "Internal Server Error",
247
+ });
248
+
249
+ const result = await deliverA2AReply(baseCallbackUrl, {
250
+ chatId: "chat-1",
251
+ text: "Done",
252
+ });
253
+
254
+ // Delivery still succeeds even though push will fail
255
+ expect(result.ok).toBe(true);
256
+ });
257
+
258
+ test("stops retrying on non-retryable client error", async () => {
259
+ pushUrlByTaskId["task-123"] = "https://requester.example.com/push";
260
+ fetchResponses = [{ ok: false, status: 404, body: "Not Found" }];
261
+
262
+ await deliverA2AReply(baseCallbackUrl, {
263
+ chatId: "chat-1",
264
+ text: "Done",
265
+ });
266
+
267
+ // Wait for the fire-and-forget push to settle
268
+ await new Promise((r) => setTimeout(r, 50));
269
+
270
+ // Should only attempt once on a 4xx (non-429) error
271
+ expect(fetchCalls).toHaveLength(1);
272
+ });
273
+ });
274
+ });