@vellumai/assistant 0.6.4 → 0.6.6

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 (1008) hide show
  1. package/.prettierignore +5 -0
  2. package/AGENTS.md +9 -1
  3. package/ARCHITECTURE.md +43 -49
  4. package/Dockerfile +17 -3
  5. package/README.md +3 -4
  6. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  7. package/bun.lock +8 -3
  8. package/docs/architecture/integrations.md +33 -59
  9. package/docs/architecture/memory.md +25 -30
  10. package/docs/architecture/security.md +19 -18
  11. package/docs/browser-use-architecture-phase2.md +63 -20
  12. package/docs/error-handling.md +111 -0
  13. package/docs/plugins.md +761 -0
  14. package/docs/skills.md +10 -10
  15. package/docs/stt-provider-onboarding.md +2 -1
  16. package/examples/plugins/echo/README.md +132 -0
  17. package/examples/plugins/echo/package.json +17 -0
  18. package/examples/plugins/echo/register.ts +187 -0
  19. package/knip.json +9 -2
  20. package/node_modules/@vellumai/ces-contracts/package.json +2 -1
  21. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
  22. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
  23. package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
  24. package/node_modules/@vellumai/credential-storage/package.json +2 -2
  25. package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
  26. package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
  27. package/node_modules/@vellumai/egress-proxy/package.json +2 -2
  28. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  29. package/openapi.yaml +334 -78
  30. package/package.json +6 -3
  31. package/scripts/generate-openapi.ts +50 -11
  32. package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
  33. package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
  34. package/src/__tests__/agent-loop.test.ts +112 -1
  35. package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
  36. package/src/__tests__/anthropic-provider.test.ts +171 -2
  37. package/src/__tests__/app-compiler.test.ts +57 -0
  38. package/src/__tests__/approval-cascade.test.ts +36 -10
  39. package/src/__tests__/approval-routes-http.test.ts +134 -10
  40. package/src/__tests__/assistant-attachments.test.ts +44 -0
  41. package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
  42. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  43. package/src/__tests__/avatar-generator.test.ts +4 -2
  44. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  45. package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
  46. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
  47. package/src/__tests__/browser-skill-endstate.test.ts +51 -182
  48. package/src/__tests__/btw-routes.test.ts +47 -1
  49. package/src/__tests__/bundled-asset.test.ts +6 -6
  50. package/src/__tests__/call-controller.test.ts +1 -2
  51. package/src/__tests__/call-site-routing-provider.test.ts +214 -0
  52. package/src/__tests__/catalog-cache.test.ts +96 -4
  53. package/src/__tests__/channel-approval-routes.test.ts +4 -4
  54. package/src/__tests__/channel-reply-delivery.test.ts +300 -2
  55. package/src/__tests__/checker.test.ts +870 -655
  56. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  57. package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
  58. package/src/__tests__/compaction-events.test.ts +501 -0
  59. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  60. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  61. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  62. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  63. package/src/__tests__/config-analysis.test.ts +11 -28
  64. package/src/__tests__/config-loader-backfill.test.ts +174 -0
  65. package/src/__tests__/config-loader-corrupt.test.ts +183 -0
  66. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
  67. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  68. package/src/__tests__/config-schema-cmd.test.ts +11 -5
  69. package/src/__tests__/config-schema.test.ts +440 -114
  70. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  71. package/src/__tests__/config-watcher.test.ts +2 -2
  72. package/src/__tests__/contact-store-user-file.test.ts +72 -73
  73. package/src/__tests__/contacts-tools.test.ts +26 -0
  74. package/src/__tests__/contacts-write.test.ts +4 -4
  75. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  76. package/src/__tests__/context-token-estimator.test.ts +191 -1
  77. package/src/__tests__/context-window-manager.test.ts +883 -4
  78. package/src/__tests__/conversation-abort-tool-results.test.ts +32 -15
  79. package/src/__tests__/conversation-agent-loop-overflow.test.ts +86 -46
  80. package/src/__tests__/conversation-agent-loop.test.ts +435 -216
  81. package/src/__tests__/conversation-attachments.test.ts +1 -1
  82. package/src/__tests__/conversation-confirmation-signals.test.ts +36 -10
  83. package/src/__tests__/conversation-error.test.ts +37 -6
  84. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  85. package/src/__tests__/conversation-init.benchmark.test.ts +34 -12
  86. package/src/__tests__/conversation-lifecycle.test.ts +336 -0
  87. package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
  88. package/src/__tests__/conversation-pairing.test.ts +174 -10
  89. package/src/__tests__/conversation-pre-run-repair.test.ts +32 -15
  90. package/src/__tests__/conversation-process-callsite.test.ts +309 -0
  91. package/src/__tests__/conversation-provider-retry-repair.test.ts +44 -21
  92. package/src/__tests__/conversation-queue.test.ts +68 -38
  93. package/src/__tests__/conversation-routes-disk-view.test.ts +36 -7
  94. package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
  95. package/src/__tests__/conversation-runtime-assembly.test.ts +2877 -152
  96. package/src/__tests__/conversation-runtime-workspace.test.ts +35 -50
  97. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  98. package/src/__tests__/conversation-skill-tools.test.ts +12 -146
  99. package/src/__tests__/conversation-slash-queue.test.ts +39 -19
  100. package/src/__tests__/conversation-slash-unknown.test.ts +53 -16
  101. package/src/__tests__/conversation-speed-override.test.ts +36 -12
  102. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
  103. package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
  104. package/src/__tests__/conversation-title-service.test.ts +118 -2
  105. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  106. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
  107. package/src/__tests__/conversation-unread-route.test.ts +2 -2
  108. package/src/__tests__/conversation-usage.test.ts +4 -2
  109. package/src/__tests__/conversation-workspace-cache-state.test.ts +33 -9
  110. package/src/__tests__/conversation-workspace-injection.test.ts +46 -15
  111. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -15
  112. package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
  113. package/src/__tests__/credential-health-service.test.ts +78 -9
  114. package/src/__tests__/credential-security-invariants.test.ts +5 -2
  115. package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
  116. package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
  117. package/src/__tests__/credential-vault-unit.test.ts +135 -19
  118. package/src/__tests__/credentials-cli.test.ts +1 -9
  119. package/src/__tests__/cross-provider-web-search.test.ts +84 -0
  120. package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
  121. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  122. package/src/__tests__/delete-propagation.test.ts +437 -0
  123. package/src/__tests__/dm-backfill.test.ts +417 -0
  124. package/src/__tests__/dm-persistence.test.ts +227 -0
  125. package/src/__tests__/edit-propagation.test.ts +280 -0
  126. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  127. package/src/__tests__/ephemeral-permissions.test.ts +93 -3
  128. package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
  129. package/src/__tests__/estimator-calibration.test.ts +213 -0
  130. package/src/__tests__/extension-id-sync-guard.test.ts +29 -10
  131. package/src/__tests__/file-write-tool.test.ts +151 -1
  132. package/src/__tests__/filing-service.test.ts +255 -0
  133. package/src/__tests__/first-greeting.test.ts +247 -5
  134. package/src/__tests__/gemini-provider.test.ts +0 -3
  135. package/src/__tests__/guardian-grant-minting.test.ts +8 -0
  136. package/src/__tests__/headless-browser-interactions.test.ts +1 -1
  137. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  138. package/src/__tests__/heartbeat-service.test.ts +96 -15
  139. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  140. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  141. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  142. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  143. package/src/__tests__/host-shell-tool.test.ts +124 -18
  144. package/src/__tests__/http-user-message-parity.test.ts +29 -1
  145. package/src/__tests__/image-credentials.test.ts +137 -0
  146. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  147. package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
  148. package/src/__tests__/injector-chain.test.ts +526 -0
  149. package/src/__tests__/intent-routing.test.ts +1 -66
  150. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  151. package/src/__tests__/llm-catalog-parity.test.ts +174 -0
  152. package/src/__tests__/llm-context-normalization.test.ts +121 -0
  153. package/src/__tests__/llm-resolver.test.ts +214 -0
  154. package/src/__tests__/llm-schema.test.ts +223 -0
  155. package/src/__tests__/managed-proxy-context.test.ts +6 -2
  156. package/src/__tests__/media-generate-image.test.ts +119 -13
  157. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  158. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  159. package/src/__tests__/messaging-skill-split.test.ts +3 -34
  160. package/src/__tests__/migration-import-from-url.test.ts +621 -0
  161. package/src/__tests__/model-intents.test.ts +11 -83
  162. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  163. package/src/__tests__/notification-decision-fallback.test.ts +0 -10
  164. package/src/__tests__/notification-decision-identity.test.ts +0 -9
  165. package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
  166. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  167. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  168. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  169. package/src/__tests__/oauth-cli.test.ts +14 -12
  170. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  171. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  172. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  173. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  174. package/src/__tests__/oauth-store.test.ts +46 -78
  175. package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
  176. package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
  177. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  178. package/src/__tests__/openai-image-service.test.ts +368 -0
  179. package/src/__tests__/openai-provider.test.ts +7 -0
  180. package/src/__tests__/openai-responses-provider.test.ts +396 -0
  181. package/src/__tests__/openrouter-provider-only.test.ts +135 -0
  182. package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
  183. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  184. package/src/__tests__/permission-checker-host-gate.test.ts +1 -25
  185. package/src/__tests__/permission-mode.test.ts +16 -0
  186. package/src/__tests__/permission-types.test.ts +0 -1
  187. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  188. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  189. package/src/__tests__/persona-resolver.test.ts +13 -13
  190. package/src/__tests__/pipeline-runner.test.ts +565 -0
  191. package/src/__tests__/pkb-autoinject.test.ts +37 -1
  192. package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
  193. package/src/__tests__/platform.test.ts +5 -2
  194. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  195. package/src/__tests__/plugin-registry.test.ts +273 -0
  196. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  197. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  198. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  199. package/src/__tests__/plugin-types.test.ts +320 -0
  200. package/src/__tests__/pricing.test.ts +93 -14
  201. package/src/__tests__/profiler-routes.test.ts +1 -1
  202. package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
  203. package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
  204. package/src/__tests__/provider-error-scenarios.test.ts +135 -6
  205. package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
  206. package/src/__tests__/provider-registry-ollama.test.ts +1 -2
  207. package/src/__tests__/proxy-approval-callback.test.ts +69 -9
  208. package/src/__tests__/reaction-persistence.test.ts +561 -0
  209. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  210. package/src/__tests__/registry.test.ts +0 -2
  211. package/src/__tests__/relay-server.test.ts +1 -1
  212. package/src/__tests__/require-fresh-approval.test.ts +1 -1
  213. package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
  214. package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
  215. package/src/__tests__/risk-classifier-parity.test.ts +230 -0
  216. package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
  217. package/src/__tests__/schedule-routes.test.ts +131 -1
  218. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  219. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  220. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  221. package/src/__tests__/secret-ingress-http.test.ts +28 -0
  222. package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
  223. package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
  224. package/src/__tests__/secret-scanner-executor.test.ts +1 -1
  225. package/src/__tests__/send-endpoint-busy.test.ts +29 -1
  226. package/src/__tests__/server-history-render.test.ts +31 -0
  227. package/src/__tests__/shell-identity.test.ts +0 -134
  228. package/src/__tests__/shell-parser-property.test.ts +13 -13
  229. package/src/__tests__/skill-cache-store.test.ts +182 -0
  230. package/src/__tests__/skills.test.ts +19 -33
  231. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  232. package/src/__tests__/slack-skill.test.ts +3 -8
  233. package/src/__tests__/starter-bundle.test.ts +35 -0
  234. package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
  235. package/src/__tests__/suggestion-routes.test.ts +259 -3
  236. package/src/__tests__/system-prompt.test.ts +22 -35
  237. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  238. package/src/__tests__/task-runner.test.ts +3 -1
  239. package/src/__tests__/task-scheduler.test.ts +3 -15
  240. package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
  241. package/src/__tests__/terminal-tools.test.ts +8 -0
  242. package/src/__tests__/test-preload.ts +11 -0
  243. package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
  244. package/src/__tests__/thread-backfill.test.ts +941 -0
  245. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  246. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  247. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  248. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  249. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -8
  250. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
  251. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  252. package/src/__tests__/tool-executor.test.ts +201 -94
  253. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  254. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  255. package/src/__tests__/trust-store.test.ts +442 -109
  256. package/src/__tests__/update-bulletin-job.test.ts +389 -0
  257. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
  258. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  259. package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
  260. package/src/__tests__/voice-session-bridge.test.ts +39 -0
  261. package/src/__tests__/volume-security-guard.test.ts +3 -2
  262. package/src/__tests__/web-search-history.test.ts +337 -0
  263. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
  264. package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
  265. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
  266. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  267. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  268. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  269. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  270. package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
  271. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  272. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
  273. package/src/__tests__/workspace-policy.test.ts +22 -16
  274. package/src/acp/client-handler.ts +1 -2
  275. package/src/agent/loop.ts +545 -115
  276. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  277. package/src/approvals/guardian-request-resolvers.ts +80 -0
  278. package/src/avatar/resvg-lazy.test.ts +136 -0
  279. package/src/avatar/resvg-lazy.ts +82 -9
  280. package/src/avatar/traits-png-sync.ts +21 -1
  281. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  282. package/src/backup/backup-worker.ts +3 -15
  283. package/src/browser/__tests__/operations.test.ts +163 -0
  284. package/src/browser/identifiers.ts +51 -0
  285. package/src/browser/operations.ts +660 -0
  286. package/src/browser/types.ts +81 -0
  287. package/src/bundler/app-compiler.ts +84 -1
  288. package/src/calls/call-state.ts +2 -2
  289. package/src/calls/guardian-question-copy.ts +2 -2
  290. package/src/calls/telephony-stt-routing.ts +1 -1
  291. package/src/calls/voice-session-bridge.ts +1 -0
  292. package/src/channels/__tests__/types.test.ts +3 -3
  293. package/src/channels/types.ts +6 -4
  294. package/src/cli/AGENTS.md +1 -1
  295. package/src/cli/__tests__/notifications.test.ts +87 -211
  296. package/src/cli/commands/__tests__/attachment.test.ts +438 -0
  297. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  298. package/src/cli/commands/__tests__/browser.test.ts +554 -0
  299. package/src/cli/commands/__tests__/cache.test.ts +623 -0
  300. package/src/cli/commands/__tests__/email-list.test.ts +6 -0
  301. package/src/cli/commands/__tests__/email-send.test.ts +93 -1
  302. package/src/cli/commands/__tests__/image-generation.test.ts +886 -0
  303. package/src/cli/commands/__tests__/inference-send.test.ts +463 -0
  304. package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
  305. package/src/cli/commands/__tests__/task.test.ts +913 -0
  306. package/src/cli/commands/__tests__/tts-synthesize.test.ts +606 -0
  307. package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
  308. package/src/cli/commands/__tests__/ui.test.ts +1215 -0
  309. package/src/cli/commands/__tests__/watchers.test.ts +716 -0
  310. package/src/cli/commands/attachment.ts +182 -0
  311. package/src/cli/commands/backup.ts +2 -2
  312. package/src/cli/commands/browser.ts +350 -0
  313. package/src/cli/commands/cache.ts +341 -0
  314. package/src/cli/commands/clients.ts +138 -0
  315. package/src/cli/commands/completions.ts +2 -12
  316. package/src/cli/commands/config.ts +6 -6
  317. package/src/cli/commands/conversations-import.ts +347 -0
  318. package/src/cli/commands/conversations.ts +69 -8
  319. package/src/cli/commands/email.ts +234 -194
  320. package/src/cli/commands/image-generation.ts +299 -0
  321. package/src/cli/commands/inference.ts +200 -0
  322. package/src/cli/commands/memory.ts +127 -17
  323. package/src/cli/commands/notifications.ts +68 -103
  324. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  325. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  326. package/src/cli/commands/oauth/connect.ts +2 -2
  327. package/src/cli/commands/oauth/providers.ts +176 -8
  328. package/src/cli/commands/oauth/status.ts +46 -36
  329. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  330. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
  331. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
  332. package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
  333. package/src/cli/commands/skills.ts +3 -4
  334. package/src/cli/commands/stt.ts +339 -0
  335. package/src/cli/commands/task.ts +795 -0
  336. package/src/cli/commands/trust.ts +50 -19
  337. package/src/cli/commands/tts.ts +273 -0
  338. package/src/cli/commands/ui.ts +670 -0
  339. package/src/cli/commands/watchers.ts +509 -0
  340. package/src/cli/lib/daemon-credential-client.ts +0 -19
  341. package/src/cli/program.ts +39 -24
  342. package/src/cli.ts +0 -37
  343. package/src/config/__tests__/backup-schema.test.ts +7 -2
  344. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  345. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  346. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  347. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  348. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  349. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  350. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  351. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  352. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  353. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  354. package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
  355. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  356. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +20 -1
  357. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
  358. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
  359. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +69 -12
  360. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
  361. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  362. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  363. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  364. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  365. package/src/config/bundled-tool-registry.ts +0 -190
  366. package/src/config/env.ts +7 -2
  367. package/src/config/feature-flag-registry.json +42 -10
  368. package/src/config/llm-resolver.ts +128 -0
  369. package/src/config/loader.ts +194 -10
  370. package/src/config/raw-config-utils.ts +30 -2
  371. package/src/config/sanitize-for-transfer.ts +35 -0
  372. package/src/config/schema.ts +49 -41
  373. package/src/config/schemas/analysis.ts +3 -22
  374. package/src/config/schemas/backup.ts +1 -1
  375. package/src/config/schemas/calls.ts +0 -4
  376. package/src/config/schemas/conversations.ts +16 -0
  377. package/src/config/schemas/filing.ts +2 -7
  378. package/src/config/schemas/heartbeat.ts +0 -5
  379. package/src/config/schemas/inference.ts +3 -23
  380. package/src/config/schemas/llm.ts +317 -0
  381. package/src/config/schemas/memory-processing.ts +1 -9
  382. package/src/config/schemas/notifications.ts +4 -11
  383. package/src/config/schemas/platform.ts +3 -9
  384. package/src/config/schemas/security.ts +33 -0
  385. package/src/config/schemas/services.ts +9 -4
  386. package/src/config/schemas/stt.ts +1 -0
  387. package/src/config/schemas/tts.ts +64 -0
  388. package/src/config/schemas/updates.ts +1 -1
  389. package/src/config/schemas/workspace-git.ts +3 -40
  390. package/src/config/skill-state.ts +6 -2
  391. package/src/config/skills.ts +96 -7
  392. package/src/context/__tests__/compact-prompt.test.ts +63 -0
  393. package/src/context/__tests__/microcompact.test.ts +805 -0
  394. package/src/context/estimator-calibration.ts +136 -0
  395. package/src/context/microcompact.ts +443 -0
  396. package/src/context/prompts/compact.md +26 -0
  397. package/src/context/token-estimator.ts +61 -3
  398. package/src/context/tool-result-truncation.ts +3 -63
  399. package/src/context/window-manager.ts +417 -39
  400. package/src/credential-execution/approval-bridge.ts +0 -1
  401. package/src/credential-execution/executable-discovery.ts +19 -8
  402. package/src/credential-execution/process-manager.test.ts +109 -0
  403. package/src/credential-execution/process-manager.ts +65 -2
  404. package/src/credential-health/credential-health-service.ts +19 -6
  405. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  406. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  407. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  408. package/src/daemon/approval-generators.ts +29 -4
  409. package/src/daemon/assistant-attachments.ts +24 -13
  410. package/src/daemon/classifier.ts +2 -2
  411. package/src/daemon/config-watcher.ts +0 -3
  412. package/src/daemon/context-overflow-policy.ts +4 -13
  413. package/src/daemon/context-overflow-reducer.ts +4 -1
  414. package/src/daemon/conversation-agent-loop-handlers.ts +162 -34
  415. package/src/daemon/conversation-agent-loop.ts +1282 -599
  416. package/src/daemon/conversation-attachments.ts +2 -6
  417. package/src/daemon/conversation-error.ts +36 -1
  418. package/src/daemon/conversation-history.ts +10 -19
  419. package/src/daemon/conversation-lifecycle.ts +59 -17
  420. package/src/daemon/conversation-messaging.ts +73 -4
  421. package/src/daemon/conversation-notifiers.ts +2 -110
  422. package/src/daemon/conversation-process.ts +24 -11
  423. package/src/daemon/conversation-queue-manager.ts +3 -0
  424. package/src/daemon/conversation-runtime-assembly.ts +1063 -211
  425. package/src/daemon/conversation-slash.ts +2 -2
  426. package/src/daemon/conversation-surfaces.ts +389 -1
  427. package/src/daemon/conversation-tool-setup.ts +51 -9
  428. package/src/daemon/conversation-usage.ts +1 -1
  429. package/src/daemon/conversation.ts +197 -64
  430. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  431. package/src/daemon/external-skills-bootstrap.ts +41 -0
  432. package/src/daemon/first-greeting.ts +191 -14
  433. package/src/daemon/guardian-action-generators.ts +34 -14
  434. package/src/daemon/handlers/config-model.test.ts +86 -0
  435. package/src/daemon/handlers/config-model.ts +65 -12
  436. package/src/daemon/handlers/conversations.ts +9 -2
  437. package/src/daemon/handlers/shared.ts +39 -11
  438. package/src/daemon/handlers/skills.ts +7 -3
  439. package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
  440. package/src/daemon/lifecycle.ts +109 -82
  441. package/src/daemon/message-types/computer-use.ts +2 -34
  442. package/src/daemon/message-types/conversations.ts +63 -0
  443. package/src/daemon/message-types/messages.ts +21 -1
  444. package/src/daemon/message-types/trust.ts +0 -2
  445. package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
  446. package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
  447. package/src/daemon/pkb-context-tracker.test.ts +169 -0
  448. package/src/daemon/pkb-context-tracker.ts +125 -0
  449. package/src/daemon/pkb-reminder-builder.test.ts +70 -0
  450. package/src/daemon/pkb-reminder-builder.ts +31 -0
  451. package/src/daemon/providers-setup.ts +6 -0
  452. package/src/daemon/server.ts +122 -12
  453. package/src/daemon/shutdown-handlers.ts +2 -12
  454. package/src/daemon/tool-side-effects.ts +14 -65
  455. package/src/daemon/web-search-history.ts +126 -0
  456. package/src/events/domain-events.ts +0 -1
  457. package/src/filing/filing-service.ts +9 -10
  458. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  459. package/src/heartbeat/heartbeat-service.ts +99 -28
  460. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  461. package/src/home/__tests__/feed-scheduler.test.ts +39 -11
  462. package/src/home/__tests__/rollup-producer.test.ts +44 -0
  463. package/src/home/assistant-feed-authoring.ts +4 -0
  464. package/src/home/emit-feed-event.ts +11 -0
  465. package/src/home/feed-scheduler.ts +20 -4
  466. package/src/home/feed-types.ts +97 -4
  467. package/src/home/relationship-state-writer.ts +2 -2
  468. package/src/home/rewrite-command-preview.ts +66 -0
  469. package/src/home/rollup-producer.ts +34 -5
  470. package/src/home/suggested-prompts.ts +101 -0
  471. package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
  472. package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
  473. package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
  474. package/src/ipc/__tests__/socket-path.test.ts +34 -0
  475. package/src/ipc/__tests__/task-ipc.test.ts +577 -0
  476. package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
  477. package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
  478. package/src/ipc/cli-client.ts +2 -1
  479. package/src/ipc/cli-server.ts +26 -8
  480. package/src/ipc/gateway-client.ts +6 -3
  481. package/src/ipc/routes/attachment.ts +114 -0
  482. package/src/ipc/routes/browser-context.ts +63 -0
  483. package/src/ipc/routes/browser.ts +97 -0
  484. package/src/ipc/routes/cache.ts +96 -0
  485. package/src/ipc/routes/get-contact.ts +16 -0
  486. package/src/ipc/routes/index.ts +31 -1
  487. package/src/ipc/routes/list-clients.ts +31 -0
  488. package/src/ipc/routes/merge-contacts.ts +17 -0
  489. package/src/ipc/routes/notification.ts +133 -0
  490. package/src/ipc/routes/rename-conversation.ts +59 -0
  491. package/src/ipc/routes/search-contacts.ts +19 -0
  492. package/src/ipc/routes/task-queue.ts +226 -0
  493. package/src/ipc/routes/task.ts +173 -0
  494. package/src/ipc/routes/ui-request.ts +50 -0
  495. package/src/ipc/routes/upsert-contact.ts +25 -0
  496. package/src/ipc/routes/watcher.ts +203 -0
  497. package/src/ipc/socket-path.ts +76 -0
  498. package/src/media/app-icon-generator.ts +23 -46
  499. package/src/media/avatar-router.ts +26 -41
  500. package/src/media/gemini-image-service.ts +8 -41
  501. package/src/media/image-credentials.ts +73 -0
  502. package/src/media/image-service.ts +85 -0
  503. package/src/media/openai-image-service.ts +131 -0
  504. package/src/media/types.ts +46 -0
  505. package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
  506. package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
  507. package/src/memory/admin.ts +18 -0
  508. package/src/memory/conversation-analyze-job.ts +14 -13
  509. package/src/memory/conversation-attention-store.ts +13 -6
  510. package/src/memory/conversation-crud.ts +133 -3
  511. package/src/memory/conversation-group-migration.ts +38 -6
  512. package/src/memory/conversation-queries.ts +57 -4
  513. package/src/memory/conversation-title-service.ts +32 -4
  514. package/src/memory/db-init.ts +10 -0
  515. package/src/memory/embedding-backend.ts +1 -1
  516. package/src/memory/embedding-gemini.test.ts +41 -2
  517. package/src/memory/embedding-gemini.ts +6 -1
  518. package/src/memory/graph/bootstrap.test.ts +282 -0
  519. package/src/memory/graph/bootstrap.ts +8 -5
  520. package/src/memory/graph/compaction.ts +299 -0
  521. package/src/memory/graph/consolidation.ts +4 -4
  522. package/src/memory/graph/conversation-graph-memory.ts +89 -29
  523. package/src/memory/graph/extraction.test.ts +272 -2
  524. package/src/memory/graph/extraction.ts +183 -53
  525. package/src/memory/graph/graph-search.test.ts +93 -0
  526. package/src/memory/graph/graph-search.ts +4 -1
  527. package/src/memory/graph/inspect.ts +2 -2
  528. package/src/memory/graph/narrative.ts +2 -2
  529. package/src/memory/graph/pattern-scan.ts +2 -2
  530. package/src/memory/graph/retriever.test.ts +459 -0
  531. package/src/memory/graph/retriever.ts +237 -48
  532. package/src/memory/graph/store.ts +41 -0
  533. package/src/memory/graph/tool-handlers.ts +27 -0
  534. package/src/memory/graph/tools.ts +6 -1
  535. package/src/memory/indexer.ts +5 -5
  536. package/src/memory/job-handlers/conversation-starters.ts +23 -20
  537. package/src/memory/job-handlers/summarization.ts +2 -2
  538. package/src/memory/job-utils.ts +7 -1
  539. package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
  540. package/src/memory/jobs/embed-pkb-file.ts +54 -0
  541. package/src/memory/jobs-store.ts +44 -3
  542. package/src/memory/jobs-worker.ts +4 -0
  543. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  544. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
  545. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  546. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
  547. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
  548. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  549. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  550. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  551. package/src/memory/migrations/index.ts +5 -0
  552. package/src/memory/pkb/pkb-index.test.ts +369 -0
  553. package/src/memory/pkb/pkb-index.ts +255 -0
  554. package/src/memory/pkb/pkb-reconcile.test.ts +252 -0
  555. package/src/memory/pkb/pkb-reconcile.ts +148 -0
  556. package/src/memory/pkb/pkb-search.test.ts +499 -0
  557. package/src/memory/pkb/pkb-search.ts +159 -0
  558. package/src/memory/pkb/types.ts +53 -0
  559. package/src/memory/qdrant-client.test.ts +60 -0
  560. package/src/memory/qdrant-client.ts +147 -1
  561. package/src/memory/schema/infrastructure.ts +1 -0
  562. package/src/memory/schema/oauth.ts +4 -1
  563. package/src/memory/slack-thread-store.ts +37 -0
  564. package/src/messaging/providers/gmail/adapter.ts +6 -16
  565. package/src/messaging/providers/gmail/client.ts +22 -0
  566. package/src/messaging/providers/gmail/types.ts +7 -0
  567. package/src/messaging/providers/slack/adapter.ts +14 -2
  568. package/src/messaging/providers/slack/backfill.test.ts +257 -0
  569. package/src/messaging/providers/slack/backfill.ts +101 -0
  570. package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
  571. package/src/messaging/providers/slack/message-metadata.ts +123 -0
  572. package/src/messaging/providers/slack/render-transcript.test.ts +1421 -0
  573. package/src/messaging/providers/slack/render-transcript.ts +501 -0
  574. package/src/messaging/style-analyzer.ts +5 -2
  575. package/src/notifications/README.md +9 -5
  576. package/src/notifications/conversation-pairing.ts +78 -19
  577. package/src/notifications/copy-composer.ts +0 -5
  578. package/src/notifications/decision-engine.ts +3 -9
  579. package/src/notifications/emit-signal.ts +1 -1
  580. package/src/notifications/preference-extractor.ts +2 -6
  581. package/src/notifications/signal.ts +1 -2
  582. package/src/oauth/AGENTS.md +1 -1
  583. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  584. package/src/oauth/connect-orchestrator.ts +8 -34
  585. package/src/oauth/connect-types.ts +6 -10
  586. package/src/oauth/manual-token-connection.ts +23 -0
  587. package/src/oauth/oauth-store.ts +31 -14
  588. package/src/oauth/platform-connection.test.ts +47 -0
  589. package/src/oauth/platform-connection.ts +15 -5
  590. package/src/oauth/provider-serializer.ts +6 -1
  591. package/src/oauth/seed-providers.ts +56 -106
  592. package/src/outbound-proxy/http-forwarder.ts +9 -0
  593. package/src/permissions/approval-policy.test.ts +1223 -0
  594. package/src/permissions/approval-policy.ts +309 -0
  595. package/src/permissions/arg-parser.test.ts +161 -0
  596. package/src/permissions/arg-parser.ts +141 -0
  597. package/src/permissions/bash-risk-classifier.test.ts +1620 -0
  598. package/src/permissions/bash-risk-classifier.ts +950 -0
  599. package/src/permissions/checker.ts +348 -711
  600. package/src/permissions/command-registry.test.ts +774 -0
  601. package/src/permissions/command-registry.ts +1005 -0
  602. package/src/permissions/defaults.ts +28 -79
  603. package/src/permissions/file-risk-classifier.test.ts +535 -0
  604. package/src/permissions/file-risk-classifier.ts +274 -0
  605. package/src/permissions/gateway-threshold-reader.ts +196 -0
  606. package/src/permissions/prompter.ts +4 -0
  607. package/src/permissions/risk-types.ts +262 -0
  608. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  609. package/src/permissions/schedule-risk-classifier.ts +85 -0
  610. package/src/permissions/secret-prompter.ts +53 -2
  611. package/src/permissions/shell-identity.ts +2 -42
  612. package/src/permissions/skill-risk-classifier.test.ts +311 -0
  613. package/src/permissions/skill-risk-classifier.ts +214 -0
  614. package/src/permissions/trust-client.ts +52 -25
  615. package/src/permissions/trust-store-interface.ts +1 -6
  616. package/src/permissions/trust-store.ts +161 -62
  617. package/src/permissions/types.ts +25 -14
  618. package/src/permissions/web-risk-classifier.test.ts +170 -0
  619. package/src/permissions/web-risk-classifier.ts +89 -0
  620. package/src/permissions/workspace-policy.ts +9 -19
  621. package/src/platform/client.ts +19 -1
  622. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  623. package/src/plugins/defaults/compaction.ts +145 -0
  624. package/src/plugins/defaults/empty-response.ts +126 -0
  625. package/src/plugins/defaults/history-repair.ts +85 -0
  626. package/src/plugins/defaults/index.ts +116 -0
  627. package/src/plugins/defaults/injectors.ts +491 -0
  628. package/src/plugins/defaults/llm-call.ts +82 -0
  629. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  630. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  631. package/src/plugins/defaults/persistence.ts +129 -0
  632. package/src/plugins/defaults/title-generate.ts +95 -0
  633. package/src/plugins/defaults/token-estimate.ts +104 -0
  634. package/src/plugins/defaults/tool-error.ts +126 -0
  635. package/src/plugins/defaults/tool-execute.ts +89 -0
  636. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  637. package/src/plugins/pipeline.ts +316 -0
  638. package/src/plugins/plugin-skill-contributions.ts +292 -0
  639. package/src/plugins/registry.ts +241 -0
  640. package/src/plugins/types.ts +1134 -0
  641. package/src/plugins/user-loader.ts +177 -0
  642. package/src/prompts/persona-resolver.ts +3 -3
  643. package/src/prompts/system-prompt.ts +19 -20
  644. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  645. package/src/prompts/templates/SOUL.md +2 -2
  646. package/src/prompts/update-bulletin-job.ts +190 -0
  647. package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
  648. package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
  649. package/src/providers/__tests__/retry-callsite.test.ts +424 -0
  650. package/src/providers/anthropic/client.ts +183 -14
  651. package/src/providers/call-site-routing.ts +71 -0
  652. package/src/providers/gemini/client.ts +65 -2
  653. package/src/providers/managed-proxy/constants.ts +2 -1
  654. package/src/providers/model-catalog.ts +524 -33
  655. package/src/providers/model-intents.ts +4 -4
  656. package/src/providers/openai/chat-completions-provider.ts +57 -1
  657. package/src/providers/openai/responses-provider.ts +86 -9
  658. package/src/providers/openrouter/client.ts +80 -9
  659. package/src/providers/provider-env-vars.ts +56 -0
  660. package/src/providers/provider-send-message.ts +22 -5
  661. package/src/providers/ratelimit.ts +4 -0
  662. package/src/providers/registry.ts +19 -8
  663. package/src/providers/retry.ts +174 -39
  664. package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
  665. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  666. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  667. package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
  668. package/src/providers/speech-to-text/provider-catalog.ts +17 -0
  669. package/src/providers/speech-to-text/resolve.ts +7 -0
  670. package/src/providers/speech-to-text/xai-realtime.test.ts +646 -0
  671. package/src/providers/speech-to-text/xai-realtime.ts +821 -0
  672. package/src/providers/speech-to-text/xai.test.ts +155 -0
  673. package/src/providers/speech-to-text/xai.ts +97 -0
  674. package/src/providers/types.ts +93 -3
  675. package/src/runtime/AGENTS.md +27 -18
  676. package/src/runtime/__tests__/agent-wake.test.ts +43 -2
  677. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  678. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  679. package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
  680. package/src/runtime/agent-wake.ts +63 -22
  681. package/src/runtime/auth/route-policy.ts +4 -0
  682. package/src/runtime/btw-sidechain.ts +13 -3
  683. package/src/runtime/channel-reply-delivery.ts +106 -2
  684. package/src/runtime/client-registry.ts +261 -0
  685. package/src/runtime/decision-token.ts +116 -0
  686. package/src/runtime/gateway-client.ts +2 -2
  687. package/src/runtime/http-router.ts +32 -0
  688. package/src/runtime/http-server.ts +129 -9
  689. package/src/runtime/http-types.ts +23 -3
  690. package/src/runtime/interactive-ui.ts +362 -0
  691. package/src/runtime/invite-instruction-generator.ts +2 -2
  692. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
  693. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
  694. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
  695. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
  696. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
  697. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
  698. package/src/runtime/migrations/gcs-signed-url.ts +162 -0
  699. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  700. package/src/runtime/migrations/vbundle-importer.ts +154 -9
  701. package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
  702. package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
  703. package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
  704. package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
  705. package/src/runtime/migrations/vbundle-validator.ts +15 -6
  706. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
  707. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
  708. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
  709. package/src/runtime/routes/approval-prompt-ts-tracker.ts +78 -0
  710. package/src/runtime/routes/approval-routes.ts +29 -17
  711. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
  712. package/src/runtime/routes/avatar-routes.ts +20 -4
  713. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  714. package/src/runtime/routes/btw-routes.ts +1 -4
  715. package/src/runtime/routes/conversation-management-routes.ts +20 -2
  716. package/src/runtime/routes/conversation-routes.ts +351 -138
  717. package/src/runtime/routes/debug-routes.ts +1 -1
  718. package/src/runtime/routes/diagnostics-routes.ts +6 -4
  719. package/src/runtime/routes/events-routes.ts +16 -0
  720. package/src/runtime/routes/guardian-approval-interception.ts +33 -3
  721. package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
  722. package/src/runtime/routes/home-feed-routes.ts +120 -2
  723. package/src/runtime/routes/inbound-message-handler.ts +987 -2
  724. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
  725. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
  726. package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
  727. package/src/runtime/routes/integrations/slack/channel.ts +25 -3
  728. package/src/runtime/routes/llm-context-normalization.ts +23 -1
  729. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  730. package/src/runtime/routes/migration-routes.ts +720 -127
  731. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  732. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  733. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  734. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  735. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  736. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  737. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  738. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  739. package/src/runtime/routes/playground/deps.ts +56 -0
  740. package/src/runtime/routes/playground/force-compact.ts +73 -0
  741. package/src/runtime/routes/playground/guard.ts +37 -0
  742. package/src/runtime/routes/playground/index.ts +28 -0
  743. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  744. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  745. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  746. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  747. package/src/runtime/routes/playground/state.ts +78 -0
  748. package/src/runtime/routes/schedule-routes.ts +89 -8
  749. package/src/runtime/routes/settings-routes.ts +4 -2
  750. package/src/runtime/routes/trust-rules-routes.ts +30 -14
  751. package/src/runtime/routes/work-items-routes.test.ts +1 -1
  752. package/src/runtime/routes/work-items-routes.ts +3 -2
  753. package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
  754. package/src/runtime/services/analyze-conversation.ts +12 -16
  755. package/src/runtime/skill-route-registry.ts +97 -15
  756. package/src/schedule/run-script.ts +68 -0
  757. package/src/schedule/schedule-store.ts +7 -1
  758. package/src/schedule/scheduler.ts +56 -8
  759. package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
  760. package/src/security/__tests__/untrusted-content.test.ts +109 -0
  761. package/src/security/oauth2.ts +98 -35
  762. package/src/security/secure-keys.ts +7 -8
  763. package/src/security/token-manager.ts +27 -13
  764. package/src/security/untrusted-content.ts +102 -0
  765. package/src/skills/catalog-cache.ts +35 -9
  766. package/src/skills/catalog-install.ts +31 -3
  767. package/src/skills/skill-cache-store.ts +97 -0
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
  769. package/src/stt/daemon-batch-transcriber.ts +33 -0
  770. package/src/stt/stt-stream-session.ts +8 -1
  771. package/src/stt/types.ts +5 -1
  772. package/src/subagent/manager.ts +41 -13
  773. package/src/tasks/ephemeral-permissions.ts +9 -4
  774. package/src/telemetry/usage-telemetry-reporter.ts +27 -5
  775. package/src/tools/browser/__tests__/browser-status.test.ts +234 -2
  776. package/src/tools/browser/browser-execution.ts +150 -54
  777. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  778. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  779. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
  780. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  781. package/src/tools/browser/cdp-client/factory.ts +15 -4
  782. package/src/tools/credentials/tool-policy.ts +39 -5
  783. package/src/tools/credentials/vault.ts +9 -4
  784. package/src/tools/executor.ts +129 -73
  785. package/src/tools/filesystem/write.ts +52 -0
  786. package/src/tools/host-terminal/host-shell.ts +45 -5
  787. package/src/tools/memory/register.test.ts +185 -0
  788. package/src/tools/memory/register.ts +3 -1
  789. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  790. package/src/tools/network/web-fetch.ts +20 -10
  791. package/src/tools/network/web-search.ts +19 -4
  792. package/src/tools/permission-checker.ts +116 -46
  793. package/src/tools/policy-context.ts +29 -8
  794. package/src/tools/registry.ts +195 -6
  795. package/src/tools/schedule/create.ts +23 -8
  796. package/src/tools/schedule/update.ts +3 -1
  797. package/src/tools/secret-detection-handler.ts +0 -51
  798. package/src/tools/side-effects.ts +0 -11
  799. package/src/tools/skills/execute.ts +2 -2
  800. package/src/tools/skills/sandbox-runner.ts +5 -2
  801. package/src/tools/system/avatar-generator.ts +6 -2
  802. package/src/tools/terminal/backends/native.ts +51 -2
  803. package/src/tools/terminal/safe-env.ts +3 -2
  804. package/src/tools/terminal/shell.ts +1 -0
  805. package/src/tools/tool-manifest.ts +6 -21
  806. package/src/tools/types.ts +40 -5
  807. package/src/tools/verification-control-plane-policy.ts +1 -1
  808. package/src/tts/__tests__/provider-adapters.test.ts +240 -13
  809. package/src/tts/provider-catalog.ts +18 -0
  810. package/src/tts/providers/index.ts +2 -0
  811. package/src/tts/providers/xai-provider.ts +224 -0
  812. package/src/tts/types.ts +46 -0
  813. package/src/types/tar-stream.d.ts +66 -0
  814. package/src/util/json.ts +17 -0
  815. package/src/util/platform.ts +9 -4
  816. package/src/util/pricing.ts +41 -8
  817. package/src/watcher/engine.ts +1 -1
  818. package/src/watcher/providers/google-calendar.ts +134 -8
  819. package/src/watcher/providers/outlook-calendar.ts +42 -2
  820. package/src/workspace/git-service.ts +23 -4
  821. package/src/workspace/migrations/006-services-config.ts +2 -4
  822. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  823. package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
  824. package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
  825. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
  826. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +56 -0
  827. package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
  828. package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
  829. package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
  830. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
  831. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  832. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  833. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  834. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  835. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  836. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  837. package/src/workspace/migrations/AGENTS.md +1 -1
  838. package/src/workspace/migrations/registry.ts +28 -0
  839. package/src/workspace/provider-commit-message-generator.ts +19 -38
  840. package/tsconfig.json +1 -1
  841. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  842. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  843. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  844. package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
  845. package/src/__tests__/gmail-archive-gate.test.ts +0 -246
  846. package/src/__tests__/gmail-preferences.test.ts +0 -117
  847. package/src/__tests__/hooks-blocking.test.ts +0 -178
  848. package/src/__tests__/hooks-cli.test.ts +0 -182
  849. package/src/__tests__/hooks-config.test.ts +0 -108
  850. package/src/__tests__/hooks-discovery.test.ts +0 -211
  851. package/src/__tests__/hooks-integration.test.ts +0 -196
  852. package/src/__tests__/hooks-manager.test.ts +0 -226
  853. package/src/__tests__/hooks-runner.test.ts +0 -175
  854. package/src/__tests__/hooks-settings.test.ts +0 -160
  855. package/src/__tests__/hooks-templates.test.ts +0 -169
  856. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  857. package/src/__tests__/hooks-watch.test.ts +0 -112
  858. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  859. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  860. package/src/__tests__/outlook-attachments.test.ts +0 -301
  861. package/src/__tests__/outlook-automation-tools.test.ts +0 -425
  862. package/src/__tests__/outlook-categories.test.ts +0 -212
  863. package/src/__tests__/outlook-compose-tools.test.ts +0 -325
  864. package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
  865. package/src/__tests__/outlook-follow-up.test.ts +0 -196
  866. package/src/__tests__/outlook-trash.test.ts +0 -77
  867. package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
  868. package/src/__tests__/send-notification-tool.test.ts +0 -83
  869. package/src/__tests__/update-bulletin-format.test.ts +0 -181
  870. package/src/__tests__/update-bulletin-state.test.ts +0 -135
  871. package/src/__tests__/update-bulletin.test.ts +0 -478
  872. package/src/__tests__/update-template-contract.test.ts +0 -29
  873. package/src/cli/commands/doctor.ts +0 -341
  874. package/src/cli/commands/shotgun.ts +0 -266
  875. package/src/config/bundled-skills/browser/SKILL.md +0 -88
  876. package/src/config/bundled-skills/browser/TOOLS.json +0 -516
  877. package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
  878. package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
  879. package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
  880. package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
  881. package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
  882. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
  883. package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
  884. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
  885. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
  886. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
  887. package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
  888. package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
  889. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
  890. package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
  891. package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
  892. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
  893. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
  894. package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
  895. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
  896. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
  897. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  898. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  899. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -66
  900. package/src/config/bundled-skills/gmail/SKILL.md +0 -221
  901. package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
  902. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
  903. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
  904. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
  905. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
  906. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
  907. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
  908. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
  909. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
  910. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
  911. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
  912. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
  913. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
  914. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
  915. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
  916. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
  917. package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
  918. package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
  919. package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
  920. package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
  921. package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
  922. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
  923. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
  924. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
  925. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
  926. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
  927. package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
  928. package/src/config/bundled-skills/google-calendar/types.ts +0 -97
  929. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  930. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  931. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  932. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  933. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  934. package/src/config/bundled-skills/outlook/SKILL.md +0 -196
  935. package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
  936. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
  937. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
  938. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
  939. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
  940. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
  941. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
  942. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
  943. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
  944. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
  945. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
  946. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
  947. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
  948. package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
  949. package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
  950. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
  951. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
  952. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
  953. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
  954. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
  955. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
  956. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
  957. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
  958. package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
  959. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  960. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  961. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  962. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  963. package/src/config/bundled-skills/slack/SKILL.md +0 -108
  964. package/src/config/bundled-skills/tasks/SKILL.md +0 -37
  965. package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
  966. package/src/config/bundled-skills/tasks/icon.svg +0 -34
  967. package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
  968. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
  969. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
  970. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
  971. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
  972. package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
  973. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
  974. package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
  975. package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
  976. package/src/config/bundled-skills/watcher/SKILL.md +0 -31
  977. package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
  978. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
  979. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
  980. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
  981. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
  982. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
  983. package/src/daemon/context-overflow-approval.ts +0 -52
  984. package/src/daemon/watch-handler.ts +0 -399
  985. package/src/hooks/cli.ts +0 -253
  986. package/src/hooks/config.ts +0 -100
  987. package/src/hooks/discovery.ts +0 -135
  988. package/src/hooks/manager.ts +0 -179
  989. package/src/hooks/runner.ts +0 -117
  990. package/src/hooks/templates.ts +0 -77
  991. package/src/hooks/types.ts +0 -75
  992. package/src/oauth/scope-policy.ts +0 -89
  993. package/src/prompts/templates/UPDATES.md +0 -50
  994. package/src/prompts/update-bulletin-format.ts +0 -85
  995. package/src/prompts/update-bulletin-state.ts +0 -58
  996. package/src/prompts/update-bulletin-template-path.ts +0 -13
  997. package/src/prompts/update-bulletin.ts +0 -139
  998. package/src/runtime/gateway-internal-client.ts +0 -94
  999. package/src/runtime/routes/watch-routes.ts +0 -156
  1000. package/src/shared/provider-env-vars.ts +0 -19
  1001. package/src/signals/shotgun.ts +0 -203
  1002. package/src/tools/watch/screen-watch.ts +0 -144
  1003. package/src/tools/watch/watch-state.ts +0 -142
  1004. package/src/tools/watcher/create.ts +0 -86
  1005. package/src/tools/watcher/delete.ts +0 -36
  1006. package/src/tools/watcher/digest.ts +0 -54
  1007. package/src/tools/watcher/list.ts +0 -83
  1008. package/src/tools/watcher/update.ts +0 -71
