@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,234 +1,46 @@
1
- import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
1
+ /**
2
+ * Context window manager — the surface the rest of the daemon talks to
3
+ * when it needs to know whether and how to compact a conversation.
4
+ *
5
+ * The actual compaction work is delegated to {@link runAssistantDrivenCompaction}
6
+ * in `./compactor.js`, which hands the model the full conversation plus a
7
+ * user-role instruction message and lets the assistant write its own
8
+ * summary and choose its own cut point.
9
+ *
10
+ * This module retains a small set of legacy exports — `CONTEXT_SUMMARY_MARKER`,
11
+ * `createContextSummaryMessage`, `getSummaryFromContextMessage` — because
12
+ * conversation reload, fork inheritance, and Slack chronological-context
13
+ * assembly all detect a previously-produced summary via the marker. The
14
+ * marker is wrapped around the assistant-role memory message we emit on
15
+ * successful compaction so those code paths keep working unchanged.
16
+ */
17
+ import { getConfig } from "../config/loader.js";
18
+ import type { CompactionConfig } from "../config/schemas/compaction.js";
4
19
  import type { LLMCallSite } from "../config/schemas/llm.js";
5
20
  import type { ContextWindowConfig } from "../config/types.js";
6
21
  import type {
7
22
  ContentBlock,
8
- ImageContent,
9
23
  Message,
10
24
  Provider,
25
+ ToolDefinition,
11
26
  } from "../providers/types.js";
12
- import { resolveBundledDir } from "../util/bundled-asset.js";
13
27
  import { getLogger } from "../util/logger.js";
14
- import { safeStringSlice } from "../util/unicode.js";
15
28
  import {
16
- estimateContentBlockTokens,
17
- estimatePromptTokens,
18
- estimateTextTokens,
19
- } from "./token-estimator.js";
20
- import { truncateToolResultsAcrossHistory } from "./tool-result-truncation.js";
29
+ type CompactionRunArgs,
30
+ runAssistantDrivenCompaction,
31
+ } from "./compactor.js";
32
+ import { estimatePromptTokens } from "./token-estimator.js";
21
33
 
22
34
  const log = getLogger("context-window");
23
35
 
24
36
  export const CONTEXT_SUMMARY_MARKER = "<context_summary>";
25
- const CONVERSATION_SUMMARY_CALL_SITE: LLMCallSite = "conversationSummarization";
26
- const MAX_BLOCK_PREVIEW_CHARS = 3000;
27
- const MAX_FALLBACK_SUMMARY_CHARS = 12000;
28
- const COMPACTION_COOLDOWN_MS = 2 * 60 * 1000;
29
- const MIN_GAIN_TOKENS_DURING_COOLDOWN = 1200;
30
- const SEVERE_PRESSURE_RATIO = 0.95;
31
- const COMPACTION_TOOL_RESULT_MAX_CHARS = 6_000;
32
- const MIN_COMPACTABLE_PERSISTED_MESSAGES = 2;
37
+ const CONTEXT_SUMMARY_CLOSE = "</context_summary>";
33
38
  const INTERNAL_CONTEXT_SUMMARY_MESSAGES = new WeakSet<Message>();
34
39
 
35
- /**
36
- * When the existing summary is this fraction or more of the per-summary
37
- * token budget, inject a "compress older content aggressively" instruction
38
- * so incremental-update passes don't let the summary grow unboundedly.
39
- */
40
- const SUMMARY_COMPRESSION_PRESSURE_RATIO = 0.6;
41
-
42
- /**
43
- * Text-block prefixes that persist in live history (for prefix-caching
44
- * stability and model grounding) but pollute the summarizer's view of the
45
- * actual conversation. These blocks are system-metadata attached to user
46
- * turns — memory injections, turn context, workspace hints, etc. They are
47
- * stripped ONLY from the messages fed to the summarization LLM call. Live
48
- * history is never mutated, so prefix caching is preserved.
49
- *
50
- * This list intentionally overlaps with `RUNTIME_INJECTION_PREFIXES` in
51
- * `conversation-runtime-assembly.ts`. That list governs in-flight turn
52
- * assembly via pure prefix matching; this one governs compaction input.
53
- * Keep the two lists in sync when a new injection type is added.
54
- *
55
- * Compaction strip coverage is two-tier: this prefix list catches
56
- * internal-vocabulary tags and any tag carrying the `__injected`
57
- * attribute, while `COMPACTION_ONLY_WRAPPED_STRIP_TAGS` below matches
58
- * ambiguous bare-tag blocks that are shaped like a runtime-emitted
59
- * open/close wrap. A new ambiguous tag added upstream needs to be
60
- * evaluated against both tiers — internal-vocabulary names go here,
61
- * and names whose bare form collides with ordinary English
62
- * (`<memory>`, `<workspace>`, `<knowledge_base>`, `<pkb>`,
63
- * `<system_reminder>`) go in the wrapped-strip list so user prose
64
- * mentioning the tag is preserved.
65
- */
66
- const COMPACTION_ONLY_STRIP_PREFIXES = [
67
- "<memory __injected>",
68
- "<memory_image __injected>",
69
- "</memory_image>",
70
- "<memory_context __injected>",
71
- "<turn_context>",
72
- "<channel_turn_context>",
73
- "<guardian_context>",
74
- "<inbound_actor_context>",
75
- "<interface_turn_context>",
76
- "<workspace_top_level>",
77
- "<now_scratchpad>",
78
- "<NOW.md Always keep this up to date",
79
- "<active_thread>",
80
- "<active_subagents>",
81
- "<active_workspace>",
82
- "<active_dynamic_page>",
83
- "<channel_capabilities>",
84
- "<channel_command_context>",
85
- "<voice_call_control>",
86
- "<transport_hints>",
87
- "<system_notice>",
88
- "<non_interactive_context>",
89
- "<temporal_context>",
90
- ];
91
-
92
- /**
93
- * Tags whose bare form (`<tag>`) is common English vocabulary or markup a
94
- * user might legitimately type in prose. For these we only strip a text
95
- * block if it is shaped exactly like a runtime injection: starts with
96
- * `<tag>\n` and ends with `</tag>`. This bare-tag wrapped shape
97
- * (e.g. `<memory>\n...\n</memory>`) appears in persisted history
98
- * alongside the `__injected`-attributed variants, which the prefix list
99
- * above already catches via `<memory __injected>`. A user who mentions
100
- * `<memory>` in a sentence or inlines `<workspace>...</workspace>` within
101
- * other prose will not match this shape.
102
- */
103
- const COMPACTION_ONLY_WRAPPED_STRIP_TAGS = [
104
- "memory",
105
- "memory_context",
106
- "workspace",
107
- "knowledge_base",
108
- "pkb",
109
- "system_reminder",
110
- ];
111
-
112
- function isCompactionInjectedBlock(text: string): boolean {
113
- if (COMPACTION_ONLY_STRIP_PREFIXES.some((p) => text.startsWith(p))) {
114
- return true;
115
- }
116
- return COMPACTION_ONLY_WRAPPED_STRIP_TAGS.some(
117
- (tag) => text.startsWith(`<${tag}>\n`) && text.endsWith(`</${tag}>`),
118
- );
119
- }
120
-
121
- /**
122
- * Remove text blocks that look like runtime injections from user messages.
123
- * Non-text blocks (images, tool_use, tool_result, etc.) are untouched.
124
- * Empty messages (every block filtered out) are dropped from the output.
125
- *
126
- * Used only on the `compactableMessages` slice right before it is
127
- * serialized for the summarization LLM — the caller's original message
128
- * array is never mutated.
129
- */
130
- export function stripCompactionOnlyInjections(messages: Message[]): Message[] {
131
- return messages
132
- .map((message) => {
133
- if (message.role !== "user") return message;
134
- const nextContent = message.content.filter((block) => {
135
- if (block.type !== "text") return true;
136
- return !isCompactionInjectedBlock(block.text);
137
- });
138
- if (nextContent.length === message.content.length) return message;
139
- if (nextContent.length === 0) return null;
140
- return { ...message, content: nextContent };
141
- })
142
- .filter(
143
- (message): message is NonNullable<typeof message> => message != null,
144
- );
145
- }
146
-
147
- /**
148
- * Load the compaction summary system prompt from the bundled markdown asset.
149
- *
150
- * `resolveBundledDir` handles the compiled-binary case where the caller path
151
- * points to `/$bunfs/` and the asset lives next to the executable (macOS app
152
- * bundle `Contents/Resources/` or sibling dir). In source mode it falls back
153
- * to the sibling `prompts/` directory.
154
- */
155
- export function loadCompactPrompt(): string {
156
- const callerDir = import.meta.dirname ?? __dirname;
157
- const promptsDir = resolveBundledDir(callerDir, "prompts", "compact-prompts");
158
- const promptPath = join(promptsDir, "compact.md");
159
- const contents = readFileSync(promptPath, "utf-8");
160
- if (contents.length === 0) {
161
- throw new Error(
162
- `compact.md at ${promptPath} is empty — compaction summary prompt missing`,
163
- );
164
- }
165
- return contents;
166
- }
167
-
168
- /**
169
- * Hardcoded fallback prompt used when the bundled `compact.md` asset is
170
- * missing or unreadable, so the daemon can still compact conversations
171
- * rather than failing module import at startup.
172
- */
173
- const SUMMARY_PROMPT_FALLBACK = [
174
- "You are summarizing a long conversation so that the assistant can keep working with it after older messages are dropped. Your summary will REPLACE those messages — the assistant's only access to what was said earlier will be what you write here.",
175
- "",
176
- "Be thorough. Capture what happened, why it mattered, what's unresolved, and what was felt. Do not compress away emotional tone, relationship context, or nuance. Keep specific details (names, numbers, file paths, commands, URLs, exact phrasings) when they might matter later.",
177
- "",
178
- "Target length: aim for 1500–4000 tokens. Use the upper end when the conversation is rich in decisions, relationships, emotional content, or threads that are still open. Use the lower end for short or simple task execution.",
179
- "",
180
- "Open with a 1–2 paragraph narrative describing what the conversation is about and where it currently stands. Then use `## ` section headers. Use these when they apply; skip sections that have nothing to say; add your own headers when something doesn't fit:",
181
- "- `## What We're Working On`",
182
- "- `## Decisions & Commitments`",
183
- "- `## Facts Worth Remembering`",
184
- "- `## Open Threads`",
185
- "- `## Emotional Arc / Relationship Notes` (include when relevant)",
186
- "- `## Artifacts & References`",
187
- "",
188
- "If an existing summary is provided, update it: merge new information in, prefer the most recent and explicit detail on conflicts, and preserve anything still unresolved or still true. Do not restart from scratch.",
189
- "",
190
- "Never include in the summary: content inside `<memory __injected>`, `<memory>`, `<turn_context>`, `<workspace>`, `<knowledge_base>`, `<system_reminder>`, `<now_scratchpad>`, `<NOW.md …>`, `<active_thread>`, `<channel_capabilities>`, `<transport_hints>`, `<system_notice>`, or any other angle-bracket-tagged system blocks. Tool-call boilerplate (retries, failed attempts the assistant recovered from, routine status updates) — summarize the outcome instead. Repetitive chit-chat that adds nothing.",
191
- "",
192
- 'Thread anchors (Slack only): if the input includes a "Retained Thread References" section, each listed reply cites its parent via `→ Mxxxxxx`. If that parent appears in the Transcript, preserve its text verbatim. Omit when absent.',
193
- "",
194
- "Return only the summary itself in markdown — no preamble, no meta-commentary.",
195
- ].join("\n");
196
-
197
- /**
198
- * Load the compact prompt with graceful fallback. If `loader` throws (missing
199
- * or unreadable bundled asset, partial deployment, filesystem corruption),
200
- * logs a warning and returns the hardcoded fallback string so module import
201
- * never fails. The loader is injectable for testability.
202
- */
203
- export function loadCompactPromptOrFallback(
204
- loader: () => string = loadCompactPrompt,
205
- ): string {
206
- try {
207
- return loader();
208
- } catch (err) {
209
- log.warn(
210
- { err },
211
- "Failed to load compact.md from bundle; using inline fallback prompt. The bundled asset may be missing or unreadable.",
212
- );
213
- return SUMMARY_PROMPT_FALLBACK;
214
- }
215
- }
216
-
217
- const SUMMARY_SYSTEM_PROMPT = loadCompactPromptOrFallback();
218
-
219
- /**
220
- * Pattern matching a Slack-style reply tag-line's parent-alias reference.
221
- * The chronological renderer emits reply lines as
222
- * `[MM/DD/YY HH:MM @sender → Mxxxxxx]: body`, or, for edited replies,
223
- * `[MM/DD/YY HH:MM @sender → Mxxxxxx, edited MM/DD/YY HH:MM]: body`. The
224
- * character after the 6-hex parent alias is therefore `]` for a plain reply
225
- * or `,` for an edited one — the regex accepts either. `Mxxxxxx` is the
226
- * first 6 hex chars of sha256(threadTs). A retained-tail text block that
227
- * contains this pattern is carrying a live reference to a parent that may
228
- * still live in the compactable region — the summarizer needs to know about
229
- * it to act on the Thread-anchors clause of SUMMARY_SYSTEM_PROMPT.
230
- */
231
- const THREAD_REPLY_REFERENCE_PATTERN = /→ M[0-9a-f]{6}[,\]]/;
40
+ // ---------------------------------------------------------------------------
41
+ // Public types preserved for downstream consumers (agent loop, conversation,
42
+ // plugin pipeline, applyCompactionResult, routes/playground/force-compact).
43
+ // ---------------------------------------------------------------------------
232
44
 
