@vellumai/assistant 0.8.0 → 0.8.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 (991) hide show
  1. package/AGENTS.md +11 -0
  2. package/ARCHITECTURE.md +2 -7
  3. package/Dockerfile +80 -5
  4. package/README.md +2 -2
  5. package/bun.lock +11 -1
  6. package/docker-entrypoint.sh +21 -0
  7. package/docker-init-apt-root.sh +94 -0
  8. package/docker-kata-apt-env.sh +39 -0
  9. package/docs/plugins.md +88 -47
  10. package/docs/skills.md +9 -7
  11. package/eslint-rules/__tests__/cli-no-daemon-internals.test.ts +420 -0
  12. package/eslint-rules/cli-no-daemon-internals.js +283 -0
  13. package/eslint.config.mjs +12 -0
  14. package/examples/plugins/echo/README.md +27 -27
  15. package/examples/plugins/echo/package.json +3 -0
  16. package/examples/plugins/echo/register.ts +31 -31
  17. package/knip.json +2 -1
  18. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -1
  19. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  20. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  21. package/openapi.yaml +4462 -991
  22. package/package.json +5 -1
  23. package/scripts/generate-openapi.ts +135 -14
  24. package/scripts/sync-llm-catalog.ts +165 -0
  25. package/scripts/sync-web-search-catalog.ts +129 -0
  26. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +169 -0
  27. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  28. package/src/__tests__/agent-loop-override-profile.test.ts +26 -1
  29. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  30. package/src/__tests__/anthropic-provider.test.ts +137 -2
  31. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  32. package/src/__tests__/app-control-flow.test.ts +7 -0
  33. package/src/__tests__/app-executors.test.ts +220 -4
  34. package/src/__tests__/assistant-events-sse-shed.test.ts +232 -0
  35. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  36. package/src/__tests__/avatar-identity-sync.test.ts +87 -0
  37. package/src/__tests__/background-workers-disk-pressure.test.ts +11 -22
  38. package/src/__tests__/btw-routes.test.ts +1 -0
  39. package/src/__tests__/bundled-asset.test.ts +6 -6
  40. package/src/__tests__/call-site-routing-provider.test.ts +172 -45
  41. package/src/__tests__/cancel-resolves-conversation-key.test.ts +44 -3
  42. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  43. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  44. package/src/__tests__/channel-policy.test.ts +12 -0
  45. package/src/__tests__/checker.test.ts +89 -0
  46. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  47. package/src/__tests__/clawhub.test.ts +75 -16
  48. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +35 -7
  49. package/src/__tests__/compact-event-conversation-id-guard.test.ts +33 -5
  50. package/src/__tests__/compaction-strip-metadata-clear.test.ts +26 -1
  51. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  52. package/src/__tests__/config-loader-backfill.test.ts +526 -102
  53. package/src/__tests__/config-loader-corrupt.test.ts +68 -0
  54. package/src/__tests__/config-loader-platform-defaults.test.ts +77 -23
  55. package/src/__tests__/config-schema-cmd.test.ts +63 -29
  56. package/src/__tests__/config-schema.test.ts +35 -3
  57. package/src/__tests__/config-set-platform-guard.test.ts +75 -152
  58. package/src/__tests__/config-set-route.test.ts +278 -0
  59. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  60. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  61. package/src/__tests__/config-watcher.test.ts +6 -0
  62. package/src/__tests__/contacts-tools.test.ts +51 -199
  63. package/src/__tests__/context-search-agent-protocol.test.ts +21 -2
  64. package/src/__tests__/context-search-agent-runner.test.ts +22 -138
  65. package/src/__tests__/context-search-conversations-source.test.ts +159 -18
  66. package/src/__tests__/context-search-fanout.test.ts +20 -157
  67. package/src/__tests__/context-search-memory-v2-source.test.ts +3 -4
  68. package/src/__tests__/context-search-types.test.ts +7 -2
  69. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  70. package/src/__tests__/context-token-estimator.test.ts +1 -0
  71. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  72. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  73. package/src/__tests__/conversation-agent-loop-overflow.test.ts +93 -92
  74. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  75. package/src/__tests__/conversation-crud-inference-profile.test.ts +100 -0
  76. package/src/__tests__/conversation-error.test.ts +80 -3
  77. package/src/__tests__/conversation-fork-crud.test.ts +323 -1
  78. package/src/__tests__/conversation-inference-profile-route.test.ts +54 -18
  79. package/src/__tests__/conversation-init.benchmark.test.ts +1 -0
  80. package/src/__tests__/conversation-lifecycle.test.ts +297 -0
  81. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  82. package/src/__tests__/conversation-pairing.test.ts +54 -0
  83. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +100 -1
  84. package/src/__tests__/conversation-process-callsite.test.ts +25 -2
  85. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  86. package/src/__tests__/conversation-queue.test.ts +4 -1
  87. package/src/__tests__/conversation-runtime-assembly.test.ts +80 -13
  88. package/src/__tests__/conversation-slash-commands.test.ts +194 -2
  89. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  90. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  91. package/src/__tests__/conversation-surfaces-app-control.test.ts +323 -3
  92. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  93. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  94. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  95. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  96. package/src/__tests__/credential-security-invariants.test.ts +8 -8
  97. package/src/__tests__/daemon-credential-client.test.ts +56 -1
  98. package/src/__tests__/db-activation-state-fk-cascade.test.ts +132 -0
  99. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +37 -0
  100. package/src/__tests__/db-memory-graph-event-date-repair.test.ts +43 -20
  101. package/src/__tests__/db-proxy-transaction.test.ts +206 -0
  102. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  103. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  104. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  105. package/src/__tests__/dm-backfill.test.ts +121 -10
  106. package/src/__tests__/document-tool-security.test.ts +258 -0
  107. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  108. package/src/__tests__/edit-propagation.test.ts +33 -0
  109. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  110. package/src/__tests__/external-plugin-loader.test.ts +482 -0
  111. package/src/__tests__/filing-service.test.ts +163 -3
  112. package/src/__tests__/fixtures/mock-chrome-extension.ts +5 -0
  113. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  114. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  115. package/src/__tests__/graph-extraction-event-date.test.ts +34 -0
  116. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +42 -69
  117. package/src/__tests__/heartbeat-disk-pressure.test.ts +21 -8
  118. package/src/__tests__/heartbeat-service.test.ts +50 -233
  119. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  120. package/src/__tests__/helpers/wait-for.ts +21 -0
  121. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  122. package/src/__tests__/history-repair.test.ts +162 -0
  123. package/src/__tests__/host-app-control-proxy.test.ts +365 -1
  124. package/src/__tests__/host-app-control-routes.test.ts +247 -1
  125. package/src/__tests__/host-browser-proxy.test.ts +416 -20
  126. package/src/__tests__/host-browser-routes.test.ts +325 -33
  127. package/src/__tests__/host-proxy-preactivation.test.ts +211 -0
  128. package/src/__tests__/image-credentials.test.ts +1 -1
  129. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  130. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +246 -0
  131. package/src/__tests__/inference-profile-reaper.test.ts +156 -0
  132. package/src/__tests__/inference-profile-session-handler.test.ts +410 -0
  133. package/src/__tests__/inference-profile-session-ipc.test.ts +248 -0
  134. package/src/__tests__/injector-chain.test.ts +10 -8
  135. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -1
  136. package/src/__tests__/install-skill-routing.test.ts +157 -39
  137. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +107 -3
  138. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  139. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  140. package/src/__tests__/llm-callsite-catalog.test.ts +20 -1
  141. package/src/__tests__/llm-catalog-parity.test.ts +190 -2
  142. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +222 -0
  143. package/src/__tests__/llm-request-log-source-factory.test.ts +100 -0
  144. package/src/__tests__/llm-resolver.test.ts +46 -0
  145. package/src/__tests__/llm-usage-store.test.ts +114 -0
  146. package/src/__tests__/managed-profile-guard.test.ts +145 -14
  147. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  148. package/src/__tests__/managed-store.test.ts +84 -192
  149. package/src/__tests__/mcp-auth-routes.test.ts +1 -0
  150. package/src/__tests__/mcp-cli.test.ts +182 -220
  151. package/src/__tests__/mcp-health-check.test.ts +56 -27
  152. package/src/__tests__/media-generate-image.test.ts +1 -1
  153. package/src/__tests__/memory-jobs-worker-lanes.test.ts +18 -11
  154. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  155. package/src/__tests__/message-complete-display-id.test.ts +175 -0
  156. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  157. package/src/__tests__/notification-platform-adapter.test.ts +229 -0
  158. package/src/__tests__/oauth-cli.test.ts +38 -2009
  159. package/src/__tests__/oauth-commands-routes.test.ts +863 -0
  160. package/src/__tests__/oauth-connect-routes.test.ts +174 -11
  161. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  162. package/src/__tests__/oauth-providers-routes.test.ts +14 -10
  163. package/src/__tests__/openai-provider.test.ts +24 -0
  164. package/src/__tests__/openai-responses-cutover-guard.test.ts +48 -19
  165. package/src/__tests__/openai-responses-provider.test.ts +17 -0
  166. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  167. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  168. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  169. package/src/__tests__/platform.test.ts +2 -0
  170. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  171. package/src/__tests__/plugin-bootstrap.test.ts +41 -38
  172. package/src/__tests__/plugin-external-api.test.ts +68 -0
  173. package/src/__tests__/plugin-registry.test.ts +0 -77
  174. package/src/__tests__/plugin-route-contribution.test.ts +31 -4
  175. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  176. package/src/__tests__/plugin-tool-contribution.test.ts +47 -18
  177. package/src/__tests__/plugin-types.test.ts +15 -23
  178. package/src/__tests__/process-message-background-slack.test.ts +53 -0
  179. package/src/__tests__/process-message-display-content.test.ts +421 -0
  180. package/src/__tests__/profile-entry-status.test.ts +43 -0
  181. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  182. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  183. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +20 -12
  184. package/src/__tests__/provider-registry-ollama.test.ts +12 -4
  185. package/src/__tests__/provider-send-message-override-profile.test.ts +10 -4
  186. package/src/__tests__/relay-server.test.ts +118 -0
  187. package/src/__tests__/retry-thinking-tool-choice.test.ts +15 -0
  188. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  189. package/src/__tests__/schedule-retry.test.ts +56 -4
  190. package/src/__tests__/schedule-routes.test.ts +151 -0
  191. package/src/__tests__/schedule-store.test.ts +94 -0
  192. package/src/__tests__/scheduler-disk-pressure.test.ts +0 -4
  193. package/src/__tests__/scheduler-recurrence.test.ts +87 -34
  194. package/src/__tests__/scheduler-reuse-conversation.test.ts +208 -5
  195. package/src/__tests__/scheduler-wake.test.ts +0 -63
  196. package/src/__tests__/schema-transforms.test.ts +20 -0
  197. package/src/__tests__/search-skills-unified.test.ts +0 -5
  198. package/src/__tests__/secret-allowlist.test.ts +1 -0
  199. package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +12 -4
  200. package/src/__tests__/server-history-render.test.ts +43 -0
  201. package/src/__tests__/shell-credential-ref.test.ts +95 -3
  202. package/src/__tests__/shell-tool-proxy-mode.test.ts +14 -0
  203. package/src/__tests__/skill-load-feature-flag.test.ts +1 -12
  204. package/src/__tests__/skill-load-tool.test.ts +29 -93
  205. package/src/__tests__/skill-memory.test.ts +23 -3
  206. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  207. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  208. package/src/__tests__/skills-install-extract.test.ts +49 -38
  209. package/src/__tests__/skills-install-staging.test.ts +159 -0
  210. package/src/__tests__/skills-uninstall.test.ts +9 -41
  211. package/src/__tests__/skills.test.ts +51 -58
  212. package/src/__tests__/slack-channel-config.test.ts +9 -0
  213. package/src/__tests__/subagent-call-site-routing.test.ts +78 -16
  214. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  215. package/src/__tests__/suggestion-routes.test.ts +3 -3
  216. package/src/__tests__/sync-message-contract.test.ts +63 -0
  217. package/src/__tests__/system-prompt.test.ts +737 -63
  218. package/src/__tests__/task-scheduler.test.ts +88 -23
  219. package/src/__tests__/terminal-tools.test.ts +28 -1
  220. package/src/__tests__/thread-backfill.test.ts +557 -27
  221. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  222. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  223. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  224. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  225. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  226. package/src/__tests__/tool-executor.test.ts +16 -4
  227. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  228. package/src/__tests__/turn-events-store.test.ts +256 -0
  229. package/src/__tests__/twilio-routes.test.ts +4 -0
  230. package/src/__tests__/update-bulletin-job.test.ts +96 -193
  231. package/src/__tests__/usage-cli.test.ts +11 -73
  232. package/src/__tests__/user-plugin-loader.test.ts +143 -5
  233. package/src/__tests__/vercel-config.test.ts +168 -0
  234. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  235. package/src/__tests__/web-search-catalog-parity.test.ts +108 -0
  236. package/src/__tests__/web-search.test.ts +303 -2
  237. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +1 -21
  238. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +170 -0
  239. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +53 -20
  240. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +241 -0
  241. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  242. package/src/__tests__/workspace-migration-076-drop-services-inference-mode.test.ts +211 -0
  243. package/src/__tests__/workspace-migration-077-seed-memory-router-callsite.test.ts +174 -0
  244. package/src/__tests__/workspace-migration-079-home-feed-notification-only.test.ts +323 -0
  245. package/src/__tests__/workspace-migration-080-restrict-vercel-api-token-metadata.test.ts +299 -0
  246. package/src/__tests__/workspace-migration-081-backfill-bash-allowed-tools.test.ts +410 -0
  247. package/src/__tests__/workspace-migration-082-backfill-managed-profile-labels.test.ts +268 -0
  248. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  249. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  250. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  251. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +3 -3
  252. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  253. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +115 -0
  254. package/src/acp/__tests__/helpers/which-stub.ts +4 -2
  255. package/src/acp/resolve-agent.test.ts +25 -0
  256. package/src/acp/resolve-agent.ts +13 -2
  257. package/src/acp/session-manager.ts +14 -0
  258. package/src/agent/image-optimize.ts +13 -5
  259. package/src/approvals/guardian-request-resolvers.ts +32 -87
  260. package/src/calls/relay-server.ts +35 -0
  261. package/src/calls/relay-setup-router.ts +36 -0
  262. package/src/calls/types.ts +1 -0
  263. package/src/calls/voice-session-bridge.ts +74 -36
  264. package/src/channels/config.ts +14 -1
  265. package/src/channels/types.ts +109 -0
  266. package/src/cli/AGENTS.md +164 -4
  267. package/src/cli/__tests__/notifications.test.ts +54 -0
  268. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  269. package/src/cli/commands/__tests__/avatar.test.ts +540 -0
  270. package/src/cli/commands/__tests__/backup.test.ts +236 -776
  271. package/src/cli/commands/__tests__/cache.test.ts +1 -1
  272. package/src/cli/commands/__tests__/changelog.test.ts +578 -0
  273. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +503 -0
  274. package/src/cli/commands/__tests__/conversations-import.test.ts +515 -0
  275. package/src/cli/commands/__tests__/domain-register.test.ts +140 -167
  276. package/src/cli/commands/__tests__/domain-status.test.ts +137 -76
  277. package/src/cli/commands/__tests__/email-attachment.test.ts +314 -337
  278. package/src/cli/commands/__tests__/email-core.test.ts +579 -0
  279. package/src/cli/commands/__tests__/image-generation.test.ts +87 -824
  280. package/src/cli/commands/__tests__/inference-send.test.ts +30 -266
  281. package/src/cli/commands/__tests__/inference-session.test.ts +423 -0
  282. package/src/cli/commands/__tests__/memory-v2.test.ts +81 -110
  283. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  284. package/src/cli/commands/__tests__/skills.test.ts +563 -0
  285. package/src/cli/commands/__tests__/status.test.ts +249 -0
  286. package/src/cli/commands/__tests__/stt.test.ts +320 -0
  287. package/src/cli/commands/__tests__/tts-synthesize.test.ts +4 -603
  288. package/src/cli/commands/__tests__/tts.test.ts +321 -0
  289. package/src/cli/commands/__tests__/webhooks.test.ts +86 -511
  290. package/src/cli/commands/attachment.ts +8 -3
  291. package/src/cli/commands/audit.ts +95 -64
  292. package/src/cli/commands/auth.ts +61 -58
  293. package/src/cli/commands/avatar.ts +276 -390
  294. package/src/cli/commands/backup.ts +409 -505
  295. package/src/cli/commands/bash.ts +9 -5
  296. package/src/cli/commands/browser.ts +28 -9
  297. package/src/cli/commands/cache.ts +9 -4
  298. package/src/cli/commands/changelog.ts +478 -0
  299. package/src/cli/commands/channel-verification-sessions.ts +238 -317
  300. package/src/cli/commands/clients.ts +8 -3
  301. package/src/cli/commands/completions.ts +9 -9
  302. package/src/cli/commands/config.ts +102 -72
  303. package/src/cli/commands/contacts.ts +575 -696
  304. package/src/cli/commands/conversations-defer.ts +17 -69
  305. package/src/cli/commands/conversations-import.ts +90 -253
  306. package/src/cli/commands/conversations.ts +429 -434
  307. package/src/cli/commands/credential-execution.ts +9 -6
  308. package/src/cli/commands/credentials.ts +456 -736
  309. package/src/cli/commands/default-action.ts +10 -53
  310. package/src/cli/commands/domain.ts +128 -206
  311. package/src/cli/commands/email.ts +606 -794
  312. package/src/cli/commands/gateway.ts +8 -1
  313. package/src/cli/commands/image-generation.ts +157 -205
  314. package/src/cli/commands/inference-providers.ts +352 -0
  315. package/src/cli/commands/inference-session.ts +415 -0
  316. package/src/cli/commands/inference.ts +87 -65
  317. package/src/cli/commands/keys.ts +8 -3
  318. package/src/cli/commands/mcp.ts +103 -287
  319. package/src/cli/commands/memory-v2.ts +162 -516
  320. package/src/cli/commands/notifications.ts +342 -304
  321. package/src/cli/commands/oauth/apps.ts +292 -261
  322. package/src/cli/commands/oauth/connect.ts +176 -297
  323. package/src/cli/commands/oauth/disconnect.ts +16 -215
  324. package/src/cli/commands/oauth/index.ts +49 -45
  325. package/src/cli/commands/oauth/mode.ts +43 -199
  326. package/src/cli/commands/oauth/ping.ts +17 -125
  327. package/src/cli/commands/oauth/providers.ts +732 -921
  328. package/src/cli/commands/oauth/request.ts +60 -350
  329. package/src/cli/commands/oauth/shared.ts +11 -121
  330. package/src/cli/commands/oauth/status.ts +31 -121
  331. package/src/cli/commands/oauth/token.ts +13 -55
  332. package/src/cli/commands/pending.ts +19 -10
  333. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +133 -183
  334. package/src/cli/commands/platform/__tests__/connect.test.ts +66 -181
  335. package/src/cli/commands/platform/__tests__/disconnect.test.ts +71 -227
  336. package/src/cli/commands/platform/__tests__/status.test.ts +169 -287
  337. package/src/cli/commands/platform/connect.ts +16 -80
  338. package/src/cli/commands/platform/disconnect.ts +14 -112
  339. package/src/cli/commands/platform/index.ts +177 -246
  340. package/src/cli/commands/plugins.ts +185 -0
  341. package/src/cli/commands/routes.ts +153 -336
  342. package/src/cli/commands/schedules.ts +391 -0
  343. package/src/cli/commands/sequence.ts +316 -360
  344. package/src/cli/commands/skills.ts +449 -671
  345. package/src/cli/commands/status.ts +58 -37
  346. package/src/cli/commands/stt.ts +94 -262
  347. package/src/cli/commands/task.ts +14 -40
  348. package/src/cli/commands/telemetry.ts +40 -0
  349. package/src/cli/commands/trust.ts +8 -3
  350. package/src/cli/commands/tts.ts +162 -167
  351. package/src/cli/commands/ui.ts +35 -42
  352. package/src/cli/commands/usage.ts +188 -126
  353. package/src/cli/commands/watchers.ts +8 -3
  354. package/src/cli/commands/webhooks.ts +99 -193
  355. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  356. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  357. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  358. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  359. package/src/cli/lib/__tests__/register-command.test.ts +85 -0
  360. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  361. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  362. package/src/cli/lib/cli-colors.ts +12 -0
  363. package/src/cli/lib/confirm-prompt.ts +79 -0
  364. package/src/cli/lib/daemon-credential-client.ts +4 -5
  365. package/src/cli/lib/install-from-github.ts +304 -0
  366. package/src/cli/lib/list-installed-plugins.ts +137 -0
  367. package/src/cli/lib/nested-value.ts +44 -0
  368. package/src/cli/lib/open-browser.ts +36 -0
  369. package/src/cli/lib/register-command.ts +19 -0
  370. package/src/cli/lib/time-ago.ts +34 -0
  371. package/src/cli/lib/uninstall-plugin.ts +82 -0
  372. package/src/cli/lib/unknown-command.ts +111 -0
  373. package/src/cli/program.ts +40 -6
  374. package/src/cli/utils/__tests__/conversation-id.test.ts +66 -0
  375. package/src/cli/utils/__tests__/parse-duration.test.ts +49 -0
  376. package/src/cli/utils/conversation-id.ts +30 -0
  377. package/src/cli/utils/parse-duration.ts +41 -0
  378. package/src/config/acp-defaults.test.ts +5 -1
  379. package/src/config/acp-defaults.ts +11 -4
  380. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  381. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  382. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  383. package/src/config/bundled-skills/app-control/TOOLS.json +32 -0
  384. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  385. package/src/config/bundled-skills/contacts/SKILL.md +12 -45
  386. package/src/config/bundled-skills/contacts/TOOLS.json +0 -57
  387. package/src/config/bundled-skills/document/SKILL.md +23 -3
  388. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  389. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  390. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  391. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  392. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +0 -12
  393. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +0 -58
  394. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  395. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  396. package/src/config/bundled-tool-registry.ts +6 -2
  397. package/src/config/feature-flag-registry.json +57 -1
  398. package/src/config/llm-resolver.ts +16 -1
  399. package/src/config/loader.ts +140 -52
  400. package/src/config/raw-config-utils.ts +2 -30
  401. package/src/config/schema.ts +8 -7
  402. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  403. package/src/config/schemas/__tests__/memory-v2.test.ts +49 -0
  404. package/src/config/schemas/call-site-catalog.ts +29 -7
  405. package/src/config/schemas/channels.ts +8 -0
  406. package/src/config/schemas/compaction.ts +28 -0
  407. package/src/config/schemas/heartbeat.ts +9 -0
  408. package/src/config/schemas/llm-request-logs.ts +81 -0
  409. package/src/config/schemas/llm.ts +55 -2
  410. package/src/config/schemas/memory-retrieval.ts +18 -0
  411. package/src/config/schemas/memory-retrospective.ts +48 -0
  412. package/src/config/schemas/memory-v2.ts +32 -1
  413. package/src/config/schemas/memory.ts +4 -0
  414. package/src/config/schemas/services.ts +15 -12
  415. package/src/config/schemas/tools.ts +14 -0
  416. package/src/config/seed-inference-profiles.ts +195 -134
  417. package/src/config/skills.ts +3 -96
  418. package/src/contacts/contact-store.ts +0 -61
  419. package/src/context/compactor.ts +1047 -0
  420. package/src/context/token-estimator.ts +2 -2
  421. package/src/context/window-manager.ts +197 -1334
  422. package/src/credential-execution/managed-catalog.ts +37 -0
  423. package/src/credential-health/credential-health-service.ts +280 -19
  424. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +113 -0
  425. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  426. package/src/daemon/__tests__/conversation-tool-setup.test.ts +183 -4
  427. package/src/daemon/__tests__/daemon-skill-host.test.ts +10 -4
  428. package/src/daemon/approval-generators.ts +26 -30
  429. package/src/daemon/config-watcher.ts +94 -29
  430. package/src/daemon/conversation-agent-loop-handlers.ts +24 -0
  431. package/src/daemon/conversation-agent-loop.ts +293 -103
  432. package/src/daemon/conversation-error.ts +188 -33
  433. package/src/daemon/conversation-lifecycle.ts +80 -26
  434. package/src/daemon/conversation-messaging.ts +25 -6
  435. package/src/daemon/conversation-process.ts +85 -31
  436. package/src/daemon/conversation-runtime-assembly.ts +30 -6
  437. package/src/daemon/conversation-slash.ts +184 -25
  438. package/src/daemon/conversation-store.ts +24 -10
  439. package/src/daemon/conversation-surfaces.ts +76 -12
  440. package/src/daemon/conversation-tool-setup.ts +63 -21
  441. package/src/daemon/conversation.ts +81 -10
  442. package/src/daemon/external-plugins-bootstrap.ts +231 -185
  443. package/src/daemon/first-greeting.ts +22 -2
  444. package/src/daemon/guardian-action-generators.ts +7 -22
  445. package/src/daemon/handlers/config-model.ts +13 -130
  446. package/src/daemon/handlers/config-slack-channel.ts +25 -10
  447. package/src/daemon/handlers/config-vercel.ts +3 -1
  448. package/src/daemon/handlers/shared.ts +14 -5
  449. package/src/daemon/handlers/skills.ts +166 -84
  450. package/src/daemon/history-repair.ts +61 -7
  451. package/src/daemon/host-app-control-proxy.ts +129 -29
  452. package/src/daemon/host-bash-proxy.ts +85 -158
  453. package/src/daemon/host-browser-proxy.ts +96 -35
  454. package/src/daemon/host-proxy-base.ts +13 -1
  455. package/src/daemon/host-proxy-preactivation.ts +25 -1
  456. package/src/daemon/identity-helpers.ts +19 -0
  457. package/src/daemon/lifecycle.ts +79 -70
  458. package/src/daemon/meet-host-supervisor.ts +20 -19
  459. package/src/daemon/memory-v2-startup.ts +58 -2
  460. package/src/daemon/message-protocol.ts +7 -0
  461. package/src/daemon/message-types/bookmarks.ts +18 -0
  462. package/src/daemon/message-types/conversations.ts +37 -9
  463. package/src/daemon/message-types/messages.ts +70 -1
  464. package/src/daemon/message-types/subagents.ts +1 -0
  465. package/src/daemon/message-types/sync.ts +61 -0
  466. package/src/daemon/pkb-reminder-builder.test.ts +54 -13
  467. package/src/daemon/pkb-reminder-builder.ts +21 -7
  468. package/src/daemon/plugin-source-watcher.ts +146 -0
  469. package/src/daemon/process-message.ts +77 -26
  470. package/src/daemon/server.ts +34 -20
  471. package/src/daemon/shutdown-handlers.ts +0 -2
  472. package/src/daemon/skill-memory-refresh.ts +29 -0
  473. package/src/daemon/tool-setup-types.ts +9 -0
  474. package/src/daemon/tool-side-effects.ts +6 -4
  475. package/src/daemon/wake-target-adapter.ts +11 -0
  476. package/src/documents/document-store.ts +221 -3
  477. package/src/embedded/plugin-api.ts +40 -0
  478. package/src/export/transcript-formatter.ts +61 -2
  479. package/src/filing/filing-service.ts +79 -53
  480. package/src/heartbeat/__tests__/heartbeat-service.test.ts +444 -0
  481. package/src/heartbeat/heartbeat-run-store.ts +3 -1
  482. package/src/heartbeat/heartbeat-service.ts +189 -127
  483. package/src/home/__tests__/feed-types.test.ts +99 -127
  484. package/src/home/__tests__/feed-writer.test.ts +77 -278
  485. package/src/home/__tests__/post-connect-feed.test.ts +9 -12
  486. package/src/home/feed-types.ts +41 -73
  487. package/src/home/feed-writer.ts +25 -156
  488. package/src/home/post-connect-feed.ts +2 -3
  489. package/src/index.ts +18 -1
  490. package/src/ipc/__tests__/cli-ipc.test.ts +2 -0
  491. package/src/ipc/__tests__/email-ipc.test.ts +506 -0
  492. package/src/ipc/__tests__/exit-helper.test.ts +104 -0
  493. package/src/ipc/__tests__/streaming-client.test.ts +237 -0
  494. package/src/ipc/__tests__/streaming-framing.test.ts +142 -0
  495. package/src/ipc/assistant-server.ts +55 -6
  496. package/src/ipc/cli-client.ts +370 -50
  497. package/src/ipc/routes/db-proxy-transaction.ts +151 -0
  498. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +60 -0
  499. package/src/ipc/skill-routes/events.ts +30 -3
  500. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +46 -0
  501. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  502. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +1 -0
  503. package/src/live-voice/live-voice-session-manager.ts +11 -4
  504. package/src/live-voice/live-voice-session.ts +14 -6
  505. package/src/mcp/client.ts +20 -4
  506. package/src/media/image-credentials.ts +3 -3
  507. package/src/memory/__tests__/bookmark-crud.test.ts +264 -0
  508. package/src/memory/__tests__/bookmark-schema.test.ts +181 -0
  509. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  510. package/src/memory/__tests__/conversation-types.test.ts +36 -0
  511. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +130 -0
  512. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  513. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +177 -0
  514. package/src/memory/__tests__/memory-retrospective-job.test.ts +328 -0
  515. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +318 -0
  516. package/src/memory/__tests__/memory-retrospective-trigger-check.test.ts +90 -0
  517. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +69 -0
  518. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +3 -0
  519. package/src/memory/__tests__/message-content.test.ts +35 -0
  520. package/src/memory/bookmark-crud.ts +211 -0
  521. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +31 -9
  522. package/src/memory/context-search/agent-protocol.ts +5 -1
  523. package/src/memory/context-search/agent-runner.ts +60 -85
  524. package/src/memory/context-search/limits.ts +1 -4
  525. package/src/memory/context-search/search.ts +23 -113
  526. package/src/memory/context-search/sources/conversations.ts +80 -8
  527. package/src/memory/context-search/sources/memory-v2.ts +39 -14
  528. package/src/memory/context-search/sources/memory.ts +7 -0
  529. package/src/memory/context-search/sources/workspace.ts +17 -10
  530. package/src/memory/context-search/types.ts +1 -1
  531. package/src/memory/conversation-bootstrap.ts +11 -0
  532. package/src/memory/conversation-crud.ts +368 -22
  533. package/src/memory/conversation-queries.ts +116 -12
  534. package/src/memory/conversation-title-service.ts +1 -0
  535. package/src/memory/conversation-types.ts +16 -0
  536. package/src/memory/db-init.ts +20 -0
  537. package/src/memory/delivery-crud.ts +152 -5
  538. package/src/memory/embedding-backend.ts +6 -5
  539. package/src/memory/embedding-runtime-manager.ts +1 -2
  540. package/src/memory/external-conversation-store.ts +66 -5
  541. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  542. package/src/memory/graph/__tests__/remember-description.test.ts +55 -0
  543. package/src/memory/graph/conversation-graph-memory.ts +92 -5
  544. package/src/memory/graph/extraction.ts +4 -0
  545. package/src/memory/graph/graph-memory-state-store.ts +16 -3
  546. package/src/memory/graph/tool-handlers.ts +17 -7
  547. package/src/memory/graph/tools.ts +45 -6
  548. package/src/memory/indexer.ts +51 -29
  549. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +86 -15
  550. package/src/memory/jobs/embed-concept-page.ts +65 -20
  551. package/src/memory/jobs-store.ts +51 -1
  552. package/src/memory/jobs-worker.ts +57 -3
  553. package/src/memory/llm-request-log-source-clickhouse.ts +324 -0
  554. package/src/memory/llm-request-log-source-local.ts +26 -0
  555. package/src/memory/llm-request-log-source.ts +64 -0
  556. package/src/memory/llm-request-log-store.ts +1 -1
  557. package/src/memory/llm-usage-store.ts +125 -5
  558. package/src/memory/memory-retrospective-constants.ts +13 -0
  559. package/src/memory/memory-retrospective-enqueue.ts +114 -0
  560. package/src/memory/memory-retrospective-job.ts +351 -0
  561. package/src/memory/memory-retrospective-startup-cleanup.ts +175 -0
  562. package/src/memory/memory-retrospective-state.ts +162 -0
  563. package/src/memory/memory-retrospective-trigger-check.ts +91 -0
  564. package/src/memory/memory-v2-activation-log-store.ts +49 -5
  565. package/src/memory/memory-v2-concept-frequency.ts +4 -0
  566. package/src/memory/message-content.ts +38 -1
  567. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  568. package/src/memory/migrations/227-add-conversation-inference-profile.ts +6 -1
  569. package/src/memory/migrations/228-rename-inference-profile-snake-case.ts +20 -7
  570. package/src/memory/migrations/229-delete-private-conversations.test.ts +107 -1
  571. package/src/memory/migrations/229-delete-private-conversations.ts +19 -0
  572. package/src/memory/migrations/231-repair-memory-graph-event-dates.ts +16 -2
  573. package/src/memory/migrations/240-conversation-inference-profile-session.ts +25 -0
  574. package/src/memory/migrations/241-activation-state-fk-cascade.ts +50 -0
  575. package/src/memory/migrations/242-message-bookmarks.ts +38 -0
  576. package/src/memory/migrations/243-provider-connections.ts +68 -0
  577. package/src/memory/migrations/244-provider-connection-status-label.ts +23 -0
  578. package/src/memory/migrations/245-memory-retrospective-state.ts +36 -0
  579. package/src/memory/migrations/246-backfill-provider-connection-label.ts +81 -0
  580. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  581. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  582. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  583. package/src/memory/migrations/__tests__/244-provider-connection-status-label.test.ts +84 -0
  584. package/src/memory/migrations/__tests__/245-memory-retrospective-state.test.ts +125 -0
  585. package/src/memory/migrations/__tests__/246-backfill-provider-connection-label.test.ts +192 -0
  586. package/src/memory/migrations/index.ts +13 -0
  587. package/src/memory/migrations/registry.ts +8 -0
  588. package/src/memory/onboarding-events-store.ts +106 -0
  589. package/src/memory/published-pages-store.ts +16 -0
  590. package/src/memory/schema/bookmarks.ts +36 -0
  591. package/src/memory/schema/calls.ts +1 -0
  592. package/src/memory/schema/conversations.ts +2 -0
  593. package/src/memory/schema/index.ts +2 -0
  594. package/src/memory/schema/inference.ts +27 -0
  595. package/src/memory/schema/infrastructure.ts +12 -0
  596. package/src/memory/schema/memory-core.ts +9 -0
  597. package/src/memory/search/semantic.ts +1 -4
  598. package/src/memory/turn-events-store.ts +127 -2
  599. package/src/memory/v2/__tests__/__snapshots__/prompts-router.test.ts.snap +27 -0
  600. package/src/memory/v2/__tests__/activation-store.test.ts +5 -5
  601. package/src/memory/v2/__tests__/activation.test.ts +11 -12
  602. package/src/memory/v2/__tests__/backfill-jobs.test.ts +38 -21
  603. package/src/memory/v2/__tests__/consolidation-job.test.ts +123 -135
  604. package/src/memory/v2/__tests__/edge-index.test.ts +1 -1
  605. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +111 -0
  606. package/src/memory/v2/__tests__/injection.test.ts +726 -18
  607. package/src/memory/v2/__tests__/migration.test.ts +94 -3
  608. package/src/memory/v2/__tests__/page-index.test.ts +360 -0
  609. package/src/memory/v2/__tests__/page-store.test.ts +14 -1
  610. package/src/memory/v2/__tests__/prompts-router.test.ts +309 -0
  611. package/src/memory/v2/__tests__/qdrant.test.ts +138 -3
  612. package/src/memory/v2/__tests__/reranker.test.ts +4 -4
  613. package/src/memory/v2/__tests__/router.test.ts +531 -0
  614. package/src/memory/v2/__tests__/sim.test.ts +45 -1
  615. package/src/memory/v2/__tests__/skill-store.test.ts +445 -11
  616. package/src/memory/v2/__tests__/static-context.test.ts +7 -22
  617. package/src/memory/v2/__tests__/sweep-job.test.ts +95 -0
  618. package/src/memory/v2/activation-store.ts +34 -5
  619. package/src/memory/v2/activation.ts +40 -27
  620. package/src/memory/v2/backfill-jobs.ts +17 -84
  621. package/src/memory/v2/consolidation-job.ts +85 -78
  622. package/src/memory/v2/frontmatter-sweep.ts +91 -0
  623. package/src/memory/v2/injection.ts +466 -109
  624. package/src/memory/v2/migration.ts +147 -20
  625. package/src/memory/v2/page-index.ts +221 -0
  626. package/src/memory/v2/page-store.ts +3 -0
  627. package/src/memory/v2/prompts/consolidation.ts +9 -7
  628. package/src/memory/v2/prompts/router.ts +195 -0
  629. package/src/memory/v2/prompts/sweep.ts +2 -2
  630. package/src/memory/v2/qdrant.ts +234 -93
  631. package/src/memory/v2/reranker.ts +14 -7
  632. package/src/memory/v2/router.ts +323 -0
  633. package/src/memory/v2/sim.ts +25 -12
  634. package/src/memory/v2/skill-store.ts +204 -30
  635. package/src/memory/v2/static-context.ts +16 -9
  636. package/src/memory/v2/sweep-job.ts +122 -96
  637. package/src/memory/v2/types.ts +10 -6
  638. package/src/memory/validation.ts +13 -0
  639. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  640. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  641. package/src/messaging/providers/slack/adapter.ts +43 -5
  642. package/src/messaging/providers/slack/client.ts +27 -0
  643. package/src/messaging/providers/slack/deep-link.ts +65 -0
  644. package/src/messaging/providers/slack/download.ts +104 -0
  645. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  646. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  647. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  648. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  649. package/src/messaging/providers/slack/types.ts +20 -1
  650. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +182 -0
  651. package/src/notifications/__tests__/home-feed-side-effect.test.ts +199 -0
  652. package/src/notifications/__tests__/signal-registry.test.ts +17 -0
  653. package/src/notifications/adapters/platform.ts +171 -0
  654. package/src/notifications/conversation-pairing.ts +4 -3
  655. package/src/notifications/copy-composer.ts +15 -0
  656. package/src/notifications/decision-engine.ts +2 -1
  657. package/src/notifications/destination-resolver.ts +21 -0
  658. package/src/notifications/emit-signal.ts +48 -2
  659. package/src/notifications/home-feed-side-effect.ts +165 -0
  660. package/src/notifications/signal.ts +8 -1
  661. package/src/oauth/connection-resolver.ts +8 -4
  662. package/src/oauth/platform-connection.ts +6 -2
  663. package/src/oauth/seed-providers.ts +10 -1
  664. package/src/permissions/checker.ts +14 -0
  665. package/src/permissions/ipc-risk-types.ts +3 -0
  666. package/src/permissions/question-prompter.test.ts +416 -0
  667. package/src/permissions/question-prompter.ts +294 -0
  668. package/src/platform/client.test.ts +1 -1
  669. package/src/platform/client.ts +1 -1
  670. package/src/plugin-api/constants.ts +26 -0
  671. package/src/plugin-api/index.ts +46 -0
  672. package/src/plugin-api/package.json +12 -0
  673. package/src/plugin-api/types.ts +144 -0
  674. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  675. package/src/plugins/defaults/compaction.ts +0 -4
  676. package/src/plugins/defaults/empty-response.ts +0 -2
  677. package/src/plugins/defaults/history-repair.ts +0 -2
  678. package/src/plugins/defaults/injectors.ts +55 -6
  679. package/src/plugins/defaults/llm-call.ts +0 -2
  680. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  681. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  682. package/src/plugins/defaults/persistence.ts +0 -2
  683. package/src/plugins/defaults/title-generate.ts +0 -5
  684. package/src/plugins/defaults/token-estimate.ts +0 -2
  685. package/src/plugins/defaults/tool-error.ts +0 -7
  686. package/src/plugins/defaults/tool-execute.ts +0 -2
  687. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  688. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  689. package/src/plugins/external-api.ts +104 -0
  690. package/src/plugins/external-plugin-loader.ts +367 -0
  691. package/src/plugins/feature-gate.ts +22 -0
  692. package/src/plugins/pipeline.ts +37 -0
  693. package/src/plugins/registry.ts +48 -80
  694. package/src/plugins/types.ts +74 -53
  695. package/src/plugins/user-loader.ts +85 -43
  696. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  697. package/src/proactive-artifact/job.test.ts +49 -9
  698. package/src/proactive-artifact/job.ts +4 -0
  699. package/src/proactive-artifact/trigger-state.test.ts +9 -0
  700. package/src/proactive-artifact/trigger-state.ts +4 -0
  701. package/src/prompts/__tests__/system-prompt.test.ts +117 -0
  702. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  703. package/src/prompts/normalize-onboarding.ts +27 -0
  704. package/src/prompts/sections.ts +302 -0
  705. package/src/prompts/system-prompt.ts +72 -154
  706. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  707. package/src/prompts/templates/system-sections.ts +173 -0
  708. package/src/prompts/update-bulletin-job.ts +61 -73
  709. package/src/providers/__tests__/dispatch-connection-routing.test.ts +279 -0
  710. package/src/providers/__tests__/inference.test.ts +303 -0
  711. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  712. package/src/providers/__tests__/provider-secret-catalog.test.ts +6 -0
  713. package/src/providers/__tests__/retry-callsite.test.ts +14 -32
  714. package/src/providers/__tests__/satellite-connection-routing.test.ts +510 -0
  715. package/src/providers/__tests__/search-provider-catalog.test.ts +80 -0
  716. package/src/providers/anthropic/client.ts +123 -54
  717. package/src/providers/call-site-routing.ts +94 -16
  718. package/src/providers/connection-resolution.ts +170 -0
  719. package/src/providers/inference/__tests__/connections-status-label.test.ts +250 -0
  720. package/src/providers/inference/adapter-factory.ts +210 -0
  721. package/src/providers/inference/auth.ts +112 -0
  722. package/src/providers/inference/backfill.ts +196 -0
  723. package/src/providers/inference/connections.ts +401 -0
  724. package/src/providers/inference/resolve-auth.ts +73 -0
  725. package/src/providers/model-catalog.ts +386 -6
  726. package/src/providers/openai/chat-completions-provider.ts +10 -2
  727. package/src/providers/openai/responses-provider.ts +4 -2
  728. package/src/providers/openrouter/client.ts +7 -0
  729. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  730. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  731. package/src/providers/provider-availability.ts +17 -2
  732. package/src/providers/provider-catalog-visibility.ts +36 -0
  733. package/src/providers/provider-env-vars.ts +17 -7
  734. package/src/providers/provider-secret-catalog.ts +49 -30
  735. package/src/providers/provider-send-message.ts +41 -20
  736. package/src/providers/registry.ts +151 -159
  737. package/src/providers/retry.ts +65 -11
  738. package/src/providers/search-provider-catalog.ts +121 -0
  739. package/src/runtime/AGENTS.md +18 -5
  740. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  741. package/src/runtime/__tests__/background-job-runner.test.ts +357 -0
  742. package/src/runtime/__tests__/pre-first-message-gate.test.ts +82 -0
  743. package/src/runtime/actor-trust-resolver.ts +32 -10
  744. package/src/runtime/agent-wake.ts +64 -7
  745. package/src/runtime/assistant-event-hub.ts +3 -85
  746. package/src/runtime/auth/route-policy.ts +311 -9
  747. package/src/runtime/auth/same-actor.ts +2 -0
  748. package/src/runtime/background-job-runner.ts +339 -0
  749. package/src/runtime/btw-sidechain.ts +3 -0
  750. package/src/runtime/http-router.ts +36 -1
  751. package/src/runtime/http-server.ts +31 -5
  752. package/src/runtime/http-types.ts +21 -0
  753. package/src/runtime/middleware/__tests__/request-logger.test.ts +162 -0
  754. package/src/runtime/middleware/request-logger.ts +62 -1
  755. package/src/runtime/migrations/origin-mode.ts +1 -1
  756. package/src/runtime/pending-interactions.ts +1 -0
  757. package/src/runtime/pre-first-message-gate.ts +83 -0
  758. package/src/runtime/routes/__tests__/backup-routes.test.ts +8 -1
  759. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +268 -0
  760. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +142 -0
  761. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +319 -0
  762. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +280 -4
  763. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +15 -136
  764. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +736 -0
  765. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +4 -4
  766. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  767. package/src/runtime/routes/__tests__/stt-routes.test.ts +5 -1
  768. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +384 -0
  769. package/src/runtime/routes/__tests__/tts-routes.test.ts +70 -3
  770. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  771. package/src/runtime/routes/acp-routes.ts +12 -8
  772. package/src/runtime/routes/app-management-routes.ts +228 -3
  773. package/src/runtime/routes/approval-routes.ts +0 -18
  774. package/src/runtime/routes/audit-routes.ts +43 -0
  775. package/src/runtime/routes/auth-routes.ts +72 -0
  776. package/src/runtime/routes/avatar-routes.ts +273 -20
  777. package/src/runtime/routes/backup-routes.ts +406 -2
  778. package/src/runtime/routes/bookmark-routes.ts +156 -0
  779. package/src/runtime/routes/btw-routes.ts +5 -1
  780. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  781. package/src/runtime/routes/channel-verification-routes.ts +2 -1
  782. package/src/runtime/routes/contact-routes.ts +0 -160
  783. package/src/runtime/routes/conversation-cli-routes.ts +233 -0
  784. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  785. package/src/runtime/routes/conversation-management-routes.ts +47 -85
  786. package/src/runtime/routes/conversation-query-routes.ts +350 -97
  787. package/src/runtime/routes/conversation-routes.ts +121 -21
  788. package/src/runtime/routes/conversations-import-routes.ts +229 -0
  789. package/src/runtime/routes/credential-routes.ts +540 -0
  790. package/src/runtime/routes/debug-routes.ts +2 -2
  791. package/src/runtime/routes/document-pdf-renderer.ts +5 -1
  792. package/src/runtime/routes/documents-routes.ts +25 -86
  793. package/src/runtime/routes/domain-routes.ts +167 -0
  794. package/src/runtime/routes/email-routes.ts +603 -0
  795. package/src/runtime/routes/errors.ts +2 -2
  796. package/src/runtime/routes/events-routes.ts +192 -0
  797. package/src/runtime/routes/group-routes.ts +5 -0
  798. package/src/runtime/routes/home-feed-routes.ts +6 -78
  799. package/src/runtime/routes/host-app-control-routes.ts +44 -2
  800. package/src/runtime/routes/host-browser-routes.ts +103 -22
  801. package/src/runtime/routes/http-adapter.ts +2 -0
  802. package/src/runtime/routes/identity-routes.ts +5 -0
  803. package/src/runtime/routes/image-generation-routes.ts +99 -0
  804. package/src/runtime/routes/inbound-conversation.ts +28 -8
  805. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  806. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +248 -1
  807. package/src/runtime/routes/inbound-stages/background-dispatch.ts +118 -7
  808. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  809. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +156 -0
  810. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +22 -4
  811. package/src/runtime/routes/index.ts +42 -0
  812. package/src/runtime/routes/inference-profile-session-handler.ts +285 -0
  813. package/src/runtime/routes/inference-profile-session-reaper.ts +84 -0
  814. package/src/runtime/routes/inference-profile-session-routes.ts +146 -0
  815. package/src/runtime/routes/inference-provider-connection-routes.ts +361 -0
  816. package/src/runtime/routes/inference-send-routes.ts +115 -0
  817. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  818. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  819. package/src/runtime/routes/integrations/twilio.ts +7 -13
  820. package/src/runtime/routes/mcp-auth-routes.ts +283 -9
  821. package/src/runtime/routes/memory-v2-routes.ts +13 -398
  822. package/src/runtime/routes/notification-routes.ts +3 -1
  823. package/src/runtime/routes/oauth-apps.ts +112 -7
  824. package/src/runtime/routes/oauth-commands-routes.ts +1097 -0
  825. package/src/runtime/routes/oauth-connect-routes.ts +67 -5
  826. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  827. package/src/runtime/routes/oauth-providers.ts +298 -8
  828. package/src/runtime/routes/platform-routes.ts +336 -0
  829. package/src/runtime/routes/playground/inject-failures.ts +2 -1
  830. package/src/runtime/routes/playground/reset-circuit.ts +2 -1
  831. package/src/runtime/routes/playground/state.ts +2 -1
  832. package/src/runtime/routes/publish-routes.ts +221 -0
  833. package/src/runtime/routes/question-routes.ts +259 -0
  834. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  835. package/src/runtime/routes/schedule-routes.ts +79 -0
  836. package/src/runtime/routes/sequence-routes.ts +291 -0
  837. package/src/runtime/routes/settings-routes.ts +2 -10
  838. package/src/runtime/routes/skills-routes.ts +31 -1
  839. package/src/runtime/routes/stt-routes.ts +240 -3
  840. package/src/runtime/routes/subagents-routes.ts +57 -18
  841. package/src/runtime/routes/surface-action-routes.ts +43 -7
  842. package/src/runtime/routes/telemetry-routes.ts +27 -0
  843. package/src/runtime/routes/tts-routes.ts +93 -1
  844. package/src/runtime/routes/types.ts +32 -0
  845. package/src/runtime/routes/user-routes-cli.ts +243 -0
  846. package/src/runtime/routes/webhook-routes.ts +165 -0
  847. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  848. package/src/runtime/routes/workspace-routes.ts +28 -0
  849. package/src/runtime/services/conversation-serializer.ts +39 -7
  850. package/src/runtime/sync/resource-sync-events.ts +117 -0
  851. package/src/runtime/sync/sync-publisher.test.ts +105 -0
  852. package/src/runtime/sync/sync-publisher.ts +21 -0
  853. package/src/schedule/schedule-store.ts +27 -2
  854. package/src/schedule/scheduler.ts +208 -123
  855. package/src/security/__tests__/provider-key-env-fallback.test.ts +12 -6
  856. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  857. package/src/security/secret-patterns.ts +3 -0
  858. package/src/security/untrusted-content.ts +93 -8
  859. package/src/sequence/engine.ts +38 -40
  860. package/src/skills/catalog-files.ts +1 -1
  861. package/src/skills/catalog-install.ts +233 -116
  862. package/src/skills/clawhub.ts +70 -13
  863. package/src/skills/managed-store.ts +4 -119
  864. package/src/skills/skillssh-registry.ts +27 -48
  865. package/src/subagent/manager.ts +28 -15
  866. package/src/telemetry/types.ts +113 -1
  867. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  868. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  869. package/src/tools/apps/executors.ts +58 -7
  870. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  871. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  872. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +206 -0
  873. package/src/tools/browser/browser-execution.ts +29 -14
  874. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +174 -0
  875. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +16 -13
  876. package/src/tools/browser/cdp-client/extension-cdp-client.ts +24 -1
  877. package/src/tools/browser/cdp-client/factory.ts +66 -5
  878. package/src/tools/browser/runtime-check.ts +77 -0
  879. package/src/tools/computer-use/definitions.ts +3 -3
  880. package/src/tools/credentials/vault.ts +1 -1
  881. package/src/tools/document/document-tool.ts +124 -1
  882. package/src/tools/filesystem/edit.ts +1 -1
  883. package/src/tools/filesystem/list.ts +1 -1
  884. package/src/tools/filesystem/read.ts +1 -1
  885. package/src/tools/filesystem/write.ts +5 -2
  886. package/src/tools/host-filesystem/transfer.ts +1 -1
  887. package/src/tools/host-terminal/host-shell.ts +1 -1
  888. package/src/tools/memory/register.test.ts +3 -3
  889. package/src/tools/memory/register.ts +9 -1
  890. package/src/tools/network/__tests__/web-search.test.ts +156 -0
  891. package/src/tools/network/web-search.ts +280 -37
  892. package/src/tools/permission-checker.ts +14 -6
  893. package/src/tools/registry.ts +17 -7
  894. package/src/tools/schedule/create.ts +2 -2
  895. package/src/tools/schema-transforms.ts +7 -2
  896. package/src/tools/side-effects.ts +1 -0
  897. package/src/tools/skills/delete-managed.ts +4 -4
  898. package/src/tools/skills/execute.ts +1 -1
  899. package/src/tools/skills/scaffold-managed.ts +3 -2
  900. package/src/tools/subagent/notify-parent.ts +1 -1
  901. package/src/tools/subagent/spawn.ts +3 -3
  902. package/src/tools/system/request-permission.ts +2 -2
  903. package/src/tools/terminal/safe-env.ts +60 -1
  904. package/src/tools/terminal/shell.ts +44 -0
  905. package/src/tools/tool-manifest.ts +2 -0
  906. package/src/tools/types.ts +72 -21
  907. package/src/tools/ui-surface/definitions.ts +6 -5
  908. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  909. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  910. package/src/types/onboarding-context.ts +2 -0
  911. package/src/usage/attribution.ts +3 -2
  912. package/src/util/errors.ts +17 -0
  913. package/src/util/platform.ts +10 -0
  914. package/src/util/pricing.ts +86 -160
  915. package/src/watcher/__tests__/engine.test.ts +323 -0
  916. package/src/watcher/constants.ts +7 -0
  917. package/src/watcher/engine.ts +94 -90
  918. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +6 -9
  919. package/src/workspace/migrations/054-seed-recall-callsite.ts +10 -1
  920. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +94 -5
  921. package/src/workspace/migrations/069-seed-onboarding-threads.ts +8 -2
  922. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +117 -0
  923. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +95 -0
  924. package/src/workspace/migrations/074-drop-deprecated-secret-detection-keys.ts +117 -0
  925. package/src/workspace/migrations/075-memory-v2-bm25-b-default-reembed.ts +61 -0
  926. package/src/workspace/migrations/076-drop-services-inference-mode.ts +62 -0
  927. package/src/workspace/migrations/077-seed-memory-router-callsite.ts +89 -0
  928. package/src/workspace/migrations/078-release-notes-tavily-web-search.ts +66 -0
  929. package/src/workspace/migrations/079-home-feed-notification-only.ts +197 -0
  930. package/src/workspace/migrations/080-restrict-vercel-api-token-metadata.ts +182 -0
  931. package/src/workspace/migrations/081-backfill-bash-allowed-tools-for-injection-credentials.ts +160 -0
  932. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +154 -0
  933. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  934. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  935. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  936. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  937. package/src/workspace/migrations/registry.ts +30 -0
  938. package/src/workspace/migrations/runner.ts +46 -5
  939. package/src/workspace/migrations/types.ts +17 -3
  940. package/src/workspace/provider-commit-message-generator.ts +3 -2
  941. package/examples/plugins/echo/bun.lock +0 -25
  942. package/src/__tests__/context-search-pkb-source.test.ts +0 -498
  943. package/src/__tests__/context-window-manager.test.ts +0 -2093
  944. package/src/__tests__/credentials-cli.test.ts +0 -1225
  945. package/src/__tests__/memory-admin-recall.test.ts +0 -213
  946. package/src/approvals/__tests__/guardian-feed-event.test.ts +0 -303
  947. package/src/cli/commands/__tests__/email-download.test.ts +0 -260
  948. package/src/cli/commands/__tests__/email-list.test.ts +0 -216
  949. package/src/cli/commands/__tests__/email-register.test.ts +0 -186
  950. package/src/cli/commands/__tests__/email-send.test.ts +0 -416
  951. package/src/cli/commands/__tests__/email-status.test.ts +0 -185
  952. package/src/cli/commands/__tests__/email-unregister.test.ts +0 -168
  953. package/src/cli/commands/__tests__/routes.test.ts +0 -562
  954. package/src/cli/commands/__tests__/stt-transcribe.test.ts +0 -454
  955. package/src/cli/commands/autonomy.ts +0 -365
  956. package/src/cli/commands/memory.ts +0 -424
  957. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -947
  958. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +0 -686
  959. package/src/cli/commands/oauth/__tests__/mode.test.ts +0 -632
  960. package/src/cli/commands/oauth/__tests__/ping.test.ts +0 -631
  961. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +0 -573
  962. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +0 -330
  963. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +0 -521
  964. package/src/cli/commands/oauth/__tests__/status.test.ts +0 -551
  965. package/src/cli/commands/oauth/__tests__/token.test.ts +0 -420
  966. package/src/cli/lib/daemon-avatar-client.ts +0 -37
  967. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -87
  968. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +0 -207
  969. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  970. package/src/context/prompts/compact.md +0 -26
  971. package/src/daemon/__tests__/conversation-feed-event.test.ts +0 -304
  972. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +0 -233
  973. package/src/home/__tests__/assistant-feed-authoring.test.ts +0 -156
  974. package/src/home/__tests__/emit-feed-event.test.ts +0 -169
  975. package/src/home/__tests__/feed-population-integration.test.ts +0 -312
  976. package/src/home/__tests__/feed-scheduler.test.ts +0 -222
  977. package/src/home/__tests__/phase5-exit-criteria.test.ts +0 -229
  978. package/src/home/__tests__/platform-gmail-digest.test.ts +0 -222
  979. package/src/home/__tests__/rollup-producer.test.ts +0 -507
  980. package/src/home/assistant-feed-authoring.ts +0 -135
  981. package/src/home/emit-feed-event.ts +0 -169
  982. package/src/home/feed-scheduler.ts +0 -281
  983. package/src/home/platform-gmail-digest.ts +0 -163
  984. package/src/home/rewrite-command-preview.ts +0 -66
  985. package/src/home/rewrite-feed-title.ts +0 -58
  986. package/src/home/rollup-producer.ts +0 -426
  987. package/src/memory/admin.ts +0 -326
  988. package/src/memory/context-search/sources/pkb.ts +0 -476
  989. package/src/memory/graph/compaction.ts +0 -299
  990. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  991. /package/src/cli/{commands → lib}/cache-fs.ts +0 -0