@@ -7,6 +7,8 @@
7
7
  * runAgentLoop method here via the AgentLoopConversationContext interface.
8
8
  */
9
9
 
10
+ import { join } from "node:path";
11
+
10
12
  import { v4 as uuid } from "uuid";
11
13
 
12
14
  import type {
@@ -24,15 +26,19 @@ import type {
24
26
  } from "../channels/types.js";
25
27
  import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
26
28
  import { getConfig } from "../config/loader.js";
29
+ import type { LLMCallSite } from "../config/schemas/llm.js";
27
30
  import {
28
31
  derefToolResultReReads,
29
32
  postTurnTruncateToolResults,
30
33
  } from "../context/post-turn-tool-result-truncation.js";
31
- import { estimatePromptTokens } from "../context/token-estimator.js";
34
+ import {
35
+ estimatePromptTokens,
36
+ getCalibrationProviderKey,
37
+ } from "../context/token-estimator.js";
32
38
  import type { ContextWindowManager } from "../context/window-manager.js";
33
39
  import type { ToolProfiler } from "../events/tool-profiling-listener.js";
40
+ import { emitFeedEvent } from "../home/emit-feed-event.js";
34
41
  import { writeRelationshipState } from "../home/relationship-state-writer.js";
35
- import { getHookManager } from "../hooks/manager.js";
36
42
  import {
37
43
  clearSentryConversationContext,
38
44
  setSentryConversationContext,
@@ -41,8 +47,7 @@ import { commitAppTurnChanges } from "../memory/app-git-service.js";
41
47
  import { getApp, listAppFiles, resolveAppDir } from "../memory/app-store.js";
42
48
  import { enqueueAutoAnalysisOnCompaction } from "../memory/auto-analysis-enqueue.js";
43
49
  import {
44
- addMessage,
45
- deleteMessageById,
50
+ clearStrippedInjectionMetadataForConversation,
46
51
  getConversation,
47
52
  getConversationOriginChannel,
48
53
  getConversationOriginInterface,
@@ -50,27 +55,60 @@ import {
50
55
  getMessageById,
51
56
  provenanceFromTrustContext,
52
57
  updateConversationContextWindow,
53
- updateConversationTitle,
54
- updateMessageMetadata,
55
58
  } from "../memory/conversation-crud.js";
56
59
  import { getResolvedConversationDirPath } from "../memory/conversation-directories.js";
57
60
  import { syncMessageToDisk } from "../memory/conversation-disk-view.js";
58
61
  import {
59
62
  isReplaceableTitle,
60
- queueGenerateConversationTitle,
61
63
  queueRegenerateConversationTitle,
62
- UNTITLED_FALLBACK,
63
64
  } from "../memory/conversation-title-service.js";
64
65
  import type { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
65
66
  import { recordMemoryRecallLog } from "../memory/memory-recall-log-store.js";
67
+ import { PKB_WORKSPACE_SCOPE } from "../memory/pkb/types.js";
66
68
  import type { PermissionPrompter } from "../permissions/prompter.js";
67
- import type { ContentBlock, Message } from "../providers/types.js";
69
+ import { defaultCompactionTerminal } from "../plugins/defaults/compaction.js";
70
+ import { defaultHistoryRepairTerminal } from "../plugins/defaults/history-repair.js";
71
+ import {
72
+ asDefaultGraphPayload,
73
+ type DefaultMemoryRetrievalDeps,
74
+ type GraphMemoryPayload,
75
+ runDefaultMemoryRetrieval,
76
+ } from "../plugins/defaults/memory-retrieval.js";
77
+ import { defaultPersistenceTerminal } from "../plugins/defaults/persistence.js";
78
+ import { defaultTitleGenerateTerminal } from "../plugins/defaults/title-generate.js";
79
+ import { defaultTokenEstimateTerminal } from "../plugins/defaults/token-estimate.js";
80
+ import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
81
+ import { getMiddlewaresFor } from "../plugins/registry.js";
82
+ import type {
83
+ CircuitBreakerArgs,
84
+ CircuitBreakerResult,
85
+ CompactionArgs,
86
+ CompactionResult,
87
+ EstimateArgs,
88
+ EstimateResult,
89
+ HistoryRepairArgs,
90
+ HistoryRepairResult,
91
+ MemoryArgs,
92
+ MemoryResult,
93
+ OverflowReduceArgs,
94
+ OverflowReduceResult,
95
+ PersistArgs,
96
+ PersistResult,
97
+ TurnContext as PluginTurnContext,
98
+ } from "../plugins/types.js";
99
+ import { PluginExecutionError, PluginTimeoutError } from "../plugins/types.js";
100
+ import type {
101
+ ContentBlock,
102
+ Message,
103
+ ToolDefinition,
104
+ } from "../providers/types.js";
68
105
  import type { Provider } from "../providers/types.js";
69
106
  import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
70
107
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
71
108
  import { getSubagentManager } from "../subagent/index.js";
72
109
  import type { UsageActor } from "../usage/actors.js";
73
110
  import { getLogger } from "../util/logger.js";
111
+ import { getWorkspaceDir } from "../util/platform.js";
74
112
  import { timeAgo } from "../util/time.js";
75
113
  import { truncate } from "../util/truncate.js";
76
114
  import { getWorkspaceGitService } from "../workspace/git-service.js";
@@ -79,7 +117,6 @@ import {
79
117
  type AssistantAttachmentDraft,
80
118
  cleanAssistantContent,
81
119
  } from "./assistant-attachments.js";
82
- import { requestCompressionApproval } from "./context-overflow-approval.js";
83
120
  import { resolveOverflowAction } from "./context-overflow-policy.js";
84
121
  import {
85
122
  createInitialReducerState,
@@ -115,10 +152,11 @@ import {
115
152
  buildSubagentStatusBlock,
116
153
  buildUnifiedTurnContextBlock,
117
154
  findLastInjectedNowContent,
155
+ getPkbAutoInjectList,
118
156
  inboundActorContextFromTrust,
119
157
  inboundActorContextFromTrustContext,
120
- readNowScratchpad,
121
- readPkbContext,
158
+ loadSlackActiveThreadFocusBlock,
159
+ loadSlackChronologicalMessages,
122
160
  stripInjectionsForCompaction,
123
161
  } from "./conversation-runtime-assembly.js";
124
162
  import type { SkillProjectionCache } from "./conversation-skill-tools.js";
@@ -126,7 +164,7 @@ import { markSurfaceCompleted } from "./conversation-surfaces.js";
126
164
  import { resolveTrustClass } from "./conversation-tool-setup.js";
127
165
  import { recordUsage } from "./conversation-usage.js";
128
166
  import { formatTurnTimestamp } from "./date-context.js";
129
- import { deepRepairHistory, repairHistory } from "./history-repair.js";
167
+ import { deepRepairHistory } from "./history-repair.js";
130
168
  import type {
131
169
  DynamicPageSurfaceData,
132
170
  ServerMessage,
@@ -135,44 +173,12 @@ import type {
135
173
  UsageStats,
136
174
  } from "./message-protocol.js";
137
175
  import type { MemoryRecalled } from "./message-types/memory.js";
176
+ import { parseActualTokensFromError } from "./parse-actual-tokens-from-error.js";
138
177
  import type { TraceEmitter } from "./trace-emitter.js";
178
+ import { stripHistoricalWebSearchResults } from "./web-search-history.js";
139
179
 
140
180
  const log = getLogger("conversation-agent-loop");
141
181
 
142
- /**
143
- * Parse the actual token count reported by the provider in a context-too-large
144
- * error message. Providers typically include the prompt size, e.g.:
145
- * "prompt is too long: 242201 tokens > 200000 maximum"
146
- * "too many input tokens: 242201 > 200000"
147
- *
148
- * Returns the actual token count or null if it cannot be parsed.
149
- */
150
- export function parseActualTokensFromError(
151
- errorMessage: string | null,
152
- ): number | null {
153
- if (!errorMessage) return null;
154
-
155
- // Match patterns like "242201 tokens > 200000" or "242201 > 200000 maximum"
156
- const match = errorMessage.match(
157
- /(\d[\d,]*)\s*tokens?\s*[>≥]|:\s*(\d[\d,]*)\s*[>≥]/i,
158
- );
159
- if (match) {
160
- const raw = (match[1] || match[2]).replace(/,/g, "");
161
- const parsed = parseInt(raw, 10);
162
- if (!isNaN(parsed) && parsed > 0) return parsed;
163
- }
164
-
165
- // Fallback: match "too many input tokens: N > M"
166
- const fallback = errorMessage.match(/(\d[\d,]*)\s*[>≥]\s*\d/);
167
- if (fallback) {
168
- const raw = fallback[1].replace(/,/g, "");
169
- const parsed = parseInt(raw, 10);
170
- if (!isNaN(parsed) && parsed > 0) return parsed;
171
- }
172
-
173
- return null;
174
- }
175
-
176
182
  /** Title-cased friendly labels for tool names, used in confirmation chips. */
177
183
  const TOOL_FRIENDLY_LABEL: Record<string, string> = {
178
184
  bash: "Run Command",
@@ -181,12 +187,6 @@ const TOOL_FRIENDLY_LABEL: Record<string, string> = {
181
187
  file_read: "Read File",
182
188
  file_write: "Write File",
183
189
  file_edit: "Edit File",
184
- browser_navigate: "Browser",
185
- browser_click: "Browser",
186
- browser_type: "Browser",
187
- browser_screenshot: "Browser",
188
- browser_scroll: "Browser",
189
- browser_wait: "Browser",
190
190
  app_create: "Create App",
191
191
  app_refresh: "Refresh App",
192
192
  skill_load: "Load Skill",
@@ -197,6 +197,212 @@ type GitServiceInitializer = {
197
197
  ensureInitialized(): Promise<void>;
198
198
  };
199
199
 
200
+ // ── Compaction circuit-breaker pipeline helpers ─────────────────────
201
+ //
202
+ // The circuit-breaker behavior (3 consecutive summary-LLM failures trips a
203
+ // 1-hour cooldown) is now implemented by the `circuitBreaker` plugin
204
+ // pipeline. The default plugin (`plugins/defaults/circuit-breaker.ts`)
205
+ // replicates the legacy threshold/cooldown constants and event-emission
206
+ // semantics exactly — it operates on the `consecutiveCompactionFailures` /
207
+ // `compactionCircuitOpenUntil` fields the conversation still owns so the
208
+ // dev-only playground routes (`POST /playground/reset-compaction-circuit`,
209
+ // `POST /playground/inject-compaction-failures`) continue to read and
210
+ // mutate those fields directly.
211
+ //
212
+ // The helpers below build the pipeline inputs and invoke the runner. They
213
+ // are the sole entry points the rest of the daemon uses to query or update
214
+ // the compaction circuit.
215
+
216
+ /** Circuit-breaker key for a specific conversation's compaction pipeline. */
217
+ function compactionCircuitKey(conversationId: string): string {
218
+ return `compaction:${conversationId}`;
219
+ }
220
+
221
+ /**
222
+ * Build the minimal {@link TurnContext} the pipeline runner requires. Called
223
+ * both from inside the agent loop (where turn identifiers are available) and
224
+ * from non-turn invocations like `Conversation.forceCompact` (which falls
225
+ * back to stable placeholders so the runner's log records still carry the
226
+ * conversation identifier).
227
+ */
228
+ function buildCircuitTurnContext(ctx: {
229
+ readonly conversationId: string;
230
+ currentRequestId?: string;
231
+ currentTurnTrustContext?: TrustContext;
232
+ trustContext?: TrustContext;
233
+ turnCount: number;
234
+ }): PluginTurnContext {
235
+ const trust: TrustContext =
236
+ ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
237
+ return {
238
+ requestId: ctx.currentRequestId ?? "circuit-breaker",
239
+ conversationId: ctx.conversationId,
240
+ turnIndex: ctx.turnCount,
241
+ trust,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Run the `circuitBreaker` pipeline for the compaction circuit on this
247
+ * conversation. When `outcome` is provided, state is updated (and transition
248
+ * events emit via `onEvent`); when omitted the call is query-only.
249
+ *
250
+ * Returns the post-call decision from the pipeline. Callers gate auto-paths
251
+ * on `!result.open` and admit forced paths regardless of the decision.
252
+ */
253
+ async function runCompactionCircuitPipeline(
254
+ ctx: {
255
+ readonly conversationId: string;
256
+ consecutiveCompactionFailures: number;
257
+ compactionCircuitOpenUntil: number | null;
258
+ currentRequestId?: string;
259
+ currentTurnTrustContext?: TrustContext;
260
+ trustContext?: TrustContext;
261
+ turnCount: number;
262
+ },
263
+ args: {
264
+ outcome?: "success" | "failure";
265
+ onEvent?: (msg: ServerMessage) => void;
266
+ },
267
+ ): Promise<CircuitBreakerResult> {
268
+ const turnContext = buildCircuitTurnContext(ctx);
269
+ return runPipeline<CircuitBreakerArgs, CircuitBreakerResult>(
270
+ "circuitBreaker",
271
+ getMiddlewaresFor("circuitBreaker"),
272
+ async (terminalArgs) => {
273
+ // No plugin in the chain produced a decision. This should be
274
+ // unreachable in production because the default plugin registers a
275
+ // `circuitBreaker` middleware that always returns a decision, but we
276
+ // defensively derive the state here so test setups that intentionally
277
+ // omit the default plugin still get a sensible response.
278
+ const openUntil = terminalArgs.state.compactionCircuitOpenUntil;
279
+ const now = Date.now();
280
+ if (openUntil !== null && now < openUntil) {
281
+ return { open: true, cooldownRemainingMs: openUntil - now };
282
+ }
283
+ return { open: false };
284
+ },
285
+ {
286
+ key: compactionCircuitKey(ctx.conversationId),
287
+ // Pass the ctx directly as the mutable state container. The
288
+ // `CircuitBreakerArgs.state` shape deliberately matches the subset of
289
+ // fields the conversation owns so plugins mutate the same object the
290
+ // playground routes read and write.
291
+ state: ctx,
292
+ ...(args.outcome !== undefined ? { outcome: args.outcome } : {}),
293
+ ...(args.onEvent ? { onEvent: args.onEvent } : {}),
294
+ },
295
+ turnContext,
296
+ DEFAULT_TIMEOUTS.circuitBreaker,
297
+ );
298
+ }
299
+
300
+ /**
301
+ * Query-only: is the compaction circuit breaker currently open for this
302
+ * conversation? Thin wrapper around {@link runCompactionCircuitPipeline}
303
+ * with no outcome. Async because the pipeline runner is async, but the
304
+ * default plugin resolves synchronously on its microtask.
305
+ */
306
+ export async function isCompactionCircuitOpen(ctx: {
307
+ readonly conversationId: string;
308
+ consecutiveCompactionFailures: number;
309
+ compactionCircuitOpenUntil: number | null;
310
+ currentRequestId?: string;
311
+ currentTurnTrustContext?: TrustContext;
312
+ trustContext?: TrustContext;
313
+ turnCount: number;
314
+ }): Promise<boolean> {
315
+ const decision = await runCompactionCircuitPipeline(ctx, {});
316
+ return decision.open;
317
+ }
318
+
319
+ /**
320
+ * Update the compaction circuit breaker with the outcome of a `maybeCompact`
321
+ * call and emit any transition event. A `summaryFailed` value of `undefined`
322
+ * means the summary LLM never ran (early return) — callers must guard with
323
+ * `summaryFailed !== undefined` before invoking this helper so early-return
324
+ * paths don't silently reset the 3-strike counter.
325
+ *
326
+ * The default plugin handles threshold-based tripping and cooldown reset;
327
+ * see `plugins/defaults/circuit-breaker.ts` for the canonical semantics.
328
+ */
329
+ export async function trackCompactionOutcome(
330
+ ctx: {
331
+ readonly conversationId: string;
332
+ consecutiveCompactionFailures: number;
333
+ compactionCircuitOpenUntil: number | null;
334
+ currentRequestId?: string;
335
+ currentTurnTrustContext?: TrustContext;
336
+ trustContext?: TrustContext;
337
+ turnCount: number;
338
+ },
339
+ summaryFailed: boolean,
340
+ onEvent: (msg: ServerMessage) => void,
341
+ ): Promise<void> {
342
+ await runCompactionCircuitPipeline(ctx, {
343
+ outcome: summaryFailed ? "failure" : "success",
344
+ onEvent,
345
+ });
346
+ }
347
+
348
+ // ── Plugin pipeline helpers ──────────────────────────────────────────
349
+ //
350
+ // Canonical {@link PluginTurnContext} builder threaded into every
351
+ // `runPipeline` call inside `runAgentLoopImpl`. The orchestrator composes
352
+ // the context on demand at each call site from ambient state rather than
353
+ // carrying a persistent `TurnContext` instance across the turn.
354
+
355
+ /**
356
+ * Synthetic fallback trust context used when the orchestrator fires a pipeline
357
+ * before the per-turn trust snapshot has been captured (e.g. invocations that
358
+ * bypass `processMessage` / `drainQueue`). We bias to `unknown` rather than
359
+ * `guardian` so a missing snapshot cannot accidentally grant elevated trust
360
+ * to a custom plugin reading `ctx.trust`.
361
+ */
362
+ export const FALLBACK_TURN_TRUST: TrustContext = {
363
+ sourceChannel: "vellum",
364
+ trustClass: "unknown",
365
+ };
366
+
367
+ /**
368
+ * Build the {@link TurnContext} passed to {@link runPipeline}.
369
+ *
370
+ * Canonical source of truth for every pipeline call site inside the agent
371
+ * loop. Every `runPipeline` invocation in `runAgentLoopImpl` (and in the
372
+ * handlers that share its ambient state) must route through this helper
373
+ * rather than constructing a `TurnContext` literal inline — this keeps
374
+ * `turnIndex`, trust resolution, and the `contextWindowManager` attachment
375
+ * consistent across pipeline slots, which in turn keeps structured logs
376
+ * filtered by `conversationId`/`turnIndex` coherent across slots.
377
+ *
378
+ * Behavior:
379
+ * - `turnIndex` is always `ctx.turnCount` — the orchestrator-owned
380
+ * 0-based turn counter. Reading from a single source avoids the
381
+ * earlier inconsistency (`ctx.turnCount`, `ctx.messages.length - 1`,
382
+ * `ctx.messages.length`, and `0` were all used for the same turn).
383
+ * - Trust pulls from the per-turn snapshot first, then the conversation-
384
+ * level context, then {@link FALLBACK_TURN_TRUST}. The cascade matches
385
+ * the one inside the orchestrator's inline injection assembly so
386
+ * middleware reads the same trust class the runtime sees.
387
+ * - `contextWindowManager` is attached unconditionally. Pipelines that
388
+ * don't need it can ignore it; the default compaction plugin reads it
389
+ * via the typed optional field on `TurnContext`.
390
+ */
391
+ export function buildPluginTurnContext(
392
+ ctx: AgentLoopConversationContext,
393
+ requestId: string,
394
+ ): PluginTurnContext {
395
+ const trust =
396
+ ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
397
+ return {
398
+ requestId,
399
+ conversationId: ctx.conversationId,
400
+ turnIndex: ctx.turnCount,
401
+ trust,
402
+ contextWindowManager: ctx.contextWindowManager,
403
+ };
404
+ }
405
+
200
406
  // ── Context Interface ────────────────────────────────────────────────
201
407
 
202
408
  export interface AgentLoopConversationContext {
@@ -213,6 +419,10 @@ export interface AgentLoopConversationContext {
213
419
  readonly contextWindowManager: ContextWindowManager;
214
420
  contextCompactedMessageCount: number;
215
421
  contextCompactedAt: number | null;
422
+ /** Tracks consecutive compaction failures (summary LLM call threw). */
423
+ consecutiveCompactionFailures: number;
424
+ /** Timestamp (ms since epoch) until which the circuit breaker is open. */
425
+ compactionCircuitOpenUntil: number | null;
216
426
 
217
427
  readonly memoryPolicy: { scopeId: string; includeDefaultFallback: boolean };
218
428
  readonly graphMemory: ConversationGraphMemory;
@@ -235,6 +445,7 @@ export interface AgentLoopConversationContext {
235
445
  >;
236
446
  pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
237
447
  surfaceActionRequestIds: Set<string>;
448
+ approvedViaPromptThisTurn?: boolean;
238
449
  currentTurnSurfaces: Array<{
239
450
  surfaceId: string;
240
451
  surfaceType: SurfaceType;
@@ -352,10 +563,16 @@ export async function runAgentLoopImpl(
352
563
  userMessageId: string,
353
564
  onEvent: (msg: ServerMessage) => void,
354
565
  options?: {
355
- skipPreMessageRollback?: boolean;
356
566
  isInteractive?: boolean;
357
567
  isUserMessage?: boolean;
358
568
  titleText?: string;
569
+ /**
570
+ * LLM call-site identifier threaded into the per-call provider config.
571
+ * Adapter callers (heartbeat, filing, scheduler, etc.) pass their own
572
+ * call-site id so the resolver picks `llm.callSites.<id>`. When unset,
573
+ * the agent loop defaults to `'mainAgent'` for user-initiated turns.
574
+ */
575
+ callSite?: LLMCallSite;
359
576
  },
360
577
  ): Promise<void> {
361
578
  if (!ctx.abortController) {
@@ -379,6 +596,13 @@ export async function runAgentLoopImpl(
379
596
  });
380
597
  let yieldedForHandoff = false;
381
598
 
599
+ // Default user-initiated turns to the `mainAgent` call site. Other
600
+ // invocation contexts (heartbeat, filing, analyze, etc.) pass their own
601
+ // `callSite`. The provider layer resolves provider/model/maxTokens via
602
+ // `resolveCallSiteConfig`, picking up any user overrides under
603
+ // `llm.callSites.mainAgent` (falling back to `llm.default` when absent).
604
+ const turnCallSite: LLMCallSite = options?.callSite ?? "mainAgent";
605
+
382
606
  // Capture the turn channel context *before* any awaits so a second
383
607
  // message from a different channel can't overwrite it mid-flight.
384
608
  // When context is unavailable (e.g. regenerate after daemon restart),
@@ -462,40 +686,10 @@ export async function runAgentLoopImpl(
462
686
  }
463
687
  }
464
688
 
465
- const preMessageResult = await getHookManager().trigger("pre-message", {
466
- conversationId: ctx.conversationId,
467
- messagePreview: truncate(content, 200, ""),
468
- });
469
-
470
- if (preMessageResult.blocked) {
471
- if (!options?.skipPreMessageRollback) {
472
- ctx.messages.pop();
473
- deleteMessageById(userMessageId);
474
- }
475
- // Replace loading placeholder so the conversation isn't stuck as "Generating title..."
476
- const currentConv = getConversation(ctx.conversationId);
477
- if (
478
- isReplaceableTitle(currentConv?.title ?? null) &&
479
- currentConv?.title !== UNTITLED_FALLBACK
480
- ) {
481
- updateConversationTitle(ctx.conversationId, UNTITLED_FALLBACK);
482
- onEvent({
483
- type: "conversation_title_updated",
484
- conversationId: ctx.conversationId,
485
- title: UNTITLED_FALLBACK,
486
- });
487
- }
488
- onEvent({
489
- type: "error",
490
- message: `Message blocked by hook "${preMessageResult.blockedBy}"`,
491
- });
492
- return;
493
- }
494
-
495
689
  // Generate title early — the user message alone is sufficient context.
496
- // Firing after hook gating but before the main LLM call removes the
497
- // delay of waiting for the full assistant response. The second-pass
498
- // regeneration at turn 3 will refine the title with more context.
690
+ // Firing before the main LLM call removes the delay of waiting for the
691
+ // full assistant response. The second-pass regeneration at turn 3 will
692
+ // refine the title with more context.
499
693
  // No abort signal — title generation should complete even if the user
500
694
  // cancels the response, since the user message is already persisted.
501
695
  // Deferred via setTimeout so the main agent loop LLM call enqueues
@@ -503,18 +697,38 @@ export async function runAgentLoopImpl(
503
697
  if (
504
698
  isReplaceableTitle(getConversation(ctx.conversationId)?.title ?? null)
505
699
  ) {
700
+ // TurnContext routed through the canonical builder so the pipeline's
701
+ // log record reports the same `conversationId`/`turnIndex` shape as
702
+ // every other slot in this turn. Title generation does not depend on
703
+ // the context-window manager attached by the builder, but sharing the
704
+ // builder keeps the invariant enforced in one place.
705
+ const titlePipelineCtx = buildPluginTurnContext(ctx, reqId);
706
+ const titleArgs = {
707
+ conversationId: ctx.conversationId,
708
+ provider: ctx.provider,
709
+ userMessage: options?.titleText ?? content,
710
+ onTitleUpdated: (title: string) => {
711
+ onEvent({
712
+ type: "conversation_title_updated",
713
+ conversationId: ctx.conversationId,
714
+ title,
715
+ });
716
+ },
717
+ };
506
718
  setTimeout(() => {
507
- queueGenerateConversationTitle({
508
- conversationId: ctx.conversationId,
509
- provider: ctx.provider,
510
- userMessage: options?.titleText ?? content,
511
- onTitleUpdated: (title) => {
512
- onEvent({
513
- type: "conversation_title_updated",
514
- conversationId: ctx.conversationId,
515
- title,
516
- });
517
- },
719
+ runPipeline(
720
+ "titleGenerate",
721
+ getMiddlewaresFor("titleGenerate"),
722
+ defaultTitleGenerateTerminal,
723
+ titleArgs,
724
+ titlePipelineCtx,
725
+ DEFAULT_TIMEOUTS.titleGenerate,
726
+ ).catch((err) => {
727
+ // Fire-and-forget — keep previous non-propagating semantics.
728
+ // queueGenerateConversationTitle already swallows internal
729
+ // errors; this catch covers pipeline-layer errors (timeouts,
730
+ // middleware throws) without surfacing them to the agent loop.
731
+ rlog.warn({ err }, "titleGenerate pipeline failed (non-fatal)");
518
732
  });
519
733
  }, 0);
520
734
  }
@@ -524,7 +738,10 @@ export async function runAgentLoopImpl(
524
738
  let compactedThisTurn = false;
525
739
 
526
740
  const compactCheck = ctx.contextWindowManager.shouldCompact(ctx.messages);
527
- if (compactCheck.needed) {
741
+ // Skip auto-compaction while the circuit breaker is open. Force paths
742
+ // and user-initiated /compact bypass this check.
743
+ const autoCompactAllowed = !(await isCompactionCircuitOpen(ctx));
744
+ if (compactCheck.needed && autoCompactAllowed) {
528
745
  ctx.emitActivityState(
529
746
  "thinking",
530
747
  "context_compacting",
@@ -532,57 +749,59 @@ export async function runAgentLoopImpl(
532
749
  reqId,
533
750
  );
534
751
  }
535
- const compacted = await ctx.contextWindowManager.maybeCompact(
536
- ctx.messages,
537
- abortController.signal,
538
- {
539
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
540
- precomputedEstimate: compactCheck.estimatedTokens,
541
- },
542
- );
543
- if (compacted.compacted) {
544
- ctx.messages = compacted.messages;
545
- ctx.contextCompactedMessageCount += compacted.compactedPersistedMessages;
546
- ctx.contextCompactedAt = Date.now();
547
- // Notify memory graph that compaction happened — triggers full context
548
- // reload on the next turn to replenish lost memory context.
549
- ctx.graphMemory.onCompacted(compacted.compactedPersistedMessages);
550
- updateConversationContextWindow(
551
- ctx.conversationId,
552
- compacted.summaryText,
553
- ctx.contextCompactedMessageCount,
554
- );
555
- // Fire auto-analysis on compaction so the reflective agent can
556
- // crystallize anything worth remembering before the context window
557
- // narrows further.
558
- enqueueAutoAnalysisOnCompaction(
559
- ctx.conversationId,
560
- ctx.trustContext?.trustClass,
561
- );
562
- onEvent({
563
- type: "context_compacted",
564
- previousEstimatedInputTokens: compacted.previousEstimatedInputTokens,
565
- estimatedInputTokens: compacted.estimatedInputTokens,
566
- maxInputTokens: compacted.maxInputTokens,
567
- thresholdTokens: compacted.thresholdTokens,
568
- compactedMessages: compacted.compactedMessages,
569
- summaryCalls: compacted.summaryCalls,
570
- summaryInputTokens: compacted.summaryInputTokens,
571
- summaryOutputTokens: compacted.summaryOutputTokens,
572
- summaryModel: compacted.summaryModel,
573
- });
574
- emitUsage(
575
- ctx,
576
- compacted.summaryInputTokens,
577
- compacted.summaryOutputTokens,
578
- compacted.summaryModel,
579
- onEvent,
580
- "context_compactor",
581
- reqId,
582
- compacted.summaryCacheCreationInputTokens ?? 0,
583
- compacted.summaryCacheReadInputTokens ?? 0,
584
- collapseRawResponses(compacted.summaryRawResponses),
585
- );
752
+ let compacted: Awaited<
753
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
754
+ > | null = null;
755
+ if (autoCompactAllowed) {
756
+ try {
757
+ compacted = (await runPipeline<CompactionArgs, CompactionResult>(
758
+ "compaction",
759
+ getMiddlewaresFor("compaction"),
760
+ (args) =>
761
+ defaultCompactionTerminal(args, buildPluginTurnContext(ctx, reqId)),
762
+ {
763
+ messages: ctx.messages,
764
+ signal: abortController.signal,
765
+ options: {
766
+ lastCompactedAt: ctx.contextCompactedAt ?? undefined,
767
+ precomputedEstimate: compactCheck.estimatedTokens,
768
+ conversationOriginChannel:
769
+ getConversationOriginChannel(ctx.conversationId) ?? undefined,
770
+ },
771
+ },
772
+ buildPluginTurnContext(ctx, reqId),
773
+ DEFAULT_TIMEOUTS.compaction,
774
+ )) as Awaited<ReturnType<typeof ctx.contextWindowManager.maybeCompact>>;
775
+ } catch (err) {
776
+ if (err instanceof PluginTimeoutError) {
777
+ // Pipeline exceeded its budget. Record the failure so the circuit
778
+ // breaker tracks consecutive timeouts (it trips after three),
779
+ // then degrade gracefully by skipping compaction this turn —
780
+ // the turn proceeds with the un-compacted history rather than
781
+ // hard-failing. The inner summary call has been aborted by the
782
+ // runner's signal-linking, so updateSummary's local fallback
783
+ // also ran before this catch block is reached.
784
+ rlog.warn(
785
+ { err, phase: "start-of-turn-compaction" },
786
+ "Compaction pipeline timed out — skipping compaction this turn",
787
+ );
788
+ await trackCompactionOutcome(ctx, true, onEvent);
789
+ compacted = null;
790
+ } else {
791
+ throw err;
792
+ }
793
+ }
794
+ }
795
+ // Only track circuit-breaker state when a summary LLM call actually ran.
796
+ // `summaryFailed` is `undefined` on early returns (compaction disabled,
797
+ // below threshold, cooldown active, no eligible messages, truncation-only
798
+ // path) — treating those as "successful" compactions would silently reset
799
+ // the 3-strike counter and break the invariant.
800
+ if (compacted && compacted.summaryFailed !== undefined) {
801
+ await trackCompactionOutcome(ctx, compacted.summaryFailed, onEvent);
802
+ }
803
+ if (compacted?.compacted) {
804
+ applyCompactionResult(ctx, compacted, onEvent, reqId);
586
805
  shouldInjectWorkspace = true;
587
806
  if (compacted.compactedPersistedMessages > 0) {
588
807
  compactedThisTurn = true;
@@ -630,26 +849,98 @@ export async function runAgentLoopImpl(
630
849
 
631
850
  let runMessages = ctx.messages;
632
851
 
633
- // Memory graph retrieval — dispatches to context-load / per-turn based on
634
- // conversation state.
852
+ // Memory retrieval pipeline fetches PKB, NOW.md, and memory-graph
853
+ // outputs through a single `memoryRetrieval` pipeline. Plugins may
854
+ // replace the terminal behavior by registering a middleware that
855
+ // short-circuits with its own `MemoryResult`; the default terminal
856
+ // below runs `runDefaultMemoryRetrieval` which reproduces the prior
857
+ // in-lined behavior (PKB/NOW reads + gated graph call).
635
858
  const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
636
- if (isTrustedActor) {
637
- const graphResult = await ctx.graphMemory.prepareMemory(
638
- ctx.messages,
639
- getConfig(),
640
- abortController.signal,
641
- onEvent,
642
- );
859
+ // Canonical builder — pulls trust from per-turn snapshot, then
860
+ // conversation-level, then the synthetic fallback. Memory retrieval
861
+ // does not need the context-window handle the builder attaches, but
862
+ // keeping every call site on one helper is load-bearing for log
863
+ // coherence across pipeline slots.
864
+ const memoryPluginTurnCtx = buildPluginTurnContext(ctx, reqId);
865
+ const memoryArgs: MemoryArgs = {
866
+ conversationId: ctx.conversationId,
867
+ trustContext: ctx.trustContext,
868
+ turnIndex: ctx.turnCount,
869
+ // Pass the abort signal via `args` (not `deps`) so the pipeline
870
+ // runner's `linkAbortSignal` can swap it for a signal linked to the
871
+ // pipeline's internal controller — on a plugin-set timeout or
872
+ // external cancel, the linked signal aborts and `prepareMemory`
873
+ // stops mutating graph state / emitting events after the pipeline
874
+ // has already errored.
875
+ signal: abortController.signal,
876
+ };
877
+ const memoryDeps: DefaultMemoryRetrievalDeps = {
878
+ messages: ctx.messages,
879
+ graphMemory: ctx.graphMemory,
880
+ config: getConfig(),
881
+ onEvent,
882
+ isTrustedActor,
883
+ };
884
+ const memoryResult: MemoryResult = await runPipeline(
885
+ "memoryRetrieval",
886
+ getMiddlewaresFor("memoryRetrieval"),
887
+ (args) => runDefaultMemoryRetrieval(args, memoryDeps),
888
+ memoryArgs,
889
+ memoryPluginTurnCtx,
890
+ DEFAULT_TIMEOUTS.memoryRetrieval,
891
+ );
892
+
893
+ // Consume the memory-graph block when the default retriever emitted
894
+ // one. Custom plugins that substitute their own blocks without the
895
+ // default discriminator are expected to handle their own side effects
896
+ // (event emission, metric persistence) inside their middleware; this
897
+ // block short-circuits to the original no-op behavior in that case.
898
+ const defaultGraphPayload: GraphMemoryPayload | null =
899
+ asDefaultGraphPayload(memoryResult.memoryGraphBlocks);
900
+ let pkbQueryVector: number[] | undefined;
901
+ let pkbSparseVector:
902
+ | import("../memory/qdrant-client.js").QdrantSparseVector
903
+ | undefined;
904
+ if (defaultGraphPayload) {
905
+ const graphResult = defaultGraphPayload.result;
643
906
  runMessages = graphResult.runMessages;
907
+ // Select dense+sparse as a matched pair so RRF fusion combines two
908
+ // signals aligned to the same query text:
909
+ // 1. Context-load with a user query: user-query dense + user-query
910
+ // sparse — the cleanest pairing.
911
+ // 2. Otherwise (context-load without a user query, or per-turn):
912
+ // whatever `queryVector` / `sparseVector` the retriever produced,
913
+ // which are themselves co-aligned (both summary-derived in
914
+ // context-load, both user-last-message-derived in per-turn).
915
+ // Never pair a user-query dense with a summary-aligned sparse.
916
+ if (graphResult.userQueryVector) {
917
+ pkbQueryVector = graphResult.userQueryVector;
918
+ pkbSparseVector = graphResult.userQuerySparseVector;
919
+ } else {
920
+ pkbQueryVector = graphResult.queryVector;
921
+ pkbSparseVector = graphResult.sparseVector;
922
+ }
644
923
 
645
924
  // Persist the injected block text in message metadata so it survives
646
925
  // conversation reloads (eviction, restart, fork). loadFromDb re-injects
647
- // from metadata.
926
+ // from metadata. Routed through the `persistence` pipeline so plugins
927
+ // can observe or override metadata updates alongside add/delete.
648
928
  if (graphResult.injectedBlockText) {
649
929
  try {
650
- updateMessageMetadata(userMessageId, {
651
- memoryInjectedBlock: graphResult.injectedBlockText,
652
- });
930
+ await runPipeline<PersistArgs, PersistResult>(
931
+ "persistence",
932
+ getMiddlewaresFor("persistence"),
933
+ defaultPersistenceTerminal,
934
+ {
935
+ op: "update",
936
+ messageId: userMessageId,
937
+ updates: {
938
+ memoryInjectedBlock: graphResult.injectedBlockText,
939
+ },
940
+ },
941
+ buildPluginTurnContext(ctx, reqId),
942
+ DEFAULT_TIMEOUTS.persistence,
943
+ );
653
944
  } catch (err) {
654
945
  rlog.warn(
655
946
  { err },
@@ -831,14 +1122,31 @@ export async function runAgentLoopImpl(
831
1122
  // Inject NOW.md and PKB content only on the first turn (or after
832
1123
  // compaction re-strips them). Old injections persist in history and
833
1124
  // are never stripped on normal turns — this preserves the cached prefix.
834
- const currentNowContent = readNowScratchpad();
1125
+ // PKB/NOW content is sourced from the `memoryRetrieval` pipeline above
1126
+ // so plugins can override either source without touching the agent loop.
1127
+ const currentNowContent = memoryResult.nowContent;
835
1128
  const shouldInjectNowAndPkb = isFirstMessage || compactedThisTurn;
836
1129
  const nowScratchpad = shouldInjectNowAndPkb ? currentNowContent : null;
837
1130
 
838
- const currentPkbContent = readPkbContext();
1131
+ const currentPkbContent = memoryResult.pkbContent;
839
1132
  const pkbContext = shouldInjectNowAndPkb ? currentPkbContent : null;
840
1133
  const pkbActive = currentPkbContent !== null;
841
1134
 
1135
+ // PKB relevance-hint inputs. Resolved once per turn and reused across
1136
+ // re-injections so post-compaction rebuilds pick up fresh hints against
1137
+ // the updated conversation history.
1138
+ const pkbRoot = pkbActive ? join(getWorkspaceDir(), "pkb") : undefined;
1139
+ const pkbAutoInjectList = pkbRoot
1140
+ ? getPkbAutoInjectList(pkbRoot)
1141
+ : undefined;
1142
+ // Pass `ctx` directly — `PkbContextConversation` is structural and
1143
+ // `getInContextPkbPaths` re-reads `conversation.messages` on each call,
1144
+ // so post-compaction re-injects see the updated history.
1145
+ const pkbConversation = pkbActive ? ctx : undefined;
1146
+ // PKB points live under a single workspace sentinel scope, not the
1147
+ // conversation's memoryPolicy.scopeId. See `PKB_WORKSPACE_SCOPE` for why.
1148
+ const pkbScopeId = pkbActive ? PKB_WORKSPACE_SCOPE : undefined;
1149
+
842
1150
  // Subagent status injection — gives the parent LLM visibility into active/completed children.
843
1151
  // Skipped when this conversation IS a subagent (no nesting) or has no children.
844
1152
  const subagentStatusBlock = ctx.isSubagent
@@ -847,6 +1155,43 @@ export async function runAgentLoopImpl(
847
1155
  getSubagentManager().getChildrenOf(ctx.conversationId),
848
1156
  );
849
1157
 
1158
+ // For any Slack conversation (channels and DMs alike), build a
1159
+ // chronological transcript from the persisted message rows so the
1160
+ // model sees one channel-wide view instead of the gateway's per-turn
1161
+ // hints. DMs render as a flat sequence (no thread tags), channels
1162
+ // include sibling threads.
1163
+ const isSlackConversation = ctx.channelCapabilities?.channel === "slack";
1164
+ const slackChronologicalMessages = isSlackConversation
1165
+ ? loadSlackChronologicalMessages(
1166
+ ctx.conversationId,
1167
+ ctx.channelCapabilities!,
1168
+ { trustClass: ctx.trustContext?.trustClass },
1169
+ )
1170
+ : null;
1171
+
1172
+ // Active-thread focus block: when the inbound user message belongs to
1173
+ // a Slack thread, append a non-persisted `<active_thread>` tail block
1174
+ // to the final user turn listing the thread's parent + replies. Helps
1175
+ // the model orient when the channel transcript is long and
1176
+ // interleaved. Replays strip the block via RUNTIME_INJECTION_PREFIXES.
1177
+ // DMs short-circuit to null inside `loadSlackActiveThreadFocusBlock`
1178
+ // since DMs do not have threads.
1179
+ const slackActiveThreadFocusBlock = isSlackConversation
1180
+ ? loadSlackActiveThreadFocusBlock(
1181
+ ctx.conversationId,
1182
+ ctx.channelCapabilities!,
1183
+ { trustClass: ctx.trustContext?.trustClass },
1184
+ )
1185
+ : null;
1186
+
1187
+ // Guards the chronological-transcript override on re-injection after
1188
+ // the reducer compacts `ctx.messages`. The captured transcript is the
1189
+ // full persisted history; blindly replaying it on every re-inject would
1190
+ // overwrite the reducer's compacted messages and undo compaction. Flip
1191
+ // to `true` after any compaction so subsequent re-injections fall back
1192
+ // to the reduced `ctx.messages`.
1193
+ let reducerCompacted = compactedThisTurn;
1194
+
850
1195
  // Shared injection options — reused whenever we need to re-inject after reduction.
851
1196
  const injectionOpts = {
852
1197
  activeSurface,
@@ -858,27 +1203,97 @@ export async function runAgentLoopImpl(
858
1203
  unifiedTurnContext: unifiedTurnContextStr,
859
1204
  pkbContext,
860
1205
  pkbActive,
1206
+ pkbQueryVector,
1207
+ pkbSparseVector,
1208
+ pkbScopeId,
1209
+ pkbConversation,
1210
+ pkbAutoInjectList,
1211
+ pkbRoot,
1212
+ pkbWorkingDir: pkbActive ? ctx.workingDir : undefined,
861
1213
  nowScratchpad,
862
1214
  voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
863
1215
  transportHints: ctx.transportHints ?? null,
864
1216
  isNonInteractive: !isInteractiveResolved,
865
1217
  subagentStatusBlock,
1218
+ slackChronologicalMessages,
1219
+ slackActiveThreadFocusBlock,
866
1220
  } as const;
867
1221
 
868
1222
  let currentInjectionMode: InjectionMode = "full";
869
1223
 
870
- runMessages = applyRuntimeInjections(runMessages, {
1224
+ // Canonical per-turn TurnContext forwarded to the injector chain. The
1225
+ // per-turn injection inputs are built inside `applyRuntimeInjections`
1226
+ // from the `injectionOpts` bag; we only need to hand in identity +
1227
+ // trust here so third-party injectors see the real turn metadata.
1228
+ const injectionTurnCtx = buildPluginTurnContext(ctx, reqId);
1229
+
1230
+ const injection = await applyRuntimeInjections(runMessages, {
871
1231
  ...injectionOpts,
1232
+ slackChronologicalMessages: reducerCompacted
1233
+ ? null
1234
+ : injectionOpts.slackChronologicalMessages,
872
1235
  mode: currentInjectionMode,
1236
+ turnContext: injectionTurnCtx,
873
1237
  });
1238
+ runMessages = injection.messages;
1239
+
1240
+ // Persist injected blocks in message metadata so they survive conversation
1241
+ // reloads (eviction, restart, fork). loadFromDb re-injects from metadata.
1242
+ // Only the first call site persists — the overflow-recovery re-entry sites
1243
+ // send identical bytes and the tail row may not correspond to
1244
+ // `userMessageId`. All blocks are written in a single call to avoid
1245
+ // doubling SQLite SELECT+UPDATE work on every turn.
1246
+ if (
1247
+ injection.blocks.unifiedTurnContext ||
1248
+ injection.blocks.pkbSystemReminder ||
1249
+ injection.blocks.workspaceBlock ||
1250
+ injection.blocks.nowScratchpadBlock ||
1251
+ injection.blocks.pkbContextBlock
1252
+ ) {
1253
+ try {
1254
+ const metadataUpdates: Record<string, unknown> = {};
1255
+ if (injection.blocks.unifiedTurnContext) {
1256
+ metadataUpdates.turnContextBlock =
1257
+ injection.blocks.unifiedTurnContext;
1258
+ }
1259
+ if (injection.blocks.pkbSystemReminder) {
1260
+ metadataUpdates.pkbSystemReminderBlock =
1261
+ injection.blocks.pkbSystemReminder;
1262
+ }
1263
+ if (injection.blocks.workspaceBlock) {
1264
+ metadataUpdates.workspaceBlock = injection.blocks.workspaceBlock;
1265
+ }
1266
+ if (injection.blocks.nowScratchpadBlock) {
1267
+ metadataUpdates.nowScratchpadBlock =
1268
+ injection.blocks.nowScratchpadBlock;
1269
+ }
1270
+ if (injection.blocks.pkbContextBlock) {
1271
+ metadataUpdates.pkbContextBlock = injection.blocks.pkbContextBlock;
1272
+ }
1273
+ await runPipeline<PersistArgs, PersistResult>(
1274
+ "persistence",
1275
+ getMiddlewaresFor("persistence"),
1276
+ defaultPersistenceTerminal,
1277
+ {
1278
+ op: "update",
1279
+ messageId: userMessageId,
1280
+ updates: metadataUpdates,
1281
+ },
1282
+ buildPluginTurnContext(ctx, reqId),
1283
+ DEFAULT_TIMEOUTS.persistence,
1284
+ );
1285
+ } catch (err) {
1286
+ rlog.warn({ err }, "Failed to persist injection metadata (non-fatal)");
1287
+ }
1288
+ }
874
1289
 
875
1290
  // ── Preflight budget evaluation ──────────────────────────────
876
1291
  // After runtime injections are applied, estimate the prompt token count
877
1292
  // and proactively invoke the reducer if already above budget. This avoids
878
1293
  // a wasted provider round-trip that would just fail with context_too_large.
879
1294
  const config = getConfig();
880
- const overflowRecovery = config.contextWindow.overflowRecovery;
881
- const providerMaxTokens = config.contextWindow.maxInputTokens;
1295
+ const overflowRecovery = config.llm.default.contextWindow.overflowRecovery;
1296
+ const providerMaxTokens = config.llm.default.contextWindow.maxInputTokens;
882
1297
  // Widen safety margin for large conversations where estimation error
883
1298
  // compounds across many messages with tool results.
884
1299
  const baseSafetyMargin = overflowRecovery.safetyMarginRatio;
@@ -889,11 +1304,51 @@ export async function runAgentLoopImpl(
889
1304
  let reducerState: ReducerState | undefined;
890
1305
 
891
1306
  const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
892
- const preflightTokens = estimatePromptTokens(
893
- runMessages,
894
- ctx.systemPrompt,
895
- { providerName: ctx.provider.name, toolTokenBudget },
896
- );
1307
+ // Canonical calibration key — passed to the `tokenEstimate` pipeline for
1308
+ // every preflight/mid-loop estimate, the overflow reducer config, and the
1309
+ // convergence-path `estimatePromptTokens` call. Matches the key recorded
1310
+ // by `handleUsage` for wrapper providers (OpenRouter routing to
1311
+ // Anthropic → key is `"anthropic"`).
1312
+ const estimationProviderName = getCalibrationProviderKey(ctx.provider);
1313
+
1314
+ // Shared `TurnContext` for every `tokenEstimate` pipeline invocation in
1315
+ // this turn. The pipeline is the extension point for plugins that want
1316
+ // to substitute an alternate estimator (e.g. provider-native tokenization)
1317
+ // without touching orchestrator code.
1318
+ //
1319
+ // Routed through the canonical builder — `turnIndex` is `ctx.turnCount`,
1320
+ // trust cascades through per-turn/conversation-level/fallback, and the
1321
+ // context-window handle rides along so any middleware that wants to
1322
+ // reuse the manager (e.g. to compute compaction-aware estimates) can.
1323
+ const pipelineTurnCtx = buildPluginTurnContext(ctx, reqId);
1324
+
1325
+ const runTokenEstimatePipeline = (
1326
+ history: Message[],
1327
+ ): Promise<EstimateResult> =>
1328
+ runPipeline<EstimateArgs, EstimateResult>(
1329
+ "tokenEstimate",
1330
+ getMiddlewaresFor("tokenEstimate"),
1331
+ defaultTokenEstimateTerminal,
1332
+ {
1333
+ // Shallow-frozen copies so a misbehaving middleware that mutates
1334
+ // `args.history` or `args.tools` in place (e.g. trims the array
1335
+ // before calling next) can't silently strip prompt context from
1336
+ // the orchestrator's live `runMessages` / resolved-tools arrays.
1337
+ // TypeScript `readonly` on `EstimateArgs` does not prevent
1338
+ // `push`/`splice` at runtime; the frozen wrapper throws in strict
1339
+ // mode and isolates any mutation attempts from the call-site state.
1340
+ history: Object.freeze([...history]) as Message[],
1341
+ systemPrompt: ctx.systemPrompt,
1342
+ tools: Object.freeze([
1343
+ ...ctx.agentLoop.getResolvedTools(history),
1344
+ ]) as ToolDefinition[],
1345
+ providerName: estimationProviderName,
1346
+ },
1347
+ pipelineTurnCtx,
1348
+ DEFAULT_TIMEOUTS.tokenEstimate,
1349
+ );
1350
+
1351
+ const preflightTokens = await runTokenEstimatePipeline(runMessages);
897
1352
 
898
1353
  if (overflowRecovery.enabled && preflightTokens > preflightBudget) {
899
1354
  rlog.warn(
@@ -905,127 +1360,198 @@ export async function runAgentLoopImpl(
905
1360
  "Preflight budget exceeded — running overflow reducer before provider call",
906
1361
  );
907
1362
 
908
- reducerState = createInitialReducerState();
909
- let preflightAttempts = 0;
910
-
911
- while (
912
- preflightAttempts < overflowRecovery.maxAttempts &&
913
- !reducerState.exhausted
914
- ) {
915
- preflightAttempts++;
916
- ctx.emitActivityState(
917
- "thinking",
918
- "context_compacting",
919
- "assistant_turn",
920
- reqId,
921
- );
922
- const step = await reduceContextOverflow(
923
- ctx.messages,
924
- {
925
- providerName: ctx.provider.name,
926
- systemPrompt: ctx.systemPrompt,
927
- contextWindow: config.contextWindow,
928
- targetTokens: preflightBudget,
929
- toolTokenBudget,
930
- },
931
- reducerState,
932
- (msgs, signal, opts) =>
933
- ctx.contextWindowManager.maybeCompact(msgs, signal!, opts),
934
- abortController.signal,
935
- );
936
-
937
- reducerState = step.state;
938
- ctx.messages = step.messages;
939
- currentInjectionMode = step.state.injectionMode;
940
-
941
- if (step.compactionResult?.compacted) {
942
- ctx.contextCompactedMessageCount +=
943
- step.compactionResult.compactedPersistedMessages;
944
- ctx.contextCompactedAt = Date.now();
945
- updateConversationContextWindow(
946
- ctx.conversationId,
947
- step.compactionResult.summaryText,
948
- ctx.contextCompactedMessageCount,
949
- );
950
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
951
- enqueueAutoAnalysisOnCompaction(
952
- ctx.conversationId,
953
- ctx.trustContext?.trustClass,
954
- );
955
- onEvent({
956
- type: "context_compacted",
957
- previousEstimatedInputTokens:
958
- step.compactionResult.previousEstimatedInputTokens,
959
- estimatedInputTokens: step.compactionResult.estimatedInputTokens,
960
- maxInputTokens: step.compactionResult.maxInputTokens,
961
- thresholdTokens: step.compactionResult.thresholdTokens,
962
- compactedMessages: step.compactionResult.compactedMessages,
963
- summaryCalls: step.compactionResult.summaryCalls,
964
- summaryInputTokens: step.compactionResult.summaryInputTokens,
965
- summaryOutputTokens: step.compactionResult.summaryOutputTokens,
966
- summaryModel: step.compactionResult.summaryModel,
967
- });
968
- emitUsage(
969
- ctx,
970
- step.compactionResult.summaryInputTokens,
971
- step.compactionResult.summaryOutputTokens,
972
- step.compactionResult.summaryModel,
973
- onEvent,
974
- "context_compactor",
1363
+ // Overflow reduction runs through the plugin pipeline. The default
1364
+ // middleware (`default-overflow-reduce`, registered at bootstrap)
1365
+ // contains the historical tier loop — forced compaction → tool-result
1366
+ // truncation → media stubbing → injection downgrade — plus the
1367
+ // re-inject/re-estimate convergence check. The callbacks below are
1368
+ // the orchestrator-specific side effects that the plugin coordinates
1369
+ // per iteration (activity emission, compaction application, runtime
1370
+ // injection reassembly, token re-estimation). Registered plugins that
1371
+ // wrap the `overflowReduce` slot see each iteration through their own
1372
+ // middleware `next` callback.
1373
+ const overflowArgs: OverflowReduceArgs = {
1374
+ messages: ctx.messages,
1375
+ runMessages,
1376
+ systemPrompt: ctx.systemPrompt,
1377
+ providerName: estimationProviderName,
1378
+ contextWindow: config.llm.default.contextWindow,
1379
+ preflightBudget,
1380
+ toolTokenBudget,
1381
+ maxAttempts: overflowRecovery.maxAttempts,
1382
+ abortSignal: abortController.signal,
1383
+ compactFn: async (msgs, signal, opts) =>
1384
+ // Route the reducer's forced-compaction tier through the
1385
+ // `compaction` pipeline so registered plugins observe these
1386
+ // invocations. Without this, custom compaction middleware only
1387
+ // sees the three orchestrator-owned call sites and misses the
1388
+ // reducer-initiated forced compactions entirely.
1389
+ (await runPipeline<CompactionArgs, CompactionResult>(
1390
+ "compaction",
1391
+ getMiddlewaresFor("compaction"),
1392
+ (args) =>
1393
+ defaultCompactionTerminal(
1394
+ args,
1395
+ buildPluginTurnContext(ctx, reqId),
1396
+ ),
1397
+ {
1398
+ messages: msgs,
1399
+ signal,
1400
+ options: opts,
1401
+ },
1402
+ buildPluginTurnContext(ctx, reqId),
1403
+ DEFAULT_TIMEOUTS.compaction,
1404
+ )) as Awaited<
1405
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
1406
+ >,
1407
+ emitActivityState: () => {
1408
+ ctx.emitActivityState(
1409
+ "thinking",
1410
+ "context_compacting",
1411
+ "assistant_turn",
975
1412
  reqId,
976
- step.compactionResult.summaryCacheCreationInputTokens ?? 0,
977
- step.compactionResult.summaryCacheReadInputTokens ?? 0,
978
- collapseRawResponses(step.compactionResult.summaryRawResponses),
979
- );
980
- ctx.graphMemory.onCompacted(
981
- step.compactionResult.compactedPersistedMessages,
982
1413
  );
983
- shouldInjectWorkspace = true;
984
- }
985
-
986
- // Re-inject with potentially downgraded injection mode.
987
- // When compaction ran it strips existing NOW.md / PKB blocks, so we
988
- // must re-inject the current content. Otherwise rely on the deduplicated
989
- // value from injectionOpts to avoid duplicate injection.
990
- runMessages = applyRuntimeInjections(ctx.messages, {
991
- ...injectionOpts,
992
- ...(step.compactionResult?.compacted && {
993
- pkbContext: currentPkbContent,
994
- }),
995
- ...(step.compactionResult?.compacted && {
996
- nowScratchpad: currentNowContent,
1414
+ },
1415
+ onCompactionResult: async (result) => {
1416
+ // Track circuit-breaker state whenever the reducer invoked
1417
+ // compaction. The reducer's forced_compaction tier uses
1418
+ // force:true, so it bypasses the open-circuit check, but we
1419
+ // still want failure tracking to detect a run of broken
1420
+ // summaries and clear the counter on success. Only track when
1421
+ // the summary LLM actually ran — `summaryFailed === undefined`
1422
+ // indicates an early return (no eligible messages,
1423
+ // truncation-only path, etc.) that shouldn't influence the
1424
+ // breaker.
1425
+ if (result.summaryFailed !== undefined) {
1426
+ await trackCompactionOutcome(ctx, result.summaryFailed, onEvent);
1427
+ }
1428
+ if (result.compacted) {
1429
+ applyCompactionResult(ctx, result, onEvent, reqId);
1430
+ shouldInjectWorkspace = true;
1431
+ }
1432
+ },
1433
+ reinjectForMode: async (
1434
+ reducedMessages,
1435
+ mode,
1436
+ stepCompacted,
1437
+ accumulatedCompacted,
1438
+ ) => {
1439
+ // Mirror the pre-PR-23 behavior: `ctx.messages` must track the
1440
+ // reducer's latest output before re-injection runs, because other
1441
+ // sites consulted through `injectionOpts` (`workspaceTopLevelContext`,
1442
+ // slack history, etc.) depend on it and `applyCompactionResult`
1443
+ // only updates `ctx.messages` on a compaction tier. Assigning here
1444
+ // keeps non-compaction tiers (tool-result truncation, media
1445
+ // stubbing, injection downgrade) observable to downstream
1446
+ // injection assembly on the same turn.
1447
+ ctx.messages = reducedMessages;
1448
+
1449
+ // When THIS iteration compacted, it stripped existing NOW.md /
1450
+ // PKB blocks — so we re-inject current content. A later iteration
1451
+ // that only truncates or downgrades must NOT re-force PKB/NOW,
1452
+ // or each round would grow the token count. Matches the
1453
+ // pre-PR-23 per-iteration `step.compactionResult?.compacted` gate.
1454
+ const injection = await applyRuntimeInjections(reducedMessages, {
1455
+ ...injectionOpts,
1456
+ ...(stepCompacted && { pkbContext: currentPkbContent }),
1457
+ ...(stepCompacted && { nowScratchpad: currentNowContent }),
1458
+ workspaceTopLevelContext: shouldInjectWorkspace
1459
+ ? ctx.workspaceTopLevelContext
1460
+ : null,
1461
+ // Once ANY iteration has compacted `ctx.messages`, the captured
1462
+ // `slackChronologicalMessages` snapshot (built from the full
1463
+ // persisted transcript) would overwrite the compacted history
1464
+ // and undo compaction. Suppress the override from here on —
1465
+ // sticky across subsequent non-compacting iterations.
1466
+ slackChronologicalMessages: accumulatedCompacted
1467
+ ? null
1468
+ : injectionOpts.slackChronologicalMessages,
1469
+ mode,
1470
+ turnContext: buildPluginTurnContext(ctx, reqId),
1471
+ });
1472
+ let next = injection.messages;
1473
+ if (isTrustedActor && mode !== "minimal") {
1474
+ const memResult = ctx.graphMemory.reinjectCachedMemory(next);
1475
+ next = memResult.runMessages;
1476
+ }
1477
+ return next;
1478
+ },
1479
+ estimatePostInjection: (runMsgs) =>
1480
+ estimatePromptTokens(runMsgs, ctx.systemPrompt, {
1481
+ providerName: estimationProviderName,
1482
+ toolTokenBudget,
997
1483
  }),
998
- workspaceTopLevelContext: shouldInjectWorkspace
999
- ? ctx.workspaceTopLevelContext
1000
- : null,
1001
- mode: currentInjectionMode,
1002
- });
1003
- if (isTrustedActor && currentInjectionMode !== "minimal") {
1004
- const memResult = ctx.graphMemory.reinjectCachedMemory(runMessages);
1005
- runMessages = memResult.runMessages;
1006
- }
1484
+ };
1007
1485
 
1008
- // Re-estimate with injections included — step.estimatedTokens was
1009
- // computed on bare history (ctx.messages) and doesn't account for
1010
- // tokens added by runtime injections.
1011
- const postInjectionTokens = estimatePromptTokens(
1012
- runMessages,
1013
- ctx.systemPrompt,
1014
- { providerName: ctx.provider.name, toolTokenBudget },
1015
- );
1486
+ const overflowResult = await runPipeline<
1487
+ OverflowReduceArgs,
1488
+ OverflowReduceResult
1489
+ >(
1490
+ "overflowReduce",
1491
+ getMiddlewaresFor("overflowReduce"),
1492
+ // Terminal only reached when every registered middleware calls
1493
+ // `next` and delegates past the innermost layer. The default plugin
1494
+ // is a terminal itself (it doesn't call `next`), so in practice
1495
+ // this fallback fires only when the default has been explicitly
1496
+ // deregistered (tests) and no user plugin replaces it. Strict-fail
1497
+ // semantics: throw so the missing terminal surfaces as a visible
1498
+ // error instead of silently returning the history untouched.
1499
+ async () => {
1500
+ throw new PluginExecutionError(
1501
+ "overflowReduce pipeline has no terminal handler — every reducer middleware called next() without providing a replacement",
1502
+ "overflowReduce",
1503
+ );
1504
+ },
1505
+ overflowArgs,
1506
+ buildPluginTurnContext(ctx, reqId),
1507
+ DEFAULT_TIMEOUTS.overflowReduce,
1508
+ );
1016
1509
 
1017
- if (postInjectionTokens <= preflightBudget) break;
1510
+ ctx.messages = overflowResult.messages;
1511
+ runMessages = overflowResult.runMessages;
1512
+ currentInjectionMode = overflowResult.injectionMode;
1513
+ reducerState = overflowResult.reducerState;
1514
+ if (overflowResult.reducerCompacted) {
1515
+ reducerCompacted = true;
1018
1516
  }
1019
1517
  }
1020
1518
 
1021
- // Pre-run repair
1519
+ // Pre-run repair — routed through the `historyRepair` plugin pipeline so
1520
+ // plugins can observe or override repair behavior. The default plugin's
1521
+ // middleware is a passthrough; the actual repair runs in the terminal
1522
+ // (`defaultHistoryRepairTerminal`).
1022
1523
  let preRepairMessages = runMessages;
1023
- const preRunRepair = repairHistory(runMessages);
1524
+ let preRunRepair: HistoryRepairResult | null = null;
1525
+ try {
1526
+ preRunRepair = await runPipeline<HistoryRepairArgs, HistoryRepairResult>(
1527
+ "historyRepair",
1528
+ getMiddlewaresFor("historyRepair"),
1529
+ async (args) => defaultHistoryRepairTerminal(args),
1530
+ { history: runMessages, provider: ctx.provider.name },
1531
+ buildPluginTurnContext(ctx, reqId),
1532
+ DEFAULT_TIMEOUTS.historyRepair,
1533
+ );
1534
+ } catch (err) {
1535
+ if (err instanceof PluginTimeoutError) {
1536
+ // Pipeline exceeded its budget — likely a misbehaving third-party
1537
+ // middleware. Degrade gracefully by proceeding with the un-repaired
1538
+ // history rather than turn-fatal-erroring; un-repaired history is
1539
+ // strictly better than no turn at all, and the provider call itself
1540
+ // will still error visibly if the drift is unrecoverable.
1541
+ rlog.warn(
1542
+ { err, phase: "pre_run" },
1543
+ "historyRepair pipeline timed out — proceeding with un-repaired history",
1544
+ );
1545
+ } else {
1546
+ throw err;
1547
+ }
1548
+ }
1024
1549
  if (
1025
- preRunRepair.stats.assistantToolResultsMigrated > 0 ||
1026
- preRunRepair.stats.missingToolResultsInserted > 0 ||
1027
- preRunRepair.stats.orphanToolResultsDowngraded > 0 ||
1028
- preRunRepair.stats.consecutiveSameRoleMerged > 0
1550
+ preRunRepair !== null &&
1551
+ (preRunRepair.stats.assistantToolResultsMigrated > 0 ||
1552
+ preRunRepair.stats.missingToolResultsInserted > 0 ||
1553
+ preRunRepair.stats.orphanToolResultsDowngraded > 0 ||
1554
+ preRunRepair.stats.consecutiveSameRoleMerged > 0)
1029
1555
  ) {
1030
1556
  rlog.warn(
1031
1557
  { phase: "pre_run", ...preRunRepair.stats },
@@ -1034,6 +1560,20 @@ export async function runAgentLoopImpl(
1034
1560
  runMessages = preRunRepair.messages;
1035
1561
  }
1036
1562
 
1563
+ // Replace historical web_search_tool_result blocks with text summaries.
1564
+ // The opaque `encrypted_content` tokens Anthropic attaches to each result
1565
+ // expire / are route-scoped; replaying a stale token is rejected with
1566
+ // `Invalid encrypted_content in search_result block`. Titles + URLs
1567
+ // preserve enough context for the model on follow-up turns.
1568
+ const webSearchStrip = stripHistoricalWebSearchResults(runMessages);
1569
+ if (webSearchStrip.stats.blocksStripped > 0) {
1570
+ rlog.info(
1571
+ { phase: "pre_run", ...webSearchStrip.stats },
1572
+ "Converted historical web_search_tool_result blocks to text summaries",
1573
+ );
1574
+ runMessages = webSearchStrip.messages;
1575
+ }
1576
+
1037
1577
  let preRunHistoryLength = runMessages.length;
1038
1578
 
1039
1579
  const shouldGenerateTitle = isReplaceableTitle(
@@ -1055,18 +1595,14 @@ export async function runAgentLoopImpl(
1055
1595
 
1056
1596
  let yieldedForBudget = false;
1057
1597
 
1058
- const onCheckpoint = (checkpoint: CheckpointInfo): CheckpointDecision => {
1059
- const turnTools = state.currentTurnToolNames;
1598
+ const onCheckpoint = async (
1599
+ checkpoint: CheckpointInfo,
1600
+ ): Promise<CheckpointDecision> => {
1060
1601
  state.currentTurnToolNames = [];
1061
1602
 
1062
1603
  if (ctx.canHandoffAtCheckpoint()) {
1063
- const inBrowserFlow =
1064
- turnTools.length > 0 &&
1065
- turnTools.every((n) => n.startsWith("browser_"));
1066
- if (!inBrowserFlow) {
1067
- yieldedForHandoff = true;
1068
- return "yield";
1069
- }
1604
+ yieldedForHandoff = true;
1605
+ return "yield";
1070
1606
  }
1071
1607
 
1072
1608
  // Mid-loop token budget check: estimate current context size and
@@ -1074,11 +1610,7 @@ export async function runAgentLoopImpl(
1074
1610
  // conversation-agent-loop run compaction before the provider rejects.
1075
1611
  if (overflowRecovery.enabled) {
1076
1612
  const midLoopThreshold = preflightBudget * 0.85;
1077
- const estimated = estimatePromptTokens(
1078
- checkpoint.history,
1079
- ctx.systemPrompt,
1080
- { providerName: ctx.provider.name, toolTokenBudget },
1081
- );
1613
+ const estimated = await runTokenEstimatePipeline(checkpoint.history);
1082
1614
  if (estimated > midLoopThreshold) {
1083
1615
  rlog.warn(
1084
1616
  { phase: "mid-loop", estimated, threshold: midLoopThreshold },
@@ -1094,7 +1626,15 @@ export async function runAgentLoopImpl(
1094
1626
 
1095
1627
  turnStarted = true;
1096
1628
 
1097
- let denyCompressionMessage: Message | null = null;
1629
+ rlog.info({ callSite: turnCallSite }, "Starting agent loop run");
1630
+
1631
+ // Thread the orchestrator's canonical per-turn context into the agent
1632
+ // loop so its internal pipeline invocations (llmCall, emptyResponse,
1633
+ // toolError, toolResultTruncate, toolExecute) see the real
1634
+ // conversation identity / trust / contextWindowManager instead of the
1635
+ // synthesized `"agent-loop"` placeholder. The loop clones this value
1636
+ // and overwrites `turnIndex` with its own tool-use iteration counter.
1637
+ const loopTurnCtx = buildPluginTurnContext(ctx, reqId);
1098
1638
 
1099
1639
  let updatedHistory = await ctx.agentLoop.run(
1100
1640
  runMessages,
@@ -1102,6 +1642,13 @@ export async function runAgentLoopImpl(
1102
1642
  abortController.signal,
1103
1643
  reqId,
1104
1644
  onCheckpoint,
1645
+ turnCallSite,
1646
+ loopTurnCtx,
1647
+ );
1648
+
1649
+ rlog.info(
1650
+ { resultMessageCount: updatedHistory.length },
1651
+ "Agent loop run completed",
1105
1652
  );
1106
1653
 
1107
1654
  // ── Proactive mid-loop compaction ───────────────────────────────
@@ -1129,6 +1676,14 @@ export async function runAgentLoopImpl(
1129
1676
  // so we compact the "raw" persistent messages.
1130
1677
  const rawHistory = stripInjectionsForCompaction(updatedHistory);
1131
1678
  ctx.messages = rawHistory;
1679
+ try {
1680
+ clearStrippedInjectionMetadataForConversation(ctx.conversationId);
1681
+ } catch (err) {
1682
+ rlog.warn(
1683
+ { err },
1684
+ "Failed to clear stripped-injection metadata after compaction strip (non-fatal)",
1685
+ );
1686
+ }
1132
1687
 
1133
1688
  ctx.emitActivityState(
1134
1689
  "thinking",
@@ -1137,56 +1692,61 @@ export async function runAgentLoopImpl(
1137
1692
  reqId,
1138
1693
  "Compacting context",
1139
1694
  );
1140
- const midLoopCompact = await ctx.contextWindowManager.maybeCompact(
1141
- ctx.messages,
1142
- abortController.signal,
1143
- {
1144
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1145
- force: true,
1146
- targetInputTokensOverride: preflightBudget,
1147
- },
1148
- );
1149
- if (midLoopCompact.compacted) {
1150
- ctx.messages = midLoopCompact.messages;
1151
- ctx.contextCompactedMessageCount +=
1152
- midLoopCompact.compactedPersistedMessages;
1153
- ctx.contextCompactedAt = Date.now();
1154
- updateConversationContextWindow(
1155
- ctx.conversationId,
1156
- midLoopCompact.summaryText,
1157
- ctx.contextCompactedMessageCount,
1158
- );
1159
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
1160
- enqueueAutoAnalysisOnCompaction(
1161
- ctx.conversationId,
1162
- ctx.trustContext?.trustClass,
1163
- );
1164
- onEvent({
1165
- type: "context_compacted",
1166
- previousEstimatedInputTokens:
1167
- midLoopCompact.previousEstimatedInputTokens,
1168
- estimatedInputTokens: midLoopCompact.estimatedInputTokens,
1169
- maxInputTokens: midLoopCompact.maxInputTokens,
1170
- thresholdTokens: midLoopCompact.thresholdTokens,
1171
- compactedMessages: midLoopCompact.compactedMessages,
1172
- summaryCalls: midLoopCompact.summaryCalls,
1173
- summaryInputTokens: midLoopCompact.summaryInputTokens,
1174
- summaryOutputTokens: midLoopCompact.summaryOutputTokens,
1175
- summaryModel: midLoopCompact.summaryModel,
1176
- });
1177
- emitUsage(
1695
+ let midLoopCompact: Awaited<
1696
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
1697
+ >;
1698
+ try {
1699
+ midLoopCompact = (await runPipeline<CompactionArgs, CompactionResult>(
1700
+ "compaction",
1701
+ getMiddlewaresFor("compaction"),
1702
+ (args) =>
1703
+ defaultCompactionTerminal(args, buildPluginTurnContext(ctx, reqId)),
1704
+ {
1705
+ messages: ctx.messages,
1706
+ signal: abortController.signal,
1707
+ options: {
1708
+ lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1709
+ force: true,
1710
+ targetInputTokensOverride: preflightBudget,
1711
+ conversationOriginChannel:
1712
+ getConversationOriginChannel(ctx.conversationId) ?? undefined,
1713
+ },
1714
+ },
1715
+ buildPluginTurnContext(ctx, reqId),
1716
+ DEFAULT_TIMEOUTS.compaction,
1717
+ )) as Awaited<ReturnType<typeof ctx.contextWindowManager.maybeCompact>>;
1718
+ } catch (err) {
1719
+ if (err instanceof PluginTimeoutError) {
1720
+ // Mid-loop compaction timed out. Record the failure for the
1721
+ // circuit breaker and escalate to the convergence loop's more
1722
+ // aggressive reducer tiers (tool-result truncation, media
1723
+ // stubbing, injection downgrade) by flipping the overflow flag
1724
+ // and breaking out of the mid-loop retry. The existing
1725
+ // "exhausted all attempts" block further down handles the
1726
+ // escalation.
1727
+ rlog.warn(
1728
+ { err, phase: "mid-loop-compact" },
1729
+ "Compaction pipeline timed out — escalating to convergence loop",
1730
+ );
1731
+ await trackCompactionOutcome(ctx, true, onEvent);
1732
+ state.contextTooLargeDetected = true;
1733
+ break;
1734
+ }
1735
+ throw err;
1736
+ }
1737
+ // `force: true` bypasses the cooldown/threshold gates but early returns
1738
+ // for "no eligible messages" / "insufficient messages" still leave
1739
+ // `summaryFailed` undefined. Only track when the summary LLM actually ran.
1740
+ if (midLoopCompact.summaryFailed !== undefined) {
1741
+ await trackCompactionOutcome(
1178
1742
  ctx,
1179
- midLoopCompact.summaryInputTokens,
1180
- midLoopCompact.summaryOutputTokens,
1181
- midLoopCompact.summaryModel,
1743
+ midLoopCompact.summaryFailed,
1182
1744
  onEvent,
1183
- "context_compactor",
1184
- reqId,
1185
- midLoopCompact.summaryCacheCreationInputTokens ?? 0,
1186
- midLoopCompact.summaryCacheReadInputTokens ?? 0,
1187
- collapseRawResponses(midLoopCompact.summaryRawResponses),
1188
1745
  );
1189
- ctx.graphMemory.onCompacted(midLoopCompact.compactedPersistedMessages);
1746
+ }
1747
+ if (midLoopCompact.compacted) {
1748
+ applyCompactionResult(ctx, midLoopCompact, onEvent, reqId);
1749
+ reducerCompacted = true;
1190
1750
  shouldInjectWorkspace = true;
1191
1751
  }
1192
1752
 
@@ -1194,18 +1754,34 @@ export async function runAgentLoopImpl(
1194
1754
  // stripInjectionsForCompaction() unconditionally removed the existing
1195
1755
  // NOW.md block from ctx.messages above, so we must always re-inject
1196
1756
  // the current content regardless of whether compaction actually ran.
1197
- runMessages = applyRuntimeInjections(ctx.messages, {
1757
+ const injection = await applyRuntimeInjections(ctx.messages, {
1198
1758
  ...injectionOpts,
1199
1759
  pkbContext: currentPkbContent,
1200
1760
  nowScratchpad: currentNowContent,
1201
1761
  workspaceTopLevelContext: shouldInjectWorkspace
1202
1762
  ? ctx.workspaceTopLevelContext
1203
1763
  : null,
1764
+ // Suppress the chronological-transcript snapshot once the reducer
1765
+ // has collapsed `ctx.messages`; the captured snapshot reflects the
1766
+ // full persisted transcript and would overwrite compaction.
1767
+ slackChronologicalMessages: reducerCompacted
1768
+ ? null
1769
+ : injectionOpts.slackChronologicalMessages,
1204
1770
  mode: currentInjectionMode,
1771
+ turnContext: buildPluginTurnContext(ctx, reqId),
1205
1772
  });
1773
+ runMessages = injection.messages;
1206
1774
  if (isTrustedActor && currentInjectionMode !== "minimal") {
1207
1775
  ctx.graphMemory.retrackCachedNodes();
1208
1776
  }
1777
+ const midLoopCompactStrip = stripHistoricalWebSearchResults(runMessages);
1778
+ if (midLoopCompactStrip.stats.blocksStripped > 0) {
1779
+ rlog.info(
1780
+ { phase: "mid-loop-compact", ...midLoopCompactStrip.stats },
1781
+ "Converted historical web_search_tool_result blocks to text summaries",
1782
+ );
1783
+ runMessages = midLoopCompactStrip.messages;
1784
+ }
1209
1785
  preRepairMessages = runMessages;
1210
1786
  preRunHistoryLength = runMessages.length;
1211
1787
 
@@ -1215,6 +1791,8 @@ export async function runAgentLoopImpl(
1215
1791
  abortController.signal,
1216
1792
  reqId,
1217
1793
  onCheckpoint,
1794
+ turnCallSite,
1795
+ loopTurnCtx,
1218
1796
  );
1219
1797
  }
1220
1798
 
@@ -1244,9 +1822,20 @@ export async function runAgentLoopImpl(
1244
1822
  { phase: "retry" },
1245
1823
  "Provider ordering error detected, attempting one-shot deep-repair retry",
1246
1824
  );
1825
+ // Design note: deep-repair intentionally bypasses the `historyRepair`
1826
+ // plugin pipeline. Deep-repair is a recovery-only path triggered by a
1827
+ // provider ordering error — it must be deterministic and unaffected by
1828
+ // user middleware that might have caused (or be unable to recover from)
1829
+ // the original drift. Plugins can already observe / override the
1830
+ // pre-run repair via the `historyRepair` pipeline above; widening that
1831
+ // surface to deep-repair is intentionally deferred until there's a
1832
+ // concrete plugin-level use case. Do not route this call through
1833
+ // `runPipeline` without first revisiting that contract.
1247
1834
  const retryRepair = deepRepairHistory(runMessages);
1248
1835
  runMessages = retryRepair.messages;
1249
- preRepairMessages = retryRepair.messages;
1836
+ const retryStrip = stripHistoricalWebSearchResults(runMessages);
1837
+ runMessages = retryStrip.messages;
1838
+ preRepairMessages = runMessages;
1250
1839
  preRunHistoryLength = runMessages.length;
1251
1840
  state.orderingErrorDetected = false;
1252
1841
  state.deferredOrderingError = null;
@@ -1257,6 +1846,8 @@ export async function runAgentLoopImpl(
1257
1846
  abortController.signal,
1258
1847
  reqId,
1259
1848
  onCheckpoint,
1849
+ turnCallSite,
1850
+ loopTurnCtx,
1260
1851
  );
1261
1852
 
1262
1853
  if (state.orderingErrorDetected) {
@@ -1270,8 +1861,7 @@ export async function runAgentLoopImpl(
1270
1861
  // ── Bounded context overflow convergence loop ──────────────────
1271
1862
  // When the provider rejects with context-too-large, iterate through
1272
1863
  // reducer tiers (forced compaction, tool-result truncation, media
1273
- // stubbing, injection downgrade) with optional approval gating for
1274
- // interactive latest-turn compression.
1864
+ // stubbing, injection downgrade).
1275
1865
  //
1276
1866
  // When progress was made (agent added messages before hitting the
1277
1867
  // limit), incorporate those new messages into ctx.messages so the
@@ -1286,6 +1876,14 @@ export async function runAgentLoopImpl(
1286
1876
 
1287
1877
  if (updatedHistory.length > preRunHistoryLength) {
1288
1878
  ctx.messages = stripInjectionsForCompaction(updatedHistory);
1879
+ try {
1880
+ clearStrippedInjectionMetadataForConversation(ctx.conversationId);
1881
+ } catch (err) {
1882
+ rlog.warn(
1883
+ { err },
1884
+ "Failed to clear stripped-injection metadata after compaction strip (non-fatal)",
1885
+ );
1886
+ }
1289
1887
  convergenceStripped = true;
1290
1888
  preRepairMessages = updatedHistory;
1291
1889
  preRunHistoryLength = updatedHistory.length;
@@ -1298,14 +1896,19 @@ export async function runAgentLoopImpl(
1298
1896
  // message (e.g. "242201 tokens > 200000"), use it to correct the
1299
1897
  // compaction target. The estimator may significantly underestimate
1300
1898
  // (e.g. estimated 185k but actual was 242k), so using the
1301
- // uncorrected preflightBudget would still be too high.
1899
+ // uncorrected preflightBudget would still be too high. Passes the raw
1900
+ // error so ContextOverflowError.actualTokens can short-circuit the
1901
+ // string-regex path for proxy-rewrapped untyped errors.
1302
1902
  const actualTokens = parseActualTokensFromError(
1303
- state.contextTooLargeErrorMessage,
1903
+ state.contextTooLargeError,
1304
1904
  );
1305
1905
  const estimatedTokensAtOverflow = estimatePromptTokens(
1306
1906
  ctx.messages,
1307
1907
  ctx.systemPrompt,
1308
- { providerName: ctx.provider.name, toolTokenBudget },
1908
+ {
1909
+ providerName: estimationProviderName,
1910
+ toolTokenBudget,
1911
+ },
1309
1912
  );
1310
1913
  let correctedTarget = preflightBudget;
1311
1914
  if (actualTokens && estimatedTokensAtOverflow > 0) {
@@ -1353,9 +1956,9 @@ export async function runAgentLoopImpl(
1353
1956
  const step = await reduceContextOverflow(
1354
1957
  ctx.messages,
1355
1958
  {
1356
- providerName: ctx.provider.name,
1959
+ providerName: estimationProviderName,
1357
1960
  systemPrompt: ctx.systemPrompt,
1358
- contextWindow: config.contextWindow,
1961
+ contextWindow: config.llm.default.contextWindow,
1359
1962
  targetTokens: correctedTarget,
1360
1963
  toolTokenBudget,
1361
1964
  },
@@ -1369,66 +1972,55 @@ export async function runAgentLoopImpl(
1369
1972
  ctx.messages = step.messages;
1370
1973
  currentInjectionMode = step.state.injectionMode;
1371
1974
 
1372
- if (step.compactionResult?.compacted) {
1373
- ctx.contextCompactedMessageCount +=
1374
- step.compactionResult.compactedPersistedMessages;
1375
- ctx.contextCompactedAt = Date.now();
1376
- updateConversationContextWindow(
1377
- ctx.conversationId,
1378
- step.compactionResult.summaryText,
1379
- ctx.contextCompactedMessageCount,
1380
- );
1381
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
1382
- enqueueAutoAnalysisOnCompaction(
1383
- ctx.conversationId,
1384
- ctx.trustContext?.trustClass,
1385
- );
1386
- onEvent({
1387
- type: "context_compacted",
1388
- previousEstimatedInputTokens:
1389
- step.compactionResult.previousEstimatedInputTokens,
1390
- estimatedInputTokens: step.compactionResult.estimatedInputTokens,
1391
- maxInputTokens: step.compactionResult.maxInputTokens,
1392
- thresholdTokens: step.compactionResult.thresholdTokens,
1393
- compactedMessages: step.compactionResult.compactedMessages,
1394
- summaryCalls: step.compactionResult.summaryCalls,
1395
- summaryInputTokens: step.compactionResult.summaryInputTokens,
1396
- summaryOutputTokens: step.compactionResult.summaryOutputTokens,
1397
- summaryModel: step.compactionResult.summaryModel,
1398
- });
1399
- emitUsage(
1975
+ // See the preflight reducer call above for rationale. Only track when
1976
+ // the summary LLM actually ran — `summaryFailed === undefined`
1977
+ // indicates the reducer's forced compaction took an early-return path
1978
+ // without calling the summary LLM.
1979
+ if (
1980
+ step.compactionResult &&
1981
+ step.compactionResult.summaryFailed !== undefined
1982
+ ) {
1983
+ await trackCompactionOutcome(
1400
1984
  ctx,
1401
- step.compactionResult.summaryInputTokens,
1402
- step.compactionResult.summaryOutputTokens,
1403
- step.compactionResult.summaryModel,
1985
+ step.compactionResult.summaryFailed,
1404
1986
  onEvent,
1405
- "context_compactor",
1406
- reqId,
1407
- step.compactionResult.summaryCacheCreationInputTokens ?? 0,
1408
- step.compactionResult.summaryCacheReadInputTokens ?? 0,
1409
- collapseRawResponses(step.compactionResult.summaryRawResponses),
1410
- );
1411
- ctx.graphMemory.onCompacted(
1412
- step.compactionResult.compactedPersistedMessages,
1413
1987
  );
1988
+ }
1989
+
1990
+ if (step.compactionResult?.compacted) {
1991
+ applyCompactionResult(ctx, step.compactionResult, onEvent, reqId);
1414
1992
  shouldInjectWorkspace = true;
1993
+ reducerCompacted = true;
1415
1994
  }
1416
1995
 
1417
1996
  // Only re-inject NOW.md when ctx.messages was actually stripped;
1418
1997
  // otherwise the existing NOW.md block is still present and
1419
1998
  // re-injecting would duplicate it.
1420
- runMessages = applyRuntimeInjections(ctx.messages, {
1999
+ const injection = await applyRuntimeInjections(ctx.messages, {
1421
2000
  ...injectionOpts,
1422
2001
  pkbContext: currentPkbContent,
1423
2002
  nowScratchpad: convergenceStripped ? currentNowContent : null,
1424
2003
  workspaceTopLevelContext: shouldInjectWorkspace
1425
2004
  ? ctx.workspaceTopLevelContext
1426
2005
  : null,
2006
+ slackChronologicalMessages: reducerCompacted
2007
+ ? null
2008
+ : injectionOpts.slackChronologicalMessages,
1427
2009
  mode: currentInjectionMode,
2010
+ turnContext: buildPluginTurnContext(ctx, reqId),
1428
2011
  });
2012
+ runMessages = injection.messages;
1429
2013
  if (isTrustedActor && currentInjectionMode !== "minimal") {
1430
2014
  ctx.graphMemory.retrackCachedNodes();
1431
2015
  }
2016
+ const convergenceStrip = stripHistoricalWebSearchResults(runMessages);
2017
+ if (convergenceStrip.stats.blocksStripped > 0) {
2018
+ rlog.info(
2019
+ { phase: "convergence", ...convergenceStrip.stats },
2020
+ "Converted historical web_search_tool_result blocks to text summaries",
2021
+ );
2022
+ runMessages = convergenceStrip.messages;
2023
+ }
1432
2024
  preRepairMessages = runMessages;
1433
2025
  preRunHistoryLength = runMessages.length;
1434
2026
  state.contextTooLargeDetected = false;
@@ -1440,6 +2032,8 @@ export async function runAgentLoopImpl(
1440
2032
  abortController.signal,
1441
2033
  reqId,
1442
2034
  onCheckpoint,
2035
+ turnCallSite,
2036
+ loopTurnCtx,
1443
2037
  );
1444
2038
 
1445
2039
  // If the rerun still yields at checkpoint, the turn is still
@@ -1461,6 +2055,14 @@ export async function runAgentLoopImpl(
1461
2055
  // pre-rerun messages.
1462
2056
  if (updatedHistory.length > preRunHistoryLength) {
1463
2057
  ctx.messages = stripInjectionsForCompaction(updatedHistory);
2058
+ try {
2059
+ clearStrippedInjectionMetadataForConversation(ctx.conversationId);
2060
+ } catch (err) {
2061
+ rlog.warn(
2062
+ { err },
2063
+ "Failed to clear stripped-injection metadata after compaction strip (non-fatal)",
2064
+ );
2065
+ }
1464
2066
  convergenceStripped = true;
1465
2067
  preRepairMessages = updatedHistory;
1466
2068
  preRunHistoryLength = updatedHistory.length;
@@ -1470,215 +2072,113 @@ export async function runAgentLoopImpl(
1470
2072
 
1471
2073
  // All reducer tiers exhausted but provider still rejects —
1472
2074
  // consult the overflow policy for latest-turn compression.
1473
- // Emergency compaction is deferred to the policy-gated paths below
1474
- // so that `request_user_approval` sessions collect consent first.
2075
+ // The policy either auto-compresses the latest turn or falls
2076
+ // through to the final graceful-error fallback below.
1475
2077
  if (state.contextTooLargeDetected) {
1476
2078
  const action = resolveOverflowAction({
1477
2079
  overflowRecovery,
1478
2080
  isInteractive: isInteractiveResolved,
1479
2081
  });
1480
2082
 
1481
- if (action === "request_user_approval") {
1482
- const approval = await requestCompressionApproval(ctx.prompter, {
1483
- signal: abortController.signal,
1484
- });
1485
-
1486
- if (approval.approved) {
1487
- // User approved — force emergency compaction with aggressive settings
1488
- const emergencyCompact =
1489
- await ctx.contextWindowManager.maybeCompact(
1490
- ctx.messages,
1491
- abortController.signal,
1492
- {
2083
+ if (action === "auto_compress_latest_turn") {
2084
+ // Auto-compress without asking — users opt out via the "drop" policy.
2085
+ ctx.emitActivityState(
2086
+ "thinking",
2087
+ "context_compacting",
2088
+ "assistant_turn",
2089
+ reqId,
2090
+ );
2091
+ let emergencyCompact: Awaited<
2092
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
2093
+ > | null = null;
2094
+ try {
2095
+ emergencyCompact = (await runPipeline<
2096
+ CompactionArgs,
2097
+ CompactionResult
2098
+ >(
2099
+ "compaction",
2100
+ getMiddlewaresFor("compaction"),
2101
+ (args) =>
2102
+ defaultCompactionTerminal(
2103
+ args,
2104
+ buildPluginTurnContext(ctx, reqId),
2105
+ ),
2106
+ {
2107
+ messages: ctx.messages,
2108
+ signal: abortController.signal,
2109
+ options: {
1493
2110
  lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1494
2111
  force: true,
1495
2112
  minKeepRecentUserTurns: 0,
1496
2113
  targetInputTokensOverride: correctedTarget,
1497
2114
  },
2115
+ },
2116
+ buildPluginTurnContext(ctx, reqId),
2117
+ DEFAULT_TIMEOUTS.compaction,
2118
+ )) as Awaited<
2119
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
2120
+ >;
2121
+ } catch (err) {
2122
+ if (err instanceof PluginTimeoutError) {
2123
+ // Emergency compaction timed out. Record the circuit-breaker
2124
+ // failure and fall through to the graceful-error path below
2125
+ // (the unsuccessful-compaction fallback) rather than hard-
2126
+ // failing the turn.
2127
+ rlog.warn(
2128
+ { err, phase: "emergency-compaction" },
2129
+ "Emergency compaction pipeline timed out — continuing with overflow fallback",
1498
2130
  );
1499
- if (emergencyCompact.compacted) {
1500
- ctx.messages = emergencyCompact.messages;
1501
- ctx.contextCompactedMessageCount +=
1502
- emergencyCompact.compactedPersistedMessages;
1503
- ctx.contextCompactedAt = Date.now();
1504
- updateConversationContextWindow(
1505
- ctx.conversationId,
1506
- emergencyCompact.summaryText,
1507
- ctx.contextCompactedMessageCount,
1508
- );
1509
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
1510
- enqueueAutoAnalysisOnCompaction(
1511
- ctx.conversationId,
1512
- ctx.trustContext?.trustClass,
1513
- );
1514
- onEvent({
1515
- type: "context_compacted",
1516
- previousEstimatedInputTokens:
1517
- emergencyCompact.previousEstimatedInputTokens,
1518
- estimatedInputTokens: emergencyCompact.estimatedInputTokens,
1519
- maxInputTokens: emergencyCompact.maxInputTokens,
1520
- thresholdTokens: emergencyCompact.thresholdTokens,
1521
- compactedMessages: emergencyCompact.compactedMessages,
1522
- summaryCalls: emergencyCompact.summaryCalls,
1523
- summaryInputTokens: emergencyCompact.summaryInputTokens,
1524
- summaryOutputTokens: emergencyCompact.summaryOutputTokens,
1525
- summaryModel: emergencyCompact.summaryModel,
1526
- });
1527
- emitUsage(
1528
- ctx,
1529
- emergencyCompact.summaryInputTokens,
1530
- emergencyCompact.summaryOutputTokens,
1531
- emergencyCompact.summaryModel,
1532
- onEvent,
1533
- "context_compactor",
1534
- reqId,
1535
- emergencyCompact.summaryCacheCreationInputTokens ?? 0,
1536
- emergencyCompact.summaryCacheReadInputTokens ?? 0,
1537
- collapseRawResponses(emergencyCompact.summaryRawResponses),
1538
- );
1539
- ctx.graphMemory.onCompacted(
1540
- emergencyCompact.compactedPersistedMessages,
1541
- );
1542
- shouldInjectWorkspace = true;
2131
+ await trackCompactionOutcome(ctx, true, onEvent);
2132
+ emergencyCompact = null;
2133
+ } else {
2134
+ throw err;
1543
2135
  }
1544
-
1545
- // Only re-inject NOW.md when ctx.messages was actually stripped;
1546
- // otherwise the existing block is still present.
1547
- runMessages = applyRuntimeInjections(ctx.messages, {
1548
- ...injectionOpts,
1549
- pkbContext: currentPkbContent,
1550
- nowScratchpad: convergenceStripped ? currentNowContent : null,
1551
- workspaceTopLevelContext: shouldInjectWorkspace
1552
- ? ctx.workspaceTopLevelContext
1553
- : null,
1554
- mode: currentInjectionMode,
1555
- });
1556
- if (isTrustedActor && currentInjectionMode !== "minimal") {
1557
- ctx.graphMemory.retrackCachedNodes();
1558
- }
1559
- preRepairMessages = runMessages;
1560
- preRunHistoryLength = runMessages.length;
1561
- state.contextTooLargeDetected = false;
1562
-
1563
- updatedHistory = await ctx.agentLoop.run(
1564
- runMessages,
1565
- eventHandler,
1566
- abortController.signal,
1567
- reqId,
1568
- onCheckpoint,
1569
- );
1570
- } else {
1571
- // User denied compression — emit a graceful assistant explanation
1572
- // instead of a conversation_error, and end the turn cleanly.
1573
- state.contextTooLargeDetected = false;
1574
- const denyText =
1575
- "The conversation has grown too long for the model to process, " +
1576
- "and compression was declined. Please start a new conversation " +
1577
- "or manually shorten the conversation to continue.";
1578
- const loopChannelMeta = {
1579
- ...provenanceFromTrustContext(ctx.trustContext),
1580
- userMessageChannel: capturedTurnChannelContext.userMessageChannel,
1581
- assistantMessageChannel:
1582
- capturedTurnChannelContext.assistantMessageChannel,
1583
- userMessageInterface:
1584
- capturedTurnInterfaceContext.userMessageInterface,
1585
- assistantMessageInterface:
1586
- capturedTurnInterfaceContext.assistantMessageInterface,
1587
- };
1588
- const denyMessage = createAssistantMessage(denyText);
1589
- await addMessage(
1590
- ctx.conversationId,
1591
- "assistant",
1592
- JSON.stringify(denyMessage.content),
1593
- loopChannelMeta,
1594
- );
1595
- denyCompressionMessage = denyMessage;
1596
- onEvent({
1597
- type: "assistant_text_delta",
1598
- text: denyText,
1599
- conversationId: ctx.conversationId,
1600
- });
1601
- // Prevent the final error fallback from firing
1602
- state.providerErrorUserMessage = null;
1603
2136
  }
1604
- } else if (action === "auto_compress_latest_turn") {
1605
- // Non-interactive auto-compress without asking
1606
- ctx.emitActivityState(
1607
- "thinking",
1608
- "context_compacting",
1609
- "assistant_turn",
1610
- reqId,
1611
- );
1612
- const emergencyCompact = await ctx.contextWindowManager.maybeCompact(
1613
- ctx.messages,
1614
- abortController.signal,
1615
- {
1616
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1617
- force: true,
1618
- minKeepRecentUserTurns: 0,
1619
- targetInputTokensOverride: correctedTarget,
1620
- },
1621
- );
1622
- if (emergencyCompact.compacted) {
1623
- ctx.messages = emergencyCompact.messages;
1624
- ctx.contextCompactedMessageCount +=
1625
- emergencyCompact.compactedPersistedMessages;
1626
- ctx.contextCompactedAt = Date.now();
1627
- updateConversationContextWindow(
1628
- ctx.conversationId,
1629
- emergencyCompact.summaryText,
1630
- ctx.contextCompactedMessageCount,
1631
- );
1632
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
1633
- enqueueAutoAnalysisOnCompaction(
1634
- ctx.conversationId,
1635
- ctx.trustContext?.trustClass,
1636
- );
1637
- onEvent({
1638
- type: "context_compacted",
1639
- previousEstimatedInputTokens:
1640
- emergencyCompact.previousEstimatedInputTokens,
1641
- estimatedInputTokens: emergencyCompact.estimatedInputTokens,
1642
- maxInputTokens: emergencyCompact.maxInputTokens,
1643
- thresholdTokens: emergencyCompact.thresholdTokens,
1644
- compactedMessages: emergencyCompact.compactedMessages,
1645
- summaryCalls: emergencyCompact.summaryCalls,
1646
- summaryInputTokens: emergencyCompact.summaryInputTokens,
1647
- summaryOutputTokens: emergencyCompact.summaryOutputTokens,
1648
- summaryModel: emergencyCompact.summaryModel,
1649
- });
1650
- emitUsage(
2137
+ // Only track when the summary LLM actually ran; `force: true`
2138
+ // bypasses the cooldown but not the early-return paths.
2139
+ if (
2140
+ emergencyCompact &&
2141
+ emergencyCompact.summaryFailed !== undefined
2142
+ ) {
2143
+ await trackCompactionOutcome(
1651
2144
  ctx,
1652
- emergencyCompact.summaryInputTokens,
1653
- emergencyCompact.summaryOutputTokens,
1654
- emergencyCompact.summaryModel,
2145
+ emergencyCompact.summaryFailed,
1655
2146
  onEvent,
1656
- "context_compactor",
1657
- reqId,
1658
- emergencyCompact.summaryCacheCreationInputTokens ?? 0,
1659
- emergencyCompact.summaryCacheReadInputTokens ?? 0,
1660
- collapseRawResponses(emergencyCompact.summaryRawResponses),
1661
- );
1662
- ctx.graphMemory.onCompacted(
1663
- emergencyCompact.compactedPersistedMessages,
1664
2147
  );
2148
+ }
2149
+ if (emergencyCompact?.compacted) {
2150
+ applyCompactionResult(ctx, emergencyCompact, onEvent, reqId);
2151
+ reducerCompacted = true;
1665
2152
  shouldInjectWorkspace = true;
1666
2153
  }
1667
2154
 
1668
2155
  // Only re-inject NOW.md when ctx.messages was actually stripped;
1669
2156
  // otherwise the existing block is still present.
1670
- runMessages = applyRuntimeInjections(ctx.messages, {
2157
+ const injection = await applyRuntimeInjections(ctx.messages, {
1671
2158
  ...injectionOpts,
1672
2159
  pkbContext: currentPkbContent,
1673
2160
  nowScratchpad: convergenceStripped ? currentNowContent : null,
1674
2161
  workspaceTopLevelContext: shouldInjectWorkspace
1675
2162
  ? ctx.workspaceTopLevelContext
1676
2163
  : null,
2164
+ slackChronologicalMessages: reducerCompacted
2165
+ ? null
2166
+ : injectionOpts.slackChronologicalMessages,
1677
2167
  mode: currentInjectionMode,
2168
+ turnContext: buildPluginTurnContext(ctx, reqId),
1678
2169
  });
2170
+ runMessages = injection.messages;
1679
2171
  if (isTrustedActor && currentInjectionMode !== "minimal") {
1680
2172
  ctx.graphMemory.retrackCachedNodes();
1681
2173
  }
2174
+ const fallbackStrip = stripHistoricalWebSearchResults(runMessages);
2175
+ if (fallbackStrip.stats.blocksStripped > 0) {
2176
+ rlog.info(
2177
+ { phase: "fail_gracefully_compact", ...fallbackStrip.stats },
2178
+ "Converted historical web_search_tool_result blocks to text summaries",
2179
+ );
2180
+ runMessages = fallbackStrip.messages;
2181
+ }
1682
2182
  preRepairMessages = runMessages;
1683
2183
  preRunHistoryLength = runMessages.length;
1684
2184
  state.contextTooLargeDetected = false;
@@ -1689,6 +2189,8 @@ export async function runAgentLoopImpl(
1689
2189
  abortController.signal,
1690
2190
  reqId,
1691
2191
  onCheckpoint,
2192
+ turnCallSite,
2193
+ loopTurnCtx,
1692
2194
  );
1693
2195
  }
1694
2196
  // action === "fail_gracefully" falls through to the final error below
@@ -1753,11 +2255,19 @@ export async function runAgentLoopImpl(
1753
2255
  assistantMessageInterface:
1754
2256
  capturedTurnInterfaceContext.assistantMessageInterface,
1755
2257
  };
1756
- await addMessage(
1757
- ctx.conversationId,
1758
- "user",
1759
- JSON.stringify(toolResultBlocks),
1760
- toolResultMetadata,
2258
+ await runPipeline<PersistArgs, PersistResult>(
2259
+ "persistence",
2260
+ getMiddlewaresFor("persistence"),
2261
+ defaultPersistenceTerminal,
2262
+ {
2263
+ op: "add",
2264
+ conversationId: ctx.conversationId,
2265
+ role: "user",
2266
+ content: JSON.stringify(toolResultBlocks),
2267
+ metadata: toolResultMetadata,
2268
+ },
2269
+ buildPluginTurnContext(ctx, reqId),
2270
+ DEFAULT_TIMEOUTS.persistence,
1761
2271
  );
1762
2272
  state.pendingToolResults.clear();
1763
2273
  }
@@ -1770,10 +2280,6 @@ export async function runAgentLoopImpl(
1770
2280
  return { ...msg, content: cleanedBlocks };
1771
2281
  });
1772
2282
 
1773
- if (denyCompressionMessage) {
1774
- newMessages.push(denyCompressionMessage);
1775
- }
1776
-
1777
2283
  const hasAssistantResponse = newMessages.some(
1778
2284
  (msg) => msg.role === "assistant",
1779
2285
  );
@@ -1795,11 +2301,19 @@ export async function runAgentLoopImpl(
1795
2301
  const errorAssistantMessage = createAssistantMessage(
1796
2302
  state.providerErrorUserMessage,
1797
2303
  );
1798
- await addMessage(
1799
- ctx.conversationId,
1800
- "assistant",
1801
- JSON.stringify(errorAssistantMessage.content),
1802
- errChannelMeta,
2304
+ await runPipeline<PersistArgs, PersistResult>(
2305
+ "persistence",
2306
+ getMiddlewaresFor("persistence"),
2307
+ defaultPersistenceTerminal,
2308
+ {
2309
+ op: "add",
2310
+ conversationId: ctx.conversationId,
2311
+ role: "assistant",
2312
+ content: JSON.stringify(errorAssistantMessage.content),
2313
+ metadata: errChannelMeta,
2314
+ },
2315
+ buildPluginTurnContext(ctx, reqId),
2316
+ DEFAULT_TIMEOUTS.persistence,
1803
2317
  );
1804
2318
  newMessages.push(errorAssistantMessage);
1805
2319
  // Do NOT send assistant_text_delta here — handleProviderError already
@@ -1863,14 +2377,10 @@ export async function runAgentLoopImpl(
1863
2377
  state.exchangeLlmCallCount,
1864
2378
  {
1865
2379
  tokens: state.lastCallInputTokens,
1866
- maxTokens: config.contextWindow.maxInputTokens,
2380
+ maxTokens: config.llm.default.contextWindow.maxInputTokens,
1867
2381
  },
1868
2382
  );
1869
2383
 
1870
- void getHookManager().trigger("post-message", {
1871
- conversationId: ctx.conversationId,
1872
- });
1873
-
1874
2384
  const syncLastAssistantMessageToDisk = (): void => {
1875
2385
  if (!state.lastAssistantMessageId) return;
1876
2386
  const convForDisk = getConversation(ctx.conversationId);
@@ -1987,13 +2497,65 @@ export async function runAgentLoopImpl(
1987
2497
  ? { messageId: state.lastAssistantMessageId }
1988
2498
  : {}),
1989
2499
  });
2500
+
2501
+ // Emit a home-feed event for background/scheduled conversation completions.
2502
+ // Scoped to message_complete only (not cancelled/handoff), wrapped in
2503
+ // try-catch so malformed message content can never propagate errors.
2504
+ try {
2505
+ const conv = getConversation(ctx.conversationId);
2506
+ if (
2507
+ conv &&
2508
+ (conv.conversationType === "background" ||
2509
+ conv.conversationType === "scheduled")
2510
+ ) {
2511
+ const lastMsg = state.lastAssistantMessageId
2512
+ ? getMessageById(state.lastAssistantMessageId, ctx.conversationId)
2513
+ : undefined;
2514
+ let summary: string;
2515
+ if (lastMsg) {
2516
+ const parsed: unknown = JSON.parse(lastMsg.content);
2517
+ if (typeof parsed === "string") {
2518
+ summary = parsed.slice(0, 200);
2519
+ } else if (Array.isArray(parsed)) {
2520
+ const textBlock = parsed.find(
2521
+ (b: { type?: string }) => b.type === "text",
2522
+ );
2523
+ summary =
2524
+ typeof textBlock?.text === "string"
2525
+ ? textBlock.text.slice(0, 200)
2526
+ : (conv.title ?? "Background task completed.");
2527
+ } else {
2528
+ summary = conv.title ?? "Background task completed.";
2529
+ }
2530
+ } else {
2531
+ summary = conv.title ?? "Background task completed.";
2532
+ }
2533
+ void emitFeedEvent({
2534
+ source: "assistant",
2535
+ title: conv.title ?? "Background Task",
2536
+ summary,
2537
+ dedupKey: `bg-conv:${ctx.conversationId}`,
2538
+ }).catch((err) => {
2539
+ log.warn(
2540
+ { err, conversationId: ctx.conversationId },
2541
+ "Failed to emit background conversation feed event",
2542
+ );
2543
+ });
2544
+ }
2545
+ } catch (feedErr) {
2546
+ log.warn(
2547
+ { err: feedErr, conversationId: ctx.conversationId },
2548
+ "Failed to build home-feed event for background conversation",
2549
+ );
2550
+ }
1990
2551
  }
1991
2552
  }
1992
2553
 
1993
2554
  // Second title pass: after 3 completed turns, re-generate the title
1994
2555
  // using the last 3 messages for better context. Only fires when the
1995
- // current title was auto-generated (isAutoTitle = 1).
1996
- if (ctx.turnCount === 2) {
2556
+ // current title was auto-generated (isAutoTitle = 1) and the user
2557
+ // has not opted out via `conversations.skipAutoRetitling`.
2558
+ if (ctx.turnCount === 2 && !getConfig().conversations.skipAutoRetitling) {
1997
2559
  // turnCount is 0-indexed, incremented in finally; 2 = about to become 3rd turn
1998
2560
  queueRegenerateConversationTitle({
1999
2561
  conversationId: ctx.conversationId,
@@ -2046,12 +2608,6 @@ export async function runAgentLoopImpl(
2046
2608
  });
2047
2609
  onEvent({ type: "error", message: classified.userMessage });
2048
2610
  onEvent(buildConversationErrorMessage(ctx.conversationId, classified));
2049
- void getHookManager().trigger("on-error", {
2050
- error: err instanceof Error ? err.name : "Error",
2051
- message,
2052
- stack: err instanceof Error ? err.stack : undefined,
2053
- conversationId: ctx.conversationId,
2054
- });
2055
2611
  }
2056
2612
  } finally {
2057
2613
  if (turnStarted) {
@@ -2095,6 +2651,7 @@ export async function runAgentLoopImpl(
2095
2651
  ctx.processing = false;
2096
2652
  ctx.onConfirmationOutcome = undefined;
2097
2653
  ctx.surfaceActionRequestIds.delete(ctx.currentRequestId ?? "");
2654
+ ctx.approvedViaPromptThisTurn = false;
2098
2655
  ctx.currentRequestId = undefined;
2099
2656
  ctx.currentActiveSurfaceId = undefined;
2100
2657
  ctx.allowedToolNames = undefined;
@@ -2160,7 +2717,133 @@ function emitUsage(
2160
2717
  );
2161
2718
  }
2162
2719
 
2163
- function collapseRawResponses(rawResponses?: unknown[]): unknown | undefined {
2720
+ /**
2721
+ * Minimal context shape consumed by `applyCompactionResult`. Both
2722
+ * `AgentLoopConversationContext` and `Conversation` satisfy this via structural
2723
+ * typing, so the helper can back both the 5 agent-loop auto-compaction sites
2724
+ * and the single `forceCompact` user-initiated site.
2725
+ */
2726
+ export interface CompactionApplyContext {
2727
+ readonly conversationId: string;
2728
+ messages: Message[];
2729
+ contextCompactedMessageCount: number;
2730
+ contextCompactedAt: number | null;
2731
+ readonly graphMemory: ConversationGraphMemory;
2732
+ readonly provider: Provider;
2733
+ usageStats: UsageStats;
2734
+ trustContext?: TrustContext;
2735
+ }
2736
+
2737
+ /**
2738
+ * Applies a successful `ContextWindowResult` to a conversation: updates the
2739
+ * in-memory message buffer and compaction counters, notifies the graph memory
2740
+ * and conversation-summary store, enqueues auto-analysis, emits the
2741
+ * `context_compacted` event, and records a `context_compactor` usage event.
2742
+ *
2743
+ * The emitted `usage_update` intentionally omits `contextWindow` — the
2744
+ * `context_compacted` event already carries the fresh
2745
+ * `estimatedInputTokens` / `maxInputTokens` and is the single source of
2746
+ * truth for the UI indicator after compaction. Emitting both caused a
2747
+ * redundant SwiftUI invalidation on every compaction.
2748
+ */
2749
+ export function applyCompactionResult(
2750
+ ctx: CompactionApplyContext,
2751
+ result: {
2752
+ messages: Message[];
2753
+ compactedPersistedMessages: number;
2754
+ previousEstimatedInputTokens: number;
2755
+ estimatedInputTokens: number;
2756
+ maxInputTokens: number;
2757
+ thresholdTokens: number;
2758
+ compactedMessages: number;
2759
+ summaryCalls: number;
2760
+ summaryInputTokens: number;
2761
+ summaryOutputTokens: number;
2762
+ summaryModel: string;
2763
+ summaryText: string;
2764
+ summaryCacheCreationInputTokens?: number;
2765
+ summaryCacheReadInputTokens?: number;
2766
+ summaryRawResponses?: unknown[];
2767
+ },
2768
+ onEvent: (msg: ServerMessage) => void,
2769
+ reqId: string | null,
2770
+ ): void {
2771
+ ctx.messages = result.messages;
2772
+ ctx.contextCompactedMessageCount += result.compactedPersistedMessages;
2773
+ ctx.contextCompactedAt = Date.now();
2774
+ ctx.graphMemory.onCompacted(result.compactedPersistedMessages);
2775
+ updateConversationContextWindow(
2776
+ ctx.conversationId,
2777
+ result.summaryText,
2778
+ ctx.contextCompactedMessageCount,
2779
+ );
2780
+ enqueueAutoAnalysisOnCompaction(
2781
+ ctx.conversationId,
2782
+ ctx.trustContext?.trustClass,
2783
+ );
2784
+ const summarySignals = computeSummaryQualitySignals(result.summaryText);
2785
+ onEvent({
2786
+ type: "context_compacted",
2787
+ conversationId: ctx.conversationId,
2788
+ previousEstimatedInputTokens: result.previousEstimatedInputTokens,
2789
+ estimatedInputTokens: result.estimatedInputTokens,
2790
+ maxInputTokens: result.maxInputTokens,
2791
+ thresholdTokens: result.thresholdTokens,
2792
+ compactedMessages: result.compactedMessages,
2793
+ summaryCalls: result.summaryCalls,
2794
+ summaryInputTokens: result.summaryInputTokens,
2795
+ summaryOutputTokens: result.summaryOutputTokens,
2796
+ summaryModel: result.summaryModel,
2797
+ summaryCharCount: summarySignals.charCount,
2798
+ summaryHeaderCount: summarySignals.headerCount,
2799
+ summaryHadMemoryEcho: summarySignals.hadMemoryEcho,
2800
+ });
2801
+ emitUsage(
2802
+ ctx,
2803
+ result.summaryInputTokens,
2804
+ result.summaryOutputTokens,
2805
+ result.summaryModel,
2806
+ onEvent,
2807
+ "context_compactor",
2808
+ reqId,
2809
+ result.summaryCacheCreationInputTokens ?? 0,
2810
+ result.summaryCacheReadInputTokens ?? 0,
2811
+ collapseRawResponses(result.summaryRawResponses),
2812
+ undefined /* providerName */,
2813
+ 1 /* llmCallCount */,
2814
+ );
2815
+ }
2816
+
2817
+ export function collapseRawResponses(
2818
+ rawResponses?: unknown[],
2819
+ ): unknown | undefined {
2164
2820
  if (!rawResponses || rawResponses.length === 0) return undefined;
2165
2821
  return rawResponses.length === 1 ? rawResponses[0] : rawResponses;
2166
2822
  }
2823
+
2824
+ /**
2825
+ * Matches any runtime-injection tag that should never appear inside a
2826
+ * generated summary. If the regex hits, either the compaction strip logic
2827
+ * failed to drop an injected block from the summarizer input, or the
2828
+ * summarizer invented tag-like text on its own — both are quality bugs
2829
+ * worth surfacing via telemetry.
2830
+ */
2831
+ const SUMMARY_MEMORY_ECHO_PATTERN =
2832
+ /<(?:memory|memory_context|memory_image|turn_context|workspace|workspace_top_level|knowledge_base|pkb|system_reminder|now_scratchpad|NOW\.md|active_thread|active_subagents|active_workspace|active_dynamic_page|channel_capabilities|transport_hints|system_notice|non_interactive_context|temporal_context|guardian_context|inbound_actor_context|channel_turn_context|interface_turn_context|channel_command_context|voice_call_control)\b/i;
2833
+
2834
+ /**
2835
+ * Compute light-weight quality signals for a compaction summary. Emitted
2836
+ * on every `context_compacted` event so regressions (short outputs,
2837
+ * header collapse, memory-injection leakage) are visible without having
2838
+ * to read the summary text from the DB.
2839
+ */
2840
+ export function computeSummaryQualitySignals(summaryText: string): {
2841
+ charCount: number;
2842
+ headerCount: number;
2843
+ hadMemoryEcho: boolean;
2844
+ } {
2845
+ const charCount = summaryText.length;
2846
+ const headerCount = (summaryText.match(/^## /gm) ?? []).length;
2847
+ const hadMemoryEcho = SUMMARY_MEMORY_ECHO_PATTERN.test(summaryText);
2848
+ return { charCount, headerCount, hadMemoryEcho };
2849
+ }