@vellumai/assistant 0.8.12 → 0.9.0-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -56,6 +56,7 @@ import {
56
56
  TimeoutConfigSchema,
57
57
  } from "./schemas/timeouts.js";
58
58
  import { ToolsConfigSchema } from "./schemas/tools.js";
59
+ import { WorkflowsConfigSchema } from "./schemas/workflows.js";
59
60
  import { WorkspaceGitConfigSchema } from "./schemas/workspace-git.js";
60
61
 
61
62
  export const AssistantConfigSchema = z
@@ -121,6 +122,7 @@ export const AssistantConfigSchema = z
121
122
  ),
122
123
  ui: UiConfigSchema.default(UiConfigSchema.parse({})),
123
124
  tools: ToolsConfigSchema.default(ToolsConfigSchema.parse({})),
125
+ workflows: WorkflowsConfigSchema.default(WorkflowsConfigSchema.parse({})),
124
126
  // Per-plugin config blocks keyed by plugin name. The schema is intentionally
125
127
  // permissive — each plugin's manifest supplies its own validator which the
126
128
  // plugin bootstrap (`external-plugins-bootstrap.ts`) runs against the raw
@@ -82,6 +82,13 @@ const CATALOG_RECORD: CatalogRecord = {
82
82
  description: "Handles voice call conversations.",
83
83
  domain: "agentLoop",
84
84
  },
85
+ workflowLeaf: {
86
+ id: "workflowLeaf",
87
+ displayName: "Workflow Leaf",
88
+ description:
89
+ "Runs an ephemeral leaf agent fanned out by the workflow orchestration engine.",
90
+ domain: "agentLoop",
91
+ },
85
92
 
86
93
  // memory
