@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
@@ -35,6 +35,7 @@ import { getDb } from "../memory/db-connection.js";
35
35
  import { initializeDb } from "../memory/db-init.js";
36
36
  import {
37
37
  getCompactionLogsBetween,
38
+ getRequestLogMetaById,
38
39
  recordRequestLog,
39
40
  } from "../memory/llm-request-log-store.js";
40
41
  import { llmRequestLogs } from "../memory/schema.js";
@@ -56,10 +57,11 @@ function insertLogAt(
56
57
  conversationId: string,
57
58
  createdAt: number,
58
59
  callSite: "mainAgent" | "compactionAgent" | null,
60
+ requestPayload = "{}",
59
61
  ): string {
60
62
  const id = recordRequestLog(
61
63
  conversationId,
62
- "{}",
64
+ requestPayload,
63
65
  "{}",
64
66
  undefined,
65
67
  "anthropic",
@@ -183,4 +185,75 @@ describe("getCompactionLogsBetween", () => {
183
185
  const result = getCompactionLogsBetween("nonexistent-conv", null, 500);
184
186
  expect(result).toEqual([]);
185
187
  });
188
+
189
+ test("computes requestMessageCount in SQL without returning the request payload", () => {
190
+ // GIVEN compaction rows whose request payloads use each provider
191
+ // shape (Anthropic/OpenAI `messages`, Gemini `contents`, OpenAI
192
+ // Responses `input`), plus rows where the count can't be derived
193
+ const conv = createConversation("msg-count");
194
+ insertLogAt(
195
+ conv.id,
196
+ 100,
197
+ "compactionAgent",
198
+ JSON.stringify({ messages: [{}, {}, {}] }),
199
+ );
200
+ insertLogAt(
201
+ conv.id,
202
+ 200,
203
+ "compactionAgent",
204
+ JSON.stringify({ contents: [{}, {}] }),
205
+ );
206
+ insertLogAt(
207
+ conv.id,
208
+ 300,
209
+ "compactionAgent",
210
+ JSON.stringify({ input: [{}] }),
211
+ );
212
+ insertLogAt(conv.id, 400, "compactionAgent", "not-json{{{");
213
+ insertLogAt(conv.id, 500, "compactionAgent", JSON.stringify({}));
214
+
215
+ // WHEN querying the trail window
216
+ const result = getCompactionLogsBetween(conv.id, null, 600);
217
+
218
+ // THEN each row carries the SQL-computed message count (null when
219
+ // the payload is malformed or has no known message array)
220
+ expect(result.map((r) => r.requestMessageCount)).toEqual([
221
+ 3,
222
+ 2,
223
+ 1,
224
+ null,
225
+ null,
226
+ ]);
227
+ // AND the rows never include the request payload column
228
+ for (const row of result) {
229
+ expect("requestPayload" in row).toBe(false);
230
+ }
231
+ });
232
+ });
233
+
234
+ describe("getRequestLogMetaById", () => {
235
+ beforeEach(() => {
236
+ resetTables();
237
+ });
238
+
239
+ test("returns the row's metadata without either payload column", () => {
240
+ // GIVEN a stored log row
241
+ const conv = createConversation("meta-lookup");
242
+ const id = insertLogAt(conv.id, 100, "compactionAgent", '{"big":true}');
243
+
244
+ // WHEN looking it up by id
245
+ const row = getRequestLogMetaById(id);
246
+
247
+ // THEN the metadata comes back payload-free
248
+ expect(row).not.toBeNull();
249
+ expect(row!.id).toBe(id);
250
+ expect(row!.conversationId).toBe(conv.id);
251
+ expect(row!.createdAt).toBe(100);
252
+ expect("requestPayload" in row!).toBe(false);
253
+ expect("responsePayload" in row!).toBe(false);
254
+ });
255
+
256
+ test("returns null for an unknown id", () => {
257
+ expect(getRequestLogMetaById("nonexistent")).toBeNull();
258
+ });
186
259
  });
