@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
@@ -194,6 +194,19 @@ export const MemoryV2ConfigSchema = z
194
194
  .describe(
195
195
  "Hours between scheduled consolidation runs that synthesize buffered memories into concept pages",
196
196
  ),
197
+ consolidation_max_buffer_lines: z
198
+ .number({
199
+ error: "memory.v2.consolidation_max_buffer_lines must be a number",
200
+ })
201
+ .int("memory.v2.consolidation_max_buffer_lines must be an integer")
202
+ .positive(
203
+ "memory.v2.consolidation_max_buffer_lines must be a positive integer",
204
+ )
205
+ .nullable()
206
+ .default(100)
207
+ .describe(
208
+ "Size-based trigger for consolidation. When `memory/buffer.md` reaches this many non-empty lines, consolidation runs even if the time-based interval hasn't elapsed. Defaults to 100. Set to `null` to disable the size trigger and rely solely on `consolidation_interval_hours`.",
209
+ ),
197
210
  max_page_chars: z
198
211
  .number({ error: "memory.v2.max_page_chars must be a number" })
199
212
  .int("memory.v2.max_page_chars must be an integer")
@@ -260,9 +273,9 @@ export const MemoryV2ConfigSchema = z
260
273
  .object({
261
274
  enabled: z
262
275
  .boolean()
263
- .default(false)
276
+ .default(true)
264
277
  .describe(
265
- "Whether to use the LLM router as the per-turn page-selection mechanism in place of spreading activation. Disabled by default — opt in once the router orchestration and dispatcher land.",
278
+ "Whether to use the LLM router as the per-turn page-selection mechanism in place of spreading activation. Enabled by default.",
266
279
  ),
267
280
  max_page_ids: z
268
281
  .number()
@@ -282,10 +295,44 @@ export const MemoryV2ConfigSchema = z
282
295
  .describe(
283
296
  "Optional path to a file whose contents replace the bundled router prompt. Absolute paths are used as-is, a leading `~/` is expanded to the home directory, otherwise the path is resolved under the workspace root. The loaded contents may include `{{ASSISTANT_NAME}}`, `{{USER_NAME}}`, and `{{PAGE_INDEX}}`, which are substituted at runtime. If the file is missing, unreadable, or empty, the bundled prompt is used and a warning is logged.",
284
297
  ),
298
+ batch_size: z
299
+ .number()
300
+ .int()
301
+ .min(1)
302
+ .nullable()
303
+ .default(null)
304
+ .describe(
305
+ "Target batch size for parallel page-index routing. `null` (default) sends the entire page index in one call — identical to v3 behavior. When set, pages are split into `ceil(N / batch_size)` batches by stable FNV-1a hash on slug (so adding/removing a single page only invalidates one batch's KV cache), routed in parallel, and the selected slugs are unioned. A failure in one batch does not abort the turn as long as at least one batch succeeds.",
306
+ ),
307
+ tier1_size: z
308
+ .number()
309
+ .int()
310
+ .min(1)
311
+ .nullable()
312
+ .default(null)
313
+ .describe(
314
+ "Pool size for the tier-1 'recently modified' batch. `null` (default) disables tier 1 entirely — all pages flow through tier 3 batching. When set, the top-N concept pages by file mtime become their own dedicated parallel batch with mtime-desc ordering; everything else is partitioned into tier 3 batches by `batch_size`. Synthetic entries (skills, CLI commands) have mtime=0 and naturally rank below real concept pages so they don't crowd tier 1.",
315
+ ),
316
+ tier2_size: z
317
+ .number()
318
+ .int()
319
+ .min(1)
320
+ .nullable()
321
+ .default(null)
322
+ .describe(
323
+ "Pool size for the tier-2 'useful' batch. `null` (default) disables tier 2 — pages skip straight from tier 1 to tier 3. When set, the top-M pages by injection-frequency EMA (excluding tier 1) become their own parallel batch ordered by score desc. Pages with score 0 (never selected since EMA tracking began) are ineligible for tier 2 and stay in tier 3 regardless of `tier2_size`. Score is the time-decayed sum `Σ exp(-λ(now - tᵢ))` with 3-day half-life, computed on read from `memory_v2_injection_events`.",
324
+ ),
325
+ })
326
+ .default({
327
+ enabled: true,
328
+ max_page_ids: 25,
329
+ router_prompt_path: null,
330
+ batch_size: null,
331
+ tier1_size: null,
332
+ tier2_size: null,
285
333
  })