87
94
  memoryExtraction: {
@@ -5,7 +5,10 @@ export const HeartbeatConfigSchema = z
5
5
  .object({
6
6
  enabled: z
7
7
  .boolean({ error: "heartbeat.enabled must be a boolean" })
8
- .default(true)
8
+ // Heartbeats are opt-in for new workspaces. Workspaces created while
9
+ // the default was true keep their behavior via workspace migration
10
+ // 102-preserve-heartbeat-enabled-for-existing-workspaces.
11
+ .default(false)
9
12
  .describe("Whether periodic heartbeat checks are enabled"),
10
13
  intervalMs: z
11
14
  .number({ error: "heartbeat.intervalMs must be a number" })
@@ -13,16 +13,18 @@ import { z } from "zod";
13
13
  // Provider enum
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
- const LLMProvider = z.enum([
17
- "anthropic",
18
- "openai",
19
- "gemini",
20
- "ollama",
21
- "fireworks",
22
- "openrouter",
23
- "openai-compatible",
24
- "minimax",
25
- ]);
16
+ export const LLMProvider = z
17
+ .enum([
18
+ "anthropic",
19
+ "openai",
20
+ "gemini",
21
+ "ollama",
22
+ "fireworks",
23
+ "openrouter",
24
+ "openai-compatible",
25
+ "minimax",
26
+ ])
27
+ .meta({ id: "LLMProvider" });
26
28
  type LLMProvider = z.infer<typeof LLMProvider>;
27
29
 
28
30
  // ---------------------------------------------------------------------------
@@ -78,6 +80,7 @@ export const LLMCallSiteEnum = z.enum([
78
80
  "trustRuleSuggestion",
79
81
  "homeGreeting",
80
82
  "homeSuggestedPrompts",
83
+ "workflowLeaf",
81
84
  ]);
82
85
  export type LLMCallSite = z.infer<typeof LLMCallSiteEnum>;
83
86
 
@@ -349,23 +352,27 @@ export type LLMConfigBase = z.infer<typeof LLMConfigBase>;
349
352
  * objects so callers can override individual leaves (e.g. `{ thinking:
350
353
  * { enabled: false } }`).
351
354
  */
352
- const LLMConfigFragment = z.object({
353
- provider: LLMProvider.optional(),
354
- model: ModelSchema.optional(),
355
- maxTokens: MaxTokensSchema.optional(),
356
- effort: EffortEnum.optional(),
357
- speed: SpeedEnum.optional(),
358
- verbosity: VerbosityEnum.optional(),
359
- temperature: TemperatureSchema.optional(),
360
- thinking: ThinkingFragmentSchema.optional(),
361
- contextWindow: ContextWindowDeepPartialSchema.optional(),
362
- openrouter: OpenRouterDeepPartialSchema.optional(),
363
- logitBias: LogitBiasPresetSchema.optional(),
364
- disableCache: z.boolean().optional(),
365
- });
366
- type LLMConfigFragment = z.infer<typeof LLMConfigFragment>;
355
+ export const LLMConfigFragment = z
356
+ .object({
357
+ provider: LLMProvider.optional(),
358
+ model: ModelSchema.optional(),
359
+ maxTokens: MaxTokensSchema.optional(),
360
+ effort: EffortEnum.optional(),
361
+ speed: SpeedEnum.optional(),
362
+ verbosity: VerbosityEnum.optional(),
363
+ temperature: TemperatureSchema.optional(),
364
+ thinking: ThinkingFragmentSchema.optional(),
365
+ contextWindow: ContextWindowDeepPartialSchema.optional(),
366
+ openrouter: OpenRouterDeepPartialSchema.optional(),
367
+ logitBias: LogitBiasPresetSchema.optional(),
368
+ disableCache: z.boolean().optional(),
369
+ })
370
+ .meta({ id: "LLMConfigFragment" });
371
+ export type LLMConfigFragment = z.infer<typeof LLMConfigFragment>;
367
372
 
368
- export const ProfileStatusSchema = z.enum(["active", "disabled"]);
373
+ export const ProfileStatusSchema = z
374
+ .enum(["active", "disabled"])
375
+ .meta({ id: "ProfileStatus" });
369
376
  export type ProfileStatus = z.infer<typeof ProfileStatusSchema>;
370
377
 
371
378
  // ---------------------------------------------------------------------------
@@ -38,6 +38,25 @@ export const MemoryRetrospectiveConfigSchema = z
38
38
  .describe(
39
39
  "Minimum milliseconds between attempts (success or failure). Prevents tight retry loops across trigger types. Pre-compaction bypasses this gate.",
40
40
  ),
41
+
42
+ keepSupersededRuns: z
43
+ .boolean({
44
+ error: "memory.retrospective.keepSupersededRuns must be a boolean",
45
+ })
46
+ .default(false)
47
+ .describe(
48
+ "When false (default), superseded retrospective conversations are deleted once a newer run succeeds — the persisted remembered_log on memory_retrospective_state is the dedup baseline (the most recent run is scanned only as a fallback for state rows that predate the log column), so older runs are dead weight (fork-based runs each carry a full copy of the source conversation's messages). Operators who want to retain the full run history set this to true; retained runs also skip the startup orphan sweep so they survive restarts.",
49
+ ),
50
+
51
+ matchConversationProfile: z
52
+ .boolean({
53
+ error:
54
+ "memory.retrospective.matchConversationProfile must be a boolean",
55
+ })
56
+ .default(false)
57
+ .describe(
58
+ "When true, fork-based retrospectives run under the source conversation's inference profile (which forkConversation copies onto the fork) instead of the call site's default. Provider prompt caches are byte-exact prefix matches scoped per model, and a thinking enable/disable mismatch invalidates the messages cache tier — so reusing the source's cached prefix requires the retrospective to resolve the SAME model/thinking/effort as the conversation's own turns. Falls back to the call site's default when the conversation has no profile or the referenced profile no longer exists.",
59
+ ),
41
60
  })
42
61
  .describe(
43
62
  "Controls the memory-retrospective background pass. Model selection lives under llm.callSites.memoryRetrospective.",
@@ -110,6 +110,14 @@ export const UiConfigSchema = z
110
110
  .describe(
111
111
  "IANA timezone identifier detected from the client environment for assistant temporal grounding when no manual override is configured (e.g. 'America/New_York'). Use an empty string to clear the setting.",
112
112
  ),
113
+ emptyStateGreetingCacheTtlMs: z
114
+ .number({ error: "ui.emptyStateGreetingCacheTtlMs must be a number" })
115
+ .int("ui.emptyStateGreetingCacheTtlMs must be an integer")
116
+ .min(0, "ui.emptyStateGreetingCacheTtlMs must be >= 0")
117
+ .default(4 * 60 * 60 * 1000)
118
+ .describe(
119
+ "Server-side cache TTL (ms) for the generated empty-state (new-chat) greeting. 0 disables caching, so a fresh greeting is generated on every request.",
120
+ ),
113
121
  })