@@ -4,21 +4,54 @@
4
4
  * Measures compaction cost with a mock provider:
5
5
  * - compaction latency under threshold pressure
6
6
  * - no-op fast path for below-threshold histories
7
- * - token reduction below target budget
7
+ * - token reduction below the low-watermark target budget
8
8
  * - single-pass summarization (exactly 1 call)
9
- * - severe pressure overriding cooldown
10
9
  */
11
10
  import { describe, expect, mock, test } from "bun:test";
12
11
 
12
+ mock.module("../util/logger.js", () => ({
13
+ getLogger: () =>
14
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
15
+ }));
16
+
17
+ // The compactor reads the conversation's image attachments from the DB to
18
+ // build its manifest; with no images these return empty.
19
+ mock.module("../memory/conversation-crud.js", () => ({
20
+ getMessages: () => [],
21
+ }));
22
+ mock.module("../memory/attachments-store.js", () => ({
23
+ getAttachmentMetadataForMessage: () => [],
24
+ getAttachmentContent: () => null,
25
+ }));
26
+ mock.module("../memory/llm-request-log-store.js", () => ({
27
+ recordRequestLog: () => {},
28
+ }));
29
+
13
30
  import { DEFAULT_CONFIG } from "../config/defaults.js";
14
31
  import { estimatePromptTokens } from "../context/token-estimator.js";
15
32
  import { ContextWindowManager } from "../plugins/defaults/compaction/window-manager.js";
16
33
  import type { Message, Provider } from "../providers/types.js";
17
34
 
18
- mock.module("../util/logger.js", () => ({
19
- getLogger: () =>
20
- new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
21
- }));
35
+ // Synthetic per-turn timestamp so the model's tail_start resolves to a message
36
+ // index. Turn 10 anchors a fat verbatim tail near the front, forcing the
37
+ // deterministic forward-cut to advance to meet the low-watermark budget.
38
+ function turnTimestamp(turn: number): string {
39
+ const hour = String(10 + Math.floor(turn / 60)).padStart(2, "0");
40
+ const minute = String(turn % 60).padStart(2, "0");
41
+ return `2026-05-21 (Thursday) ${hour}:${minute}:00 -05:00 (America/Chicago)`;
42
+ }
43
+
44
+ function compactionResponse(callIndex: number): string {
45
+ return `<compaction_result>
46
+ <summary>
47
+ Compacted summary, pass ${callIndex}. Goals preserved, constraints noted.
48
+ </summary>
49
+ <key_state>
50
+ - Nothing critical pending.
51
+ </key_state>
52
+ <tail_start timestamp="${turnTimestamp(10)}" preview="[U10] User message with enough" />
53
+ </compaction_result>`;
54
+ }
22
55
 