286
- .default({ enabled: false, max_page_ids: 25, router_prompt_path: null })
287
334
  .describe(
288
- "LLM router configuration. When enabled, a single Sonnet router call replaces spreading activation for per-turn page selection.",
335
+ "LLM router configuration. When enabled, a single router LLM call replaces spreading activation for per-turn page selection.",
289
336
  ),
290
337
  })
291
338
  .describe(
@@ -23,7 +23,9 @@ export const MemoryConfigSchema = z
23
23
  enabled: z
24
24
  .boolean({ error: "memory.enabled must be a boolean" })
25
25
  .default(true)
26
- .describe("Whether the long-term memory system is enabled"),
26
+ .describe(
27
+ "Whether the long-term memory system is enabled — gates background memory jobs, embedding generation, and `<memory>` block injection into user messages",
28
+ ),
27
29
  embeddings: MemoryEmbeddingsConfigSchema.default(
28
30
  MemoryEmbeddingsConfigSchema.parse({}),
29
31
  ),
@@ -3,6 +3,8 @@ import {
3
3
  createConnection,
4
4
  disableManagedConnectionsForByokHatch,
5
5
  getConnection,
6
+ MANAGED_CONNECTION_NAMES,
7
+ PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
6
8
  } from "../providers/inference/connections.js";
7
9
  import { PROVIDER_CATALOG } from "../providers/model-catalog.js";
8
10
  import { resolveModelIntent } from "../providers/model-intents.js";
@@ -17,10 +19,6 @@ import {
17
19
 
18
20
  const log = getLogger("seed-inference-profiles");
19
21
 
20
- const MANAGED_CONNECTION_NAME = "anthropic-managed";
21
- const MANAGED_PROFILE_PROVIDER: NonNullable<ProfileEntry["provider"]> =
22
- "anthropic";
23
-
24
22
  /**
25
23
  * Template for a daemon-managed inference profile. The profile's model is
26
24
  * resolved at seed time from `PROVIDER_MODEL_INTENTS` so the catalog stays the
@@ -31,16 +29,20 @@ type ManagedProfileTemplate = Omit<
31
29
  "provider" | "model" | "provider_connection"
32
30
  > & {
33
31
  intent: ModelIntent;
32
+ provider: NonNullable<ProfileEntry["provider"]>;
33
+ connectionName: string;
34
34
  };
35
35
 
36
36
  /**
37
- * Managed Anthropic profiles. Overwritten on every daemon boot so Vellum can
38
- * push model/config updates to customers in new releases. Platform overlays
37
+ * Managed profiles. Overwritten on every daemon boot so Vellum can push
38
+ * model/config updates to customers in new releases. Platform overlays
39
39
  * (`preserveProfileNames`) take precedence when present.
40
40
  */
41
41
  const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
42
42
  balanced: {
43
43
  intent: "balanced",
44
+ provider: "anthropic",
45
+ connectionName: "anthropic-managed",
44
46
  source: "managed",
45
47
  label: "Balanced",
46
48
  description: "Good balance of quality, cost, and speed",
@@ -51,6 +53,8 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
51
53
  },
52
54
  "quality-optimized": {
53
55
  intent: "quality-optimized",
56
+ provider: "anthropic",
57
+ connectionName: "anthropic-managed",
54
58
  source: "managed",
55
59
  label: "Quality",
56
60
  description: "Best results with the most capable model",
@@ -61,6 +65,8 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
61
65
  },
62
66
  "cost-optimized": {
63
67
  intent: "latency-optimized",
68
+ provider: "anthropic",
69
+ connectionName: "anthropic-managed",
64
70
  source: "managed",
65
71
  label: "Speed",
66
72
  description: "Fastest responses at lower cost",
@@ -74,11 +80,15 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
74
80
  /**
75
81
  * User profile templates. Materialized at hatch time for off-platform
76
82
  * installations. Each points at the user's personal provider connection
77
- * (backed by their API key in CES).
83
+ * (backed by their API key in CES). The `provider` and `connectionName`
84
+ * fields are placeholders — they are overridden at hatch time with the
85
+ * user's chosen provider and personal connection name.
78
86
  */
79
87
  const USER_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
80
88
  "custom-balanced": {
81
89
  intent: "balanced",
90
+ provider: "anthropic",
91
+ connectionName: "",
82
92
  source: "user",
83
93
  label: "Balanced",
84
94
  description: "Good balance of quality, cost, and speed",
@@ -89,6 +99,8 @@ const USER_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
89
99
  },
90
100
  "custom-quality-optimized": {
91
101
  intent: "quality-optimized",
102
+ provider: "anthropic",
103
+ connectionName: "",
92
104
  source: "user",
93
105
  label: "Quality",
94
106
  description: "Best results with the most capable model",
@@ -99,6 +111,8 @@ const USER_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
99
111
  },
100
112
  "custom-cost-optimized": {
101
113
  intent: "latency-optimized",
114
+ provider: "anthropic",
115
+ connectionName: "",
102
116
  source: "user",
103
117
  label: "Speed",
104
118
  description: "Fastest responses at lower cost",
@@ -164,11 +178,12 @@ export function seedInferenceProfiles(
164
178
  // BYOK mode = off-platform installs. The user is bringing their own provider
165
179
  // API key; managed profile labels get a " (Managed)" suffix to disambiguate
166
180
  // from the personal "custom-*" profiles that share base labels. Managed
167
- // profile + connection status is initially "disabled" so the picker doesn't
168
- // offer an unusable platform-auth option on day one — but ONLY at hatch
169
- // time, and ONLY when the entry isn't already in the user's config (i.e.
170
- // first materialization). Post-hatch user toggles survive every subsequent
171
- // boot.
181
+ // profile + connection status is initially "disabled" for true BYOK hatches
182
+ // so the picker doesn't offer an unusable platform-auth option on day one.
183
+ // When the hatch overlay explicitly selects a managed profile, the matching
184
+ // managed connection stays active so the first post-onboarding message can
185
+ // use the user's chosen managed route. Post-hatch user toggles survive every
186
+ // subsequent boot.
172
187
  const isByokMode = !isPlatform;
173
188
 
174
189
  // 1. Managed profiles. Off-platform: overwrite on every boot so Vellum can
@@ -198,10 +213,18 @@ export function seedInferenceProfiles(
198
213
  // rewritten to the suffixed form. Any other previous label value
199
214
  // (user-set custom string, explicit null, already-suffixed) is
200
215
  // preserved as-is.
201
- // • status: "disabled" on fresh materialization at hatch only —
202
- // gated on (isHatch && !previous) so post-hatch boots and existing
203
- // installs are never auto-disabled. A user re-enable persists
204
- // across boots via the key-presence preservation below.
216
+ // • status: "disabled" on fresh materialization at BYOK hatch only —
217
+ // gated on (isHatch && !previous) and skipped for any managed
218
+ // connection explicitly selected by the hatch overlay. Post-hatch
219
+ // boots and existing installs are never auto-disabled. A user
220
+ // re-enable persists across boots via the key-presence preservation
221
+ // below.
222
+ const hatchSelectedManagedConnection = getHatchSelectedManagedConnection(
223
+ llm,
224
+ profiles,
225
+ options,
226
+ );
227
+
205
228
  for (const [name, template] of Object.entries(MANAGED_PROFILE_TEMPLATES)) {
206
229
  if (preservedProfileNames.has(name)) continue;
207
230
  if (isPlatform && readObject(profiles[name]) !== null) continue;
@@ -212,10 +235,15 @@ export function seedInferenceProfiles(
212
235
  : template;
213
236
  const next = materializeProfile(
214
237
  effectiveTemplate,
215
- MANAGED_PROFILE_PROVIDER,
216
- MANAGED_CONNECTION_NAME,
238
+ template.provider,
239
+ template.connectionName,
217
240
  ) as Record<string, unknown>;
218
- if (isByokMode && options.isHatch && !previous) {
241
+ if (
242
+ isByokMode &&
243
+ options.isHatch &&
244
+ !previous &&
245
+ template.connectionName !== hatchSelectedManagedConnection
246
+ ) {
219
247
  next.status = "disabled";
220
248
  }
221
249
  if (previous) {
@@ -238,17 +266,24 @@ export function seedInferenceProfiles(
238
266
  // 2. User profiles — only at hatch time for off-platform installations.
239
267
  let userConnectionName: string | undefined;
240
268
  if (options.isHatch && !isPlatform) {
241
- // BYOK hatch: disable the three canonical managed connections so the
242
- // picker doesn't surface unusable platform-auth options on day one.
243
- // Runs only here, only at hatch `seedCanonicalConnections` leaves
244
- // `status` alone on subsequent boots so a post-hatch user re-enable
245
- // persists.
269
+ // BYOK hatch: disable canonical managed connections so the picker doesn't
270
+ // surface unusable platform-auth options on day one. If the hatch overlay
271
+ // selected a managed profile, leave that connection active; the user has
272
+ // already chosen managed inference. Runs only here, only at hatch
273
+ // `seedCanonicalConnections` leaves `status` alone on subsequent boots so
274
+ // a post-hatch user re-enable persists.
246
275
  if (options.db) {
247
- disableManagedConnectionsForByokHatch(options.db);
276
+ disableManagedConnectionsForByokHatch(options.db, {
277
+ excludeConnection: hatchSelectedManagedConnection,
278
+ });
248
279
  }
249
280
 
250
281
  const hatchProvider = readString(readObject(llm.default)?.provider);
251
- if (hatchProvider && hatchProvider !== "ollama") {
282
+ if (
283
+ hatchProvider &&
284
+ hatchProvider !== "ollama" &&
285
+ !PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(hatchProvider)
286
+ ) {
252
287
  userConnectionName = `${hatchProvider}-personal`;
253
288
 
254
289
  if (options.db) {
@@ -269,8 +304,7 @@ export function seedInferenceProfiles(
269
304
  }
270
305
  }
271
306
 
272
- const provider =
273
- hatchProvider as NonNullable<ProfileEntry["provider"]>;
307
+ const provider = hatchProvider as NonNullable<ProfileEntry["provider"]>;
274
308
  for (const [name, template] of Object.entries(USER_PROFILE_TEMPLATES)) {
275
309
  if (preservedProfileNames.has(name)) continue;
276
310
  profiles[name] = materializeProfile(
@@ -342,7 +376,7 @@ function materializeProfile(
342
376
  provider: NonNullable<ProfileEntry["provider"]>,
343
377
  connectionName: string,
344
378
  ): ProfileEntry {
345
- const { intent, ...rest } = template;
379
+ const { intent, provider: _p, connectionName: _c, ...rest } = template;
346
380
  return {
347
381
  ...rest,
348
382
  provider,
@@ -361,6 +395,42 @@ function readString(value: unknown): string | undefined {
361
395
  return typeof value === "string" && value.length > 0 ? value : undefined;
362
396
  }
363
397
 
398
+ function getHatchSelectedManagedConnection(
399
+ llm: Record<string, unknown>,
400
+ profiles: Record<string, Record<string, unknown>>,
401
+ options: SeedInferenceProfilesOptions,
402
+ ): string | undefined {
403
+ if (!options.isHatch || options.preserveActiveProfile !== true) {
404
+ return undefined;
405
+ }
406
+
407
+ const activeProfile = readString(llm.activeProfile);
408
+ if (!activeProfile) return undefined;
409
+
410
+ const activeProfileEntry = readObject(profiles[activeProfile]);
411
+ if (
412
+ activeProfileEntry &&
413
+ Object.prototype.hasOwnProperty.call(
414
+ activeProfileEntry,
415
+ "provider_connection",
416
+ )
417
+ ) {
418
+ const explicitConnection = readString(
419
+ activeProfileEntry.provider_connection,
420
+ );
421
+ return explicitConnection &&
422
+ MANAGED_CONNECTION_NAMES.has(explicitConnection)
423
+ ? explicitConnection
424
+ : undefined;
425
+ }
426
+
427
+ const templateConnection =
428
+ MANAGED_PROFILE_TEMPLATES[activeProfile]?.connectionName;
429
+ return templateConnection && MANAGED_CONNECTION_NAMES.has(templateConnection)
430
+ ? templateConnection
431
+ : undefined;
432
+ }
433
+
364
434
  /**
365
435
  * Format the human-readable label seeded onto a personal provider connection
366
436
  * at hatch time, e.g. `"Anthropic (Personal)"`. The display name is sourced
@@ -155,6 +155,11 @@ export interface CompactionRunResult {
155
155
  thresholdTokens: number;
156
156
  compactedMessages: number;
157
157
  compactedPersistedMessages: number;
158
+ /**
159
+ * Number of recent ("tail") messages preserved verbatim alongside the
160
+ * summary. Omitted on no-op / skipped results — defaults to 0 at render.
161
+ */
162
+ preservedTailMessages?: number;
158
163
  summaryCalls: number;
159
164
  summaryInputTokens: number;
160
165
  summaryOutputTokens: number;
@@ -327,7 +332,7 @@ export function renderImageManifest(entries: ManifestEntry[]): string {
327
332
  * runtime emitted — typically
328
333
  * `2026-04-02 (Thursday) 01:52:33 -05:00 (America/Chicago)`).
329
334
  */
330
- function extractTurnContextTimestamp(message: Message): string | null {
335
+ export function extractTurnContextTimestamp(message: Message): string | null {
331
336
  if (message.role !== "user") return null;
332
337
  for (const block of message.content) {
333
338
  if (block.type !== "text") continue;
@@ -439,6 +444,44 @@ function resolveTailStartIndex(
439
444
  return null;
440
445
  }
441
446
 
447
+ /**
448
+ * Walk a model-chosen tail index backward until it lands on a user message
449
+ * that does not contain client-side `tool_result` blocks. Prevents the
450
+ * orphan-`tool_result` failure where the matching assistant `tool_use` sits
451
+ * in the discarded prefix and Anthropic rejects the next call with
452
+ * `unexpected tool_use_id found in tool_result blocks`.
453
+ *
454
+ * Walking back (rather than forward) preserves the recent context the model
455
+ * deliberately chose to keep; the tail just expands by the few messages
456
+ * needed to re-anchor the orphaned `tool_result` against its `tool_use`.
457
+ *
458
+ * Returns 0 when the walk falls off the front — the caller treats this as
459
+ * "nothing to compact" via the existing `tailIndex === 0` branch.
460
+ *
461
+ * Only `type === "tool_result"` blocks count. Server-side tools
462
+ * (`server_tool_use` / `web_search_tool_result`) are self-paired inside an
463
+ * assistant message and never trigger an adjustment.
464
+ */
465
+ export function adjustTailIndexForToolPairing(
466
+ messages: Message[],
467
+ tailIndex: number,
468
+ ): number {
469
+ let k = tailIndex;
470
+ while (k > 0) {
471
+ const m = messages[k];
472
+ if (
473
+ m.role === "user" &&
474
+ // guard:allow-tool-result-only — server-side web_search_tool_result is
475
+ // self-paired inside its assistant message and never spans user turns.
476
+ !m.content.some((block) => block.type === "tool_result")
477
+ ) {
478
+ return k;
479
+ }
480
+ k--;
481
+ }
482
+ return 0;
483
+ }
484
+
442
485
  // ---------------------------------------------------------------------------
443
486
  // Retained-image hydration
444
487
  // ---------------------------------------------------------------------------
@@ -655,8 +698,12 @@ export async function runAssistantDrivenCompaction(
655
698
  }
656
699
 
657
700
  const timestamps = buildTimestampIndex(args.messages);
658
- const tailIndex = resolveTailStartIndex(args.messages, timestamps, parsed);
659
- if (tailIndex == null) {
701
+ const resolvedTailIndex = resolveTailStartIndex(
702
+ args.messages,
703
+ timestamps,
704
+ parsed,
705
+ );
706
+ if (resolvedTailIndex == null) {
660
707
  log.warn(
661
708
  {
662
709
  timestamp: parsed.tailStartTimestamp,
@@ -680,6 +727,22 @@ export async function runAssistantDrivenCompaction(
680
727
  };
681
728
  }
682
729
 
730
+ const tailIndex = adjustTailIndexForToolPairing(
731
+ args.messages,
732
+ resolvedTailIndex,
733
+ );
734
+ if (tailIndex !== resolvedTailIndex) {
735
+ log.info(
736
+ {
737
+ conversationId: args.conversationId,
738
+ originalTailIndex: resolvedTailIndex,
739
+ tailIndex,
740
+ walkedBy: resolvedTailIndex - tailIndex,
741
+ },
742
+ "Adjusted compaction tail backward to preserve tool_use/tool_result pairing",
743
+ );
744
+ }
745
+
683
746
  if (tailIndex === 0) {
684
747
  return {
685
748
  ...emptyResult(
@@ -762,6 +825,9 @@ export async function runAssistantDrivenCompaction(
762
825
  compactedMessages: compactableMessages.length,
763
826
  compactedPersistedMessages,
764
827
  tailIndex,
828
+ ...(tailIndex !== resolvedTailIndex
829
+ ? { originalTailIndex: resolvedTailIndex }
830
+ : {}),
765
831
  retainedImages: resolved.length,
766
832
  summaryChars: summaryText.length,
767
833
  },
@@ -780,6 +846,7 @@ export async function runAssistantDrivenCompaction(
780
846
  thresholdTokens,
781
847
  compactedMessages: compactableMessages.length,
782
848
  compactedPersistedMessages,
849
+ preservedTailMessages: args.messages.length - tailIndex,
783
850
  summaryCalls: 1,
784
851
  summaryInputTokens: response.usage.inputTokens,
785
852
  summaryOutputTokens: response.usage.outputTokens,
@@ -885,10 +952,12 @@ export async function runEmergencyCompaction(
885
952
 
886
953
  const splitIndex = findLastToolPairStart(args.messages);
887
954
  if (splitIndex == null || splitIndex === 0) {
888
- log.info(
889
- "Emergency compaction: no tool pair found — falling through",
955
+ log.info("Emergency compaction: no tool pair found — falling through");
956
+ return emptyResult(
957
+ args,
958
+ thresholdTokens,
959
+ "no tool pair for emergency split",
890
960
  );
891
- return emptyResult(args, thresholdTokens, "no tool pair for emergency split");
892
961
  }
893
962
 
894
963
  const keptTail = stripInjectionsForCompaction(
@@ -904,8 +973,7 @@ export async function runEmergencyCompaction(
904
973
  const prefixBudget = args.maxInputTokens - instructionBudget - outputBudget;
905
974
 
906
975
  let prefixEstimate = estimatePromptTokens(prefix, args.systemPrompt, {
907
- providerName:
908
- args.provider.tokenEstimationProvider ?? args.provider.name,
976
+ providerName: args.provider.tokenEstimationProvider ?? args.provider.name,
909
977
  });
910
978
 
911
979
  if (prefixEstimate > prefixBudget && prefix.length > 1) {
@@ -920,10 +988,7 @@ export async function runEmergencyCompaction(
920
988
  // Drop messages from the front until we fit. Keep at least the first
921
989
  // message (may be an existing summary) and try to preserve recent context.
922
990
  let dropCount = 0;
923
- while (
924
- prefixEstimate > prefixBudget &&
925
- dropCount < prefix.length - 1
926
- ) {
991
+ while (prefixEstimate > prefixBudget && dropCount < prefix.length - 1) {
927
992
  dropCount++;
928
993
  const truncated = prefix.slice(dropCount);
929
994
  prefixEstimate = estimatePromptTokens(truncated, args.systemPrompt, {
@@ -1015,7 +1080,8 @@ export async function runEmergencyCompaction(
1015
1080
  compactedMessages: compactedCount,
1016
1081
  keptTailMessages: keptTail.length,
1017
1082
  summaryChars: summaryText.length,
1018
- prefixTruncated: prefix[0]?.content?.[0]?.type === "text" &&
1083
+ prefixTruncated:
1084
+ prefix[0]?.content?.[0]?.type === "text" &&
1019
1085
  (prefix[0].content[0] as { text: string }).text.includes("truncated"),
1020
1086
  },
1021
1087
  "Applied emergency mid-turn compaction",
@@ -1030,6 +1096,7 @@ export async function runEmergencyCompaction(
1030
1096
  thresholdTokens,
1031
1097
  compactedMessages: compactedCount,
1032
1098
  compactedPersistedMessages: Math.max(0, compactedCount - nonPersistedAway),
1099
+ preservedTailMessages: keptTail.length,
1033
1100
  summaryCalls: 1,
1034
1101
  summaryInputTokens: response.usage.inputTokens,
1035
1102
  summaryOutputTokens: response.usage.outputTokens,
@@ -33,18 +33,42 @@ const OTHER_BLOCK_TOKENS = 16;
33
33
  const SYSTEM_PROMPT_OVERHEAD_TOKENS = 8;
34
34
  const GEMINI_INLINE_FILE_MIME_TYPES = new Set(["application/pdf"]);
35
35
 
36
- // Anthropic scales images to fit within 1568x1568 maintaining aspect ratio,
37
- // then charges ~(width * height) / 750 tokens.
38
- const ANTHROPIC_IMAGE_MAX_DIMENSION = 1568;
39
- // Anthropic caps images at ~1.2 megapixels in addition to the 1568px dimension limit.
40
- // Images exceeding this are further scaled down. The docs state images above ~1,600 tokens
41
- // are resized. 1,200,000 / 750 = 1,600 tokens, matching the documented threshold.
42
- // Reference table (max sizes that won't be resized):
36
+ // Dimension-based image token estimate, used as a universal default for every
37
+ // provider. The formula and constants below come from Anthropic's published
38
+ // vision spec — scale to a 1568x1568 bounding box, then charge
39
+ // ~(width * height) / 750 tokens, with a ~1.2-megapixel cap that lands at
40
+ // ~1,600 tokens per image. Reference table (max sizes that won't be resized):
43
41
  // 1:1 → 1092x1092 (~1,590 tokens) 1:2 → 784x1568 (~1,639 tokens)
44
42
  // See: https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size
45
- const ANTHROPIC_IMAGE_MAX_PIXELS = 1_200_000;
46
- const ANTHROPIC_IMAGE_TOKENS_PER_PIXEL = 1 / 750;
47
- const ANTHROPIC_IMAGE_MAX_TOKENS = 1_600;
43
+ //
44
+ // Other multimodal providers (OpenAI/GPT-4V tile pricing, Moonshot/Kimi,
45
+ // Gemini fixed-cost, OpenRouter pass-through) price differently in detail,
46
+ // but every published rate lands in the same hundreds-to-low-thousands range
47
+ // per image. Using this formula as the default gets compaction within ~2-3x
48
+ // of reality instead of the ~30-100x over-counting produced by treating the
49
+ // raw base64 payload as if it were text.
50
+ const IMAGE_MAX_DIMENSION = 1568;
51
+ const IMAGE_MAX_PIXELS = 1_200_000;
52
+ const IMAGE_TOKENS_PER_PIXEL = 1 / 750;
53
+ const IMAGE_MAX_TOKENS = 1_600;
54
+
55
+ // Gemini prices images differently: any side ≤384px counts as a single 258-token
56
+ // tile; anything larger is resized so the longest side is ≤3072px and then
57
+ // split into 768x768 tiles at 258 tokens each. A 4000x4000 image clamps to
58
+ // 3072x3072 → ceil(3072/768)^2 = 16 tiles = 4,128 tokens. Without the clamp
59
+ // we'd over-count it as 36 tiles (~9,288 tokens) and trigger spurious
60
+ // compaction. The clamped 16-tile, 4,128-token figure is also the per-image
61
+ // ceiling we fall back to when dimensions are unparseable (e.g. HEIC/HEIF
62
+ // from iOS attachments) — the generic 1,600 cap can under-count Gemini
63
+ // images by ~2.5x.
64
+ // See: https://ai.google.dev/gemini-api/docs/tokens#multimodal-tokens
65
+ const GEMINI_IMAGE_SMALL_THRESHOLD = 384;
66
+ const GEMINI_IMAGE_TILE_SIZE = 768;
67
+ const GEMINI_IMAGE_TOKENS_PER_TILE = 258;
68
+ const GEMINI_IMAGE_MAX_DIMENSION = 3072;
69
+ const GEMINI_IMAGE_MAX_TOKENS =
70
+ Math.ceil(GEMINI_IMAGE_MAX_DIMENSION / GEMINI_IMAGE_TILE_SIZE) ** 2 *
71
+ GEMINI_IMAGE_TOKENS_PER_TILE;
48
72
 
49
73
  // Anthropic renders each PDF page as an image (~1,568 tokens at standard
50
74
  // resolution) plus any extracted text. Typical PDF pages are 50-150 KB.
@@ -103,45 +127,62 @@ function estimateFileDataTokens(
103
127
  return 0;
104
128
  }
105
129
 
106
- function estimateAnthropicImageTokens(width: number, height: number): number {
130
+ function estimateImageTokensByDimensions(
131
+ width: number,
132
+ height: number,
133
+ ): number {
107
134
  // Step 1: Scale to fit within 1568px bounding box
108
- const dimScale = Math.min(
109
- 1,
110
- ANTHROPIC_IMAGE_MAX_DIMENSION / Math.max(width, height),
111
- );
135
+ const dimScale = Math.min(1, IMAGE_MAX_DIMENSION / Math.max(width, height));
112
136
  let scaledWidth = Math.round(width * dimScale);
113
137
  let scaledHeight = Math.round(height * dimScale);
114
138
 
115
139
  // Step 2: Scale further if exceeds megapixel budget
116
140
  const pixels = scaledWidth * scaledHeight;
117
- if (pixels > ANTHROPIC_IMAGE_MAX_PIXELS) {
118
- const mpScale = Math.sqrt(ANTHROPIC_IMAGE_MAX_PIXELS / pixels);
141
+ if (pixels > IMAGE_MAX_PIXELS) {
142
+ const mpScale = Math.sqrt(IMAGE_MAX_PIXELS / pixels);
119
143
  scaledWidth = Math.round(scaledWidth * mpScale);
120
144
  scaledHeight = Math.round(scaledHeight * mpScale);
121
145
  }
122
146
 
123
- return Math.ceil(
124
- scaledWidth * scaledHeight * ANTHROPIC_IMAGE_TOKENS_PER_PIXEL,
125
- );
147
+ return Math.ceil(scaledWidth * scaledHeight * IMAGE_TOKENS_PER_PIXEL);
148
+ }
149
+
150
+ function estimateGeminiImageTokens(width: number, height: number): number {
151
+ if (
152
+ width <= GEMINI_IMAGE_SMALL_THRESHOLD &&
153
+ height <= GEMINI_IMAGE_SMALL_THRESHOLD
154
+ ) {
155
+ return GEMINI_IMAGE_TOKENS_PER_TILE;
156
+ }
157
+ // Gemini resizes images so the longest side is ≤3072px before tiling.
158
+ const clampedWidth = Math.min(width, GEMINI_IMAGE_MAX_DIMENSION);
159
+ const clampedHeight = Math.min(height, GEMINI_IMAGE_MAX_DIMENSION);
160
+ const tilesWide = Math.ceil(clampedWidth / GEMINI_IMAGE_TILE_SIZE);
161
+ const tilesHigh = Math.ceil(clampedHeight / GEMINI_IMAGE_TILE_SIZE);
162
+ return tilesWide * tilesHigh * GEMINI_IMAGE_TOKENS_PER_TILE;
126
163
  }
127
164
 
128
165
  function estimateImageTokens(
129
166
  block: Extract<ContentBlock, { type: "image" }>,
130
167
  options?: TokenEstimatorOptions,
131
168
  ): number {
132
- if (options?.providerName === "anthropic") {
133
- const dims = parseImageDimensions(
134
- block.source.data,
135
- block.source.media_type,
136
- );
137
- if (dims) {
138
- return estimateAnthropicImageTokens(dims.width, dims.height);
169
+ const dims = parseImageDimensions(block.source.data, block.source.media_type);
170
+ if (dims) {
171
+ if (options?.providerName === "gemini") {
172
+ return estimateGeminiImageTokens(dims.width, dims.height);
139
173
  }
140
- // Fallback: if dimensions can't be parsed, use Anthropic's max
141
- return ANTHROPIC_IMAGE_MAX_TOKENS;
174
+ return estimateImageTokensByDimensions(dims.width, dims.height);
175
+ }
176
+ // Dimensions unparseable (corrupt header, or formats parseImageDimensions
177
+ // doesn't recognize like HEIC/HEIF coming from iOS attachments). Fall back
178
+ // to the per-provider per-image ceiling rather than the raw base64 length,
179
+ // which over-counts by 30-100x. Gemini's tile pricing tops out well above
180
+ // the universal 1,600-token cap, so use its max-tile budget instead to
181
+ // avoid under-counting large iPhone screenshots.
182
+ if (options?.providerName === "gemini") {
183
+ return GEMINI_IMAGE_MAX_TOKENS;
142
184
  }
143
- // Non-Anthropic: keep existing base64-size heuristic
144
- return estimateTextTokens(block.source.data);
185
+ return IMAGE_MAX_TOKENS;
145
186
  }
146
187
 
147
188
  export function estimateContentBlockTokens(