114
122
  .describe(
115
123
  "User interface display settings. Empty-state greeting model selection lives under llm.callSites.emptyStateGreeting.",
@@ -1,10 +1,13 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import { DEFAULT_IMAGE_MODEL } from "../../media/image-models.js";
3
4
  import { SEARCH_PROVIDER_IDS } from "../../providers/search-provider-catalog.js";
4
5
  import { SttServiceSchema } from "./stt.js";
5
6
  import { TtsServiceSchema } from "./tts.js";
6
7
 
7
- const ServiceModeSchema = z.enum(["managed", "your-own"]);
8
+ export const ServiceModeSchema = z
9
+ .enum(["managed", "your-own"])
10
+ .meta({ id: "ServiceMode" });
8
11
  type ServiceMode = z.infer<typeof ServiceModeSchema>;
9
12
 
10
13
  export const VALID_INFERENCE_PROVIDERS = [
@@ -42,7 +45,7 @@ const InferenceServiceSchema = z.object({});
42
45
 
43
46
  const ImageGenerationServiceSchema = BaseServiceSchema.extend({
44
47
  provider: z.enum(VALID_IMAGE_GEN_PROVIDERS).default("gemini"),
45
- model: z.string().default("gemini-3.1-flash-image-preview"),
48
+ model: z.string().default(DEFAULT_IMAGE_MODEL),
46
49
  });
47
50
 
48
51
  const WebSearchServiceSchema = BaseServiceSchema.extend({
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+
3
+ export const WorkflowsConfigSchema = z
4
+ .object({
5
+ maxAgentsPerRun: z
6
+ .number({ error: "workflows.maxAgentsPerRun must be a number" })
7
+ .int()
8
+ .positive()
9
+ .default(500)
10
+ .describe(
11
+ "Maximum total leaf agents a single workflow run may spawn across all steps",
12
+ ),
13
+ maxConcurrentLeaves: z
14
+ .number({ error: "workflows.maxConcurrentLeaves must be a number" })
15
+ .int()
16
+ .positive()
17
+ .default(6)
18
+ .describe(
19
+ "Maximum number of leaf agents that may run concurrently within a single workflow run",
20
+ ),
21
+ maxConcurrentRuns: z
22
+ .number({ error: "workflows.maxConcurrentRuns must be a number" })
23
+ .int()
24
+ .positive()
25
+ .default(3)
26
+ .describe(
27
+ "Maximum number of workflow runs that may execute concurrently",
28
+ ),
29
+ journalRetentionDays: z
30
+ .number({ error: "workflows.journalRetentionDays must be a number" })
31
+ .int()
32
+ .positive()
33
+ .default(30)
34
+ .describe(
35
+ "Number of days to retain workflow run journals before pruning",
36
+ ),
37
+ })
38
+ .describe(
39
+ "Workflow orchestration engine configuration — caps and concurrency knobs",
40
+ );
41
+
42
+ export type WorkflowsConfig = z.infer<typeof WorkflowsConfigSchema>;
@@ -62,7 +62,7 @@ const SkillMetadataSchema = z
62
62
  * Origin of a skill in the merged catalog.
63
63
  *
64
64
  * - `bundled`: ships inside the assistant binary under `bundled-skills/`.
65
- * - `managed`: installed into `~/.vellum/workspace/skills/` from our catalog.
65
+ * - `managed`: installed into `$VELLUM_WORKSPACE_DIR/skills/` from our catalog.
66
66
  * - `workspace`: user-authored skill living in a conversation's working dir.
67
67
  * - `extra`: third-party directory roots passed via `loadSkillCatalog`'s
68
68
  * `extraDirs` argument (primarily for tests).
@@ -479,7 +479,7 @@ function readSkillFromDirectory(
479
479
  if (isOutsideSkillsRoot(skillsDir, directoryPath)) {
480
480
  log.warn(
481
481
  { directoryPath },
482
- "Skipping skill directory that resolves outside ~/.vellum/workspace/skills",
482
+ "Skipping skill directory that resolves outside $VELLUM_WORKSPACE_DIR/skills",
483
483
  );
484
484
  return null;
485
485
  }
@@ -496,7 +496,7 @@ function readSkillFromDirectory(
496
496
  if (isOutsideSkillsRoot(skillsDir, skillFilePath)) {
497
497
  log.warn(
498
498
  { skillFilePath },
499
- "Skipping SKILL.md that resolves outside ~/.vellum/workspace/skills",
499
+ "Skipping SKILL.md that resolves outside $VELLUM_WORKSPACE_DIR/skills",
500
500
  );
501
501
  return null;
502
502
  }
@@ -43,7 +43,10 @@ import {
43
43
  } from "../runtime/actor-trust-resolver.js";
44
44
  import { getLogger } from "../util/logger.js";
45
45
  import { stripInjectionsForCompaction } from "./strip-injections.js";
46
- import { estimatePromptTokens } from "./token-estimator.js";
46
+ import {
47
+ estimatePromptTokens,
48
+ estimateToolsTokens,
49
+ } from "./token-estimator.js";
47
50
 
48
51
  const log = getLogger("compactor");
49
52
 
@@ -134,7 +137,8 @@ Compress aggressively:
134
137
  For picking where to cut between summary and preserved tail:
135
138
  - Find the last major topic shift or energy change
136
139
  - Keep the active thread of conversation fully intact
137
- - When in doubt, preserve more rather than less
140
+ - Keep the verbatim tail within roughly {tail_budget} tokens so the
141
+ compacted conversation has room to breathe before the next pass
138
142
  - Never cut in the middle of an ongoing discussion
139
143
 
140
144
  IMAGE MANIFEST (images in this conversation):
@@ -181,6 +185,14 @@ export interface CompactionRunArgs {
181
185
  compaction: CompactionConfig;
182
186
  /** Effective context window for the conversation (in tokens). */
183
187
  maxInputTokens: number;
188
+ /**
189
+ * Low-watermark token budget the rebuilt history (summary + verbatim tail)
190
+ * should land at or below after a successful pass. Drives the deterministic
191
+ * forward-cut that advances the model's tail choice until the estimate fits,
192
+ * so one pass buys a long quiet period instead of landing a hair under the
193
+ * trigger. When omitted the forward-cut is skipped (legacy/emergency paths).
194
+ */
195
+ targetTokens?: number;
184
196
  /** Pre-computed estimated input tokens for the live history. */
185
197
  previousEstimatedInputTokens: number;
186
198
  /** Skip the autoThreshold check — fire compaction unconditionally. */
@@ -232,6 +244,19 @@ export interface CompactionRunResult {
232
244
  reason?: string;
233
245
  /** True when the provider call threw and no compaction was applied. */
234
246
  summaryFailed?: boolean;
247
+ /**
248
+ * Set on a successful pass when the deterministic forward-cut advanced to the
249
+ * tail floor (the start of the most recent complete exchange) but the rebuilt
250
+ * history still exceeds `targetTokens`. The verbatim tail alone — the
251
+ * in-flight turn during a tool-heavy round — is over budget, and no boundary
252
+ * the cut can reach fits it. The window-manager's retry loop reads this to
253
+ * stop retrying immediately: a second full-context pass would land on the same
254
+ * floor and free nothing, just paying another full cache write. Omitted (and
255
+ * treated as `false`) on no-op / skipped / legacy (`targetTokens` absent)
256
+ * results. Internal-result-only — not persisted or emitted on any external
257
+ * wire payload.
258
+ */
259
+ tailFloorReached?: boolean;
235
260
  }
236
261
 
237
262
  export interface ParsedCompactionResult {
@@ -551,6 +576,113 @@ export function adjustTailIndexForToolPairing(
551
576
  return 0;
552
577
  }
553
578
 
579
+ /**
580
+ * Whether keeping `messages[index..]` as the verbatim tail lands on a clean
581
+ * boundary: the tail must open on a user turn that does not lead with a
582
+ * client-side `tool_result` (which would orphan its matching `tool_use` in the
583
+ * summarized prefix). This is the forward-walk dual of
584
+ * {@link adjustTailIndexForToolPairing}'s backward walk — the deterministic
585
+ * budget enforcement only advances the cut to indices that satisfy it.
586
+ */
587
+ function isForwardCutBoundary(messages: Message[], index: number): boolean {
588
+ const m = messages[index];
589
+ if (m == null || m.role !== "user") return false;
590
+ // guard:allow-tool-result-only — server-side web_search_tool_result is
591
+ // self-paired inside its assistant message and never spans user turns.
592
+ return !m.content.some((block) => block.type === "tool_result");
593
+ }
594
+
595
+ /** Outcome of the deterministic forward-cut budget enforcement. */
596
+ interface ForwardCutOutcome {
597
+ /** The chosen tail index (≥ `startIndex`, ≤ `floorIndex`). */
598
+ index: number;
599
+ /**
600
+ * True when the cut ran out of forward boundaries — it advanced to (or could
601
+ * not advance past) the tail floor — yet the rebuilt-history estimate still
602
+ * exceeds `targetTokens`. Signals to the retry loop that a second pass cannot
603
+ * do better: the floor is the same and another full-context LLM pass would
604
+ * land on the same over-budget tail. See {@link CompactionRunResult.tailFloorReached}.
605
+ */
606
+ tailFloorReached: boolean;
607
+ }
608
+
609
+ /**
610
+ * Deterministic low-watermark enforcement. Given the model's tail choice
611
+ * (already resolved + back-walked for tool pairing), advance the cut FORWARD —
612
+ * dropping more leading messages into the summarized region, keeping a smaller
613
+ * verbatim tail — until the rebuilt-history estimate fits `targetTokens` or a
614
+ * minimum-tail floor is hit.
615
+ *
616
+ * The floor preserves conversational integrity: it never advances past
617
+ * `floorIndex`, which the caller anchors to the start of the most recent
618
+ * complete user→assistant exchange so the current in-flight turn is never cut.
619
+ * Every candidate cut must pass {@link isForwardCutBoundary} so tool pairs are
620
+ * never orphaned. Returns the original `startIndex` unchanged when the model's
621
+ * own tail already fits the budget (the cut is enforcement, not optimization)
622
+ * or when no forward boundary improves on it.
623
+ *
624
+ * Reports `tailFloorReached` when the cut exhausts its forward boundaries (the
625
+ * loop never broke early on a fit) while the best tail it found is still over
626
+ * `targetTokens` — the floor-dominated case where the verbatim tail (the
627
+ * in-flight turn during a tool-heavy round) alone exceeds the budget. The
628
+ * window-manager reads this to skip a futile second pass.
629
+ */
630
+ function advanceTailForBudget(args: {
631
+ messages: Message[];
632
+ startIndex: number;
633
+ floorIndex: number;
634
+ targetTokens: number;
635
+ estimateTail: (tail: Message[]) => number;
636
+ }): ForwardCutOutcome {
637
+ const { messages, startIndex, floorIndex, targetTokens, estimateTail } = args;
638
+ // The model's own tail choice already fits — keep it untouched. Without this
639
+ // early return the loop below would advance to the first forward boundary
640
+ // regardless (smaller tails also fit), needlessly dropping verbatim messages
641
+ // the budget never required us to drop.
642
+ if (estimateTail(messages.slice(startIndex)) <= targetTokens) {
643
+ return { index: startIndex, tailFloorReached: false };
644
+ }
645
+ let chosen = startIndex;
646
+ let fits = false;
647
+ for (let i = startIndex + 1; i <= floorIndex; i++) {
648
+ if (!isForwardCutBoundary(messages, i)) continue;
649
+ chosen = i;
650
+ const estimate = estimateTail(messages.slice(i));
651
+ if (estimate <= targetTokens) {
652
+ fits = true;
653
+ break;
654
+ }
655
+ }
656
+ return { index: chosen, tailFloorReached: !fits };
657
+ }
658
+
659
+ /**
660
+ * Anchor index for the forward-cut floor: the start of the most recent
661
+ * complete user→assistant exchange. The deterministic budget enforcement never
662
+ * advances the cut past this point, so a single pass always preserves at least
663
+ * the latest finished exchange and never bites into the current in-flight turn.
664
+ *
665
+ * Walks back from the end to the last assistant message, then back to the user
666
+ * message that opens its exchange (the nearest preceding clean user boundary).
667
+ * Falls back to the model's chosen `tailIndex` when no such exchange exists
668
+ * (e.g. the tail is a single in-flight turn), which makes the enforcement a
669
+ * no-op rather than cutting too aggressively.
670
+ */
671
+ function resolveTailFloorIndex(messages: Message[], tailIndex: number): number {
672
+ let lastAssistant = -1;
673
+ for (let i = messages.length - 1; i > tailIndex; i--) {
674
+ if (messages[i].role === "assistant") {
675
+ lastAssistant = i;
676
+ break;
677
+ }
678
+ }
679
+ if (lastAssistant < 0) return tailIndex;
680
+ for (let i = lastAssistant - 1; i > tailIndex; i--) {
681
+ if (isForwardCutBoundary(messages, i)) return i;
682
+ }
683
+ return tailIndex;
684
+ }
685
+
554
686
  // ---------------------------------------------------------------------------
555
687
  // Retained-image hydration
556
688
  // ---------------------------------------------------------------------------
@@ -618,12 +750,22 @@ function guessMimeFromFilename(filename: string): string {
618
750
  export function buildInstructionMessage(
619
751
  customPrompt: string | null | undefined,
620
752
  imageManifest: string,
753
+ tailBudgetTokens?: number,
621
754
  ): Message {
622
755
  const template =
623
756
  customPrompt && customPrompt.trim().length > 0
624
757
  ? customPrompt
625
758
  : DEFAULT_COMPACTION_PROMPT;
626
- const text = template.replace("{image_manifest}", imageManifest);
759
+ // `{tail_budget}` is a soft nudge — the deterministic forward-cut downstream
760
+ // is what actually enforces the budget. Custom prompts without the
761
+ // placeholder render unchanged; the default prompt always carries it.
762
+ const tailBudgetText =
763
+ tailBudgetTokens != null && tailBudgetTokens > 0
764
+ ? String(tailBudgetTokens)
765
+ : "as few as needed";
766
+ const text = template
767
+ .replace("{image_manifest}", imageManifest)
768
+ .replace("{tail_budget}", tailBudgetText);
627
769
  return {
628
770
  role: "user",
629
771
  content: [{ type: "text", text }],
@@ -736,6 +878,7 @@ export async function runAssistantDrivenCompaction(
736
878
  const instruction = buildInstructionMessage(
737
879
  args.compaction.prompt ?? null,
738
880
  manifestText,
881
+ args.targetTokens,
739
882
  );
740
883
 
741
884
  const requestMessages = buildCompactionRequest(args.messages, instruction);
@@ -826,22 +969,141 @@ export async function runAssistantDrivenCompaction(
826
969
  };
827
970
  }
828
971
 
829
- const tailIndex = adjustTailIndexForToolPairing(
972
+ const pairedTailIndex = adjustTailIndexForToolPairing(
830
973
  args.messages,
831
974
  resolvedTailIndex,
832
975
  );
833
- if (tailIndex !== resolvedTailIndex) {
976
+ if (pairedTailIndex !== resolvedTailIndex) {
834
977
  log.info(
835
978
  {
836
979
  conversationId: args.conversationId,
837
980
  originalTailIndex: resolvedTailIndex,
838
- tailIndex,
839
- walkedBy: resolvedTailIndex - tailIndex,
981
+ tailIndex: pairedTailIndex,
982
+ walkedBy: resolvedTailIndex - pairedTailIndex,
840
983
  },
841
984
  "Adjusted compaction tail backward to preserve tool_use/tool_result pairing",
842
985
  );
843
986
  }
844
987
 
988
+ const summaryText = buildSummaryMemoryText(parsed.summary, parsed.keyState);
989
+ // The durable summary. When the forward-cut below advances the tail, a
990
+ // truncation notice is appended HERE — not just on the in-memory message —
991
+ // because `applyCompactionResult` persists and rehydrates the summary from
992
+ // the result's `summaryText`: a notice only on the message would vanish on
993
+ // reload/fork, silently hiding that the dropped span was never summarized.
994
+ let finalSummaryText = summaryText;
995
+ let summaryMessage: Message = {
996
+ role: "assistant",
997
+ content: [{ type: "text", text: finalSummaryText }],
998
+ };
999
+
1000
+ const {
1001
+ blocks: retainedImageBlocks,
1002
+ resolved,
1003
+ missing,
1004
+ } = buildRetainedImageBlocks(parsed.retainedImageFilenames, manifest);
1005
+ if (missing.length > 0) {
1006
+ log.warn(
1007
+ { missing },
1008
+ "Compaction referenced images that could not be resolved against attachments — dropping",
1009
+ );
1010
+ }
1011
+
1012
+ const retainedImageMessage: Message | null =
1013
+ retainedImageBlocks.length > 0
1014
+ ? {
1015
+ role: "user",
1016
+ content: [
1017
+ {
1018
+ type: "text" as const,
1019
+ text: "Images retained from the compacted portion of the conversation:",
1020
+ },
1021
+ ...retainedImageBlocks,
1022
+ ],
1023
+ }
1024
+ : null;
1025
+
1026
+ // Deterministic low-watermark enforcement. The model was asked to keep the
1027
+ // tail within the budget, but it routinely keeps a fat tail in repetitive
1028
+ // conversations, so each pass would otherwise free almost nothing and the
1029
+ // history bounces back over the trigger within a tick or two. When the
1030
+ // rebuilt history (summary + retained images + verbatim tail) still exceeds
1031
+ // `targetTokens`, advance the cut forward — onto clean user-turn boundaries
1032
+ // only — until it fits or the most-recent-complete-exchange floor is hit.
1033
+ // No second LLM call: the summary stays as written, and the span dropped
1034
+ // between the model's cut and the enforced cut is acknowledged with an
1035
+ // explicit truncation notice appended to the summary message (see below),
1036
+ // so the loss is visible in-context rather than silent.
1037
+ let tailIndex = pairedTailIndex;
1038
+ // Whether the deterministic forward-cut hit the tail floor while still over
1039
+ // `targetTokens` — propagated onto the success result so the window-manager's
1040
+ // retry loop can skip a futile second full-context pass (the floor is the same
1041
+ // next time, so it cannot do better). Only meaningful when a `targetTokens`
1042
+ // budget drove the forward-cut.
1043
+ let tailFloorReached = false;
1044
+ if (args.targetTokens != null && pairedTailIndex > 0) {
1045
+ const providerName =
1046
+ args.provider.tokenEstimationProvider ?? args.provider.name;
1047
+ // Mirror the window-manager's post-compaction estimate (system prompt +
1048
+ // tools + messages) so the forward-cut targets the same number the manager
1049
+ // recomputes on return — otherwise the cut would under-count by the tool
1050
+ // budget and land short of the real low-watermark.
1051
+ const toolTokenBudget = args.tools ? estimateToolsTokens(args.tools) : 0;
1052
+ const fixedPrefix: Message[] = [summaryMessage];
1053
+ if (retainedImageMessage) fixedPrefix.push(retainedImageMessage);
1054
+ const estimateRebuilt = (tail: Message[]): number =>
1055
+ estimatePromptTokens(
1056
+ [...fixedPrefix, ...stripInjectionsForCompaction(tail)],
1057
+ args.systemPrompt,
1058
+ { providerName, toolTokenBudget },
1059
+ );
1060
+ const floorIndex = resolveTailFloorIndex(args.messages, pairedTailIndex);
1061
+ const advanced = advanceTailForBudget({
1062
+ messages: args.messages,
1063
+ startIndex: pairedTailIndex,
1064
+ floorIndex,
1065
+ targetTokens: args.targetTokens,
1066
+ estimateTail: estimateRebuilt,
1067
+ });
1068
+ tailFloorReached = advanced.tailFloorReached;
1069
+ if (advanced.index !== pairedTailIndex) {
1070
+ tailIndex = advanced.index;
1071
+ // The LLM wrote its summary believing the verbatim tail would start at
1072
+ // `pairedTailIndex`, so the messages in [pairedTailIndex, tailIndex) are
1073
+ // covered by neither the summary's detail nor the retained tail. Append
1074
+ // a deterministic truncation notice so the loss is visible in-context
1075
+ // instead of silent — the conversation can recompute or recall what it
1076
+ // needs rather than acting on a gap it doesn't know exists. The notice
1077
+ // is a few dozen tokens against a multi-thousand-token target, so the
1078
+ // budget estimate above remains effectively accurate.
1079
+ const dropped = args.messages.slice(pairedTailIndex, tailIndex);
1080
+ const droppedUser = dropped.filter((m) => m.role === "user").length;
1081
+ const droppedAssistant = dropped.length - droppedUser;
1082
+ const truncationNote =
1083
+ `\n\n[Context budget enforcement: ${dropped.length} message(s) ` +
1084
+ `(${droppedUser} user, ${droppedAssistant} assistant) between this ` +
1085
+ `summary and the retained tail were truncated to fit the context ` +
1086
+ `budget and are not covered in detail above.]`;
1087
+ finalSummaryText = summaryText + truncationNote;
1088
+ summaryMessage = {
1089
+ role: "assistant",
1090
+ content: [{ type: "text", text: finalSummaryText }],
1091
+ };
1092
+ log.info(
1093
+ {
1094
+ conversationId: args.conversationId,
1095
+ modelTailIndex: pairedTailIndex,
1096
+ tailIndex,
1097
+ floorIndex,
1098
+ droppedFromTail: dropped.length,
1099
+ targetTokens: args.targetTokens,
1100
+ tailFloorReached,
1101
+ },
1102
+ "Advanced compaction tail forward to meet low-watermark token budget",
1103
+ );
1104
+ }
1105
+ }
1106
+
845
1107
  if (tailIndex === 0) {
846
1108
  return {
847
1109
  ...emptyResult(
@@ -876,37 +1138,8 @@ export async function runAssistantDrivenCompaction(
876
1138
  args.messages.slice(tailIndex),
877
1139
  );
878
1140
 
879
- const summaryText = buildSummaryMemoryText(parsed.summary, parsed.keyState);
880
- const summaryMessage: Message = {
881
- role: "assistant",
882
- content: [{ type: "text", text: summaryText }],
883
- };
884
-
885
- const {
886
- blocks: retainedImageBlocks,
887
- resolved,
888
- missing,
889
- } = buildRetainedImageBlocks(parsed.retainedImageFilenames, manifest);
890
- if (missing.length > 0) {
891
- log.warn(
892
- { missing },
893
- "Compaction referenced images that could not be resolved against attachments — dropping",
894
- );
895
- }
896
-
897
1141
  const compactedMessages: Message[] = [summaryMessage];
898
- if (retainedImageBlocks.length > 0) {
899
- compactedMessages.push({
900
- role: "user",
901
- content: [
902
- {
903
- type: "text" as const,
904
- text: "Images retained from the compacted portion of the conversation:",
905
- },
906
- ...retainedImageBlocks,
907
- ],
908
- });
909
- }
1142
+ if (retainedImageMessage) compactedMessages.push(retainedImageMessage);
910
1143
  compactedMessages.push(...tailMessages);
911
1144
 
912
1145
  const nonPersistedCompactedAway = Math.min(
@@ -928,7 +1161,7 @@ export async function runAssistantDrivenCompaction(
928
1161
  ? { originalTailIndex: resolvedTailIndex }
929
1162
  : {}),
930
1163
  retainedImages: resolved.length,
931
- summaryChars: summaryText.length,
1164
+ summaryChars: finalSummaryText.length,
932
1165
  },
933
1166
  "Applied assistant-driven compaction",
934
1167
  );
@@ -956,9 +1189,10 @@ export async function runAssistantDrivenCompaction(
956
1189
  response.usage.cacheCreationInputTokens ?? 0,
957
1190
  summaryCacheReadInputTokens: response.usage.cacheReadInputTokens ?? 0,
958
1191
  summaryRawResponses: response.rawResponse ? [response.rawResponse] : [],
959
- summaryText,
1192
+ summaryText: finalSummaryText,
960
1193
  keyState: parsed.keyState,
961
1194
  summaryFailed: false,
1195
+ tailFloorReached,
962
1196
  };
963
1197
  }
964
1198