23
56
  function makeSummaryProvider(counter: { calls: number }): Provider {
24
57
  return {
@@ -26,12 +59,7 @@ function makeSummaryProvider(counter: { calls: number }): Provider {
26
59
  async sendMessage() {
27
60
  counter.calls += 1;
28
61
  return {
29
- content: [
30
- {
31
- type: "text",
32
- text: `## Goals\n- Preserve state\n## Constraints\n- Keep PRs small\n## Decisions\n- Call ${counter.calls}`,
33
- },
34
- ],
62
+ content: [{ type: "text", text: compactionResponse(counter.calls) }],
35
63
  model: "mock-model",
36
64
  usage: { inputTokens: 420, outputTokens: 85 },
37
65
  stopReason: "end_turn",
@@ -48,7 +76,9 @@ function makeLongMessages(turns: number): Message[] {
48
76
  content: [
49
77
  {
50
78
  type: "text",
51
- text: `[U${i}] User message with enough content to estimate tokens. Topic ${
79
+ text: `<turn_context>\ncurrent_time: ${turnTimestamp(
80
+ i,
81
+ )}\n</turn_context>\n[U${i}] User message with enough content to estimate tokens. Topic ${
52
82
  i % 9
53
83
  }.`,
54
84
  },
@@ -73,31 +103,35 @@ function makeConfig() {
73
103
  return {
74
104
  ...DEFAULT_CONFIG.llm.default.contextWindow,
75
105
  maxInputTokens: 6000,
76
- targetBudgetRatio: 0.58,
106
+ targetBudgetRatio: 0.4,
77
107
  compactThreshold: 0.6,
78
108
  summaryBudgetRatio: 0.05,
79
109
  };
80
110
  }
81
111
 
112
+ function makeManager(provider: Provider): ContextWindowManager {
113
+ return new ContextWindowManager({
114
+ provider,
115
+ systemPrompt: "system prompt",
116
+ config: makeConfig(),
117
+ conversationId: "conv-benchmark",
118
+ });
119
+ }
120
+
82
121
  describe("Compaction benchmark", () => {
83
122
  test("compaction with mock provider completes under 500ms", async () => {
84
123
  const counter = { calls: 0 };
85
124
  const provider = makeSummaryProvider(counter);
86
125
  const config = makeConfig();
87
- const manager = new ContextWindowManager({
88
- provider,
89
- systemPrompt: "system prompt",
90
- config,
91
- });
126
+ const manager = makeManager(provider);
92
127
 
93
128
  // 90 turns = 180 messages, well above 60% of 6000 = 3600 threshold
94
129
  const messages = makeLongMessages(90);
95
130
  const before = estimatePromptTokens(messages, "system prompt", {
96
131
  providerName: "mock",
97
132
  });
98
- expect(before).toBeGreaterThan(
99
- config.maxInputTokens * config.compactThreshold,
100
- );
133
+ // Real loader's default compaction.autoThreshold is 0.7.
134
+ expect(before).toBeGreaterThan(config.maxInputTokens * 0.7);
101
135
 
102
136
  const start = performance.now();
103
137
  const result = await manager.maybeCompact(messages);
@@ -110,12 +144,7 @@ describe("Compaction benchmark", () => {
110
144
  test("below-threshold check returns in under 50ms (no-op)", async () => {
111
145
  const counter = { calls: 0 };
112
146
  const provider = makeSummaryProvider(counter);
113
- const config = makeConfig();
114
- const manager = new ContextWindowManager({
115
- provider,
116
- systemPrompt: "system prompt",
117
- config,
118
- });
147
+ const manager = makeManager(provider);
119
148
 
120
149
  // 3 turns = 6 messages, well below threshold
121
150
  const messages = makeLongMessages(3);
@@ -125,7 +154,7 @@ describe("Compaction benchmark", () => {
125
154
  const elapsed = performance.now() - start;
126
155
 
127
156
  expect(result.compacted).toBe(false);
128
- expect(result.reason).toBe("below compaction threshold");
157
+ expect(result.reason).toBe("below auto threshold");
129
158
  expect(elapsed).toBeLessThan(50);
130
159
  expect(counter.calls).toBe(0);
131
160
  });
@@ -134,11 +163,7 @@ describe("Compaction benchmark", () => {
134
163
  const counter = { calls: 0 };
135
164
  const provider = makeSummaryProvider(counter);
136
165
  const config = makeConfig();
137
- const manager = new ContextWindowManager({
138
- provider,
139
- systemPrompt: "system prompt",
140
- config,
141
- });
166
+ const manager = makeManager(provider);
142
167
 
143
168
  const messages = makeLongMessages(90);
144
169
  const result = await manager.maybeCompact(messages);
@@ -147,7 +172,9 @@ describe("Compaction benchmark", () => {
147
172
  expect(result.estimatedInputTokens).toBeLessThan(
148
173
  result.previousEstimatedInputTokens,
149
174
  );
150
- // Target is maxInputTokens * (targetBudgetRatio - summaryBudgetRatio)
175
+ // The deterministic forward-cut drives the rebuilt history at or below the
176
+ // low-watermark budget: maxInputTokens * (targetBudgetRatio -
177
+ // summaryBudgetRatio).
151
178
  const targetTokens = Math.floor(
152
179
  config.maxInputTokens *
153
180
  (config.targetBudgetRatio - config.summaryBudgetRatio),
@@ -158,12 +185,7 @@ describe("Compaction benchmark", () => {
158
185
  test("single-pass summarization makes exactly 1 summary call", async () => {
159
186
  const counter = { calls: 0 };
160
187
  const provider = makeSummaryProvider(counter);
161
- const config = makeConfig();
162
- const manager = new ContextWindowManager({
163
- provider,
164
- systemPrompt: "system prompt",
165
- config,
166
- });
188
+ const manager = makeManager(provider);
167
189
 
168
190
  const messages = makeLongMessages(90);
169
191
  const result = await manager.maybeCompact(messages);
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Deterministic low-watermark forward-cut enforcement in
3
+ * `runAssistantDrivenCompaction`.
4
+ *
5
+ * When the model keeps a fat verbatim tail, the compactor advances the cut
6
+ * forward (dropping more leading messages into the summarized region) until the
7
+ * rebuilt-history estimate fits `targetTokens` — while never cutting past the
8
+ * most-recent-complete-exchange floor and never orphaning tool_use/tool_result
9
+ * pairs.
10
+ */
11
+ import { describe, expect, mock, test } from "bun:test";
12
+
13
+ function makeLoggerStub(): Record<string, unknown> {
14
+ const stub: Record<string, unknown> = {};
15
+ for (const m of [
16
+ "info",
17
+ "warn",
18
+ "error",
19
+ "debug",
20
+ "trace",
21
+ "fatal",
22
+ "silent",
23
+ "child",
24
+ ]) {
25
+ stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
26
+ }
27
+ return stub;
28
+ }
29
+
30
+ mock.module("../util/logger.js", () => ({
31
+ getLogger: () => makeLoggerStub(),
32
+ }));
33
+
34
+ mock.module("../memory/conversation-crud.js", () => ({
35
+ getMessages: () => [],
36
+ }));
37
+
38
+ mock.module("../memory/attachments-store.js", () => ({
39
+ getAttachmentMetadataForMessage: () => [],
40
+ getAttachmentContent: () => null,
41
+ }));
42
+
43
+ mock.module("../memory/llm-request-log-store.js", () => ({
44
+ recordRequestLog: () => {},
45
+ }));
46
+
47
+ import { runAssistantDrivenCompaction } from "../context/compactor.js";
48
+ import { estimatePromptTokens } from "../context/token-estimator.js";
49
+ import type { Message, Provider } from "../providers/types.js";
50
+
51
+ function turnTimestamp(turn: number): string {
52
+ const hour = String(10 + Math.floor(turn / 60)).padStart(2, "0");
53
+ const minute = String(turn % 60).padStart(2, "0");
54
+ return `2026-05-21 (Thursday) ${hour}:${minute}:00 -05:00 (America/Chicago)`;
55
+ }
56
+
57
+ function userTurn(turn: number, body: string): Message {
58
+ return {
59
+ role: "user",
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: `<turn_context>\ncurrent_time: ${turnTimestamp(
64
+ turn,
65
+ )}\n</turn_context>\n[U${turn}] ${body}`,
66
+ },
67
+ ],
68
+ };
69
+ }
70
+
71
+ function assistantTurn(turn: number, body: string): Message {
72
+ return {
73
+ role: "assistant",
74
+ content: [{ type: "text", text: `[A${turn}] ${body}` }],
75
+ };
76
+ }
77
+
78
+ function makeProvider(response: string): Provider {
79
+ return {
80
+ name: "mock-provider",
81
+ sendMessage: async () => ({
82
+ content: [{ type: "text", text: response }],
83
+ model: "mock-model",
84
+ usage: { inputTokens: 100, outputTokens: 50 },
85
+ stopReason: "end_turn",
86
+ }),
87
+ };
88
+ }
89
+
90
+ const SUMMARY = "Earlier turns summarized in the assistant's own voice.";
91
+
92
+ function compactionResponse(tailTurn: number, preview: string): string {
93
+ return `<compaction_result>
94
+ <summary>
95
+ ${SUMMARY}
96
+ </summary>
97
+ <key_state>
98
+ - Nothing critical pending.
99
+ </key_state>
100
+ <tail_start timestamp="${turnTimestamp(tailTurn)}" preview="${preview}" />
101
+ </compaction_result>`;
102
+ }
103
+
104
+ describe("runAssistantDrivenCompaction — low-watermark forward cut", () => {
105
+ test("advances the model's tail forward until the rebuilt history fits the target", async () => {
106
+ // 40 turns = 80 messages, each user body is heavy so the verbatim tail is
107
+ // expensive. The model anchors its tail at turn 4 (keeping ~72 messages);
108
+ // the forward-cut must advance far past that to meet the target.
109
+ const messages: Message[] = [];
110
+ for (let i = 0; i < 40; i++) {
111
+ messages.push(
112
+ userTurn(
113
+ i,
114
+ "User message with a fairly long body so that each turn carries real token weight in the estimate. ".repeat(
115
+ 3,
116
+ ),
117
+ ),
118
+ );
119
+ messages.push(
120
+ assistantTurn(
121
+ i,
122
+ "Assistant reply, also reasonably long to contribute estimate weight. ".repeat(
123
+ 3,
124
+ ),
125
+ ),
126
+ );
127
+ }
128
+
129
+ const targetTokens = 1500;
130
+ const result = await runAssistantDrivenCompaction({
131
+ conversationId: "conv-test",
132
+ messages,
133
+ provider: makeProvider(
134
+ compactionResponse(4, "User message with a fairly long body"),
135
+ ),
136
+ systemPrompt: "system",
137
+ compaction: { enabled: true, autoThreshold: 0.7 },
138
+ maxInputTokens: 100_000,
139
+ targetTokens,
140
+ previousEstimatedInputTokens: 90_000,
141
+ });
142
+
143
+ expect(result.compacted).toBe(true);
144
+
145
+ // The rebuilt history (summary + verbatim tail) lands at or below target.
146
+ const estimate = estimatePromptTokens(result.messages, "system", {
147
+ providerName: "mock-provider",
148
+ });
149
+ expect(estimate).toBeLessThanOrEqual(targetTokens);
150
+
151
+ // The forward-cut advanced well past the model's chosen anchor (turn 4 →
152
+ // index 8): far fewer tail messages survive than the model kept.
153
+ expect(result.preservedTailMessages).toBeLessThan(messages.length - 8);
154
+ expect(result.preservedTailMessages).toBeGreaterThan(0);
155
+
156
+ // The cut found a tail that fits the budget, so the floor was not the
157
+ // binding constraint — no futile-retry signal.
158
+ expect(result.tailFloorReached).toBe(false);
159
+ });
160
+
161
+ test("never advances past the most-recent-complete-exchange floor", async () => {
162
+ // A small conversation where the target is unreachably tiny: the cut must
163
+ // still preserve at least the last complete user→assistant exchange.
164
+ const messages: Message[] = [];
165
+ for (let i = 0; i < 6; i++) {
166
+ messages.push(userTurn(i, "short user turn body here"));
167
+ messages.push(assistantTurn(i, "short assistant reply body here"));
168
+ }
169
+
170
+ const result = await runAssistantDrivenCompaction({
171
+ conversationId: "conv-test",
172
+ messages,
173
+ provider: makeProvider(
174
+ compactionResponse(1, "short user turn body here"),
175
+ ),
176
+ systemPrompt: "system",
177
+ compaction: { enabled: true, autoThreshold: 0.7 },
178
+ maxInputTokens: 100_000,
179
+ // Target far below anything achievable — forces the cut to the floor.
180
+ targetTokens: 1,
181
+ previousEstimatedInputTokens: 90_000,
182
+ });
183
+
184
+ expect(result.compacted).toBe(true);
185
+ // The last complete exchange (final user + final assistant) survives: the
186
+ // floor is the start of that exchange, so at least 2 tail messages remain.
187
+ expect(result.preservedTailMessages).toBeGreaterThanOrEqual(2);
188
+ // The tail still opens on a user turn (clean forward-cut boundary).
189
+ const tail = result.messages.slice(
190
+ result.messages.length - (result.preservedTailMessages ?? 0),
191
+ );
192
+ expect(tail[0]?.role).toBe("user");
193
+ // The cut advanced to the floor but the surviving tail is still over the
194
+ // (unreachably tiny) target — this is the floor-dominated case the
195
+ // window-manager uses to skip a futile retry.
196
+ expect(result.tailFloorReached).toBe(true);
197
+ });
198
+
199
+ test("forward cut lands on a clean user boundary, never orphaning a tool_result", async () => {
200
+ // Build: [u0,a0, u1,a1(tool_use X), u2(tool_result X), a2, u3,a3].
201
+ // The model anchors at turn 0; a naive forward cut could land on the
202
+ // tool_result-only user message and orphan tool_use X. The boundary check
203
+ // must skip it.
204
+ const toolUse: Message = {
205
+ role: "assistant",
206
+ content: [{ type: "tool_use", id: "X", name: "Bash", input: {} }],
207
+ };
208
+ const toolResult: Message = {
209
+ role: "user",
210
+ content: [{ type: "tool_result", tool_use_id: "X", content: "ok" }],
211
+ };
212
+ const messages: Message[] = [
213
+ userTurn(
214
+ 0,
215
+ "first user turn with a long body to weigh the estimate. ".repeat(4),
216
+ ),
217
+ assistantTurn(0, "first assistant reply with a long body. ".repeat(4)),
218
+ userTurn(
219
+ 1,
220
+ "second user turn, also long, to add estimate weight. ".repeat(4),
221
+ ),
222
+ toolUse,
223
+ toolResult,
224
+ assistantTurn(2, "assistant continues after the tool result. ".repeat(4)),
225
+ userTurn(3, "final user turn body"),
226
+ assistantTurn(3, "final assistant reply body"),
227
+ ];
228
+
229
+ const result = await runAssistantDrivenCompaction({
230
+ conversationId: "conv-test",
231
+ messages,
232
+ provider: makeProvider(
233
+ compactionResponse(1, "second user turn, also long"),
234
+ ),
235
+ systemPrompt: "system",
236
+ compaction: { enabled: true, autoThreshold: 0.7 },
237
+ maxInputTokens: 100_000,
238
+ // Small target to force forward advancement past the tool cluster.
239
+ targetTokens: 200,
240
+ previousEstimatedInputTokens: 90_000,
241
+ });
242
+
243
+ expect(result.compacted).toBe(true);
244
+ const tail = result.messages.slice(
245
+ result.messages.length - (result.preservedTailMessages ?? 0),
246
+ );
247
+ // The tail must not open with an orphaned tool_result.
248
+ const opensWithToolResult =
249
+ tail[0]?.role === "user" &&
250
+ tail[0].content.some((b) => b.type === "tool_result");
251
+ expect(opensWithToolResult).toBe(false);
252
+ });
253
+
254
+ test("keeps the model's tail untouched when it already fits the budget", async () => {
255
+ // 10 turns = 20 messages; the model anchors its tail at turn 4 (index 8)
256
+ // and the target is far above the rebuilt-history estimate. Enforcement
257
+ // must not advance the cut at all — the cut is enforcement, not
258
+ // optimization.
259
+ const messages: Message[] = [];
260
+ for (let i = 0; i < 10; i++) {
261
+ messages.push(userTurn(i, "short user turn body here"));
262
+ messages.push(assistantTurn(i, "short assistant reply body here"));
263
+ }
264
+
265
+ const result = await runAssistantDrivenCompaction({
266
+ conversationId: "conv-test",
267
+ messages,
268
+ provider: makeProvider(
269
+ compactionResponse(4, "short user turn body here"),
270
+ ),
271
+ systemPrompt: "system",
272
+ compaction: { enabled: true, autoThreshold: 0.7 },
273
+ maxInputTokens: 100_000,
274
+ targetTokens: 50_000,
275
+ previousEstimatedInputTokens: 90_000,
276
+ });
277
+
278
+ expect(result.compacted).toBe(true);
279
+ // The model's cut (turn 4 → index 8) keeps messages 8..19 = 12 messages.
280
+ expect(result.preservedTailMessages).toBe(12);
281
+ // No span was dropped, so the summary carries no truncation notice.
282
+ const summaryBlock = result.messages[0]?.content.find(
283
+ (b) => b.type === "text",
284
+ );
285
+ expect(
286
+ summaryBlock && "text" in summaryBlock ? summaryBlock.text : "",
287
+ ).not.toContain("Context budget enforcement");
288
+ });
289
+
290
+ test("acknowledges the span dropped by an advanced cut in the summary message", async () => {
291
+ // Same shape as the first test: a heavy conversation where the model's
292
+ // tail choice overshoots the budget and the cut must advance. The span
293
+ // between the model's cut and the enforced cut is in neither the
294
+ // summary's detail nor the retained tail, so the summary message must
295
+ // carry an explicit truncation notice.
296
+ const messages: Message[] = [];
297
+ for (let i = 0; i < 40; i++) {
298
+ messages.push(
299
+ userTurn(
300
+ i,
301
+ "User message with a fairly long body so that each turn carries real token weight in the estimate. ".repeat(
302
+ 3,
303
+ ),
304
+ ),
305
+ );
306
+ messages.push(
307
+ assistantTurn(
308
+ i,
309
+ "Assistant reply, also reasonably long to contribute estimate weight. ".repeat(
310
+ 3,
311
+ ),
312
+ ),
313
+ );
314
+ }
315
+
316
+ const result = await runAssistantDrivenCompaction({
317
+ conversationId: "conv-test",
318
+ messages,
319
+ provider: makeProvider(
320
+ compactionResponse(4, "User message with a fairly long body"),
321
+ ),
322
+ systemPrompt: "system",
323
+ compaction: { enabled: true, autoThreshold: 0.7 },
324
+ maxInputTokens: 100_000,
325
+ targetTokens: 1500,
326
+ previousEstimatedInputTokens: 90_000,
327
+ });
328
+
329
+ expect(result.compacted).toBe(true);
330
+ const summaryBlock = result.messages[0]?.content.find(
331
+ (b) => b.type === "text",
332
+ );
333
+ const summaryTextOut =
334
+ summaryBlock && "text" in summaryBlock ? summaryBlock.text : "";
335
+ // The notice names the dropped span (count by role) so the loss is
336
+ // visible in-context rather than silent.
337
+ expect(summaryTextOut).toContain("Context budget enforcement");
338
+ expect(summaryTextOut).toMatch(
339
+ /\d+ message\(s\) \(\d+ user, \d+ assistant\)/,
340
+ );
341
+ // The original summary body is preserved verbatim ahead of the notice.
342
+ expect(summaryTextOut).toContain(SUMMARY);
343
+ // The notice also rides the DURABLE summary — applyCompactionResult
344
+ // persists and rehydrates from `result.summaryText`, so a notice that
345
+ // lived only on the in-memory message would vanish on reload/fork.
346
+ expect(result.summaryText).toContain("Context budget enforcement");
347
+ expect(result.summaryText).toBe(summaryTextOut);
348
+ });
349
+ });