@@ -1,2093 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import type { ContextWindowConfig } from "../config/types.js";
4
- import { estimateTextTokens } from "../context/token-estimator.js";
5
- import {
6
- clampSummaryAtSectionBoundary,
7
- CONTEXT_SUMMARY_MARKER,
8
- ContextWindowManager,
9
- createContextSummaryMessage,
10
- getSummaryFromContextMessage,
11
- stripCompactionOnlyInjections,
12
- } from "../context/window-manager.js";
13
- import type {
14
- ContentBlock,
15
- Message,
16
- Provider,
17
- ProviderResponse,
18
- SendMessageOptions,
19
- } from "../providers/types.js";
20
-
21
- function makeConfig(
22
- overrides: Partial<ContextWindowConfig> = {},
23
- ): ContextWindowConfig {
24
- return {
25
- enabled: true,
26
- maxInputTokens: 450,
27
- targetBudgetRatio: 0.67,
28
- compactThreshold: 0.6,
29
- summaryBudgetRatio: 0.05,
30
- overflowRecovery: {
31
- enabled: true,
32
- safetyMarginRatio: 0.05,
33
- maxAttempts: 3,
34
- interactiveLatestTurnCompression: "summarize",
35
- nonInteractiveLatestTurnCompression: "truncate",
36
- },
37
- ...overrides,
38
- };
39
- }
40
-
41
- function createProvider(
42
- fn: (messages: Message[]) => ProviderResponse | Promise<ProviderResponse>,
43
- name: string = "mock",
44
- ): Provider {
45
- return {
46
- name,
47
- async sendMessage(messages: Message[]): Promise<ProviderResponse> {
48
- return fn(messages);
49
- },
50
- };
51
- }
52
-
53
- function message(role: "user" | "assistant", text: string): Message {
54
- return { role, content: [{ type: "text", text }] };
55
- }
56
-
57
- describe("ContextWindowManager", () => {
58
- test("skips compaction when estimated tokens are below threshold", async () => {
59
- const provider = createProvider(() => {
60
- throw new Error("should not be called");
61
- });
62
- const manager = new ContextWindowManager({
63
- provider,
64
- systemPrompt: "system prompt",
65
- config: makeConfig(),
66
- });
67
- const history = [message("user", "hello"), message("assistant", "hi")];
68
-
69
- const result = await manager.maybeCompact(history);
70
- expect(result.compacted).toBe(false);
71
- expect(result.messages).toEqual(history);
72
- expect(result.reason).toBe("below compaction threshold");
73
- });
74
-
75
- test("explains forced compaction skip when conversation already fits target", async () => {
76
- const provider = createProvider(() => {
77
- throw new Error("summarizer should not be called");
78
- });
79
- const manager = new ContextWindowManager({
80
- provider,
81
- systemPrompt: "system prompt",
82
- config: makeConfig({
83
- maxInputTokens: 10_000,
84
- targetBudgetRatio: 0.5,
85
- }),
86
- });
87
- const history = [message("user", "hello"), message("assistant", "hi")];
88
-
89
- const result = await manager.maybeCompact(history, undefined, {
90
- force: true,
91
- });
92
-
93
- expect(result.compacted).toBe(false);
94
- expect(result.messages).toEqual(history);
95
- expect(result.reason).toBe(
96
- "conversation already fits within the compaction target",
97
- );
98
- });
99
-
100
- test("forced compaction summarizes when projection fits but real usage exceeds target", async () => {
101
- let summaryCalls = 0;
102
- const provider = createProvider(() => {
103
- summaryCalls += 1;
104
- return {
105
- content: [
106
- { type: "text", text: "## Summary\n- forced compaction ran" },
107
- ],
108
- model: "mock-model",
109
- usage: { inputTokens: 100, outputTokens: 25 },
110
- stopReason: "end_turn",
111
- };
112
- });
113
- const manager = new ContextWindowManager({
114
- provider,
115
- systemPrompt: "system prompt",
116
- config: makeConfig({
117
- maxInputTokens: 10_000,
118
- targetBudgetRatio: 0.5,
119
- }),
120
- });
121
- // Tiny live messages so the projection trivially fits target — without
122
- // the fix this would route through the "already fits" skip path.
123
- const history: Message[] = [
124
- message("user", "u1"),
125
- message("assistant", "a1"),
126
- message("user", "u2"),
127
- message("assistant", "a2"),
128
- ];
129
-
130
- const result = await manager.maybeCompact(history, undefined, {
131
- force: true,
132
- // Simulate a live conversation that's well over target. In production
133
- // this happens when synthetic tool_result truncation in the projection
134
- // is far more aggressive than what the real messages allow.
135
- precomputedEstimate: 50_000,
136
- });
137
-
138
- expect(result.compacted).toBe(true);
139
- expect(summaryCalls).toBe(1);
140
- expect(result.reason).not.toBe(
141
- "conversation already fits within the compaction target",
142
- );
143
- expect(result.compactedMessages).toBeGreaterThan(0);
144
- });
145
-
146
- test("compacts old turns and keeps recent user turns", async () => {
147
- let summaryCalls = 0;
148
- const provider = createProvider(() => {
149
- summaryCalls += 1;
150
- return {
151
- content: [
152
- { type: "text", text: `## Goals\n- summary call ${summaryCalls}` },
153
- ],
154
- model: "mock-model",
155
- usage: { inputTokens: 100, outputTokens: 25 },
156
- stopReason: "end_turn",
157
- };
158
- });
159
- const manager = new ContextWindowManager({
160
- provider,
161
- systemPrompt: "system prompt",
162
- config: makeConfig({ maxInputTokens: 600 }),
163
- });
164
- const long = "x".repeat(240);
165
- const history: Message[] = [
166
- message("user", `u1 ${long}`),
167
- message("assistant", `a1 ${long}`),
168
- message("user", `u2 ${long}`),
169
- message("assistant", `a2 ${long}`),
170
- message("user", `u3 ${long}`),
171
- message("assistant", `a3 ${long}`),
172
- ];
173
-
174
- const result = await manager.maybeCompact(history);
175
-
176
- expect(result.compacted).toBe(true);
177
- expect(result.compactedMessages).toBeGreaterThan(0);
178
- expect(result.summaryCalls).toBe(summaryCalls);
179
- expect(result.summaryInputTokens).toBeGreaterThan(0);
180
- expect(result.summaryOutputTokens).toBeGreaterThan(0);
181
- expect(result.messages[0].role).toBe("user");
182
- expect(
183
- getSummaryFromContextMessage(result.messages[0])?.length,
184
- ).toBeGreaterThan(0);
185
-
186
- const userTexts = result.messages
187
- .filter((m) => m.role === "user")
188
- .map((m) => (m.content[0].type === "text" ? m.content[0].text : ""));
189
- expect(userTexts.some((text) => text.startsWith("u1 "))).toBe(false);
190
- expect(userTexts.some((text) => text.startsWith("u2 "))).toBe(true);
191
- expect(userTexts.some((text) => text.startsWith("u3 "))).toBe(true);
192
- });
193
-
194
- test("returns cache-aware summary usage from single-pass compaction", async () => {
195
- const provider = createProvider(() => {
196
- return {
197
- content: [
198
- { type: "text", text: `## Goals\n- summary of full transcript` },
199
- ],
200
- model: "claude-opus-4-6",
201
- usage: {
202
- inputTokens: 5_000,
203
- outputTokens: 80,
204
- cacheCreationInputTokens: 50,
205
- cacheReadInputTokens: 200,
206
- },
207
- rawResponse: {
208
- usage: {
209
- cache_creation: {
210
- ephemeral_5m_input_tokens: 50,
211
- ephemeral_1h_input_tokens: 0,
212
- },
213
- cache_read_input_tokens: 200,
214
- },
215
- },
216
- stopReason: "end_turn",
217
- };
218
- });
219
- const manager = new ContextWindowManager({
220
- provider,
221
- systemPrompt: "system prompt",
222
- config: makeConfig({
223
- maxInputTokens: 7_000,
224
- targetBudgetRatio: 0.41,
225
- }),
226
- });
227
- const long = "q".repeat(6_000);
228
- const history: Message[] = [
229
- message("user", `u1 ${long}`),
230
- message("assistant", `a1 ${long}`),
231
- message("user", `u2 ${long}`),
232
- message("assistant", `a2 ${long}`),
233
- message("user", `u3 ${long}`),
234
- ];
235
-
236
- const result = await manager.maybeCompact(history);
237
-
238
- expect(result.compacted).toBe(true);
239
- expect(result.summaryCalls).toBe(1);
240
- expect(result.summaryCacheCreationInputTokens).toBe(50);
241
- expect(result.summaryCacheReadInputTokens).toBe(200);
242
- expect(result.summaryRawResponses).toHaveLength(1);
243
- expect(result.summaryRawResponses?.[0]).toMatchObject({
244
- usage: {
245
- cache_creation: { ephemeral_5m_input_tokens: 50 },
246
- cache_read_input_tokens: 200,
247
- },
248
- });
249
- });
250
-
251
- test("updates an existing summary message instead of nesting summaries", async () => {
252
- const provider = createProvider(() => ({
253
- content: [{ type: "text", text: "## Goals\n- updated summary" }],
254
- model: "mock-model",
255
- usage: { inputTokens: 50, outputTokens: 10 },
256
- stopReason: "end_turn",
257
- }));
258
- const manager = new ContextWindowManager({
259
- provider,
260
- systemPrompt: "system prompt",
261
- config: makeConfig({
262
- maxInputTokens: 300,
263
- targetBudgetRatio: 0.58,
264
- }),
265
- });
266
- const long = "y".repeat(220);
267
- const history: Message[] = [
268
- createContextSummaryMessage("## Goals\n- old summary"),
269
- message("user", `older ${long}`),
270
- message("assistant", `reply ${long}`),
271
- message("user", `latest ${long}`),
272
- ];
273
-
274
- const result = await manager.maybeCompact(history);
275
- expect(result.compacted).toBe(true);
276
- expect(result.messages.length).toBeLessThan(history.length + 1);
277
- expect(getSummaryFromContextMessage(result.messages[0])).toContain(
278
- "updated summary",
279
- );
280
- expect(
281
- result.messages.filter(
282
- (m) =>
283
- m.role === "user" &&
284
- m.content.some(
285
- (block) =>
286
- block.type === "text" &&
287
- block.text.startsWith(CONTEXT_SUMMARY_MARKER),
288
- ),
289
- ),
290
- ).toHaveLength(1);
291
- });
292
-
293
- test("falls back to local summary when provider summarization fails", async () => {
294
- const provider = createProvider(async () => {
295
- throw new Error("provider unavailable");
296
- });
297
- const manager = new ContextWindowManager({
298
- provider,
299
- systemPrompt: "system prompt",
300
- config: makeConfig({
301
- maxInputTokens: 260,
302
- targetBudgetRatio: 0.59,
303
- }),
304
- });
305
- const long = "z".repeat(220);
306
- const history = [
307
- message("user", `task ${long}`),
308
- message("assistant", `result ${long}`),
309
- message("user", `followup ${long}`),
310
- ];
311
-
312
- const result = await manager.maybeCompact(history);
313
- expect(result.compacted).toBe(true);
314
- expect(result.summaryCalls).toBeGreaterThan(0);
315
- expect(result.summaryInputTokens).toBe(0);
316
- expect(result.summaryOutputTokens).toBe(0);
317
- expect(result.summaryModel).toBe("");
318
- expect(result.summaryText).toContain("## Recent Progress");
319
- });
320
-
321
- test("marks summaryFailed when the provider throws and fallback runs", async () => {
322
- // The agent-loop circuit breaker distinguishes "LLM call failed but
323
- // fallback rescued us" from "compaction succeeded end-to-end". The
324
- // fallback path must set summaryFailed:true so callers can count
325
- // consecutive failures without losing the compacted messages.
326
- const provider = createProvider(async () => {
327
- throw new Error("provider unavailable");
328
- });
329
- const manager = new ContextWindowManager({
330
- provider,
331
- systemPrompt: "system prompt",
332
- config: makeConfig({
333
- maxInputTokens: 260,
334
- targetBudgetRatio: 0.59,
335
- }),
336
- });
337
- const long = "z".repeat(220);
338
- const history = [
339
- message("user", `task ${long}`),
340
- message("assistant", `result ${long}`),
341
- message("user", `followup ${long}`),
342
- ];
343
-
344
- const result = await manager.maybeCompact(history);
345
- expect(result.compacted).toBe(true);
346
- expect(result.summaryFailed).toBe(true);
347
- });
348
-
349
- test("does not mark summaryFailed on a successful provider call", async () => {
350
- const provider = createProvider(() => ({
351
- content: [
352
- { type: "text", text: "## Goals\n- summary produced by provider" },
353
- ],
354
- model: "mock-model",
355
- usage: { inputTokens: 60, outputTokens: 12 },
356
- stopReason: "end_turn",
357
- }));
358
- const manager = new ContextWindowManager({
359
- provider,
360
- systemPrompt: "system prompt",
361
- config: makeConfig({
362
- maxInputTokens: 260,
363
- targetBudgetRatio: 0.59,
364
- }),
365
- });
366
- const long = "z".repeat(220);
367
- const history = [
368
- message("user", `task ${long}`),
369
- message("assistant", `result ${long}`),
370
- message("user", `followup ${long}`),
371
- ];
372
-
373
- const result = await manager.maybeCompact(history);
374
- expect(result.compacted).toBe(true);
375
- expect(result.summaryFailed).toBe(false);
376
- });
377
-
378
- test("serializes file blocks for summary chunks", async () => {
379
- const prompts: string[] = [];
380
- const provider = createProvider((messages) => {
381
- for (const block of messages[0]?.content ?? []) {
382
- if (block.type === "text") {
383
- prompts.push(block.text);
384
- }
385
- }
386
- return {
387
- content: [{ type: "text", text: "## Goals\n- file summarized" }],
388
- model: "mock-model",
389
- usage: { inputTokens: 60, outputTokens: 12 },
390
- stopReason: "end_turn",
391
- };
392
- });
393
- const manager = new ContextWindowManager({
394
- provider,
395
- systemPrompt: "system prompt",
396
- config: makeConfig({
397
- maxInputTokens: 2000,
398
- targetBudgetRatio: 0.4,
399
- compactThreshold: 0.35,
400
- }),
401
- });
402
- const long = "f".repeat(1500);
403
- const history: Message[] = [
404
- {
405
- role: "user",
406
- content: [
407
- {
408
- type: "file",
409
- source: {
410
- type: "base64",
411
- media_type: "application/pdf",
412
- filename: "spec.pdf",
413
- data: "a".repeat(4096),
414
- },
415
- extracted_text: "Critical requirement from attached spec.",
416
- },
417
- ],
418
- },
419
- message("assistant", `ack ${long}`),
420
- message("user", `followup ${long}`),
421
- ];
422
-
423
- const result = await manager.maybeCompact(history);
424
- expect(result.compacted).toBe(true);
425
-
426
- const combinedPrompts = prompts.join("\n");
427
- expect(combinedPrompts).toContain("file: spec.pdf");
428
- expect(combinedPrompts).toContain("application/pdf");
429
- expect(combinedPrompts).toContain(
430
- "Critical requirement from attached spec.",
431
- );
432
- expect(combinedPrompts).not.toContain("unknown_block");
433
- });
434
-
435
- test("passes image blocks to summarizer instead of text metadata", async () => {
436
- const receivedBlocks: { type: string; mediaType?: string }[] = [];
437
- const provider = createProvider((messages) => {
438
- for (const block of messages[0]?.content ?? []) {
439
- if (block.type === "image") {
440
- receivedBlocks.push({
441
- type: "image",
442
- mediaType: (block as { source: { media_type: string } }).source
443
- .media_type,
444
- });
445
- } else if (block.type === "text") {
446
- receivedBlocks.push({ type: "text" });
447
- }
448
- }
449
- return {
450
- content: [
451
- {
452
- type: "text",
453
- text: "## Goals\n- described image: a photo of a cat",
454
- },
455
- ],
456
- model: "mock-model",
457
- usage: { inputTokens: 100, outputTokens: 20 },
458
- stopReason: "end_turn",
459
- };
460
- });
461
- // Use a large enough maxInputTokens so the image fits in the summarizer
462
- // budget after accounting for overhead (system prompt, scaffolding, output).
463
- const manager = new ContextWindowManager({
464
- provider,
465
- systemPrompt: "sys",
466
- config: makeConfig({
467
- maxInputTokens: 5000,
468
- compactThreshold: 0.3,
469
- targetBudgetRatio: 0.2,
470
- }),
471
- });
472
- const long = "x".repeat(4000);
473
- const history: Message[] = [
474
- {
475
- role: "user",
476
- content: [
477
- { type: "text", text: "look at this" },
478
- {
479
- type: "image",
480
- source: {
481
- type: "base64",
482
- media_type: "image/png",
483
- data: "iVBORw0KGgo=",
484
- },
485
- },
486
- ],
487
- },
488
- message("assistant", `a1 ${long}`),
489
- message("user", `u2 ${long}`),
490
- ];
491
-
492
- const result = await manager.maybeCompact(history);
493
- expect(result.compacted).toBe(true);
494
-
495
- // The summarizer should have received actual image blocks, not text stubs.
496
- const imageBlocks = receivedBlocks.filter((b) => b.type === "image");
497
- expect(imageBlocks.length).toBe(1);
498
- expect(imageBlocks[0].mediaType).toBe("image/png");
499
- });
500
-
501
- test("passes tool_result images to summarizer", async () => {
502
- const receivedImageCount = { count: 0 };
503
- const provider = createProvider((messages) => {
504
- for (const block of messages[0]?.content ?? []) {
505
- if (block.type === "image") {
506
- receivedImageCount.count++;
507
- }
508
- }
509
- return {
510
- content: [
511
- { type: "text", text: "## Goals\n- summarized tool output images" },
512
- ],
513
- model: "mock-model",
514
- usage: { inputTokens: 100, outputTokens: 20 },
515
- stopReason: "end_turn",
516
- };
517
- });
518
- const manager = new ContextWindowManager({
519
- provider,
520
- systemPrompt: "sys",
521
- config: makeConfig({
522
- maxInputTokens: 5000,
523
- compactThreshold: 0.3,
524
- targetBudgetRatio: 0.2,
525
- }),
526
- });
527
- const long = "x".repeat(2000);
528
- const history: Message[] = [
529
- message("assistant", "let me read that file"),
530
- {
531
- role: "user",
532
- content: [
533
- {
534
- type: "tool_result",
535
- tool_use_id: "tool_1",
536
- content: "file contents",
537
- contentBlocks: [
538
- {
539
- type: "image",
540
- source: {
541
- type: "base64",
542
- media_type: "image/jpeg",
543
- data: "iVBORw0KGgo=",
544
- },
545
- },
546
- ],
547
- is_error: false,
548
- } as import("../providers/types.js").ToolResultContent,
549
- ],
550
- },
551
- message("user", `followup ${long}`),
552
- message("assistant", `response ${long}`),
553
- message("user", `final ${long}`),
554
- ];
555
-
556
- const result = await manager.maybeCompact(history);
557
- expect(result.compacted).toBe(true);
558
- expect(receivedImageCount.count).toBe(1);
559
- });
560
-
561
- test("counts compacted persisted messages including tool-result user turns", async () => {
562
- const provider = createProvider(() => ({
563
- content: [{ type: "text", text: "## Goals\n- compacted summary" }],
564
- model: "mock-model",
565
- usage: { inputTokens: 75, outputTokens: 20 },
566
- stopReason: "end_turn",
567
- }));
568
- const manager = new ContextWindowManager({
569
- provider,
570
- systemPrompt: "system prompt",
571
- config: makeConfig({
572
- maxInputTokens: 320,
573
- targetBudgetRatio: 0.58,
574
- }),
575
- });
576
- const long = "k".repeat(220);
577
- const history: Message[] = [
578
- message("user", `u1 ${long}`),
579
- {
580
- role: "assistant",
581
- content: [
582
- {
583
- type: "tool_use",
584
- id: "t1",
585
- name: "read_file",
586
- input: { path: "/tmp/a" },
587
- },
588
- ],
589
- },
590
- {
591
- role: "user",
592
- content: [
593
- { type: "tool_result", tool_use_id: "t1", content: "contents" },
594
- ],
595
- },
596
- message("assistant", `a1 ${long}`),
597
- message("user", `u2 ${long}`),
598
- ];
599
-
600
- const result = await manager.maybeCompact(history);
601
- expect(result.compacted).toBe(true);
602
- expect(result.compactedMessages).toBe(4);
603
- // Tool-result-only user messages have DB counterparts and must be
604
- // counted so contextCompactedMessageCount indexes the DB correctly.
605
- expect(result.compactedPersistedMessages).toBe(4);
606
- });
607
-
608
- test("adjusts keep boundary to preserve tool_use/tool_result pairs", async () => {
609
- const provider = createProvider(() => ({
610
- content: [{ type: "text", text: "## Goals\n- compacted summary" }],
611
- model: "mock-model",
612
- usage: { inputTokens: 75, outputTokens: 20 },
613
- stopReason: "end_turn",
614
- }));
615
- // Configure budget so compaction keeps only the last user turn,
616
- // which would normally split the tool pair because the last user
617
- // turn start is a mixed message (tool_result + text) whose matching
618
- // tool_use lives in the preceding assistant message.
619
- const manager = new ContextWindowManager({
620
- provider,
621
- systemPrompt: "system prompt",
622
- config: makeConfig({
623
- maxInputTokens: 320,
624
- targetBudgetRatio: 0.58,
625
- }),
626
- });
627
- const long = "k".repeat(220);
628
- const history: Message[] = [
629
- message("user", `u1 ${long}`), // index 0: old user turn (long)
630
- message("assistant", `a1 ${long}`), // index 1: assistant reply (long)
631
- message("user", `u2 ${long}`), // index 2: second user turn (long)
632
- {
633
- // index 3: assistant with tool_use
634
- role: "assistant",
635
- content: [
636
- {
637
- type: "tool_use",
638
- id: "t1",
639
- name: "read_file",
640
- input: { path: "/tmp/a" },
641
- },
642
- ],
643
- },
644
- {
645
- // index 4: user with tool_result AND text (mixed = user turn start)
646
- // Without adjustForToolPairs, the raw boundary would land here,
647
- // orphaning the tool_result from its tool_use at index 3.
648
- role: "user",
649
- content: [
650
- { type: "tool_result", tool_use_id: "t1", content: "file contents" },
651
- { type: "text", text: "thanks, now continue" },
652
- ],
653
- },
654
- ];
655
-
656
- const result = await manager.maybeCompact(history);
657
- expect(result.compacted).toBe(true);
658
- // The kept messages must include the tool_use assistant message (index 3)
659
- // and tool_result user message (index 4) as a pair, not split them.
660
- // Verify no orphaned tool_result blocks exist in the kept messages.
661
- const keptMessages = result.messages;
662
- for (let i = 0; i < keptMessages.length; i++) {
663
- const msg = keptMessages[i];
664
- if (msg.role !== "user") continue;
665
- for (const block of msg.content) {
666
- if (block.type === "tool_result") {
667
- // Every tool_result must have a matching tool_use in a preceding assistant message
668
- const toolUseId = (block as { tool_use_id: string }).tool_use_id;
669
- const hasMatchingToolUse = keptMessages
670
- .slice(0, i)
671
- .some(
672
- (prev) =>
673
- prev.role === "assistant" &&
674
- prev.content.some(
675
- (b) =>
676
- b.type === "tool_use" &&
677
- (b as { id: string }).id === toolUseId,
678
- ),
679
- );
680
- expect(hasMatchingToolUse).toBe(true);
681
- }
682
- }
683
- }
684
- });
685
-
686
- test("counts mixed tool_result+text user messages as persisted", async () => {
687
- const provider = createProvider(() => ({
688
- content: [{ type: "text", text: "## Goals\n- mixed summary" }],
689
- model: "mock-model",
690
- usage: { inputTokens: 75, outputTokens: 20 },
691
- stopReason: "end_turn",
692
- }));
693
- const manager = new ContextWindowManager({
694
- provider,
695
- systemPrompt: "system prompt",
696
- config: makeConfig({
697
- maxInputTokens: 320,
698
- targetBudgetRatio: 0.58,
699
- }),
700
- });
701
- const long = "k".repeat(220);
702
- // Simulates a merged user message (repairHistory merges consecutive same-role
703
- // messages), resulting in a user turn with both tool_result and text blocks.
704
- const history: Message[] = [
705
- message("user", `u1 ${long}`),
706
- {
707
- role: "assistant",
708
- content: [
709
- {
710
- type: "tool_use",
711
- id: "t1",
712
- name: "read_file",
713
- input: { path: "/tmp/a" },
714
- },
715
- ],
716
- },
717
- {
718
- role: "user",
719
- content: [
720
- { type: "tool_result", tool_use_id: "t1", content: "contents" },
721
- { type: "text", text: `follow-up question ${long}` },
722
- ],
723
- },
724
- message("assistant", `a1 ${long}`),
725
- message("user", `u2 ${long}`),
726
- ];
727
-
728
- const result = await manager.maybeCompact(history);
729
- expect(result.compacted).toBe(true);
730
- // The mixed user message should be counted as persisted (4 = u1 + mixed + a_tooluse + a1)
731
- expect(result.compactedPersistedMessages).toBe(4);
732
- });
733
-
734
- test("returns cache-aware usage metadata for compaction summaries", async () => {
735
- const rawResponse = {
736
- usage: {
737
- cache_creation: { ephemeral_5m_input_tokens: 120 },
738
- cache_read_input_tokens: 340,
739
- },
740
- };
741
- const provider = createProvider(() => ({
742
- content: [{ type: "text", text: "## Goals\n- cache-aware summary" }],
743
- model: "claude-opus-4-6",
744
- usage: {
745
- inputTokens: 500,
746
- outputTokens: 22,
747
- cacheCreationInputTokens: 120,
748
- cacheReadInputTokens: 340,
749
- },
750
- rawResponse,
751
- stopReason: "end_turn",
752
- }));
753
- const manager = new ContextWindowManager({
754
- provider,
755
- systemPrompt: "system prompt",
756
- config: makeConfig({
757
- maxInputTokens: 2600,
758
- targetBudgetRatio: 0.63,
759
- }),
760
- });
761
- const long = "c".repeat(5000);
762
- const history: Message[] = [
763
- message("user", `u1 ${long}`),
764
- message("assistant", `a1 ${long}`),
765
- message("user", `u2 ${long}`),
766
- ];
767
-
768
- const result = await manager.maybeCompact(history);
769
-
770
- expect(result.compacted).toBe(true);
771
- expect(result.summaryCalls).toBe(1);
772
- expect(result.summaryInputTokens).toBe(500);
773
- expect(result.summaryCacheCreationInputTokens).toBe(120);
774
- expect(result.summaryCacheReadInputTokens).toBe(340);
775
- expect(result.summaryRawResponses).toEqual([rawResponse]);
776
- });
777
-
778
- test("does not parse user-authored summary marker text as internal summary", () => {
779
- const userMessage: Message = {
780
- role: "user",
781
- content: [
782
- {
783
- type: "text",
784
- text: `${CONTEXT_SUMMARY_MARKER}\nI typed this prefix myself`,
785
- },
786
- ],
787
- };
788
- expect(getSummaryFromContextMessage(userMessage)).toBeNull();
789
- });
790
-
791
- test("skips compaction during cooldown", async () => {
792
- const provider = createProvider(() => {
793
- throw new Error(
794
- "summarizer should not be called while cooldown skip is active",
795
- );
796
- });
797
- const manager = new ContextWindowManager({
798
- provider,
799
- systemPrompt: "system prompt",
800
- config: makeConfig({
801
- maxInputTokens: 260,
802
- targetBudgetRatio: 0.74,
803
- }),
804
- });
805
- const long = "c".repeat(220);
806
- const history: Message[] = [
807
- message("user", `u1 ${long}`),
808
- message("assistant", `a1 ${long}`),
809
- message("user", `u2 ${long}`),
810
- ];
811
-
812
- const result = await manager.maybeCompact(history, undefined, {
813
- lastCompactedAt: Date.now() - 30_000,
814
- });
815
- expect(result.compacted).toBe(false);
816
- expect(result.reason).toBe("compaction cooldown active");
817
- });
818
-
819
- test("ignores cooldown and compacts under severe token pressure", async () => {
820
- const provider = createProvider(() => ({
821
- content: [{ type: "text", text: "## Goals\n- compacted under pressure" }],
822
- model: "mock-model",
823
- usage: { inputTokens: 60, outputTokens: 12 },
824
- stopReason: "end_turn",
825
- }));
826
- const manager = new ContextWindowManager({
827
- provider,
828
- systemPrompt: "system prompt",
829
- config: makeConfig({
830
- maxInputTokens: 320,
831
- targetBudgetRatio: 0.61,
832
- }),
833
- });
834
- const long = "p".repeat(340);
835
- const history: Message[] = [
836
- message("user", `u1 ${long}`),
837
- message("assistant", `a1 ${long}`),
838
- message("user", `u2 ${long}`),
839
- message("assistant", `a2 ${long}`),
840
- message("user", `u3 ${long}`),
841
- ];
842
-
843
- const result = await manager.maybeCompact(history, undefined, {
844
- lastCompactedAt: Date.now() - 30_000,
845
- });
846
- expect(result.compacted).toBe(true);
847
- expect(result.reason).toBeUndefined();
848
- });
849
-
850
- test("force=true bypasses cooldown for context-too-large recovery", async () => {
851
- const provider = createProvider(() => ({
852
- content: [{ type: "text", text: "## Goals\n- forced compaction" }],
853
- model: "mock-model",
854
- usage: { inputTokens: 60, outputTokens: 12 },
855
- stopReason: "end_turn",
856
- }));
857
- const manager = new ContextWindowManager({
858
- provider,
859
- systemPrompt: "system prompt",
860
- config: makeConfig({
861
- maxInputTokens: 260,
862
- targetBudgetRatio: 0.74,
863
- }),
864
- });
865
- const long = "c".repeat(220);
866
- const history: Message[] = [
867
- message("user", `u1 ${long}`),
868
- message("assistant", `a1 ${long}`),
869
- message("user", `u2 ${long}`),
870
- ];
871
-
872
- // Same setup as the cooldown test, but with force=true — should compact.
873
- const result = await manager.maybeCompact(history, undefined, {
874
- lastCompactedAt: Date.now() - 30_000,
875
- force: true,
876
- });
877
- expect(result.compacted).toBe(true);
878
- expect(result.reason).toBeUndefined();
879
- });
880
-
881
- test("image-heavy payload is no longer underestimated as below-threshold", async () => {
882
- const provider = createProvider(() => ({
883
- content: [
884
- { type: "text", text: "## Goals\n- compacted image-heavy history" },
885
- ],
886
- model: "mock-model",
887
- usage: { inputTokens: 75, outputTokens: 20 },
888
- stopReason: "end_turn",
889
- }));
890
- const manager = new ContextWindowManager({
891
- provider,
892
- systemPrompt: "system prompt",
893
- config: makeConfig({
894
- maxInputTokens: 7000,
895
- targetBudgetRatio: 0.76,
896
- compactThreshold: 0.8,
897
- }),
898
- });
899
-
900
- const images = Array.from({ length: 5 }, (_, i) => ({
901
- type: "image" as const,
902
- source: {
903
- type: "base64" as const,
904
- media_type: "image/png",
905
- data: `${String(i)}${"A".repeat(40_000)}`,
906
- },
907
- }));
908
-
909
- const history: Message[] = [
910
- {
911
- role: "user",
912
- content: [
913
- { type: "text", text: "Please analyze these screenshots." },
914
- ...images,
915
- ],
916
- },
917
- message("assistant", "Sure, uploading now."),
918
- ];
919
-
920
- const result = await manager.maybeCompact(history);
921
- expect(result.reason).not.toBe("below compaction threshold");
922
-
923
- // Sanity check for this repro: counting raw base64 as text would exceed threshold.
924
- const rawBase64Chars = images.reduce(
925
- (sum, img) => sum + img.source.data.length,
926
- 0,
927
- );
928
- const rawBase64TokenEquivalent = estimateTextTokens(
929
- "A".repeat(rawBase64Chars),
930
- );
931
- expect(rawBase64TokenEquivalent).toBeGreaterThan(result.thresholdTokens);
932
- });
933
-
934
- test("minKeepRecentUserTurns: 0 compacts all messages into summary only", async () => {
935
- const provider = createProvider(() => ({
936
- content: [{ type: "text", text: "## Goals\n- emergency summary" }],
937
- model: "mock-model",
938
- usage: { inputTokens: 60, outputTokens: 12 },
939
- stopReason: "end_turn",
940
- }));
941
- const manager = new ContextWindowManager({
942
- provider,
943
- systemPrompt: "system prompt",
944
- config: makeConfig({
945
- maxInputTokens: 260,
946
- targetBudgetRatio: 0.28,
947
- }),
948
- });
949
- const long = "e".repeat(220);
950
- const history: Message[] = [
951
- message("user", `u1 ${long}`),
952
- message("assistant", `a1 ${long}`),
953
- message("user", `u2 ${long}`),
954
- ];
955
-
956
- const result = await manager.maybeCompact(history, undefined, {
957
- force: true,
958
- minKeepRecentUserTurns: 0,
959
- });
960
- expect(result.compacted).toBe(true);
961
- // With minKeepRecentUserTurns=0 and a tight target budget,
962
- // pickKeepBoundary drops keepTurns all the way to 0.
963
- // All three messages are compacted into a single summary message.
964
- expect(result.compactedMessages).toBe(3);
965
- expect(result.messages).toHaveLength(1);
966
- expect(getSummaryFromContextMessage(result.messages[0])).toContain(
967
- "emergency summary",
968
- );
969
- });
970
-
971
- test("force compaction with loose target override still summarizes persisted messages", async () => {
972
- // `pickKeepBoundary` clamps `targetInputTokensOverride` to
973
- // `config.targetInputTokens`, so a loose override cannot
974
- // short-circuit summarization into the truncate-only early-exit.
975
-
976
- let summaryCalls = 0;
977
- const provider = createProvider(() => {
978
- summaryCalls += 1;
979
- return {
980
- content: [{ type: "text", text: "## Goals\n- real summary" }],
981
- model: "mock-model",
982
- usage: { inputTokens: 80, outputTokens: 20 },
983
- stopReason: "end_turn",
984
- };
985
- });
986
-
987
- // Scaled from prod (max 200k → 1000) preserving key ratios: the
988
- // loose override (~0.85×max) is ~17× the post-compaction target
989
- // (~0.05×max), so history between the two exercises the clamp.
990
- const manager = new ContextWindowManager({
991
- provider,
992
- systemPrompt: "system prompt",
993
- config: makeConfig({
994
- maxInputTokens: 1000,
995
- targetBudgetRatio: 0.1,
996
- summaryBudgetRatio: 0.05,
997
- compactThreshold: 0.3,
998
- }),
999
- });
1000
-
1001
- // History in the "no-op zone": above threshold (300), below override (850).
1002
- const long = "x".repeat(180);
1003
- const history: Message[] = [
1004
- message("user", `u1 ${long}`),
1005
- message("assistant", `a1 ${long}`),
1006
- message("user", `u2 ${long}`),
1007
- message("assistant", `a2 ${long}`),
1008
- message("user", `u3 ${long}`),
1009
- message("assistant", `a3 ${long}`),
1010
- message("user", `u4 ${long}`),
1011
- message("assistant", `a4 ${long}`),
1012
- message("user", `u5 ${long}`),
1013
- ];
1014
-
1015
- const preflightBudgetAnalog = Math.floor(1000 * 0.85);
1016
- const result = await manager.maybeCompact(history, undefined, {
1017
- force: true,
1018
- targetInputTokensOverride: preflightBudgetAnalog,
1019
- });
1020
-
1021
- // Guard: we're actually above the compact threshold.
1022
- expect(result.previousEstimatedInputTokens).toBeGreaterThan(
1023
- result.thresholdTokens,
1024
- );
1025
-
1026
- // A real summarization happened (not the truncate-only no-op).
1027
- expect(result.compactedPersistedMessages).toBeGreaterThan(0);
1028
- expect(summaryCalls).toBeGreaterThan(0);
1029
- });
1030
-
1031
- test("force=true compacts below minFloor when a kept turn exceeds target", async () => {
1032
- // A giant paste in the last user turn means minFloor=1 alone exceeds target.
1033
- // Under force, pickKeepBoundary should walk keepTurns below minFloor (down to
1034
- // 0) so the huge block falls into the compacted region and gets summarized
1035
- // instead of being kept at full size.
1036
- const provider = createProvider(() => ({
1037
- content: [{ type: "text", text: "## Goals\n- compressed large paste" }],
1038
- model: "mock-model",
1039
- usage: { inputTokens: 120, outputTokens: 20 },
1040
- stopReason: "end_turn",
1041
- }));
1042
- const manager = new ContextWindowManager({
1043
- provider,
1044
- systemPrompt: "system prompt",
1045
- config: makeConfig({ maxInputTokens: 600, targetBudgetRatio: 0.2 }),
1046
- });
1047
- const hugePaste = "p".repeat(4000); // ~1000 tokens, well above targetInputTokens
1048
- const history: Message[] = [
1049
- message("user", "u1 small"),
1050
- message("assistant", "a1 small"),
1051
- message("user", `u2 ${hugePaste}`),
1052
- ];
1053
-
1054
- const result = await manager.maybeCompact(history, undefined, {
1055
- force: true,
1056
- });
1057
-
1058
- expect(result.compacted).toBe(true);
1059
- // With force=true the kept region is empty; all turns including the oversized
1060
- // paste were summarized, so the compacted result is just the summary.
1061
- expect(result.messages).toHaveLength(1);
1062
- expect(result.compactedMessages).toBe(history.length);
1063
- expect(getSummaryFromContextMessage(result.messages[0])).toContain(
1064
- "compressed large paste",
1065
- );
1066
- expect(result.estimatedInputTokens).toBeLessThan(
1067
- result.previousEstimatedInputTokens,
1068
- );
1069
- });
1070
-
1071
- test("force=false honors minFloor even when the kept turn exceeds target", async () => {
1072
- // Same oversized paste, but without force the algorithm must preserve the
1073
- // minFloor=1 recent turn (auto mid-loop compaction needs the in-flight turn
1074
- // intact). Anything compactable before the floor still gets summarized.
1075
- const provider = createProvider(() => ({
1076
- content: [{ type: "text", text: "## Goals\n- summary" }],
1077
- model: "mock-model",
1078
- usage: { inputTokens: 60, outputTokens: 10 },
1079
- stopReason: "end_turn",
1080
- }));
1081
- const manager = new ContextWindowManager({
1082
- provider,
1083
- systemPrompt: "system prompt",
1084
- config: makeConfig({ maxInputTokens: 600, targetBudgetRatio: 0.2 }),
1085
- });
1086
- const hugePaste = "p".repeat(4000);
1087
- const history: Message[] = [
1088
- message("user", "u1 small"),
1089
- message("assistant", "a1 small"),
1090
- message("user", "u2 small"),
1091
- message("assistant", "a2 small"),
1092
- message("user", `u3 ${hugePaste}`),
1093
- ];
1094
-
1095
- const result = await manager.maybeCompact(history);
1096
-
1097
- expect(result.compacted).toBe(true);
1098
- // The oversized last user turn is retained verbatim; the kept array starts
1099
- // with the summary followed by the messages from that turn onward.
1100
- const lastUser = result.messages
1101
- .filter((m) => m.role === "user")
1102
- .map((m) => (m.content[0].type === "text" ? m.content[0].text : ""))
1103
- .find((t) => t.startsWith("u3 "));
1104
- expect(lastUser).toBeDefined();
1105
- expect(lastUser!.length).toBeGreaterThan(hugePaste.length);
1106
- });
1107
-
1108
- test("shouldCompact returns needed=false with estimatedTokens when below threshold", () => {
1109
- const provider = createProvider(() => {
1110
- throw new Error("should not be called");
1111
- });
1112
- const manager = new ContextWindowManager({
1113
- provider,
1114
- systemPrompt: "system prompt",
1115
- config: makeConfig(),
1116
- });
1117
- const history = [message("user", "hello"), message("assistant", "hi")];
1118
- const result = manager.shouldCompact(history);
1119
- expect(result.needed).toBe(false);
1120
- expect(result.estimatedTokens).toBeGreaterThan(0);
1121
- });
1122
-
1123
- test("shouldCompact returns needed=true with estimatedTokens when above threshold", () => {
1124
- const provider = createProvider(() => {
1125
- throw new Error("should not be called");
1126
- });
1127
- const manager = new ContextWindowManager({
1128
- provider,
1129
- systemPrompt: "system prompt",
1130
- config: makeConfig(),
1131
- });
1132
- const long = "x".repeat(240);
1133
- const history: Message[] = [
1134
- message("user", `u1 ${long}`),
1135
- message("assistant", `a1 ${long}`),
1136
- message("user", `u2 ${long}`),
1137
- message("assistant", `a2 ${long}`),
1138
- message("user", `u3 ${long}`),
1139
- message("assistant", `a3 ${long}`),
1140
- ];
1141
- const result = manager.shouldCompact(history);
1142
- expect(result.needed).toBe(true);
1143
- expect(result.estimatedTokens).toBeGreaterThan(0);
1144
- });
1145
-
1146
- test("shouldCompact returns needed=false with zero estimatedTokens when disabled", () => {
1147
- const provider = createProvider(() => {
1148
- throw new Error("should not be called");
1149
- });
1150
- const long = "x".repeat(240);
1151
- const manager = new ContextWindowManager({
1152
- provider,
1153
- systemPrompt: "system prompt",
1154
- config: makeConfig({ enabled: false }),
1155
- });
1156
- const history: Message[] = [
1157
- message("user", `u1 ${long}`),
1158
- message("assistant", `a1 ${long}`),
1159
- message("user", `u2 ${long}`),
1160
- message("assistant", `a2 ${long}`),
1161
- ];
1162
- const result = manager.shouldCompact(history);
1163
- expect(result.needed).toBe(false);
1164
- expect(result.estimatedTokens).toBe(0);
1165
- });
1166
-
1167
- test("truncates tool results in kept turns to preserve more conversation", async () => {
1168
- const provider = createProvider(() => ({
1169
- content: [{ type: "text", text: "## Goals\n- truncation summary" }],
1170
- model: "mock-model",
1171
- usage: { inputTokens: 60, outputTokens: 12 },
1172
- stopReason: "end_turn",
1173
- }));
1174
- // Budget is tight enough that full 8K tool results would force dropping turns,
1175
- // but truncated results (≤6K chars) should allow more turns to be kept.
1176
- const config = makeConfig({
1177
- maxInputTokens: 4000,
1178
- targetBudgetRatio: 0.7,
1179
- });
1180
- const manager = new ContextWindowManager({
1181
- provider,
1182
- systemPrompt: "system prompt",
1183
- config,
1184
- });
1185
-
1186
- const largeToolResult = "x".repeat(8000);
1187
- const history: Message[] = [
1188
- message("user", "u1"),
1189
- {
1190
- role: "assistant",
1191
- content: [
1192
- {
1193
- type: "tool_use",
1194
- id: "t1",
1195
- name: "read_file",
1196
- input: { path: "/tmp/a" },
1197
- },
1198
- ],
1199
- },
1200
- {
1201
- role: "user",
1202
- content: [
1203
- {
1204
- type: "tool_result",
1205
- tool_use_id: "t1",
1206
- content: largeToolResult,
1207
- },
1208
- ],
1209
- },
1210
- message("assistant", "a1"),
1211
- message("user", "u2"),
1212
- {
1213
- role: "assistant",
1214
- content: [
1215
- {
1216
- type: "tool_use",
1217
- id: "t2",
1218
- name: "read_file",
1219
- input: { path: "/tmp/b" },
1220
- },
1221
- ],
1222
- },
1223
- {
1224
- role: "user",
1225
- content: [
1226
- {
1227
- type: "tool_result",
1228
- tool_use_id: "t2",
1229
- content: largeToolResult,
1230
- },
1231
- ],
1232
- },
1233
- message("assistant", "a2"),
1234
- message("user", "u3"),
1235
- {
1236
- role: "assistant",
1237
- content: [
1238
- {
1239
- type: "tool_use",
1240
- id: "t3",
1241
- name: "read_file",
1242
- input: { path: "/tmp/c" },
1243
- },
1244
- ],
1245
- },
1246
- {
1247
- role: "user",
1248
- content: [
1249
- {
1250
- type: "tool_result",
1251
- tool_use_id: "t3",
1252
- content: largeToolResult,
1253
- },
1254
- ],
1255
- },
1256
- message("assistant", "a3"),
1257
- message("user", "u4"),
1258
- message("assistant", "a4"),
1259
- ];
1260
-
1261
- const result = await manager.maybeCompact(history, undefined, {
1262
- force: true,
1263
- });
1264
- expect(result.compacted).toBe(true);
1265
-
1266
- // Verify tool results in output are truncated (should be < 8K chars each).
1267
- for (const msg of result.messages) {
1268
- for (const block of msg.content) {
1269
- if (block.type === "tool_result") {
1270
- expect(block.content.length).toBeLessThan(8000);
1271
- }
1272
- }
1273
- }
1274
- });
1275
-
1276
- test("targetInputTokensOverride reduces retained turns beyond normal compaction", async () => {
1277
- const provider = createProvider(() => ({
1278
- content: [{ type: "text", text: "## Goals\n- tight fit summary" }],
1279
- model: "mock-model",
1280
- usage: { inputTokens: 60, outputTokens: 12 },
1281
- stopReason: "end_turn",
1282
- }));
1283
-
1284
- // Use generous default target so normal compaction would keep all 3 user turns.
1285
- const config = makeConfig({
1286
- maxInputTokens: 1200,
1287
- targetBudgetRatio: 0.88,
1288
- });
1289
- const long = "t".repeat(220);
1290
- const history: Message[] = [
1291
- message("user", `u1 ${long}`),
1292
- message("assistant", `a1 ${long}`),
1293
- message("user", `u2 ${long}`),
1294
- message("assistant", `a2 ${long}`),
1295
- message("user", `u3 ${long}`),
1296
- message("assistant", `a3 ${long}`),
1297
- ];
1298
-
1299
- // Without override: normal compaction keeps more turns.
1300
- const normalManager = new ContextWindowManager({
1301
- provider,
1302
- systemPrompt: "system prompt",
1303
- config,
1304
- });
1305
- const normalResult = await normalManager.maybeCompact(history, undefined, {
1306
- force: true,
1307
- });
1308
-
1309
- // With a very tight override target: should keep fewer turns.
1310
- const tightManager = new ContextWindowManager({
1311
- provider,
1312
- systemPrompt: "system prompt",
1313
- config,
1314
- });
1315
- const tightResult = await tightManager.maybeCompact(history, undefined, {
1316
- force: true,
1317
- targetInputTokensOverride: 80,
1318
- });
1319
-
1320
- expect(tightResult.compacted).toBe(true);
1321
- // The tight override should compact more messages than normal.
1322
- expect(tightResult.compactedMessages).toBeGreaterThan(
1323
- normalResult.compactedMessages,
1324
- );
1325
- });
1326
-
1327
- test("subtracts summaryOffset only when summary at index 0 was injected from parent", async () => {
1328
- const provider = createProvider(() => ({
1329
- content: [{ type: "text", text: "## Goals\n- new child summary" }],
1330
- model: "mock-model",
1331
- usage: { inputTokens: 75, outputTokens: 20 },
1332
- stopReason: "end_turn",
1333
- }));
1334
- const manager = new ContextWindowManager({
1335
- provider,
1336
- systemPrompt: "system prompt",
1337
- config: makeConfig({
1338
- maxInputTokens: 320,
1339
- targetBudgetRatio: 0.58,
1340
- }),
1341
- });
1342
- const long = "k".repeat(220);
1343
- // Parent-injected summary at index 0, plus 2 injected non-persisted
1344
- // messages, plus 3 child-persisted messages. nonPersistedPrefixCount
1345
- // includes the summary (set by injectInheritedContext).
1346
- const history: Message[] = [
1347
- createContextSummaryMessage("parent summary"),
1348
- message("user", `injected-u ${long}`),
1349
- message("assistant", `injected-a ${long}`),
1350
- message("user", `persisted-u1 ${long}`),
1351
- message("assistant", `persisted-a1 ${long}`),
1352
- message("user", `persisted-u2 ${long}`),
1353
- ];
1354
- manager.nonPersistedPrefixCount = 3;
1355
- manager.summaryIsInjected = true;
1356
-
1357
- const result = await manager.maybeCompact(history, undefined, {
1358
- force: true,
1359
- });
1360
- expect(result.compacted).toBe(true);
1361
- // 4 messages compacted (2 injected + 2 child-persisted), but only the
1362
- // 2 child-persisted ones count as DB-persisted.
1363
- expect(result.compactedMessages).toBe(4);
1364
- expect(result.compactedPersistedMessages).toBe(2);
1365
- // Flag clears and prefix drains (both injected messages + summary slot).
1366
- expect(manager.summaryIsInjected).toBe(false);
1367
- expect(manager.nonPersistedPrefixCount).toBe(0);
1368
- });
1369
-
1370
- test("summary system prompt instructs verbatim thread-anchor preservation", async () => {
1371
- const capturedSystemPrompts: (string | undefined)[] = [];
1372
- const provider: Provider = {
1373
- name: "mock",
1374
- async sendMessage(
1375
- _messages: Message[],
1376
- _tools,
1377
- systemPrompt,
1378
- ): Promise<ProviderResponse> {
1379
- capturedSystemPrompts.push(systemPrompt);
1380
- return {
1381
- content: [
1382
- {
1383
- type: "text",
1384
- text: "## Goals\n- preserved thread parent verbatim",
1385
- },
1386
- ],
1387
- model: "mock-model",
1388
- usage: { inputTokens: 60, outputTokens: 12 },
1389
- stopReason: "end_turn",
1390
- };
1391
- },
1392
- };
1393
- const manager = new ContextWindowManager({
1394
- provider,
1395
- systemPrompt: "system prompt",
1396
- config: makeConfig({ maxInputTokens: 600 }),
1397
- });
1398
- const long = "x".repeat(240);
1399
- // Simulate a Slack-style transcript where an old user "thread parent"
1400
- // message is about to be compacted while a later reply survives in the
1401
- // retained tail. The clause being asserted instructs the summarizer to
1402
- // preserve that parent verbatim — we cannot verify the model's behavior
1403
- // here (the provider is a stub), so we instead assert the clause itself
1404
- // reaches the summarizer.
1405
- const history: Message[] = [
1406
- message("user", `parent: kickoff plan ${long}`),
1407
- message("assistant", `a1 ${long}`),
1408
- message("user", `u2 ${long}`),
1409
- message("assistant", `a2 ${long}`),
1410
- message("user", `reply-in-thread ${long}`),
1411
- message("assistant", `a3 ${long}`),
1412
- ];
1413
-
1414
- const result = await manager.maybeCompact(history);
1415
- expect(result.compacted).toBe(true);
1416
- expect(capturedSystemPrompts.length).toBeGreaterThan(0);
1417
- const seenPrompt = capturedSystemPrompts[0];
1418
- expect(seenPrompt).toBeDefined();
1419
- expect(seenPrompt).toContain("Thread anchors");
1420
- expect(seenPrompt).toContain("verbatim");
1421
- });
1422
-
1423
- test("summary prompt lists retained-tail thread-reply references", async () => {
1424
- const capturedMessages: Message[][] = [];
1425
- const provider: Provider = {
1426
- name: "mock",
1427
- async sendMessage(messages: Message[]): Promise<ProviderResponse> {
1428
- capturedMessages.push(messages);
1429
- return {
1430
- content: [{ type: "text", text: "## Goals\n- ok" }],
1431
- model: "mock-model",
1432
- usage: { inputTokens: 60, outputTokens: 12 },
1433
- stopReason: "end_turn",
1434
- };
1435
- },
1436
- };
1437
- const manager = new ContextWindowManager({
1438
- provider,
1439
- systemPrompt: "system prompt",
1440
- config: makeConfig({ maxInputTokens: 600 }),
1441
- });
1442
- const long = "x".repeat(240);
1443
- // Compactable region ends before the retained tail, which contains a
1444
- // Slack-style reply line that cites its parent via `→ M1a2b3c`. The
1445
- // summary prompt must surface that reference so the Thread-anchors
1446
- // instruction has something to act on.
1447
- const history: Message[] = [
1448
- message("user", `[11/14/23 14:25 @alice]: parent kickoff ${long}`),
1449
- message("assistant", `a1 ${long}`),
1450
- message("user", `u2 ${long}`),
1451
- message("assistant", `a2 ${long}`),
1452
- message("user", `[11/14/23 14:28 @bob → M1a2b3c]: reply ${long}`),
1453
- message("assistant", `a3 ${long}`),
1454
- ];
1455
-
1456
- const result = await manager.maybeCompact(history);
1457
- expect(result.compacted).toBe(true);
1458
- expect(capturedMessages.length).toBeGreaterThan(0);
1459
- const userPromptText = capturedMessages[0]
1460
- .flatMap((m) => m.content)
1461
- .filter(
1462
- (b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
1463
- )
1464
- .map((b) => b.text)
1465
- .join("\n");
1466
- expect(userPromptText).toContain("### Retained Thread References");
1467
- expect(userPromptText).toContain("→ M1a2b3c");
1468
- });
1469
-
1470
- test("summary prompt lists retained-tail thread-reply references for edited replies", async () => {
1471
- const capturedMessages: Message[][] = [];
1472
- const provider: Provider = {
1473
- name: "mock",
1474
- async sendMessage(messages: Message[]): Promise<ProviderResponse> {
1475
- capturedMessages.push(messages);
1476
- return {
1477
- content: [{ type: "text", text: "## Goals\n- ok" }],
1478
- model: "mock-model",
1479
- usage: { inputTokens: 60, outputTokens: 12 },
1480
- stopReason: "end_turn",
1481
- };
1482
- },
1483
- };
1484
- const manager = new ContextWindowManager({
1485
- provider,
1486
- systemPrompt: "system prompt",
1487
- config: makeConfig({ maxInputTokens: 600 }),
1488
- });
1489
- const long = "x".repeat(240);
1490
- // An edited reply renders with `, edited …` between the parent alias and
1491
- // the closing bracket: `→ Mxxxxxx, edited MM/DD/YY HH:MM]`. The regex
1492
- // must still flag these lines so retention works for edited replies.
1493
- const history: Message[] = [
1494
- message("user", `[11/14/23 14:25 @alice]: parent kickoff ${long}`),
1495
- message("assistant", `a1 ${long}`),
1496
- message("user", `u2 ${long}`),
1497
- message("assistant", `a2 ${long}`),
1498
- message(
1499
- "user",
1500
- `[11/14/23 14:28 @bob → M1a2b3c, edited 11/14/23 14:32]: reply ${long}`,
1501
- ),
1502
- message("assistant", `a3 ${long}`),
1503
- ];
1504
-
1505
- const result = await manager.maybeCompact(history);
1506
- expect(result.compacted).toBe(true);
1507
- expect(capturedMessages.length).toBeGreaterThan(0);
1508
- const userPromptText = capturedMessages[0]
1509
- .flatMap((m) => m.content)
1510
- .filter(
1511
- (b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
1512
- )
1513
- .map((b) => b.text)
1514
- .join("\n");
1515
- expect(userPromptText).toContain("### Retained Thread References");
1516
- expect(userPromptText).toContain("→ M1a2b3c, edited 11/14/23 14:32");
1517
- });
1518
-
1519
- test("summary prompt omits retained references when retained tail has no thread markers", async () => {
1520
- const capturedMessages: Message[][] = [];
1521
- const provider: Provider = {
1522
- name: "mock",
1523
- async sendMessage(messages: Message[]): Promise<ProviderResponse> {
1524
- capturedMessages.push(messages);
1525
- return {
1526
- content: [{ type: "text", text: "## Goals\n- ok" }],
1527
- model: "mock-model",
1528
- usage: { inputTokens: 60, outputTokens: 12 },
1529
- stopReason: "end_turn",
1530
- };
1531
- },
1532
- };
1533
- const manager = new ContextWindowManager({
1534
- provider,
1535
- systemPrompt: "system prompt",
1536
- config: makeConfig({ maxInputTokens: 600 }),
1537
- });
1538
- const long = "x".repeat(240);
1539
- const history: Message[] = [
1540
- message("user", `u1 ${long}`),
1541
- message("assistant", `a1 ${long}`),
1542
- message("user", `u2 ${long}`),
1543
- message("assistant", `a2 ${long}`),
1544
- message("user", `u3 ${long}`),
1545
- message("assistant", `a3 ${long}`),
1546
- ];
1547
-
1548
- const result = await manager.maybeCompact(history);
1549
- expect(result.compacted).toBe(true);
1550
- const userPromptText = capturedMessages[0]
1551
- .flatMap((m) => m.content)
1552
- .filter(
1553
- (b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
1554
- )
1555
- .map((b) => b.text)
1556
- .join("\n");
1557
- expect(userPromptText).not.toContain("### Retained Thread References");
1558
- expect(userPromptText).not.toMatch(/→ M[0-9a-f]{6}]/);
1559
- });
1560
-
1561
- test("does not subtract summaryOffset when summary at index 0 is child-owned from prior compaction", async () => {
1562
- const provider = createProvider(() => ({
1563
- content: [{ type: "text", text: "## Goals\n- next child summary" }],
1564
- model: "mock-model",
1565
- usage: { inputTokens: 75, outputTokens: 20 },
1566
- stopReason: "end_turn",
1567
- }));
1568
- const manager = new ContextWindowManager({
1569
- provider,
1570
- systemPrompt: "system prompt",
1571
- config: makeConfig({
1572
- maxInputTokens: 320,
1573
- targetBudgetRatio: 0.58,
1574
- }),
1575
- });
1576
- const long = "k".repeat(220);
1577
- // Post-first-compaction state: child-owned summary at index 0, 2
1578
- // still-injected messages that survived the first compaction's keep
1579
- // region, 3 child-persisted messages. nonPersistedPrefixCount reflects
1580
- // only the 2 remaining injected messages — the summary slot was already
1581
- // consumed when the flag-gated decrement ran on the prior compaction.
1582
- const history: Message[] = [
1583
- createContextSummaryMessage("prior child summary"),
1584
- message("user", `injected-u ${long}`),
1585
- message("assistant", `injected-a ${long}`),
1586
- message("user", `persisted-u1 ${long}`),
1587
- message("assistant", `persisted-a1 ${long}`),
1588
- message("user", `persisted-u2 ${long}`),
1589
- ];
1590
- manager.nonPersistedPrefixCount = 2;
1591
- manager.summaryIsInjected = false;
1592
-
1593
- const result = await manager.maybeCompact(history, undefined, {
1594
- force: true,
1595
- });
1596
- expect(result.compacted).toBe(true);
1597
- expect(result.compactedMessages).toBe(4);
1598
- // Regression guard: without the flag gate, the subtraction from the
1599
- // #24353 fix would double-apply here (nonPersistedPrefixCount - 1),
1600
- // undercounting injectedInCompactable and inflating
1601
- // compactedPersistedMessages by 1 (to 3).
1602
- expect(result.compactedPersistedMessages).toBe(2);
1603
- expect(manager.nonPersistedPrefixCount).toBe(0);
1604
- });
1605
-
1606
- test("Slack origin bumps default minKeepRecentUserTurns to 8", async () => {
1607
- const provider = createProvider(() => ({
1608
- content: [{ type: "text", text: "## Goals\n- slack thread context" }],
1609
- model: "mock-model",
1610
- usage: { inputTokens: 60, outputTokens: 12 },
1611
- stopReason: "end_turn",
1612
- }));
1613
-
1614
- // Use targetInputTokensOverride so the binary search is forced even
1615
- // for a small history. Both managers see the same tight budget; the
1616
- // only knob that varies is conversationOriginChannel.
1617
- const config = makeConfig({ maxInputTokens: 12_000 });
1618
- const long = "s".repeat(220);
1619
- // 9 user turns: enough headroom for Slack's bumped floor of 8 to be
1620
- // distinguishable from the default floor of 1.
1621
- const history: Message[] = [];
1622
- for (let i = 1; i <= 9; i++) {
1623
- history.push(message("user", `u${i} ${long}`));
1624
- history.push(message("assistant", `a${i} ${long}`));
1625
- }
1626
-
1627
- const slackManager = new ContextWindowManager({
1628
- provider,
1629
- systemPrompt: "system prompt",
1630
- config,
1631
- });
1632
- const slackResult = await slackManager.maybeCompact(history, undefined, {
1633
- force: true,
1634
- targetInputTokensOverride: 200,
1635
- conversationOriginChannel: "slack",
1636
- });
1637
-
1638
- const defaultManager = new ContextWindowManager({
1639
- provider,
1640
- systemPrompt: "system prompt",
1641
- config,
1642
- });
1643
- const defaultResult = await defaultManager.maybeCompact(
1644
- history,
1645
- undefined,
1646
- { force: true, targetInputTokensOverride: 200 },
1647
- );
1648
-
1649
- expect(slackResult.compacted).toBe(true);
1650
- expect(defaultResult.compacted).toBe(true);
1651
- // Default floor (1 user turn) compacts more of the history than the
1652
- // Slack floor (8 user turns), which preserves more recent context.
1653
- expect(defaultResult.compactedMessages).toBeGreaterThan(
1654
- slackResult.compactedMessages,
1655
- );
1656
- // Slack keeps 8 of 9 user turns: 16 kept messages, 2 compacted.
1657
- expect(slackResult.compactedMessages).toBe(2);
1658
- });
1659
-
1660
- test("non-Slack origin keeps default minKeepRecentUserTurns of 1", async () => {
1661
- const provider = createProvider(() => ({
1662
- content: [{ type: "text", text: "## Goals\n- standard summary" }],
1663
- model: "mock-model",
1664
- usage: { inputTokens: 60, outputTokens: 12 },
1665
- stopReason: "end_turn",
1666
- }));
1667
-
1668
- const config = makeConfig({ maxInputTokens: 12_000 });
1669
- const long = "n".repeat(220);
1670
- const history: Message[] = [];
1671
- for (let i = 1; i <= 9; i++) {
1672
- history.push(message("user", `u${i} ${long}`));
1673
- history.push(message("assistant", `a${i} ${long}`));
1674
- }
1675
-
1676
- // Telegram origin must behave identically to no-channel-hint default.
1677
- const telegramManager = new ContextWindowManager({
1678
- provider,
1679
- systemPrompt: "system prompt",
1680
- config,
1681
- });
1682
- const telegramResult = await telegramManager.maybeCompact(
1683
- history,
1684
- undefined,
1685
- {
1686
- force: true,
1687
- targetInputTokensOverride: 200,
1688
- conversationOriginChannel: "telegram",
1689
- },
1690
- );
1691
-
1692
- const defaultManager = new ContextWindowManager({
1693
- provider,
1694
- systemPrompt: "system prompt",
1695
- config,
1696
- });
1697
- const defaultResult = await defaultManager.maybeCompact(
1698
- history,
1699
- undefined,
1700
- { force: true, targetInputTokensOverride: 200 },
1701
- );
1702
-
1703
- expect(telegramResult.compacted).toBe(true);
1704
- expect(defaultResult.compacted).toBe(true);
1705
- expect(telegramResult.compactedMessages).toBe(
1706
- defaultResult.compactedMessages,
1707
- );
1708
- });
1709
-
1710
- test("explicit minKeepRecentUserTurns wins over Slack default", async () => {
1711
- const provider = createProvider(() => ({
1712
- content: [{ type: "text", text: "## Goals\n- emergency override" }],
1713
- model: "mock-model",
1714
- usage: { inputTokens: 60, outputTokens: 12 },
1715
- stopReason: "end_turn",
1716
- }));
1717
-
1718
- const manager = new ContextWindowManager({
1719
- provider,
1720
- systemPrompt: "system prompt",
1721
- config: makeConfig({
1722
- maxInputTokens: 260,
1723
- targetBudgetRatio: 0.28,
1724
- }),
1725
- });
1726
- const long = "e".repeat(220);
1727
- const history: Message[] = [
1728
- message("user", `u1 ${long}`),
1729
- message("assistant", `a1 ${long}`),
1730
- message("user", `u2 ${long}`),
1731
- ];
1732
-
1733
- // Emergency override (`minKeepRecentUserTurns: 0`) must take precedence
1734
- // over the Slack-bumped default of 8 — this guards the agent loop's
1735
- // context-too-large recovery path which always passes 0.
1736
- const result = await manager.maybeCompact(history, undefined, {
1737
- force: true,
1738
- minKeepRecentUserTurns: 0,
1739
- conversationOriginChannel: "slack",
1740
- });
1741
- expect(result.compacted).toBe(true);
1742
- expect(result.compactedMessages).toBe(3);
1743
- expect(result.messages).toHaveLength(1);
1744
- });
1745
-
1746
- test("summary provider call includes callSite: conversationSummarization", async () => {
1747
- // Regression guard for JARVIS-587: without the callSite, the summary
1748
- // call fell through to `llm.default` (opus + effort=max + thinking
1749
- // enabled) and exceeded the 30s plugin pipeline budget on ~150k-token
1750
- // transcripts. The fix is to route the summary call through the
1751
- // dedicated `conversationSummarization` call-site config.
1752
- const capturedOptions: (SendMessageOptions | undefined)[] = [];
1753
- const provider: Provider = {
1754
- name: "mock",
1755
- async sendMessage(
1756
- _messages: Message[],
1757
- _tools: unknown,
1758
- _systemPrompt: unknown,
1759
- options?: SendMessageOptions,
1760
- ): Promise<ProviderResponse> {
1761
- capturedOptions.push(options);
1762
- return {
1763
- content: [{ type: "text", text: "## Goals\n- summary" }],
1764
- model: "mock-model",
1765
- usage: { inputTokens: 50, outputTokens: 10 },
1766
- stopReason: "end_turn",
1767
- };
1768
- },
1769
- };
1770
- const manager = new ContextWindowManager({
1771
- provider,
1772
- systemPrompt: "system prompt",
1773
- config: makeConfig({ maxInputTokens: 600 }),
1774
- });
1775
- const long = "x".repeat(240);
1776
- const history: Message[] = [
1777
- message("user", `u1 ${long}`),
1778
- message("assistant", `a1 ${long}`),
1779
- message("user", `u2 ${long}`),
1780
- message("assistant", `a2 ${long}`),
1781
- message("user", `u3 ${long}`),
1782
- message("assistant", `a3 ${long}`),
1783
- ];
1784
-
1785
- const result = await manager.maybeCompact(history);
1786
- expect(result.compacted).toBe(true);
1787
- expect(capturedOptions.length).toBeGreaterThan(0);
1788
- for (const options of capturedOptions) {
1789
- expect(options?.config?.callSite).toBe("conversationSummarization");
1790
- }
1791
- });
1792
- });
1793
-
1794
- describe("stripCompactionOnlyInjections", () => {
1795
- test("removes memory, turn_context, and workspace text blocks from user messages", () => {
1796
- const messages: Message[] = [
1797
- {
1798
- role: "user",
1799
- content: [
1800
- {
1801
- type: "text",
1802
- text: "<memory __injected>\nrecall notes\n</memory>",
1803
- },
1804
- {
1805
- type: "text",
1806
- text: "<turn_context>\nActor: Alice\n</turn_context>",
1807
- },
1808
- { type: "text", text: "real user content" },
1809
- ],
1810
- },
1811
- {
1812
- role: "assistant",
1813
- content: [{ type: "text", text: "assistant reply" }],
1814
- },
1815
- ];
1816
- const stripped = stripCompactionOnlyInjections(messages);
1817
- expect(stripped).toHaveLength(2);
1818
- const firstText = (stripped[0].content[0] as { text: string }).text;
1819
- expect(firstText).toBe("real user content");
1820
- expect(stripped[0].content).toHaveLength(1);
1821
- });
1822
-
1823
- test("drops user messages that become empty after stripping", () => {
1824
- const messages: Message[] = [
1825
- {
1826
- role: "user",
1827
- content: [
1828
- { type: "text", text: "<memory __injected>\nonly memory\n</memory>" },
1829
- ],
1830
- },
1831
- { role: "user", content: [{ type: "text", text: "real content" }] },
1832
- ];
1833
- const stripped = stripCompactionOnlyInjections(messages);
1834
- expect(stripped).toHaveLength(1);
1835
- expect((stripped[0].content[0] as { text: string }).text).toBe(
1836
- "real content",
1837
- );
1838
- });
1839
-
1840
- test("leaves assistant messages and non-text blocks untouched", () => {
1841
- const messages: Message[] = [
1842
- {
1843
- role: "assistant",
1844
- content: [
1845
- {
1846
- type: "text",
1847
- text: "<turn_context>\nnot really injected\n</turn_context>",
1848
- },
1849
- ],
1850
- },
1851
- {
1852
- role: "user",
1853
- content: [
1854
- {
1855
- type: "tool_result",
1856
- tool_use_id: "t1",
1857
- content: "<memory>fake</memory>",
1858
- },
1859
- { type: "text", text: "user reply" },
1860
- ],
1861
- },
1862
- ];
1863
- const stripped = stripCompactionOnlyInjections(messages);
1864
- expect(stripped).toHaveLength(2);
1865
- expect((stripped[0].content[0] as { text: string }).text).toContain(
1866
- "turn_context",
1867
- );
1868
- expect(stripped[1].content).toHaveLength(2);
1869
- });
1870
-
1871
- test("preserves user prose that merely mentions ambiguous tag names", () => {
1872
- // Common-word bare tags embedded in legitimate user prose (discussions of
1873
- // XML, system terminology, etc.) must survive stripping because they are
1874
- // not shaped like a runtime injection — no leading newline after the
1875
- // open tag, or other prose surrounds the tag.
1876
- const messages: Message[] = [
1877
- {
1878
- role: "user",
1879
- content: [
1880
- {
1881
- type: "text",
1882
- text: "<memory> is a tag I'd like to add to my parser",
1883
- },
1884
- ],
1885
- },
1886
- {
1887
- role: "user",
1888
- content: [
1889
- {
1890
- type: "text",
1891
- text: "checking <workspace> usage across the repo, any thoughts?",
1892
- },
1893
- ],
1894
- },
1895
- {
1896
- role: "user",
1897
- content: [
1898
- {
1899
- type: "text",
1900
- text: "what is <knowledge_base> in this context?",
1901
- },
1902
- ],
1903
- },
1904
- {
1905
- role: "user",
1906
- content: [
1907
- { type: "text", text: "<pkb> sounds like a short name — wrong?" },
1908
- ],
1909
- },
1910
- {
1911
- role: "user",
1912
- content: [
1913
- {
1914
- type: "text",
1915
- text: "when the model hits a <system_reminder>, what happens next?",
1916
- },
1917
- ],
1918
- },
1919
- ];
1920
- const stripped = stripCompactionOnlyInjections(messages);
1921
- expect(stripped).toHaveLength(messages.length);
1922
- for (let i = 0; i < messages.length; i++) {
1923
- expect(stripped[i].content).toHaveLength(1);
1924
- expect((stripped[i].content[0] as { text: string }).text).toBe(
1925
- (messages[i].content[0] as { text: string }).text,
1926
- );
1927
- }
1928
- });
1929
-
1930
- test("still strips runtime-shaped wrapped blocks for ambiguous tag names", () => {
1931
- // Bare-tag blocks with a newline after the open tag and a matching close
1932
- // tag (e.g. `<memory>\n...\n</memory>`) match the wrapped-strip path.
1933
- // This covers both the current runtime emission shape and blocks
1934
- // persisted before the `__injected` attribute existed — the prefix list
1935
- // handles `__injected`-attributed tags, and the wrapped matcher handles
1936
- // the bare-tag wrap shape.
1937
- const messages: Message[] = [
1938
- {
1939
- role: "user",
1940
- content: [
1941
- { type: "text", text: "<memory>\nlegacy recall blob\n</memory>" },
1942
- { type: "text", text: "actual user content" },
1943
- ],
1944
- },
1945
- {
1946
- role: "user",
1947
- content: [
1948
- {
1949
- type: "text",
1950
- text: "<workspace>\nRoot: /home\nFiles: a, b\n</workspace>",
1951
- },
1952
- { type: "text", text: "more prose" },
1953
- ],
1954
- },
1955
- {
1956
- role: "user",
1957
- content: [
1958
- {
1959
- type: "text",
1960
- text: "<system_reminder>\nread your PKB\n</system_reminder>",
1961
- },
1962
- { type: "text", text: "ok" },
1963
- ],
1964
- },
1965
- ];
1966
- const stripped = stripCompactionOnlyInjections(messages);
1967
- expect(stripped).toHaveLength(3);
1968
- for (const msg of stripped) {
1969
- expect(msg.content).toHaveLength(1);
1970
- }
1971
- expect((stripped[0].content[0] as { text: string }).text).toBe(
1972
- "actual user content",
1973
- );
1974
- expect((stripped[1].content[0] as { text: string }).text).toBe(
1975
- "more prose",
1976
- );
1977
- expect((stripped[2].content[0] as { text: string }).text).toBe("ok");
1978
- });
1979
-
1980
- test("does not strip a user's inline snippet that is not shaped like an injection", () => {
1981
- // A user quoting a `<memory>...</memory>` snippet alongside prose in the
1982
- // SAME text block should survive — the block does not start with
1983
- // `<memory>\n` (there's surrounding prose) so the wrapped-tag match
1984
- // does not trigger.
1985
- const messages: Message[] = [
1986
- {
1987
- role: "user",
1988
- content: [
1989
- {
1990
- type: "text",
1991
- text: "Here's the XML I'm working with: <memory>x</memory> — what do you think?",
1992
- },
1993
- ],
1994
- },
1995
- ];
1996
- const stripped = stripCompactionOnlyInjections(messages);
1997
- expect(stripped).toHaveLength(1);
1998
- expect((stripped[0].content[0] as { text: string }).text).toContain(
1999
- "<memory>x</memory>",
2000
- );
2001
- });
2002
- });
2003
-
2004
- describe("summarizer input excludes runtime injections", () => {
2005
- test("maybeCompact does not pass memory/turn_context text to the summarizer", async () => {
2006
- const seenPrompts: string[] = [];
2007
- const provider = createProvider((messages) => {
2008
- for (const msg of messages) {
2009
- for (const block of msg.content) {
2010
- if (block.type === "text") seenPrompts.push(block.text);
2011
- }
2012
- }
2013
- return {
2014
- content: [
2015
- {
2016
- type: "text",
2017
- text: "## Facts Worth Remembering\n- summary produced",
2018
- },
2019
- ],
2020
- model: "mock",
2021
- usage: { inputTokens: 100, outputTokens: 25 },
2022
- stopReason: "end_turn",
2023
- };
2024
- });
2025
- const manager = new ContextWindowManager({
2026
- provider,
2027
- systemPrompt: "system prompt",
2028
- config: makeConfig({
2029
- maxInputTokens: 2000,
2030
- targetBudgetRatio: 0.4,
2031
- compactThreshold: 0.35,
2032
- }),
2033
- });
2034
- const long = "x".repeat(1500);
2035
- const memoryBlob =
2036
- "<memory __injected>\nBOB_ATTENDED_STANDUP_YESTERDAY\n</memory>";
2037
- const turnCtx =
2038
- "<turn_context>\nACTOR_METADATA_THAT_SHOULD_NOT_LEAK\n</turn_context>";
2039
- const history: Message[] = [
2040
- {
2041
- role: "user",
2042
- content: [
2043
- { type: "text", text: memoryBlob },
2044
- { type: "text", text: turnCtx },
2045
- { type: "text", text: `u1 ${long}` },
2046
- ],
2047
- },
2048
- message("assistant", `a1 ${long}`),
2049
- message("user", `u2 ${long}`),
2050
- message("assistant", `a2 ${long}`),
2051
- message("user", `u3 ${long}`),
2052
- ];
2053
-
2054
- const result = await manager.maybeCompact(history);
2055
- expect(result.compacted).toBe(true);
2056
- const joined = seenPrompts.join("\n");
2057
- expect(joined).not.toContain("BOB_ATTENDED_STANDUP_YESTERDAY");
2058
- expect(joined).not.toContain("ACTOR_METADATA_THAT_SHOULD_NOT_LEAK");
2059
- expect(joined).not.toContain("<memory __injected>");
2060
- expect(joined).not.toContain("<turn_context>");
2061
- // Real conversation content should survive — at least one of the
2062
- // middle turns (whose header/body is short enough to fit within the
2063
- // capped transcript budget) should appear in the summarizer input.
2064
- expect(joined).toMatch(/u2 |a1 /);
2065
- });
2066
- });
2067
-
2068
- describe("clampSummaryAtSectionBoundary", () => {
2069
- test("returns the input unchanged when under the limit", () => {
2070
- const summary = "## Decisions\nWe decided to ship.";
2071
- expect(clampSummaryAtSectionBoundary(summary, 1000)).toBe(summary);
2072
- });
2073
-
2074
- test("truncates at a `## ` boundary when one exists in the allowed region", () => {
2075
- const keeper = "## Facts\n" + "a".repeat(200);
2076
- const dropped = "## Open Threads\n" + "b".repeat(500);
2077
- const summary = `${keeper}\n${dropped}`;
2078
- const maxChars = keeper.length + 20;
2079
- const clamped = clampSummaryAtSectionBoundary(summary, maxChars);
2080
- expect(clamped.endsWith("...")).toBe(true);
2081
- expect(clamped).not.toContain("## Open Threads");
2082
- expect(clamped).toContain("## Facts");
2083
- // No mid-header cut: nothing that looks like a partial heading.
2084
- expect(/##\s*$/.test(clamped)).toBe(false);
2085
- });
2086
-
2087
- test("falls back to a hard cut when no section boundary is past the midpoint", () => {
2088
- const body = "no section headers in this output " + "z".repeat(1000);
2089
- const clamped = clampSummaryAtSectionBoundary(body, 100);
2090
- expect(clamped.endsWith("...")).toBe(true);
2091
- expect(clamped.length).toBeLessThanOrEqual(100);
2092
- });
2093
- });