233
45
  export interface ContextWindowResult {
234
46
  messages: Message[];
@@ -250,12 +62,6 @@ export interface ContextWindowResult {
250
62
  summaryRawResponses?: unknown[];
251
63
  summaryText: string;
252
64
  reason?: string;
253
- /**
254
- * True when the summary LLM call threw and the local fallback produced the
255
- * summary. Callers use this to distinguish provider-side summary failures
256
- * from successful compactions so they can apply circuit-breaker logic
257
- * without losing the fallback-compacted messages.
258
- */
259
65
  summaryFailed?: boolean;
260
66
  }
261
67
 
@@ -266,41 +72,27 @@ export interface ShouldCompactResult {
266
72
 
267
73
  export interface ContextWindowCompactOptions {
268
74
  lastCompactedAt?: number;
269
- /** Bypass the threshold check and force compaction. Used for context-too-large error recovery. */
75
+ /** Skip the auto-threshold check (used for /compact and recovery). */
270
76
  force?: boolean;
271
77
  /**
272
- * Override the minimum number of recent user turns to preserve.
273
- * Set to `0` for emergency recovery that can compact the entire history
274
- * (except the summary message itself). When omitted, the default floor
275
- * is `1` (or `8` when `conversationOriginChannel === "slack"`).
276
- */
277
- minKeepRecentUserTurns?: number;
278
- /**
279
- * Origin channel hint used when `minKeepRecentUserTurns` is omitted.
280
- * Slack-originated conversations bump the default keep floor so multi-turn
281
- * thread context (replies, quoted messages) is not summarized away too
282
- * aggressively. Explicit `minKeepRecentUserTurns` overrides this hint.
283
- */
284
- conversationOriginChannel?: string;
285
- /**
286
- * Per-conversation inference-profile override forwarded to the summary LLM
287
- * call and usage attribution.
78
+ * Per-conversation inference-profile override forwarded to the compaction
79
+ * LLM call.
288
80
  */
289
81
  overrideProfile?: string | null;
290
82
  /**
291
- * Override the target input token budget used for keep-boundary
292
- * projected-fit checks. Clamped to no looser than `config.targetInputTokens`
293
- * — i.e. the override may only demand a *stricter* fit. Passing a looser
294
- * value has no effect. Intended for forced recovery paths that need a
295
- * tighter target than the default.
83
+ * Pre-computed token estimate from a prior {@link shouldCompact} call.
84
+ * Avoids a redundant tokenization pass when the caller already has one.
296
85
  */
297
- targetInputTokensOverride?: number;
86
+ precomputedEstimate?: number;
298
87
  /**
299
- * Pre-computed token estimate from a prior `shouldCompact()` call.
300
- * When provided, `maybeCompact()` skips its own `estimatePromptTokens()`
301
- * call, avoiding a redundant O(history) tokenization pass.
88
+ * Legacy fields retained for backwards compatibility with existing
89
+ * callers. The new assistant-driven compactor does not consume them —
90
+ * the model decides where to cut and what to keep — but accepting them
91
+ * here lets callers keep their existing call sites unchanged.
302
92
  */
303
- precomputedEstimate?: number;
93
+ minKeepRecentUserTurns?: number;
94
+ conversationOriginChannel?: string;
95
+ targetInputTokensOverride?: number;
304
96
  }
305
97
 
306
98
  export interface ContextWindowManagerOptions {
@@ -309,36 +101,83 @@ export interface ContextWindowManagerOptions {
309
101
  config: ContextWindowConfig;
310
102
  /** Pre-computed tool token budget to include in all estimations. */
311
103
  toolTokenBudget?: number;
104
+ /** Conversation ID — required for image-manifest and timestamp lookups. */
105
+ conversationId?: string;
106
+ /**
107
+ * Optional tools resolver. The compactor passes tools to the provider on
108
+ * the compaction call so the cached prefix (system prompt + tools +
109
+ * conversation messages) matches the agent's main-turn cache key.
110
+ */
111
+ resolveTools?: () => ToolDefinition[] | undefined;
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Summary-message helpers (used by lifecycle rehydrate + fork inheritance)
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Build the synthetic memory message that heads a compacted conversation.
120
+ * Produces an `assistant`-role message wrapped in `<context_summary>` tags
121
+ * so reload and inheritance paths can recognize it via
122
+ * {@link getSummaryFromContextMessage}.
123
+ */
124
+ export function createContextSummaryMessage(summary: string): Message {
125
+ const message: Message = {
126
+ role: "assistant",
127
+ content: [
128
+ {
129
+ type: "text",
130
+ text: `${CONTEXT_SUMMARY_MARKER}\n${summary}\n${CONTEXT_SUMMARY_CLOSE}`,
131
+ },
132
+ ],
133
+ };
134
+ INTERNAL_CONTEXT_SUMMARY_MESSAGES.add(message);
135
+ return message;
136
+ }
137
+
138
+ export function getSummaryFromContextMessage(
139
+ message: Message | undefined,
140
+ ): string | null {
141
+ if (!message) return null;
142
+ const text = extractText(message.content).trim();
143
+ if (!text.startsWith(CONTEXT_SUMMARY_MARKER)) return null;
144
+ if (!INTERNAL_CONTEXT_SUMMARY_MESSAGES.has(message)) return null;
145
+ let inner = text.slice(CONTEXT_SUMMARY_MARKER.length);
146
+ const closeIdx = inner.lastIndexOf(CONTEXT_SUMMARY_CLOSE);
147
+ if (closeIdx !== -1) inner = inner.slice(0, closeIdx);
148
+ return inner.trim();
312
149
  }
313
150
 
151
+ function extractText(content: ContentBlock[]): string {
152
+ return content
153
+ .filter(
154
+ (b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
155
+ )
156
+ .map((b) => b.text)
157
+ .join("\n");
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // ContextWindowManager
162
+ // ---------------------------------------------------------------------------
163
+
314
164
  export class ContextWindowManager {
315
165
  private readonly provider: Provider;
316
166
  private readonly _systemPrompt: string | (() => string);
317
167
  private config: ContextWindowConfig;
318
168
  private readonly toolTokenBudget: number;
169
+ private readonly conversationId: string | undefined;
170
+ private readonly resolveTools:
171
+ | (() => ToolDefinition[] | undefined)
172
+ | undefined;
319
173
  /**
320
174
  * Number of leading messages that are non-persisted (injected inherited
321
- * context from a parent conversation). `countPersistedMessages` subtracts
322
- * this so `compactedPersistedMessages` only reflects DB-backed messages.
323
- * Set by `Conversation.injectInheritedContext` and consumed (decremented)
324
- * after a successful compaction pass.
175
+ * context from a parent conversation). The compactor subtracts this from
176
+ * `compactedMessages` so `compactedPersistedMessages` only reflects DB
177
+ * rows. Decremented after a successful compaction.
325
178
  */
326
179
  nonPersistedPrefixCount = 0;
327
- /**
328
- * True when the message at index 0 is a context summary that was inherited
329
- * from a parent fork (i.e. injected as part of the non-persisted prefix),
330
- * rather than produced by this conversation's own compaction. The parent
331
- * summary sits at index 0 but is excluded from `compactableMessages` by
332
- * `summaryOffset`, so its slot in `nonPersistedPrefixCount` must be
333
- * accounted for separately. Cleared after the first compaction replaces
334
- * the parent summary with a child-owned one.
335
- */
336
180
  summaryIsInjected = false;
337
- /**
338
- * Cached resolved system prompt. Lazily populated on first access via the
339
- * `systemPrompt` getter and cleared after each compaction pass so the next
340
- * pass picks up any prompt changes.
341
- */
342
181
  private _resolvedSystemPrompt: string | undefined;
343
182
 
344
183
  constructor(options: ContextWindowManagerOptions) {
@@ -346,27 +185,21 @@ export class ContextWindowManager {
346
185
  this._systemPrompt = options.systemPrompt;
347
186
  this.config = options.config;
348
187
  this.toolTokenBudget = options.toolTokenBudget ?? 0;
188
+ this.conversationId = options.conversationId;
189
+ this.resolveTools = options.resolveTools;
349
190
  }
350
191
 
351
192
  updateConfig(config: ContextWindowConfig): void {
352
193
  this.config = config;
353
194
  }
354
195
 
355
- /**
356
- * Provider key for the local token estimator. Wrapper providers (e.g.
357
- * OpenRouter routing to `anthropic/*`) override `tokenEstimationProvider`
358
- * so image/PDF sizing uses the same rules as the upstream API instead of
359
- * the generic `base64/4` fallback.
360
- */
361
196
  private get estimationProviderName(): string {
362
197
  return this.provider.tokenEstimationProvider ?? this.provider.name;
363
198
  }
364
199
 
365
- /** Lazily resolve and cache the system prompt for the duration of a compaction pass. */
366
200
  private get systemPrompt(): string {
367
- if (this._resolvedSystemPrompt !== undefined) {
201
+ if (this._resolvedSystemPrompt !== undefined)
368
202
  return this._resolvedSystemPrompt;
369
- }
370
203
  const resolved =
371
204
  typeof this._systemPrompt === "function"
372
205
  ? this._systemPrompt()
@@ -379,21 +212,26 @@ export class ContextWindowManager {
379
212
  this._resolvedSystemPrompt = undefined;
380
213
  }
381
214
 
215
+ private resolveCompactionConfig(): CompactionConfig {
216
+ return getConfig().compaction;
217
+ }
218
+
382
219
  /**
383
- * Cheap pre-check: returns whether the estimated token count exceeds
384
- * the compaction threshold, along with the estimated token count so
385
- * callers can pass it into `maybeCompact()` via `precomputedEstimate`
386
- * to avoid a redundant tokenization pass.
220
+ * Cheap pre-check estimate the current token count and compare against
221
+ * `compaction.autoThreshold`. Callers pass the estimate back through
222
+ * `precomputedEstimate` on the {@link maybeCompact} call to avoid
223
+ * re-tokenizing the same history twice.
387
224
  */
388
225
  shouldCompact(messages: Message[]): ShouldCompactResult {
389
- if (!this.config.enabled) return { needed: false, estimatedTokens: 0 };
226
+ const compaction = this.resolveCompactionConfig();
227
+ if (!compaction.enabled) return { needed: false, estimatedTokens: 0 };
390
228
  try {
391
229
  const estimated = estimatePromptTokens(messages, this.systemPrompt, {
392
230
  providerName: this.estimationProviderName,
393
231
  toolTokenBudget: this.toolTokenBudget,
394
232
  });
395
233
  const threshold = Math.floor(
396
- this.config.maxInputTokens * this.config.compactThreshold,
234
+ this.config.maxInputTokens * compaction.autoThreshold,
397
235
  );
398
236
  return { needed: estimated >= threshold, estimatedTokens: estimated };
399
237
  } finally {
@@ -418,6 +256,7 @@ export class ContextWindowManager {
418
256
  signal?: AbortSignal,
419
257
  options?: ContextWindowCompactOptions,
420
258
  ): Promise<ContextWindowResult> {
259
+ const compaction = this.resolveCompactionConfig();
421
260
  const previousEstimatedInputTokens =
422
261
  options?.precomputedEstimate ??
423
262
  estimatePromptTokens(messages, this.systemPrompt, {
@@ -425,1091 +264,115 @@ export class ContextWindowManager {
425
264
  toolTokenBudget: this.toolTokenBudget,
426
265
  });
427
266
  const thresholdTokens = Math.floor(
428
- this.config.maxInputTokens * this.config.compactThreshold,
267
+ this.config.maxInputTokens * compaction.autoThreshold,
429
268
  );
430
- const existingSummary = getSummaryFromContextMessage(messages[0]);
431
269
 
432
- if (!this.config.enabled) {
433
- return {
434
- messages,
435
- compacted: false,
436
- previousEstimatedInputTokens,
437
- estimatedInputTokens: previousEstimatedInputTokens,
270
+ if (!compaction.enabled) {
271
+ return noopResult(messages, previousEstimatedInputTokens, {
438
272
  maxInputTokens: this.config.maxInputTokens,
439
273
  thresholdTokens,
440
- compactedMessages: 0,
441
- compactedPersistedMessages: 0,
442
- summaryCalls: 0,
443
- summaryInputTokens: 0,
444
- summaryOutputTokens: 0,
445
- summaryModel: "",
446
- summaryText: existingSummary ?? "",
447
- reason: "context window compaction disabled",
448
- };
274
+ reason: "compaction disabled",
275
+ });
449
276
  }
450
277
 
451
- if (!options?.force && previousEstimatedInputTokens < thresholdTokens) {
452
- return {
453
- messages,
454
- compacted: false,
455
- previousEstimatedInputTokens,
456
- estimatedInputTokens: previousEstimatedInputTokens,
278
+ if (this.conversationId == null) {
279
+ // The compactor needs the conversation id to look up image
280
+ // attachments and DB timestamps. If we don't have one (legacy test
281
+ // path, ad-hoc instantiation), skip — never fabricate one.
282
+ log.warn(
283
+ "ContextWindowManager has no conversationId — skipping compaction",
284
+ );
285
+ return noopResult(messages, previousEstimatedInputTokens, {
457
286
  maxInputTokens: this.config.maxInputTokens,
458
287
  thresholdTokens,
459
- compactedMessages: 0,
460
- compactedPersistedMessages: 0,
461
- summaryCalls: 0,
462
- summaryInputTokens: 0,
463
- summaryOutputTokens: 0,
464
- summaryModel: "",
465
- summaryText: existingSummary ?? "",
466
- reason: "below compaction threshold",
467
- };
288
+ reason: "no conversation id",
289
+ });
468
290
  }
469
291
 
470
- const summaryOffset = existingSummary != null ? 1 : 0;
471
- const userTurnStarts = collectUserTurnStartIndexes(messages);
472
- if (userTurnStarts.length === 0) {
473
- return {
474
- messages,
475
- compacted: false,
476
- previousEstimatedInputTokens,
477
- estimatedInputTokens: previousEstimatedInputTokens,
292
+ if (!options?.force && previousEstimatedInputTokens < thresholdTokens) {
293
+ return noopResult(messages, previousEstimatedInputTokens, {
478
294
  maxInputTokens: this.config.maxInputTokens,
479
295
  thresholdTokens,
480
- compactedMessages: 0,
481
- compactedPersistedMessages: 0,
482
- summaryCalls: 0,
483
- summaryInputTokens: 0,
484
- summaryOutputTokens: 0,
485
- summaryModel: "",
486
- summaryText: existingSummary ?? "",
487
- reason: "no user turns available for compaction",
488
- };
296
+ reason: "below auto threshold",
297
+ });
489
298
  }
490
299
 
491
- const keepPlan = this.pickKeepBoundary(messages, userTurnStarts, {
492
- minKeepRecentUserTurns: options?.minKeepRecentUserTurns,
493
- targetInputTokensOverride: options?.targetInputTokensOverride,
494
- conversationOriginChannel: options?.conversationOriginChannel,
495
- force: options?.force,
300
+ const args: CompactionRunArgs = {
301
+ conversationId: this.conversationId,
302
+ messages,
303
+ provider: this.provider,
304
+ systemPrompt: this.systemPrompt,
305
+ tools: this.resolveTools?.(),
306
+ compaction,
307
+ maxInputTokens: this.config.maxInputTokens,
496
308
  previousEstimatedInputTokens,
497
- });
498
- if (keepPlan.keepFromIndex <= summaryOffset) {
499
- // All turns fit after truncation projection, but the real in-memory
500
- // messages may still contain un-truncated tool results. Apply truncation
501
- // so the caller gets the token savings even without summarization.
502
- const { messages: truncatedMessages, truncatedCount } =
503
- truncateToolResultsAcrossHistory(
504
- messages,
505
- COMPACTION_TOOL_RESULT_MAX_CHARS,
506
- );
507
- const didTruncate = truncatedCount > 0;
508
- const estimatedAfterTruncation = didTruncate
509
- ? estimatePromptTokens(truncatedMessages, this.systemPrompt, {
510
- providerName: this.estimationProviderName,
511
- toolTokenBudget: this.toolTokenBudget,
512
- })
513
- : previousEstimatedInputTokens;
514
- return {
515
- messages: truncatedMessages,
516
- compacted: didTruncate,
517
- previousEstimatedInputTokens,
518
- estimatedInputTokens: estimatedAfterTruncation,
519
- maxInputTokens: this.config.maxInputTokens,
520
- thresholdTokens,
521
- compactedMessages: 0,
522
- compactedPersistedMessages: 0,
523
- summaryCalls: 0,
524
- summaryInputTokens: 0,
525
- summaryOutputTokens: 0,
526
- summaryModel: "",
527
- summaryText: existingSummary ?? "",
528
- reason: didTruncate
529
- ? "truncated tool results without summarization"
530
- : "conversation already fits within the compaction target",
531
- };
532
- }
533
-
534
- const compactableMessages = messages.slice(
535
- summaryOffset,
536
- keepPlan.keepFromIndex,
537
- );
538
- if (compactableMessages.length === 0) {
539
- return {
540
- messages,
541
- compacted: false,
542
- previousEstimatedInputTokens,
543
- estimatedInputTokens: previousEstimatedInputTokens,
544
- maxInputTokens: this.config.maxInputTokens,
545
- thresholdTokens,
546
- compactedMessages: 0,
547
- compactedPersistedMessages: 0,
548
- summaryCalls: 0,
549
- summaryInputTokens: 0,
550
- summaryOutputTokens: 0,
551
- summaryModel: "",
552
- summaryText: existingSummary ?? "",
553
- reason: "no eligible messages to compact",
554
- };
555
- }
309
+ force: options?.force,
310
+ signal,
311
+ overrideProfile: options?.overrideProfile ?? null,
312
+ nonPersistedPrefixCount: this.nonPersistedPrefixCount,
313
+ };
556
314
 
557
- // When the summary at index 0 was injected from a parent fork, it
558
- // contributes 1 to `nonPersistedPrefixCount` but is excluded from
559
- // `compactableMessages` by `summaryOffset`; subtract it here so the
560
- // remaining injected count lines up with compactableMessages. A summary
561
- // produced by this conversation's own prior compaction is not part of
562
- // `nonPersistedPrefixCount` (already decremented), so no subtraction.
563
- const injectedSummaryOffset = this.summaryIsInjected ? summaryOffset : 0;
564
- const injectedInCompactable = Math.min(
565
- Math.max(0, this.nonPersistedPrefixCount - injectedSummaryOffset),
566
- compactableMessages.length,
567
- );
568
- const compactedPersistedMessages =
569
- countPersistedMessages(compactableMessages) - injectedInCompactable;
570
- const rawProjectedMessages = [
571
- createContextSummaryMessage(existingSummary ?? "Projected summary"),
572
- ...messages.slice(keepPlan.keepFromIndex),
573
- ];
574
- const { messages: projectedMessages } = truncateToolResultsAcrossHistory(
575
- rawProjectedMessages,
576
- COMPACTION_TOOL_RESULT_MAX_CHARS,
577
- );
578
- const projectedInputTokens = estimatePromptTokens(
579
- projectedMessages,
580
- this.systemPrompt,
581
- {
582
- providerName: this.estimationProviderName,
583
- toolTokenBudget: this.toolTokenBudget,
584
- },
585
- );
586
- const projectedGainTokens = Math.max(
587
- 0,
588
- previousEstimatedInputTokens - projectedInputTokens,
589
- );
590
- const severePressure =
591
- previousEstimatedInputTokens >=
592
- Math.floor(this.config.maxInputTokens * SEVERE_PRESSURE_RATIO);
593
- const lastCompactedAt = options?.lastCompactedAt;
315
+ const result = await runAssistantDrivenCompaction(args);
594
316
 
595
- // Adaptive cooldown: conversations growing quickly (high projected gain) compact
596
- // sooner. Scale the cooldown inversely with the growth-rate multiplier, capped at
597
- // 1/4 of the base cooldown so we never check more than 4× as frequently.
598
- const growthRateMultiplier = Math.max(
599
- 1,
600
- projectedGainTokens / MIN_GAIN_TOKENS_DURING_COOLDOWN,
601
- );
602
- const adaptiveCooldownMs = Math.max(
603
- COMPACTION_COOLDOWN_MS / 4,
604
- COMPACTION_COOLDOWN_MS / growthRateMultiplier,
605
- );
606
- const withinCooldown =
607
- typeof lastCompactedAt === "number" &&
608
- Date.now() - lastCompactedAt < adaptiveCooldownMs;
317
+ if (!result.compacted) return result;
609
318
 
610
- // The adaptive cooldown is already tuned to be shorter for fast-growing
611
- // conversations (high projectedGainTokens smaller adaptiveCooldownMs).
612
- // Removing the redundant MIN_GAIN_TOKENS_DURING_COOLDOWN guard here lets
613
- // that shorter cooldown actually gate compaction: high-growth conversations
614
- // break out of the cooldown sooner and compact more frequently.
615
- // force=true bypasses the cooldown so context-too-large recovery can always
616
- // attempt a compaction even within the cooldown window.
617
- if (withinCooldown && !severePressure && !options?.force) {
618
- log.debug(
319
+ // Recompute the post-compaction token estimate now that the message
320
+ // array has been rebuilt. The compactor returns a conservative
321
+ // placeholder; the agent loop wants the real number for its next
322
+ // budget decision.
323
+ let estimatedInputTokens = result.estimatedInputTokens;
324
+ try {
325
+ estimatedInputTokens = estimatePromptTokens(
326
+ result.messages,
327
+ this.systemPrompt,
619
328
  {
620
- projectedGainTokens,
621
- adaptiveCooldownMs,
622
- growthRateMultiplier,
623
- msSinceCompaction:
624
- typeof lastCompactedAt === "number"
625
- ? Date.now() - lastCompactedAt
626
- : null,
329
+ providerName: this.estimationProviderName,
330
+ toolTokenBudget: this.toolTokenBudget,
627
331
  },
628
- "Compaction cooldown active",
629
332
  );
630
- return {
631
- messages,
632
- compacted: false,
633
- previousEstimatedInputTokens,
634
- estimatedInputTokens: previousEstimatedInputTokens,
635
- maxInputTokens: this.config.maxInputTokens,
636
- thresholdTokens,
637
- compactedMessages: 0,
638
- compactedPersistedMessages: 0,
639
- summaryCalls: 0,
640
- summaryInputTokens: 0,
641
- summaryOutputTokens: 0,
642
- summaryModel: "",
643
- summaryText: existingSummary ?? "",
644
- reason: "compaction cooldown active",
645
- };
646
- }
647
-
648
- if (
649
- compactedPersistedMessages < MIN_COMPACTABLE_PERSISTED_MESSAGES &&
650
- !severePressure
651
- ) {
652
- return {
653
- messages,
654
- compacted: false,
655
- previousEstimatedInputTokens,
656
- estimatedInputTokens: previousEstimatedInputTokens,
657
- maxInputTokens: this.config.maxInputTokens,
658
- thresholdTokens,
659
- compactedMessages: 0,
660
- compactedPersistedMessages: 0,
661
- summaryCalls: 0,
662
- summaryInputTokens: 0,
663
- summaryOutputTokens: 0,
664
- summaryModel: "",
665
- summaryText: existingSummary ?? "",
666
- reason: "insufficient compactable persisted messages",
667
- };
668
- }
669
-
670
- const retainedThreadRefs = collectRetainedThreadReferences(
671
- messages.slice(keepPlan.keepFromIndex),
672
- );
673
- // Strip runtime injections (memory, turn context, workspace hints, etc.)
674
- // from the messages fed to the summarizer. These blocks are system
675
- // metadata; leaving them in causes the summary to echo rotating memory
676
- // content instead of the actual conversation. The caller's live message
677
- // array is untouched so prefix caching stays intact.
678
- const transcriptSource = stripCompactionOnlyInjections(compactableMessages);
679
- const transcriptBlocks = this.capTranscriptBlocksToTokenBudget(
680
- serializeMessagesToContentBlocks(transcriptSource),
681
- existingSummary ?? "No previous summary.",
682
- retainedThreadRefs,
683
- );
684
- const summaryUpdate = await this.updateSummary(
685
- existingSummary ?? "No previous summary.",
686
- transcriptBlocks,
687
- retainedThreadRefs,
688
- signal,
689
- options?.overrideProfile ?? null,
690
- );
691
- const summary = summaryUpdate.summary;
692
- const summaryInputTokens = summaryUpdate.inputTokens;
693
- const summaryOutputTokens = summaryUpdate.outputTokens;
694
- const summaryModel = summaryUpdate.model;
695
- const summaryCacheCreationInputTokens =
696
- summaryUpdate.cacheCreationInputTokens;
697
- const summaryCacheReadInputTokens = summaryUpdate.cacheReadInputTokens;
698
- const summaryFailed = summaryUpdate.failed;
699
- const summaryRawResponses: unknown[] = [];
700
- if (Array.isArray(summaryUpdate.rawResponse)) {
701
- summaryRawResponses.push(...summaryUpdate.rawResponse);
702
- } else if (summaryUpdate.rawResponse !== undefined) {
703
- summaryRawResponses.push(summaryUpdate.rawResponse);
333
+ } catch (err) {
334
+ log.warn({ err }, "Post-compaction token estimate failed");
704
335
  }
705
- const summaryCalls = 1;
706
-
707
- // Media (images, files) in kept turns is preserved naturally — those
708
- // turns are carried forward as-is and their token cost is already
709
- // accounted for by pickKeepBoundary's estimatePromptTokens call.
710
- // Images in compacted turns are passed to the summarizer so it can
711
- // describe their visual content in the summary text.
712
- const summaryMessage = createContextSummaryMessage(summary);
713
336
 
714
- const { messages: truncatedKeptMessages } =
715
- truncateToolResultsAcrossHistory(
716
- messages.slice(keepPlan.keepFromIndex),
717
- COMPACTION_TOOL_RESULT_MAX_CHARS,
718
- );
719
- const compactedMessages = [summaryMessage, ...truncatedKeptMessages];
720
- const estimatedInputTokens = estimatePromptTokens(
721
- compactedMessages,
722
- this.systemPrompt,
723
- {
724
- providerName: this.estimationProviderName,
725
- toolTokenBudget: this.toolTokenBudget,
726
- },
337
+ // Consume any non-persisted prefix messages that were compacted away
338
+ // and clear the injected-summary flag.
339
+ const compactedAway = Math.min(
340
+ this.nonPersistedPrefixCount,
341
+ result.compactedMessages,
727
342
  );
728
- // Consume the injected prefix messages that were compacted away. When the
729
- // parent-injected summary was replaced by a freshly produced child summary,
730
- // also consume its slot (it was excluded from injectedInCompactable via
731
- // injectedSummaryOffset) and clear the flag so subsequent compactions treat
732
- // the summary at index 0 as child-owned.
733
343
  this.nonPersistedPrefixCount = Math.max(
734
344
  0,
735
- this.nonPersistedPrefixCount -
736
- injectedInCompactable -
737
- injectedSummaryOffset,
345
+ this.nonPersistedPrefixCount - compactedAway,
738
346
  );
739
347
  this.summaryIsInjected = false;
740
348
 
741
- log.info(
742
- {
743
- previousEstimatedInputTokens,
744
- estimatedInputTokens,
745
- compactedMessages: compactableMessages.length,
746
- compactedPersistedMessages,
747
- keepTurns: keepPlan.keepTurns,
748
- summaryCalls,
749
- },
750
- "Compacted conversation context window",
751
- );
752
-
753
- return {
754
- messages: compactedMessages,
755
- compacted: true,
756
- previousEstimatedInputTokens,
757
- estimatedInputTokens,
758
- maxInputTokens: this.config.maxInputTokens,
759
- thresholdTokens,
760
- compactedMessages: compactableMessages.length,
761
- compactedPersistedMessages,
762
- summaryCalls,
763
- summaryInputTokens,
764
- summaryOutputTokens,
765
- summaryModel,
766
- summaryCallSite: CONVERSATION_SUMMARY_CALL_SITE,
767
- summaryOverrideProfile: options?.overrideProfile ?? null,
768
- summaryCacheCreationInputTokens,
769
- summaryCacheReadInputTokens,
770
- summaryRawResponses,
771
- summaryText: summary,
772
- summaryFailed,
773
- };
774
- }
775
-
776
- private get targetInputTokens(): number {
777
- return Math.floor(
778
- this.config.maxInputTokens *
779
- (this.config.targetBudgetRatio - this.config.summaryBudgetRatio),
780
- );
781
- }
782
-
783
- private pickKeepBoundary(
784
- messages: Message[],
785
- userTurnStarts: number[],
786
- opts?: {
787
- minKeepRecentUserTurns?: number;
788
- targetInputTokensOverride?: number;
789
- conversationOriginChannel?: string;
790
- force?: boolean;
791
- previousEstimatedInputTokens?: number;
792
- },
793
- ): { keepFromIndex: number; keepTurns: number } {
794
- // Slack-originated conversations rely on multi-turn thread context
795
- // (reply chains, quoted messages, contextual references). Bump the
796
- // default keep floor for them so compaction does not summarize away
797
- // recent turns that the next reply may directly cite. Explicit
798
- // `minKeepRecentUserTurns` (including emergency `0`) wins.
799
- const defaultTurns = opts?.conversationOriginChannel === "slack" ? 8 : 1;
800
- const minFloor = Math.min(
801
- Math.max(0, Math.floor(opts?.minKeepRecentUserTurns ?? defaultTurns)),
802
- userTurnStarts.length,
803
- );
804
- const targetTokens = Math.min(
805
- opts?.targetInputTokensOverride ?? this.targetInputTokens,
806
- this.targetInputTokens,
807
- );
808
-
809
- // Binary search for the maximum keepTurns whose projected tokens fit
810
- // within the budget. Token count is monotonically non-decreasing with
811
- // keepTurns (more turns = more tokens), so binary search is valid.
812
- const projectedTokensForKeep = (turns: number): number => {
813
- const fromIndex =
814
- turns === 0
815
- ? messages.length
816
- : (userTurnStarts[userTurnStarts.length - turns] ?? messages.length);
817
- const rawProjected = [
818
- createContextSummaryMessage("Projected summary"),
819
- ...messages.slice(fromIndex),
820
- ];
821
- const { messages: projectedMessages } = truncateToolResultsAcrossHistory(
822
- rawProjected,
823
- COMPACTION_TOOL_RESULT_MAX_CHARS,
824
- );
825
- return estimatePromptTokens(projectedMessages, this.systemPrompt, {
826
- providerName: this.estimationProviderName,
827
- toolTokenBudget: this.toolTokenBudget,
828
- });
829
- };
830
-
831
- let lo = minFloor;
832
- let hi = userTurnStarts.length;
833
-
834
- // Fast path: if keeping all turns already fits, skip the search.
835
- if (hi > lo && projectedTokensForKeep(hi) > targetTokens) {
836
- // Binary search: find the largest keepTurns where projected tokens fit.
837
- while (lo < hi) {
838
- const mid = lo + Math.ceil((hi - lo) / 2);
839
- if (projectedTokensForKeep(mid) <= targetTokens) {
840
- lo = mid;
841
- } else {
842
- hi = mid - 1;
843
- }
844
- }
845
- } else {
846
- lo = hi;
847
- }
848
-
849
- // Under forced compaction with only the implicit default floor in play,
850
- // that floor stops being an absolute override when the kept region still
851
- // exceeds the target. Walk keepTurns below the floor — down to 0 if
852
- // needed — so /compact can always drive the conversation toward target,
853
- // even when the floor turn itself is oversized (e.g. a huge paste in the
854
- // last user message). Exceptions that still treat the floor as hard:
855
- // - Explicit `minKeepRecentUserTurns` (the caller opted in to that
856
- // floor; emergency recovery already passes 0 when it wants to go all
857
- // the way down).
858
- // - Slack origin (the bumped 8-turn floor protects thread reply chains
859
- // and quoted-message context that the next reply may directly cite).
860
- // Automatic mid-loop compaction (force !== true) always honors the floor
861
- // so the in-flight agent turn isn't summarized away.
862
- const floorIsImplicitDefault =
863
- opts?.minKeepRecentUserTurns === undefined &&
864
- opts?.conversationOriginChannel !== "slack";
865
- if (
866
- opts?.force &&
867
- floorIsImplicitDefault &&
868
- projectedTokensForKeep(lo) > targetTokens
869
- ) {
870
- while (lo > 0 && projectedTokensForKeep(lo) > targetTokens) {
871
- lo--;
872
- }
873
- }
874
-
875
- // The projection's summary-swap and tool_result truncation can make
876
- // projectedTokensForKeep(hi) optimistically fit even when the live
877
- // conversation is well over target — sending /compact through the
878
- // "already fits" skip path as a no-op. Clamp lo so summarization runs.
879
- if (
880
- opts?.force &&
881
- floorIsImplicitDefault &&
882
- lo === userTurnStarts.length &&
883
- lo > 0 &&
884
- (opts?.previousEstimatedInputTokens ?? 0) > targetTokens
885
- ) {
886
- lo -= 1;
887
- }
888
-
889
- const keepTurns = lo;
890
- const rawKeepFromIndex =
891
- keepTurns === 0
892
- ? messages.length
893
- : (userTurnStarts[userTurnStarts.length - keepTurns] ??
894
- messages.length);
895
- const keepFromIndex = adjustForToolPairs(messages, rawKeepFromIndex);
896
- return { keepFromIndex, keepTurns };
897
- }
898
-
899
- private get summaryMaxTokens(): number {
900
- return Math.max(
901
- 1,
902
- Math.floor(this.config.maxInputTokens * this.config.summaryBudgetRatio),
903
- );
904
- }
905
-
906
- /**
907
- * Trim the serialized transcript content blocks so that the summary prompt
908
- * (system prompt + existing summary + transcript + scaffolding) fits within
909
- * the provider's input token limit, minus the output budget reserved for the
910
- * summary itself.
911
- *
912
- * When the transcript exceeds the budget, blocks are dropped from the
913
- * beginning (oldest messages first) to preserve recent context. Image blocks
914
- * are dropped before text blocks within each pass since they are expensive
915
- * and their surrounding text context already captures the conversation flow.
916
- */
917
- private capTranscriptBlocksToTokenBudget(
918
- blocks: ContentBlock[],
919
- currentSummary: string,
920
- retainedThreadRefs: string[],
921
- ): ContentBlock[] {
922
- const retainedRefsText = retainedThreadRefs.join("\n");
923
- const overheadTokens =
924
- estimateTextTokens(SUMMARY_SYSTEM_PROMPT) +
925
- estimateTextTokens(currentSummary) +
926
- estimateTextTokens(retainedRefsText) +
927
- // Scaffolding text in buildSummaryContentBlocks ("Update the summary...",
928
- // section headers, etc.) — generous fixed estimate.
929
- 200 +
930
- this.summaryMaxTokens;
931
-
932
- const maxTranscriptTokens = Math.max(
933
- 0,
934
- this.config.maxInputTokens - overheadTokens,
935
- );
936
-
937
- const estimateBlockTokens = (b: ContentBlock): number =>
938
- estimateContentBlockTokens(b, {
939
- providerName: this.estimationProviderName,
940
- });
941
-
942
- let totalTokens = 0;
943
- for (const block of blocks) {
944
- totalTokens += estimateBlockTokens(block);
945
- }
946
- const originalTotalTokens = totalTokens;
947
- if (totalTokens <= maxTranscriptTokens) return blocks;
948
-
949
- // First pass: drop images from the beginning until we fit or run out of
950
- // images to drop. Images are high-cost and their text context (message
951
- // headers, surrounding tool_use/tool_result serializations) is preserved.
952
- const result = [...blocks];
953
- for (
954
- let i = 0;
955
- i < result.length && totalTokens > maxTranscriptTokens;
956
- i++
957
- ) {
958
- if (result[i].type === "image") {
959
- totalTokens -= estimateBlockTokens(result[i]);
960
- const stub: ContentBlock = {
961
- type: "text",
962
- text: `[image omitted from summary context]`,
963
- };
964
- totalTokens += estimateBlockTokens(stub);
965
- result[i] = stub;
966
- }
967
- }
968
- if (totalTokens <= maxTranscriptTokens) return result;
969
-
970
- // Second pass: drop text blocks from the beginning (oldest) until we fit.
971
- // If a single text block exceeds the remaining budget, truncate it rather
972
- // than dropping it entirely so the summarizer always has content to work with.
973
- let dropUntil = 0;
974
- let droppedTokens = 0;
975
- for (
976
- let i = 0;
977
- i < result.length && totalTokens > maxTranscriptTokens;
978
- i++
979
- ) {
980
- const blockTokens = estimateBlockTokens(result[i]);
981
- const excess = totalTokens - maxTranscriptTokens;
982
- if (blockTokens > excess && result[i].type === "text") {
983
- // Truncate this block to shed exactly the excess tokens.
984
- // Subtract the cost of the "[...truncated] " prefix so the final
985
- // block (prefix + kept text) stays within budget.
986
- const truncationPrefix = "[...truncated] ";
987
- const prefixTokens = estimateTextTokens(truncationPrefix);
988
- const keepTokens = Math.max(1, blockTokens - excess - prefixTokens);
989
- const text = (result[i] as { type: "text"; text: string }).text;
990
- // Approximate: 1 token ≈ 4 characters for truncation purposes.
991
- const keepChars = Math.max(1, Math.floor(keepTokens * 4));
992
- const truncatedText = text.slice(-keepChars);
993
- const truncatedBlock: ContentBlock = {
994
- type: "text",
995
- text: `${truncationPrefix}${truncatedText}`,
996
- };
997
- const newBlockTokens = estimateBlockTokens(truncatedBlock);
998
- droppedTokens += blockTokens - newBlockTokens;
999
- totalTokens -= blockTokens - newBlockTokens;
1000
- result[i] = truncatedBlock;
1001
- dropUntil = i;
1002
- break;
1003
- }
1004
- droppedTokens += blockTokens;
1005
- totalTokens -= blockTokens;
1006
- dropUntil = i + 1;
1007
- }
1008
-
1009
- log.info(
1010
- {
1011
- originalTokens: originalTotalTokens,
1012
- cappedTokens: maxTranscriptTokens,
1013
- droppedTokens,
1014
- },
1015
- "Capped summary transcript blocks to fit provider input limit",
1016
- );
1017
-
1018
- return [
1019
- { type: "text", text: "[earlier messages truncated]" } as ContentBlock,
1020
- ...result.slice(dropUntil),
1021
- ];
1022
- }
1023
-
1024
- private async updateSummary(
1025
- currentSummary: string,
1026
- transcriptBlocks: ContentBlock[],
1027
- retainedThreadRefs: string[],
1028
- signal?: AbortSignal,
1029
- overrideProfile?: string | null,
1030
- ): Promise<{
1031
- summary: string;
1032
- inputTokens: number;
1033
- outputTokens: number;
1034
- model: string;
1035
- cacheCreationInputTokens: number;
1036
- cacheReadInputTokens: number;
1037
- rawResponse?: unknown;
1038
- /**
1039
- * True when the provider.sendMessage call threw and the local fallback
1040
- * was used. Callers (the agent loop) use this to drive circuit-breaker
1041
- * state without having to reimplement the fallback themselves.
1042
- */
1043
- failed: boolean;
1044
- }> {
1045
- // When the existing summary is already consuming most of its budget,
1046
- // nudge the model to compress older durable content aggressively so
1047
- // incremental-update passes don't let the summary grow unboundedly.
1048
- const existingSummaryTokens = estimateTextTokens(currentSummary);
1049
- const compressionPressure =
1050
- existingSummaryTokens >=
1051
- this.summaryMaxTokens * SUMMARY_COMPRESSION_PRESSURE_RATIO;
1052
- const contentBlocks = buildSummaryContentBlocks(
1053
- currentSummary,
1054
- transcriptBlocks,
1055
- retainedThreadRefs,
1056
- { compressionPressure },
1057
- );
1058
- const summaryMessage: Message = { role: "user", content: contentBlocks };
1059
- let failed = false;
1060
- try {
1061
- const providerConfig: Record<string, unknown> = {
1062
- callSite: CONVERSATION_SUMMARY_CALL_SITE,
1063
- usageTracking: "manual",
1064
- max_tokens: this.summaryMaxTokens,
1065
- };
1066
- if (overrideProfile) {
1067
- providerConfig.overrideProfile = overrideProfile;
1068
- }
1069
- const response = await this.provider.sendMessage(
1070
- [summaryMessage],
1071
- undefined,
1072
- SUMMARY_SYSTEM_PROMPT,
1073
- {
1074
- config: providerConfig,
1075
- signal,
1076
- },
1077
- );
1078
-
1079
- const nextSummary = extractText(response.content).trim();
1080
- if (nextSummary.length > 0) {
1081
- return {
1082
- summary: this.clampSummary(nextSummary),
1083
- inputTokens: response.usage.inputTokens,
1084
- outputTokens: response.usage.outputTokens,
1085
- model: response.model,
1086
- cacheCreationInputTokens:
1087
- response.usage.cacheCreationInputTokens ?? 0,
1088
- cacheReadInputTokens: response.usage.cacheReadInputTokens ?? 0,
1089
- rawResponse: response.rawResponse,
1090
- failed: false,
1091
- };
1092
- }
1093
- } catch (err) {
1094
- failed = true;
1095
- log.warn({ err }, "Summary generation failed, using local fallback");
1096
- }
1097
-
1098
- // Fallback: extract text-only transcript for local summary generation.
1099
- const textTranscript = transcriptBlocks
1100
- .filter(
1101
- (b): b is Extract<ContentBlock, { type: "text" }> => b.type === "text",
1102
- )
1103
- .map((b) => b.text)
1104
- .join("\n\n");
1105
-
1106
- return {
1107
- summary: fallbackSummary(currentSummary, textTranscript),
1108
- inputTokens: 0,
1109
- outputTokens: 0,
1110
- model: "",
1111
- cacheCreationInputTokens: 0,
1112
- cacheReadInputTokens: 0,
1113
- failed,
1114
- };
1115
- }
1116
-
1117
- private clampSummary(summary: string): string {
1118
- // Budget in tokens → approximate char limit (4 chars ≈ 1 token).
1119
- const maxChars = this.summaryMaxTokens * 4;
1120
- if (summary.length <= maxChars) return summary;
1121
- return clampSummaryAtSectionBoundary(summary, maxChars);
349
+ return { ...result, estimatedInputTokens };
1122
350
  }
1123
351
  }
1124
352
 
1125
- /**
1126
- * Truncate a markdown summary that exceeds `maxChars`, preferring a
1127
- * section boundary (`\n## `) so we never cut a heading mid-text. Falls
1128
- * back to a hard character slice when no boundary exists in the safe
1129
- * region (first half of the budget).
1130
- */
1131
- export function clampSummaryAtSectionBoundary(
1132
- summary: string,
1133
- maxChars: number,
1134
- ): string {
1135
- if (summary.length <= maxChars) return summary;
1136
- const ELLIPSIS = "...";
1137
- // Hard limit we must stay under, leaving room for the ellipsis suffix.
1138
- const cutoff = maxChars - ELLIPSIS.length;
1139
- if (cutoff <= 0) return ELLIPSIS;
1140
- const head = safeStringSlice(summary, 0, cutoff);
1141
- // Find the last `## ` heading at a line start. Require it to be past the
1142
- // midpoint of the allowed region so we don't drop most of the summary
1143
- // just to hit a boundary — better to cut mid-section late than to keep
1144
- // almost nothing.
1145
- const halfway = Math.floor(cutoff / 2);
1146
- const boundary = head.lastIndexOf("\n## ");
1147
- if (boundary >= halfway) {
1148
- return `${head.slice(0, boundary).trimEnd()}\n${ELLIPSIS}`;
1149
- }
1150
- return `${head}${ELLIPSIS}`;
1151
- }
1152
-
1153
- function collectUserTurnStartIndexes(messages: Message[]): number[] {
1154
- const starts: number[] = [];
1155
- for (let i = 0; i < messages.length; i++) {
1156
- const message = messages[i];
1157
- if (message.role !== "user") continue;
1158
- if (getSummaryFromContextMessage(message) != null) continue;
1159
- if (isToolResultOnly(message)) continue;
1160
- starts.push(i);
1161
- }
1162
- return starts;
1163
- }
1164
-
1165
- /**
1166
- * Count messages that have DB counterparts. Context-summary messages are
1167
- * in-memory-only and excluded; ALL other messages (including tool-result-only
1168
- * user messages) have a corresponding row in the DB and must be counted so
1169
- * that `contextCompactedMessageCount` indexes the DB array correctly.
1170
- */
1171
- function countPersistedMessages(messages: Message[]): number {
1172
- return messages.filter((message) => {
1173
- return getSummaryFromContextMessage(message) == null;
1174
- }).length;
1175
- }
1176
-
1177
- function isSystemNoticeBlock(block: ContentBlock): boolean {
1178
- if (block.type !== "text") return false;
1179
- const text = (block as { text?: string }).text ?? "";
1180
- return (
1181
- text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
1182
- );
1183
- }
1184
-
1185
- /** A user message that contains ONLY tool_result blocks (no text or other content).
1186
- * System notice text blocks (retry nudges, progress checks) do not count as user content. */
1187
- function isToolResultOnly(message: Message): boolean {
1188
- return (
1189
- message.content.length > 0 &&
1190
- message.content.every(
1191
- (block) =>
1192
- block.type === "tool_result" ||
1193
- block.type === "web_search_tool_result" ||
1194
- isSystemNoticeBlock(block),
1195
- )
1196
- );
1197
- }
353
+ // ---------------------------------------------------------------------------
354
+ // Helpers
355
+ // ---------------------------------------------------------------------------
1198
356
 
1199
- /**
1200
- * Walk the keep boundary backward to ensure tool_use/tool_result pairs are
1201
- * never split across the compaction boundary. If the first kept message is
1202
- * a user message containing tool_result blocks whose matching tool_use blocks
1203
- * live in the preceding (compacted-away) assistant message, include that
1204
- * assistant message in the kept set.
1205
- */
1206
- function adjustForToolPairs(
357
+ function noopResult(
1207
358
  messages: Message[],
1208
- keepFromIndex: number,
1209
- ): number {
1210
- let idx = keepFromIndex;
1211
- while (idx > 0) {
1212
- const msg = messages[idx];
1213
- if (!msg || msg.role !== "user") break;
1214
-
1215
- // Collect tool_use_ids referenced by tool_results in this user message
1216
- const referencedIds = new Set<string>();
1217
- for (const block of msg.content) {
1218
- if (
1219
- (block.type === "tool_result" ||
1220
- block.type === "web_search_tool_result") &&
1221
- "tool_use_id" in block
1222
- ) {
1223
- referencedIds.add((block as { tool_use_id: string }).tool_use_id);
1224
- }
1225
- }
1226
- if (referencedIds.size === 0) break;
1227
-
1228
- // Check if the preceding assistant message contains matching tool_uses
1229
- const prev = messages[idx - 1];
1230
- if (!prev || prev.role !== "assistant") break;
1231
-
1232
- const hasOrphanedPair = prev.content.some(
1233
- (block) =>
1234
- (block.type === "tool_use" || block.type === "server_tool_use") &&
1235
- "id" in block &&
1236
- referencedIds.has((block as { id: string }).id),
1237
- );
1238
- if (!hasOrphanedPair) break;
1239
-
1240
- // Include the assistant message
1241
- idx--;
1242
-
1243
- // The assistant message may itself be preceded by a tool_result user
1244
- // message that pairs with an even earlier assistant — continue the check
1245
- if (idx > 0 && messages[idx - 1]?.role === "user") {
1246
- idx--;
1247
- } else {
1248
- break;
1249
- }
1250
- }
1251
- return idx;
1252
- }
1253
-
1254
- export function getSummaryFromContextMessage(
1255
- message: Message | undefined,
1256
- ): string | null {
1257
- if (!message) return null;
1258
- const text = extractText(message.content).trim();
1259
- if (!text.startsWith(CONTEXT_SUMMARY_MARKER)) return null;
1260
- if (INTERNAL_CONTEXT_SUMMARY_MESSAGES.has(message)) {
1261
- return stripContextSummaryTags(text);
1262
- }
1263
- return null;
1264
- }
1265
-
1266
- function stripContextSummaryTags(text: string): string {
1267
- let inner = text.slice(CONTEXT_SUMMARY_MARKER.length);
1268
- const closeIdx = inner.lastIndexOf("</context_summary>");
1269
- if (closeIdx !== -1) {
1270
- inner = inner.slice(0, closeIdx);
1271
- }
1272
- return inner.trim();
1273
- }
1274
-
1275
- export function createContextSummaryMessage(summary: string): Message {
1276
- const message: Message = {
1277
- role: "user",
1278
- content: [
1279
- {
1280
- type: "text",
1281
- text: `${CONTEXT_SUMMARY_MARKER}\n${summary}\n</context_summary>`,
1282
- },
1283
- ],
359
+ estimated: number,
360
+ opts: { maxInputTokens: number; thresholdTokens: number; reason: string },
361
+ ): ContextWindowResult {
362
+ return {
363
+ messages,
364
+ compacted: false,
365
+ previousEstimatedInputTokens: estimated,
366
+ estimatedInputTokens: estimated,
367
+ maxInputTokens: opts.maxInputTokens,
368
+ thresholdTokens: opts.thresholdTokens,
369
+ compactedMessages: 0,
370
+ compactedPersistedMessages: 0,
371
+ summaryCalls: 0,
372
+ summaryInputTokens: 0,
373
+ summaryOutputTokens: 0,
374
+ summaryModel: "",
375
+ summaryText: getSummaryFromContextMessage(messages[0]) ?? "",
376
+ reason: opts.reason,
1284
377
  };
1285
- INTERNAL_CONTEXT_SUMMARY_MESSAGES.add(message);
1286
- return message;
1287
- }
1288
-
1289
- /**
1290
- * Build content blocks for the summary prompt. Returns a mix of text blocks
1291
- * (for the scaffolding, existing summary, and serialized non-image content)
1292
- * and image blocks (preserved from the original messages so the summarizer
1293
- * can describe what was in them).
1294
- */
1295
- function buildSummaryContentBlocks(
1296
- currentSummary: string,
1297
- transcriptBlocks: ContentBlock[],
1298
- retainedThreadRefs: string[],
1299
- options: { compressionPressure: boolean } = { compressionPressure: false },
1300
- ): ContentBlock[] {
1301
- const lines = [
1302
- "Update the summary with new transcript data.",
1303
- "If new information conflicts with older notes, keep the most recent and explicit detail.",
1304
- "Keep all unresolved asks and next steps.",
1305
- "For any images included below, describe their visual content in the summary so the information is preserved after compaction.",
1306
- ];
1307
- if (options.compressionPressure) {
1308
- lines.push(
1309
- "The existing summary is approaching its token budget. Compress older durable content aggressively (drop detail that is no longer load-bearing, merge bullets, tighten prose) while preserving the most recent turns' nuance.",
1310
- );
1311
- }
1312
- lines.push(
1313
- "",
1314
- "### Existing Summary",
1315
- currentSummary.trim().length > 0 ? currentSummary.trim() : "None.",
1316
- "",
1317
- );
1318
- if (retainedThreadRefs.length > 0) {
1319
- lines.push(
1320
- "### Retained Thread References",
1321
- "These reply tag lines remain in the live context after compaction. Each `→ Mxxxxxx` cites a parent message by alias; if that parent appears in the Transcript below, preserve its text verbatim.",
1322
- ...retainedThreadRefs.map((ref) => `- ${ref}`),
1323
- "",
1324
- );
1325
- }
1326
- lines.push("### Transcript");
1327
- return [
1328
- {
1329
- type: "text",
1330
- text: lines.join("\n"),
1331
- } as ContentBlock,
1332
- ...transcriptBlocks,
1333
- ];
1334
- }
1335
-
1336
- /**
1337
- * Scan retained-tail messages for Slack-style reply tag lines that cite a
1338
- * thread parent via the `→ Mxxxxxx` alias convention. Returns the full tag
1339
- * line for each match (de-duplicated, order-preserved) so the summarizer
1340
- * has a concrete list of parents whose text must be preserved verbatim.
1341
- *
1342
- * Non-slack conversations and retained tails without any reply markers
1343
- * produce an empty list — in that case the summarizer is told explicitly
1344
- * that no verbatim preservation is required.
1345
- */
1346
- function collectRetainedThreadReferences(
1347
- retainedMessages: Message[],
1348
- ): string[] {
1349
- const seen = new Set<string>();
1350
- const out: string[] = [];
1351
- for (const msg of retainedMessages) {
1352
- for (const block of msg.content) {
1353
- if (block.type !== "text") continue;
1354
- const text = (block as { text: string }).text;
1355
- for (const line of text.split("\n")) {
1356
- if (!THREAD_REPLY_REFERENCE_PATTERN.test(line)) continue;
1357
- const trimmed = line.trim();
1358
- if (trimmed.length === 0) continue;
1359
- if (seen.has(trimmed)) continue;
1360
- seen.add(trimmed);
1361
- out.push(trimmed);
1362
- }
1363
- }
1364
- }
1365
- return out;
1366
- }
1367
-
1368
- /**
1369
- * Serialize messages into a sequence of content blocks. Text-based content
1370
- * (tool calls, tool results, thinking, etc.) is serialized into text blocks.
1371
- * Image blocks — both top-level and nested inside tool_result contentBlocks —
1372
- * are preserved as-is so the summarizer LLM can see them.
1373
- */
1374
- function serializeMessagesToContentBlocks(messages: Message[]): ContentBlock[] {
1375
- const blocks: ContentBlock[] = [];
1376
- for (let i = 0; i < messages.length; i++) {
1377
- const msg = messages[i];
1378
- const textLines: string[] = [`Message #${i + 1} (${msg.role})`];
1379
-
1380
- for (const block of msg.content) {
1381
- if (block.type === "image") {
1382
- // Flush accumulated text lines before the image.
1383
- if (textLines.length > 0) {
1384
- blocks.push({ type: "text", text: textLines.join("\n") });
1385
- textLines.length = 0;
1386
- }
1387
- blocks.push(block);
1388
- } else if (block.type === "tool_result") {
1389
- // guard:allow-tool-result-only — web_search_tool_result handled by serializeBlock via else branch
1390
- // Extract images from tool_result contentBlocks before serializing.
1391
- const collectedImages: ImageContent[] = [];
1392
- textLines.push(serializeToolResultBlock(block, collectedImages));
1393
- if (collectedImages.length > 0) {
1394
- // Flush text, emit collected images, then continue.
1395
- if (textLines.length > 0) {
1396
- blocks.push({ type: "text", text: textLines.join("\n") });
1397
- textLines.length = 0;
1398
- }
1399
- blocks.push(...collectedImages);
1400
- }
1401
- } else {
1402
- textLines.push(serializeBlock(block));
1403
- }
1404
- }
1405
-
1406
- // Flush remaining text lines for this message.
1407
- if (textLines.length > 0) {
1408
- blocks.push({ type: "text", text: textLines.join("\n") });
1409
- }
1410
- }
1411
- return blocks;
1412
- }
1413
-
1414
- /**
1415
- * Serialize images nested inside tool_result contentBlocks, returning them
1416
- * as separate content blocks to preserve for the summarizer.
1417
- */
1418
- function serializeToolResultBlock(
1419
- block: Extract<ContentBlock, { type: "tool_result" }>,
1420
- collectedImages: ImageContent[],
1421
- ): string {
1422
- if (block.contentBlocks) {
1423
- for (const cb of block.contentBlocks) {
1424
- if (cb.type === "image") {
1425
- collectedImages.push(cb);
1426
- }
1427
- }
1428
- }
1429
- return `tool_result ${block.tool_use_id}${
1430
- block.is_error ? " (error)" : ""
1431
- }: ${clampText(block.content)}`;
1432
- }
1433
-
1434
- function serializeBlock(block: ContentBlock): string {
1435
- switch (block.type) {
1436
- case "text":
1437
- return `text: ${clampText(block.text)}`;
1438
- case "tool_use":
1439
- return `tool_use ${block.name}: ${clampText(stableJson(block.input))}`;
1440
- case "tool_result":
1441
- return `tool_result ${block.tool_use_id}${
1442
- block.is_error ? " (error)" : ""
1443
- }: ${clampText(block.content)}`;
1444
- case "image":
1445
- // Top-level images are handled by serializeMessagesToContentBlocks.
1446
- // This path is only hit for images in unexpected positions.
1447
- return `image: ${block.source.media_type}, ${
1448
- Math.ceil(block.source.data.length / 4) * 3
1449
- } bytes(base64)`;
1450
- case "file": {
1451
- const sizeBytes = Math.ceil(block.source.data.length / 4) * 3;
1452
- const parts = [
1453
- `file: ${block.source.filename}`,
1454
- block.source.media_type,
1455
- `${sizeBytes} bytes(base64)`,
1456
- ];
1457
- if (block.extracted_text) {
1458
- parts.push(`text=${clampText(block.extracted_text)}`);
1459
- }
1460
- return parts.join(", ");
1461
- }
1462
- case "thinking":
1463
- return `thinking: ${clampText(block.thinking)}`;
1464
- case "redacted_thinking":
1465
- return "redacted_thinking";
1466
- case "server_tool_use":
1467
- return `server_tool_use ${block.name}: ${clampText(stableJson(block.input))}`;
1468
- case "web_search_tool_result":
1469
- return `web_search_tool_result ${block.tool_use_id}`;
1470
- default:
1471
- return "unknown_block";
1472
- }
1473
- }
1474
-
1475
- function clampText(text: string): string {
1476
- if (text.length <= MAX_BLOCK_PREVIEW_CHARS) return text;
1477
- return `${safeStringSlice(text, 0, MAX_BLOCK_PREVIEW_CHARS)}... [truncated ${
1478
- text.length - MAX_BLOCK_PREVIEW_CHARS
1479
- } chars]`;
1480
- }
1481
-
1482
- function fallbackSummary(currentSummary: string, chunk: string): string {
1483
- const lines = chunk
1484
- .split("\n")
1485
- .map((line) => line.trim())
1486
- .filter((line) => line.length > 0);
1487
- const recentLines = lines.slice(-120).join("\n");
1488
- const merged = [
1489
- currentSummary.trim(),
1490
- "## Recent Progress",
1491
- recentLines.length > 0 ? recentLines : "No new details.",
1492
- ]
1493
- .filter((part) => part.length > 0)
1494
- .join("\n\n");
1495
- if (merged.length <= MAX_FALLBACK_SUMMARY_CHARS) return merged;
1496
- return merged.slice(merged.length - MAX_FALLBACK_SUMMARY_CHARS);
1497
- }
1498
-
1499
- function extractText(content: ContentBlock[]): string {
1500
- return content
1501
- .filter(
1502
- (block): block is Extract<ContentBlock, { type: "text" }> =>
1503
- block.type === "text",
1504
- )
1505
- .map((block) => block.text)
1506
- .join("\n");
1507
- }
1508
-
1509
- function stableJson(value: unknown): string {
1510
- try {
1511
- return JSON.stringify(value);
1512
- } catch {
1513
- return "[unserializable]";
1514
- }
1515
378
  }