@vellumai/assistant 0.8.8 → 0.8.9-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/examples/plugins/echo/README.md +61 -60
  4. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  5. package/examples/plugins/echo/hooks/stop.ts +16 -0
  6. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  7. package/examples/plugins/echo/package.json +1 -2
  8. package/examples/plugins/echo/src/emit.ts +19 -0
  9. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  10. package/openapi.yaml +235 -6
  11. package/package.json +2 -2
  12. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  13. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  14. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  15. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  17. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  18. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  19. package/src/__tests__/agent-loop.test.ts +441 -96
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  21. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  22. package/src/__tests__/anthropic-provider.test.ts +1 -1
  23. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  24. package/src/__tests__/app-control-flow.test.ts +1 -1
  25. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  26. package/src/__tests__/app-executors.test.ts +132 -0
  27. package/src/__tests__/approval-cascade.test.ts +5 -4
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  30. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  31. package/src/__tests__/channel-approvals.test.ts +1 -1
  32. package/src/__tests__/compaction-circuit.test.ts +258 -0
  33. package/src/__tests__/compaction-direct.test.ts +132 -0
  34. package/src/__tests__/compaction-events.test.ts +5 -5
  35. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  36. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  37. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  38. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  39. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  40. package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
  41. package/src/__tests__/conversation-agent-loop.test.ts +297 -586
  42. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  44. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  45. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  46. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  47. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
  49. package/src/__tests__/conversation-queue.test.ts +9 -9
  50. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  51. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  52. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  53. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  54. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  55. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  56. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  57. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  59. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  61. package/src/__tests__/db-acp-history.test.ts +101 -0
  62. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  63. package/src/__tests__/empty-response-hook.test.ts +1 -1
  64. package/src/__tests__/file-write-tool.test.ts +63 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  66. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  67. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  68. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  69. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/heartbeat-service.test.ts +1 -0
  71. package/src/__tests__/history-repair-hook.test.ts +1 -1
  72. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  73. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  74. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  75. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  76. package/src/__tests__/injector-background-turn.test.ts +13 -23
  77. package/src/__tests__/injector-chain.test.ts +268 -44
  78. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  79. package/src/__tests__/injector-document-comments.test.ts +97 -114
  80. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  81. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  82. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  83. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  84. package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
  85. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  86. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  87. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  88. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  89. package/src/__tests__/plugin-registry.test.ts +3 -49
  90. package/src/__tests__/plugin-types.test.ts +0 -70
  91. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  92. package/src/__tests__/reaction-persistence.test.ts +1 -1
  93. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  94. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  95. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  96. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  97. package/src/__tests__/subagent-detail.test.ts +25 -7
  98. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  99. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  100. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  101. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  102. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  103. package/src/__tests__/title-generate-hook.test.ts +1 -1
  104. package/src/__tests__/tool-error-hook.test.ts +1 -1
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  106. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  107. package/src/acp/__tests__/agent-process.test.ts +161 -0
  108. package/src/acp/__tests__/client-handler.test.ts +40 -0
  109. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  110. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  111. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  112. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  113. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  114. package/src/acp/agent-process.ts +61 -1
  115. package/src/acp/auto-install.test.ts +280 -0
  116. package/src/acp/auto-install.ts +232 -0
  117. package/src/acp/client-handler.ts +31 -0
  118. package/src/acp/feature-gate.test.ts +48 -0
  119. package/src/acp/feature-gate.ts +34 -0
  120. package/src/acp/prepare-agent-env.ts +80 -27
  121. package/src/acp/resolve-agent.test.ts +225 -9
  122. package/src/acp/resolve-agent.ts +122 -17
  123. package/src/acp/resume-hint.ts +23 -0
  124. package/src/acp/session-manager.ts +507 -73
  125. package/src/agent/compaction-circuit.ts +60 -102
  126. package/src/agent/loop.ts +414 -248
  127. package/src/api/responses/conversation-message.ts +14 -1
  128. package/src/approvals/guardian-request-resolvers.ts +1 -1
  129. package/src/background-wake/next-wake.ts +1 -0
  130. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  131. package/src/cli/commands/plugins.ts +43 -37
  132. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  133. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  134. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  135. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  136. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  137. package/src/cli/lib/install-from-github.ts +464 -55
  138. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  139. package/src/cli/lib/plugin-details.ts +409 -0
  140. package/src/cli/lib/plugin-marketplace.ts +197 -0
  141. package/src/cli/lib/search-plugins.ts +195 -29
  142. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  143. package/src/config/acp-defaults.test.ts +10 -0
  144. package/src/config/acp-defaults.ts +6 -0
  145. package/src/config/bundled-skills/acp/SKILL.md +85 -33
  146. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  147. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  148. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  149. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  150. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  151. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  152. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  153. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  154. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  155. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  156. package/src/config/bundled-tool-registry.ts +4 -0
  157. package/src/config/call-site-defaults.ts +0 -2
  158. package/src/config/feature-flag-registry.json +15 -6
  159. package/src/config/schemas/call-site-catalog.ts +0 -14
  160. package/src/config/schemas/heartbeat.ts +9 -0
  161. package/src/config/schemas/llm.ts +0 -2
  162. package/src/context/compactor.ts +22 -4
  163. package/src/context/strip-injections.ts +8 -2
  164. package/src/context/window-manager.ts +27 -13
  165. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  166. package/src/daemon/conversation-agent-loop.ts +175 -1055
  167. package/src/daemon/conversation-lifecycle.ts +11 -255
  168. package/src/daemon/conversation-process.ts +8 -136
  169. package/src/daemon/conversation-registry.ts +159 -0
  170. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  171. package/src/daemon/conversation-store.ts +9 -90
  172. package/src/daemon/conversation-surfaces.ts +24 -8
  173. package/src/daemon/conversation-workspace.ts +17 -0
  174. package/src/daemon/conversation.ts +404 -57
  175. package/src/daemon/disk-pressure-policy.ts +0 -1
  176. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  177. package/src/daemon/handlers/conversations.ts +3 -1
  178. package/src/daemon/handlers/skills.ts +4 -1
  179. package/src/daemon/host-proxy-preactivation.ts +1 -3
  180. package/src/daemon/lifecycle.ts +21 -7
  181. package/src/daemon/server.ts +2 -0
  182. package/src/daemon/wake-conversation-ops.ts +269 -0
  183. package/src/embedded/plugin-api.ts +2 -2
  184. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  185. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  186. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  187. package/src/heartbeat/heartbeat-service.ts +26 -0
  188. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  189. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  190. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  191. package/src/ipc/skill-routes/memory.ts +4 -2
  192. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  193. package/src/memory/conversation-crud.ts +29 -19
  194. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  195. package/src/memory/db-init.ts +2 -0
  196. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  197. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  198. package/src/memory/jobs-worker.ts +25 -1
  199. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  200. package/src/memory/migrations/index.ts +1 -0
  201. package/src/memory/schema/acp.ts +4 -0
  202. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  203. package/src/memory/v2/consolidation-job.ts +13 -4
  204. package/src/plugin-api/constants.ts +4 -0
  205. package/src/plugin-api/index.ts +6 -5
  206. package/src/plugin-api/types.ts +75 -0
  207. package/src/plugins/defaults/compaction/compact.ts +59 -0
  208. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  209. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  210. package/src/plugins/defaults/compaction/package.json +1 -2
  211. package/src/plugins/defaults/empty-response/package.json +0 -1
  212. package/src/plugins/defaults/history-repair/package.json +0 -1
  213. package/src/plugins/defaults/index.ts +135 -26
  214. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  215. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
  216. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  217. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  218. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  219. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  220. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  221. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  224. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  235. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  236. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  237. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  238. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  239. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  240. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  241. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  242. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  243. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  244. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  245. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  246. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  247. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  248. package/src/plugins/defaults/title-generate/package.json +0 -1
  249. package/src/plugins/defaults/tool-error/package.json +0 -1
  250. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  251. package/src/plugins/pipeline.ts +6 -293
  252. package/src/plugins/registry.ts +9 -37
  253. package/src/plugins/types.ts +76 -381
  254. package/src/plugins/user-loader.ts +30 -127
  255. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  256. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  257. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  258. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  259. package/src/runtime/agent-wake.ts +108 -209
  260. package/src/runtime/assistant-event-hub.ts +1 -1
  261. package/src/runtime/channel-approvals.ts +1 -1
  262. package/src/runtime/interactive-ui.ts +1 -1
  263. package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
  264. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  265. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  266. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  267. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  268. package/src/runtime/routes/acp-routes.test.ts +89 -25
  269. package/src/runtime/routes/acp-routes.ts +81 -29
  270. package/src/runtime/routes/approval-routes.ts +1 -1
  271. package/src/runtime/routes/browser-routes.ts +1 -1
  272. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  273. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  274. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  275. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  276. package/src/runtime/routes/conversation-routes.ts +28 -2
  277. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  278. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  279. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  280. package/src/runtime/routes/host-cu-routes.ts +1 -1
  281. package/src/runtime/routes/identity-routes.ts +18 -3
  282. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  283. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  284. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  285. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  286. package/src/runtime/routes/playground/helpers.ts +1 -1
  287. package/src/runtime/routes/plugins-routes.ts +337 -35
  288. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  289. package/src/runtime/routes/work-items-routes.ts +2 -4
  290. package/src/runtime/services/conversation-serializer.ts +1 -1
  291. package/src/signals/cancel.ts +2 -4
  292. package/src/subagent/manager.ts +21 -5
  293. package/src/tools/acp/context.ts +20 -0
  294. package/src/tools/acp/list-agents.test.ts +8 -2
  295. package/src/tools/acp/spawn.test.ts +176 -195
  296. package/src/tools/acp/spawn.ts +37 -172
  297. package/src/tools/acp/steer.test.ts +105 -8
  298. package/src/tools/acp/steer.ts +48 -17
  299. package/src/tools/apps/executors.ts +166 -50
  300. package/src/tools/filesystem/write.ts +34 -0
  301. package/src/tools/subagent/spawn.ts +2 -4
  302. package/src/tools/ui-surface/definitions.ts +25 -5
  303. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  304. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  305. package/docs/plugins.md +0 -832
  306. package/examples/plugins/echo/register.ts +0 -143
  307. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  308. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  309. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  310. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  311. package/src/__tests__/pipeline-runner.test.ts +0 -554
  312. package/src/__tests__/plugin-external-api.test.ts +0 -68
  313. package/src/daemon/wake-target-adapter.ts +0 -253
  314. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  315. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  316. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  317. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  318. package/src/plugins/defaults/compaction/register.ts +0 -35
  319. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  320. package/src/plugins/defaults/empty-response/register.ts +0 -23
  321. package/src/plugins/defaults/history-repair/register.ts +0 -24
  322. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  323. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  324. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  325. package/src/plugins/defaults/title-generate/register.ts +0 -35
  326. package/src/plugins/defaults/tool-error/register.ts +0 -23
  327. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  328. package/src/plugins/external-api.ts +0 -104
  329. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  330. package/src/proactive-artifact/decision.test.ts +0 -226
  331. package/src/proactive-artifact/decision.ts +0 -165
  332. package/src/proactive-artifact/index.ts +0 -7
  333. package/src/proactive-artifact/job.test.ts +0 -962
  334. package/src/proactive-artifact/job.ts +0 -372
  335. package/src/proactive-artifact/message-copy.ts +0 -58
  336. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  337. package/src/proactive-artifact/trigger-state.ts +0 -123
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  340. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  341. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  342. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  343. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  344. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  345. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  346. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  347. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  348. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  349. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  350. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  351. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  352. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  353. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  354. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  355. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  356. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  357. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  358. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  359. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  360. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -4,7 +4,7 @@
4
4
  * This module contains the core agent loop orchestration: pre-flight
5
5
  * setup, event handling, retry logic, history reconstruction, and
6
6
  * completion event emission. The Conversation class delegates its
7
- * runAgentLoop method here via the AgentLoopConversationContext interface.
7
+ * runAgentLoop method here, passing itself as the loop context.
8
8
  */
9
9
 
10
10
  import { v4 as uuid } from "uuid";
@@ -12,10 +12,8 @@ import { v4 as uuid } from "uuid";
12
12
  import { optimizeImageForTransport } from "../agent/image-optimize.js";
13
13
  import type {
14
14
  AgentEvent,
15
- AgentLoop,
16
15
  AgentLoopExitReason,
17
16
  CheckpointDecision,
18
- MidLoopCompaction,
19
17
  } from "../agent/loop.js";
20
18
  import { createAssistantMessage } from "../agent/message-types.js";
21
19
  import type {
@@ -24,7 +22,6 @@ import type {
24
22
  TurnChannelContext,
25
23
  TurnInterfaceContext,
26
24
  } from "../channels/types.js";
27
- import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
28
25
  import {
29
26
  contextWindowConfigFromEffective,
30
27
  type EffectiveContextWindow,
@@ -44,11 +41,9 @@ import {
44
41
  } from "../context/post-turn-tool-result-truncation.js";
45
42
  import {
46
43
  estimatePromptTokens,
47
- estimatePromptTokensWithTools,
48
44
  getCalibrationProviderKey,
49
45
  } from "../context/token-estimator.js";
50
- import type { ContextWindowManager } from "../context/window-manager.js";
51
- import type { ToolProfiler } from "../events/tool-profiling-listener.js";
46
+ import type { ContextWindowCompactOptions } from "../context/window-manager.js";
52
47
  import { writeRelationshipState } from "../home/relationship-state-writer.js";
53
48
  import {
54
49
  clearSentryConversationContext,
@@ -62,66 +57,53 @@ import {
62
57
  getConversation,
63
58
  getConversationOriginChannel,
64
59
  getConversationOriginInterface,
65
- getConversationOverrideProfileFromRow,
66
60
  getLastUserTimestampBefore,
67
61
  getMessageById,
68
62
  provenanceFromTrustContext,
63
+ resolveOverrideProfile,
69
64
  updateConversationContextWindow,
70
65
  updateConversationSlackContextWatermark,
71
- updateMessageMetadata,
72
66
  } from "../memory/conversation-crud.js";
73
67
  import { getResolvedConversationDirPath } from "../memory/conversation-directories.js";
74
68
  import { syncMessageToDisk } from "../memory/conversation-disk-view.js";
75
69
  import { isReplaceableTitle } from "../memory/conversation-title-service.js";
76
- import { isBackgroundConversationType } from "../memory/conversation-types.js";
77
70
  import type { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
78
71
  import {
79
72
  backfillMessageIdOnLogs,
80
73
  recordSyntheticAgentErrorMessageLog,
81
74
  } from "../memory/llm-request-log-store.js";
82
75
  import { enqueueMemoryRetrospectiveOnCompaction } from "../memory/memory-retrospective-enqueue.js";
83
- import type { PermissionPrompter } from "../permissions/prompter.js";
84
76
  import { HOOKS } from "../plugin-api/constants.js";
85
77
  import type { UserPromptSubmitContext } from "../plugin-api/types.js";
86
- import { defaultCompactionTerminal } from "../plugins/defaults/compaction/terminal.js";
78
+ import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
79
+ import {
80
+ createInitialReducerState,
81
+ reduceContextOverflow,
82
+ type ReducerState,
83
+ } from "../plugins/defaults/compaction/context-overflow-reducer.js";
87
84
  import { deepRepairHistory } from "../plugins/defaults/history-repair/terminal.js";
88
- import postCompactReinject from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
89
85
  import userPromptSubmitMemoryRetrieval, {
90
86
  type MemoryRetrievalHookContext,
91
87
  } from "../plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.js";
92
- import { DEFAULT_TIMEOUTS, runHook, runPipeline } from "../plugins/pipeline.js";
93
- import { getMiddlewaresFor } from "../plugins/registry.js";
94
- import type {
95
- CompactionArgs,
96
- CompactionResult,
97
- OverflowReduceArgs,
98
- OverflowReduceResult,
99
- TurnContext as PluginTurnContext,
100
- } from "../plugins/types.js";
101
- import { PluginExecutionError, PluginTimeoutError } from "../plugins/types.js";
88
+ import { runHook } from "../plugins/pipeline.js";
102
89
  import type { ContentBlock, Message } from "../providers/types.js";
103
90
  import type { Provider } from "../providers/types.js";
104
- import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
91
+ import {
92
+ isUntrustedTrustClass,
93
+ resolveActorTrust,
94
+ } from "../runtime/actor-trust-resolver.js";
105
95
  import { broadcastMessage } from "../runtime/assistant-event-hub.js";
106
96
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
107
97
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
108
- import { getSubagentManager } from "../subagent/index.js";
109
98
  import type { UsageActor } from "../usage/actors.js";
110
99
  import { getLogger } from "../util/logger.js";
111
100
  import { timeAgo } from "../util/time.js";
112
101
  import { truncate } from "../util/truncate.js";
113
102
  import { getWorkspaceGitService } from "../workspace/git-service.js";
114
103
  import { commitTurnChanges } from "../workspace/turn-commit.js";
115
- import {
116
- type AssistantAttachmentDraft,
117
- cleanAssistantContent,
118
- } from "./assistant-attachments.js";
104
+ import { cleanAssistantContent } from "./assistant-attachments.js";
119
105
  import { resolveOverflowAction } from "./context-overflow-policy.js";
120
- import {
121
- createInitialReducerState,
122
- reduceContextOverflow,
123
- type ReducerState,
124
- } from "./context-overflow-reducer.js";
106
+ import type { Conversation } from "./conversation.js";
125
107
  import {
126
108
  createEventHandlerState,
127
109
  dispatchAgentEvent,
@@ -140,35 +122,22 @@ import {
140
122
  isUserCancellation,
141
123
  } from "./conversation-error.js";
142
124
  import { raceWithTimeout } from "./conversation-media-retry.js";
143
- import type { MessageQueue } from "./conversation-queue-manager.js";
144
- import type { QueueDrainReason } from "./conversation-queue-manager.js";
145
125
  import type {
146
- ChannelCapabilities,
147
126
  InboundActorContext,
148
127
  InjectionMode,
149
128
  } from "./conversation-runtime-assembly.js";
150
129
  import {
151
130
  applyRuntimeInjections,
152
- buildActiveDocuments,
153
- buildActiveSurfaceContext,
154
- buildSubagentStatusBlock,
155
- buildUnifiedTurnContextBlock,
156
- buildWorkspaceTopLevelContext,
157
131
  getSlackCompactionWatermarkForPrefix,
158
132
  inboundActorContextFromTrust,
159
133
  inboundActorContextFromTrustContext,
160
- loadSlackActiveThreadFocusBlock,
161
134
  loadSlackChronologicalContext,
162
135
  type SlackChronologicalContext,
163
136
  stripInjectionsForCompaction,
164
137
  } from "./conversation-runtime-assembly.js";
165
- import type { SkillProjectionCache } from "./conversation-skill-tools.js";
166
138
  import { markSurfaceCompleted } from "./conversation-surfaces.js";
167
139
  import { recordUsage } from "./conversation-usage.js";
168
- import {
169
- formatTurnTimestamp,
170
- resolveTurnTimezoneContext,
171
- } from "./date-context.js";
140
+ import { resolveTurnTimezoneContext } from "./date-context.js";
172
141
  import { getDiskPressureStatus } from "./disk-pressure-guard.js";
173
142
  import { classifyDiskPressureTurnPolicy } from "./disk-pressure-policy.js";
174
143
  import type {
@@ -177,15 +146,12 @@ import type {
177
146
  SurfaceType,
178
147
  UsageStats,
179
148
  } from "./message-protocol.js";
180
- import type { ConfirmationStateChanged } from "./message-types/messages.js";
181
149
  import { parseActualTokensFromError } from "./parse-actual-tokens-from-error.js";
182
150
  import {
183
151
  persistUnsendableImageDowngrades,
184
152
  UNSENDABLE_IMAGE_NOTE,
185
153
  } from "./persist-unsendable-image.js";
186
- import type { TraceEmitter } from "./trace-emitter.js";
187
154
  import { resolveTrustClass, type TrustContext } from "./trust-context.js";
188
- import { stripHistoricalWebSearchResults } from "./web-search-history.js";
189
155
 
190
156
  const log = getLogger("conversation-agent-loop");
191
157
 
@@ -206,23 +172,14 @@ const TOOL_FRIENDLY_LABEL: Record<string, string> = {
206
172
  skill_execute: "Run Skill Tool",
207
173
  };
208
174
 
209
- type GitServiceInitializer = {
210
- ensureInitialized(): Promise<void>;
211
- };
212
-
213
175
  function formatDiskPressureBlockedMessage(): string {
214
176
  return "Storage is critically low, so background processes are paused and remote messages are ignored until the guardian frees enough space. Remote senders should try again later.";
215
177
  }
216
178
 
217
179
  // ── Plugin pipeline helpers ──────────────────────────────────────────
218
- //
219
- // Canonical {@link PluginTurnContext} builder threaded into every
220
- // `runPipeline` call inside `runAgentLoopImpl`. The orchestrator composes
221
- // the context on demand at each call site from ambient state rather than
222
- // carrying a persistent `TurnContext` instance across the turn.
223
180
 
224
181
  /**
225
- * Synthetic fallback trust context used when the orchestrator fires a pipeline
182
+ * Synthetic fallback trust context used when the orchestrator fires a hook
226
183
  * before the per-turn trust snapshot has been captured (e.g. invocations that
227
184
  * bypass `processMessage` / `drainQueue`). We bias to `unknown` rather than
228
185
  * `guardian` so a missing snapshot cannot accidentally grant elevated trust
@@ -233,65 +190,23 @@ const FALLBACK_TURN_TRUST: TrustContext = {
233
190
  trustClass: "unknown",
234
191
  };
235
192
 
236
- /**
237
- * Build the {@link TurnContext} passed to {@link runPipeline}.
238
- *
239
- * Canonical source of truth for every pipeline call site inside the agent
240
- * loop. Every `runPipeline` invocation in `runAgentLoopImpl` (and in the
241
- * handlers that share its ambient state) must route through this helper
242
- * rather than constructing a `TurnContext` literal inline — this keeps
243
- * `turnIndex`, trust resolution, and the `contextWindowManager` attachment
244
- * consistent across pipeline slots, which in turn keeps structured logs
245
- * filtered by `conversationId`/`turnIndex` coherent across slots.
246
- *
247
- * Behavior:
248
- * - `turnIndex` is always `ctx.turnCount` — the orchestrator-owned
249
- * 0-based turn counter. Reading from a single source avoids the
250
- * earlier inconsistency (`ctx.turnCount`, `ctx.messages.length - 1`,
251
- * `ctx.messages.length`, and `0` were all used for the same turn).
252
- * - Trust pulls from the per-turn snapshot first, then the conversation-
253
- * level context, then {@link FALLBACK_TURN_TRUST}. The cascade matches
254
- * the one inside the orchestrator's inline injection assembly so
255
- * middleware reads the same trust class the runtime sees.
256
- * - `contextWindowManager` is attached unconditionally. Pipelines that
257
- * don't need it can ignore it; the default compaction plugin reads it
258
- * via the typed optional field on `TurnContext`.
259
- */
260
- function buildPluginTurnContext(
261
- ctx: AgentLoopConversationContext,
262
- requestId: string,
263
- ): PluginTurnContext {
264
- const trust =
265
- ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
266
- return {
267
- requestId,
268
- conversationId: ctx.conversationId,
269
- turnIndex: ctx.turnCount,
270
- trust,
271
- contextWindowManager: ctx.contextWindowManager,
272
- callSite: ctx.currentCallSite,
273
- };
274
- }
275
-
276
193
  /**
277
194
  * Trust class of the actor whose turn is in progress, for the compactor's
278
195
  * image manifest filter. Prefers the turn-start snapshot
279
- * ({@link AgentLoopConversationContext.currentTurnTrustContext}) over the live
196
+ * ({@link Conversation.currentTurnTrustContext}) over the live
280
197
  * trust context so compaction running in a later tool iteration can't pick up
281
198
  * a concurrent request's actor.
282
199
  */
283
200
  function resolveTurnActorTrustClass(
284
- ctx: AgentLoopConversationContext,
201
+ ctx: Conversation,
285
202
  ): TrustContext["trustClass"] | undefined {
286
203
  return (ctx.currentTurnTrustContext ?? ctx.trustContext)?.trustClass;
287
204
  }
288
205
 
289
- // ── Context Interface ────────────────────────────────────────────────
290
-
291
206
  /**
292
207
  * Per-surface entry tracked on the current turn. Inline shape kept stable so
293
208
  * routes and persistence helpers can consume it via a named import instead of
294
- * `infer`-extracting from {@link AgentLoopConversationContext}.
209
+ * `infer`-extracting from {@link Conversation}.
295
210
  */
296
211
  export interface AssistantSurface {
297
212
  surfaceId: string;
@@ -310,180 +225,10 @@ export interface AssistantSurface {
310
225
  toolCallId?: string;
311
226
  }
312
227
 
313
- export interface AgentLoopConversationContext {
314
- readonly conversationId: string;
315
- messages: Message[];
316
- isProcessing(): boolean;
317
- setProcessing(value: boolean): void;
318
- abortController: AbortController | null;
319
- currentRequestId?: string;
320
- /**
321
- * The {@link LLMCallSite} of the in-flight turn, set at turn start from
322
- * `options?.callSite ?? "mainAgent"`. Read by {@link buildPluginTurnContext}
323
- * so pipeline/injector plugins can tell the main reply apart from
324
- * background agent-loop work (compaction, subagents, …) on this same
325
- * conversation. Per-turn mutable, mirroring {@link currentRequestId}.
326
- */
327
- currentCallSite?: LLMCallSite;
328
-
329
- readonly agentLoop: AgentLoop;
330
- readonly provider: Provider;
331
- readonly systemPrompt: string;
332
-
333
- readonly contextWindowManager: ContextWindowManager;
334
- contextCompactedMessageCount: number;
335
- contextCompactedAt: number | null;
336
- /**
337
- * Set by `applyCompactionResult` when compaction strips runtime injections
338
- * from the preserved tail. The next agent loop turn promotes this into a
339
- * `compactedThisTurn` signal so NOW.md, PKB, and the v2 static block are
340
- * re-injected on the first turn following `/compact` (which runs outside
341
- * the agent loop and so has no other way to surface that compaction
342
- * happened just before this turn).
343
- */
344
- pendingPostCompactReinject: boolean;
345
-
346
- readonly graphMemory: ConversationGraphMemory;
347
-
348
- currentActiveSurfaceId?: string;
349
- currentPage?: string;
350
- readonly surfaceState: Map<
351
- string,
352
- {
353
- surfaceType: SurfaceType;
354
- data: SurfaceData;
355
- title?: string;
356
- actions?: Array<{
357
- id: string;
358
- label: string;
359
- style?: string;
360
- data?: Record<string, unknown>;
361
- }>;
362
- }
363
- >;
364
- pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
365
- surfaceActionRequestIds: Set<string>;
366
- approvedViaPromptThisTurn?: boolean;
367
- currentTurnSurfaces: AssistantSurface[];
368
-
369
- workingDir: string;
370
- workspaceTopLevelContext: string | null;
371
- workspaceTopLevelDirty: boolean;
372
- channelCapabilities?: ChannelCapabilities;
373
- /** Per-turn snapshot of trustContext, frozen at message-processing start. */
374
- currentTurnTrustContext?: TrustContext;
375
- /** Per-turn snapshot of channelCapabilities, frozen at message-processing start. */
376
- currentTurnChannelCapabilities?: ChannelCapabilities;
377
- /**
378
- * Current inference-profile override for this turn. Read by
379
- * `createToolExecutor` so `ToolContext.overrideProfile` carries the same
380
- * profile the agent loop is sending to the provider. Refreshed between
381
- * model calls so an explicitly confirmed profile session opened mid-turn
382
- * is inherited by later tool executions and nested subagents.
383
- */
384
- currentTurnOverrideProfile?: string;
385
- /**
386
- * Set by the `switch_inference_profile` tool when the model self-selects a
387
- * different profile mid-turn. Read by `readCurrentOverrideProfile` in the
388
- * agent loop so the next LLM call uses the switched profile. Reset at
389
- * turn start.
390
- */
391
- toolRoutedProfile?: string;
392
- commandIntent?: { type: string; payload?: string; languageCode?: string };
393
- trustContext?: TrustContext;
394
- /** Task-run scope for the current turn. Cleared at turn end so queued/drained turns don't inherit it. */
395
- taskRunId?: string;
396
- assistantId?: string;
397
- voiceCallControlPrompt?: string;
398
- transportHints?: string[];
399
- clientTimezone?: string;
400
-
401
- readonly coreToolNames: Set<string>;
402
- allowedToolNames?: Set<string>;
403
- diskPressureCleanupModeActive?: boolean;
404
- toolsDisabledDepth: number;
405
- preactivatedSkillIds?: string[];
406
- readonly skillProjectionState: Map<string, string>;
407
- readonly skillProjectionCache: SkillProjectionCache;
408
-
409
- readonly traceEmitter: TraceEmitter;
410
- readonly profiler: ToolProfiler;
411
- usageStats: UsageStats;
412
- turnCount: number;
413
-
414
- lastAssistantAttachments: AssistantAttachmentDraft[];
415
- lastAttachmentWarnings: string[];
416
-
417
- hasNoClient: boolean;
418
- /** True when this conversation is itself a subagent (suppresses subagent status injection). */
419
- isSubagent?: boolean;
420
- headlessLock?: boolean;
421
- readonly streamThinking: boolean;
422
- readonly prompter: PermissionPrompter;
423
- readonly queue: MessageQueue;
424
-
425
- emitActivityState(
426
- phase:
427
- | "idle"
428
- | "thinking"
429
- | "streaming"
430
- | "tool_running"
431
- | "awaiting_confirmation",
432
- reason:
433
- | "message_dequeued"
434
- | "thinking_delta"
435
- | "first_text_delta"
436
- | "tool_use_start"
437
- | "preview_start"
438
- | "tool_result_received"
439
- | "confirmation_requested"
440
- | "confirmation_resolved"
441
- | "context_compacting"
442
- | "message_complete"
443
- | "generation_cancelled"
444
- | "error_terminal",
445
- options?: {
446
- anchor?: "assistant_turn" | "user_turn" | "global";
447
- requestId?: string;
448
- statusText?: string;
449
- },
450
- ): void;
451
- emitConfirmationStateChanged(
452
- params: ConfirmationStateChanged extends {
453
- type: infer _;
454
- }
455
- ? Omit<ConfirmationStateChanged, "type">
456
- : never,
457
- ): void;
458
-
459
- /**
460
- * Optional callback invoked by the Conversation when a confirmation state changes.
461
- * The agent loop registers this to track requestId → toolUseId mappings
462
- * and record confirmation outcomes for persistence.
463
- */
464
- onConfirmationOutcome?: (
465
- requestId: string,
466
- state: string,
467
- toolUseId?: string,
468
- ) => void;
469
-
470
- getWorkspaceGitService?: (workspaceDir: string) => GitServiceInitializer;
471
- commitTurnChanges?: typeof commitTurnChanges;
472
-
473
- refreshWorkspaceTopLevelContextIfNeeded(): void;
474
- markWorkspaceTopLevelDirty(): void;
475
- getQueueDepth(): number;
476
- hasQueuedMessages(): boolean;
477
- canHandoffAtCheckpoint(): boolean;
478
- drainQueue(reason: QueueDrainReason): Promise<void>;
479
- getTurnChannelContext(): TurnChannelContext | null;
480
- getTurnInterfaceContext(): TurnInterfaceContext | null;
481
- }
482
-
483
228
  // ── runAgentLoop ─────────────────────────────────────────────────────
484
229
 
485
230
  export async function runAgentLoopImpl(
486
- ctx: AgentLoopConversationContext,
231
+ ctx: Conversation,
487
232
  content: string,
488
233
  userMessageId: string,
489
234
  onEvent: (msg: ServerMessage) => void,
@@ -556,26 +301,22 @@ export async function runAgentLoopImpl(
556
301
  // `resolveCallSiteConfig`, picking up any user overrides under
557
302
  // `llm.callSites.mainAgent` (falling back to `llm.default` when absent).
558
303
  const turnCallSite: LLMCallSite = options?.callSite ?? "mainAgent";
559
- // Expose the turn's call site to plugin pipeline/injector contexts (read by
560
- // buildPluginTurnContext) so plugins can scope behaviour to the main reply.
304
+ // Expose the turn's call site on the live conversation so the runtime
305
+ // injection assembly self-resolves it for the turn's plugin contexts.
561
306
  ctx.currentCallSite = turnCallSite;
562
307
 
563
- // Read the conversation row once for both the override-profile derivation
564
- // below and the title-replaceability check at turn start. Later reads in
565
- // this function (post-turn truncation, disk sync, home-feed emission)
566
- // intentionally re-read because state can change during the turn.
567
- const turnStartConversation = getConversation(ctx.conversationId);
568
-
569
308
  // Optional per-turn inference-profile override. Plumbed through to every
570
309
  // LLM call the loop emits and inherited by any subagents spawned during
571
310
  // this turn. Caller-supplied `options.overrideProfile` (e.g.
572
311
  // SubagentManager forwarding the parent's pinned profile into the
573
- // spawned subagent's background conversation) wins over the row read
574
- // so the agent loop's own background-skip rule doesn't zero out an
575
- // explicitly inherited override.
312
+ // spawned subagent's background conversation) wins over the conversation's
313
+ // own override so the agent loop's background-skip rule doesn't zero out an
314
+ // explicitly inherited override. The override state is mirrored onto the
315
+ // live conversation (hydrated on load, kept current by the HTTP setters and
316
+ // the expiry reaper), so the derivation reads `ctx` rather than re-fetching
317
+ // the row.
576
318
  const userExplicitOverride =
577
- options?.overrideProfile ??
578
- getConversationOverrideProfileFromRow(turnStartConversation);
319
+ options?.overrideProfile ?? resolveOverrideProfile(ctx);
579
320
 
580
321
  const config = getConfig();
581
322
 
@@ -588,9 +329,7 @@ export async function runAgentLoopImpl(
588
329
 
589
330
  const readCurrentOverrideProfile = (): string | undefined =>
590
331
  options?.overrideProfile ??
591
- getConversationOverrideProfileFromRow(
592
- getConversation(ctx.conversationId),
593
- ) ??
332
+ resolveOverrideProfile(ctx) ??
594
333
  ctx.toolRoutedProfile;
595
334
 
596
335
  const effectiveContextWindow = resolveEffectiveContextWindow({
@@ -608,11 +347,7 @@ export async function runAgentLoopImpl(
608
347
  }).contextWindow,
609
348
  currentEffectiveContextWindow,
610
349
  );
611
- const contextWindowManager =
612
- ctx.contextWindowManager as ContextWindowManager & {
613
- updateConfig?: (config: ContextWindowConfig) => void;
614
- };
615
- contextWindowManager.updateConfig?.(currentContextWindowConfig);
350
+ ctx.contextWindowManager.updateConfig(currentContextWindowConfig);
616
351
 
617
352
  let appliedOverrideProfile = turnOverrideProfile;
618
353
  let emittedToolRoutedProfile: string | undefined;
@@ -632,7 +367,7 @@ export async function runAgentLoopImpl(
632
367
  }).contextWindow,
633
368
  currentEffectiveContextWindow,
634
369
  );
635
- contextWindowManager.updateConfig?.(currentContextWindowConfig);
370
+ ctx.contextWindowManager.updateConfig(currentContextWindowConfig);
636
371
  appliedOverrideProfile = currentOverrideProfile;
637
372
  rlog.info(
638
373
  { overrideProfile: currentOverrideProfile ?? null },
@@ -751,11 +486,17 @@ export async function runAgentLoopImpl(
751
486
 
752
487
  const isInteractiveResolved =
753
488
  options?.isInteractive ?? (!ctx.hasNoClient && !ctx.headlessLock);
489
+ // Whether the in-flight turn has no human present to answer clarification
490
+ // questions. Derived from the loop's `isInteractive` option (which can fall
491
+ // back to mutable client/headless state that flips mid-turn), so it is
492
+ // resolved once here and threaded into every re-injection — including the
493
+ // post-compaction hook — rather than re-read per assembly call.
494
+ const isNonInteractive = !isInteractiveResolved;
754
495
  const diskPressureDecision = classifyDiskPressureTurnPolicy(
755
496
  getDiskPressureStatus(),
756
497
  {
757
- conversationType: turnStartConversation?.conversationType ?? null,
758
- conversationSource: turnStartConversation?.source ?? null,
498
+ conversationType: ctx.conversationType ?? null,
499
+ conversationSource: ctx.source ?? null,
759
500
  callSite: turnCallSite,
760
501
  isInteractive: isInteractiveResolved,
761
502
  sourceChannel:
@@ -772,10 +513,6 @@ export async function runAgentLoopImpl(
772
513
  : null,
773
514
  },
774
515
  );
775
- const diskPressureContext =
776
- diskPressureDecision.action === "allow-cleanup-mode"
777
- ? { cleanupModeActive: true }
778
- : null;
779
516
  ctx.diskPressureCleanupModeActive =
780
517
  diskPressureDecision.action === "allow-cleanup-mode";
781
518
 
@@ -879,22 +616,7 @@ export async function runAgentLoopImpl(
879
616
  }
880
617
 
881
618
  const isFirstMessage = ctx.messages.length === 1;
882
- // Promote a pending post-compaction re-inject signal (e.g. from `/compact`)
883
- // into `compactedThisTurn` so NOW.md / PKB / v2 static blocks land on this
884
- // turn even when no mid-turn compaction fires. Clear the flag immediately
885
- // so this fires exactly once per `/compact` event.
886
- const consumedPostCompactReinject = ctx.pendingPostCompactReinject;
887
- ctx.pendingPostCompactReinject = false;
888
- state.shouldInjectWorkspace = isFirstMessage || consumedPostCompactReinject;
889
- let compactedThisTurn = consumedPostCompactReinject;
890
- let slackCompactedThisTurn = false;
891
619
  const isSlackConversation = ctx.channelCapabilities?.channel === "slack";
892
- let currentSlackContextSummary =
893
- turnStartConversation?.contextSummary ?? null;
894
- let currentSlackContextCompactedMessageCount =
895
- turnStartConversation?.contextCompactedMessageCount ?? 0;
896
- let currentSlackContextCompactionWatermarkTs =
897
- turnStartConversation?.slackContextCompactionWatermarkTs ?? null;
898
620
  const loadCurrentSlackChronologicalContext =
899
621
  (): SlackChronologicalContext | null => {
900
622
  if (!isSlackConversation) return null;
@@ -903,18 +625,15 @@ export async function runAgentLoopImpl(
903
625
  ctx.channelCapabilities!,
904
626
  {
905
627
  trustClass: ctx.trustContext?.trustClass,
906
- contextSummary: currentSlackContextSummary,
907
- contextCompactedMessageCount:
908
- currentSlackContextCompactedMessageCount,
628
+ contextSummary: ctx.contextSummary,
629
+ contextCompactedMessageCount: ctx.contextCompactedMessageCount,
909
630
  slackContextCompactionWatermarkTs:
910
- currentSlackContextCompactionWatermarkTs,
631
+ ctx.slackContextCompactionWatermarkTs,
911
632
  },
912
633
  );
913
634
  };
914
635
  let slackChronologicalContext: SlackChronologicalContext | null =
915
636
  loadCurrentSlackChronologicalContext();
916
- const messagesForStartOfTurnCompaction =
917
- slackChronologicalContext?.messages ?? ctx.messages;
918
637
  const getSlackProvenanceContextForCompactionBasis = (
919
638
  messages: Message[],
920
639
  compactedMessages: number,
@@ -1001,15 +720,6 @@ export async function runAgentLoopImpl(
1001
720
  await applyCompactionResult(ctx, result, onEvent, reqId, {
1002
721
  slackContextCompactionWatermarkTs: slackWatermarkTs,
1003
722
  });
1004
- currentSlackContextSummary = result.summaryText;
1005
- currentSlackContextCompactedMessageCount =
1006
- ctx.contextCompactedMessageCount;
1007
- if (slackWatermarkTs) {
1008
- currentSlackContextCompactionWatermarkTs = slackWatermarkTs;
1009
- }
1010
- if (isSlackConversation) {
1011
- slackCompactedThisTurn = true;
1012
- }
1013
723
  slackChronologicalContext = projectSlackProvenanceAfterCompaction(
1014
724
  provenanceContext,
1015
725
  compactedBasis,
@@ -1017,88 +727,6 @@ export async function runAgentLoopImpl(
1017
727
  );
1018
728
  };
1019
729
 
1020
- const compactCheck = ctx.contextWindowManager.shouldCompact(
1021
- messagesForStartOfTurnCompaction,
1022
- );
1023
- // Skip auto-compaction while the circuit breaker is open. Force paths
1024
- // and user-initiated /compact bypass this check.
1025
- const autoCompactAllowed =
1026
- !(await ctx.agentLoop.compactionCircuit.isOpen(ctx));
1027
- if (compactCheck.needed && autoCompactAllowed) {
1028
- ctx.emitActivityState("thinking", "context_compacting", {
1029
- requestId: reqId,
1030
- });
1031
- }
1032
- const compactionOptions = {
1033
- precomputedEstimate: compactCheck.estimatedTokens,
1034
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
1035
- actorTrustClass: resolveTurnActorTrustClass(ctx),
1036
- };
1037
- let compacted: Awaited<
1038
- ReturnType<typeof ctx.contextWindowManager.maybeCompact>
1039
- > | null = null;
1040
- if (autoCompactAllowed) {
1041
- try {
1042
- compacted = (await runPipeline<CompactionArgs, CompactionResult>(
1043
- "compaction",
1044
- getMiddlewaresFor("compaction"),
1045
- (args) =>
1046
- defaultCompactionTerminal(args, buildPluginTurnContext(ctx, reqId)),
1047
- {
1048
- messages: messagesForStartOfTurnCompaction,
1049
- signal: abortController.signal,
1050
- options: compactionOptions,
1051
- },
1052
- buildPluginTurnContext(ctx, reqId),
1053
- DEFAULT_TIMEOUTS.compaction,
1054
- )) as Awaited<ReturnType<typeof ctx.contextWindowManager.maybeCompact>>;
1055
- } catch (err) {
1056
- if (err instanceof PluginTimeoutError) {
1057
- // Pipeline exceeded its budget. Record the failure so the circuit
1058
- // breaker tracks consecutive timeouts (it trips after three),
1059
- // then degrade gracefully by skipping compaction this turn —
1060
- // the turn proceeds with the un-compacted history rather than
1061
- // hard-failing. The inner summary call has been aborted by the
1062
- // runner's signal-linking, so updateSummary's local fallback
1063
- // also ran before this catch block is reached.
1064
- rlog.warn(
1065
- { err, phase: "start-of-turn-compaction" },
1066
- "Compaction pipeline timed out — skipping compaction this turn",
1067
- );
1068
- await ctx.agentLoop.compactionCircuit.recordOutcome(
1069
- ctx,
1070
- true,
1071
- onEvent,
1072
- );
1073
- compacted = null;
1074
- } else {
1075
- throw err;
1076
- }
1077
- }
1078
- }
1079
- // Only track circuit-breaker state when a summary LLM call actually ran.
1080
- // `summaryFailed` is `undefined` on early returns (compaction disabled,
1081
- // below threshold, no eligible messages, truncation-only
1082
- // path) — treating those as "successful" compactions would silently reset
1083
- // the 3-strike counter and break the invariant.
1084
- if (compacted && compacted.summaryFailed !== undefined) {
1085
- await ctx.agentLoop.compactionCircuit.recordOutcome(
1086
- ctx,
1087
- compacted.summaryFailed,
1088
- onEvent,
1089
- );
1090
- }
1091
- if (compacted?.compacted) {
1092
- await applySuccessfulCompaction(
1093
- compacted,
1094
- messagesForStartOfTurnCompaction,
1095
- );
1096
- state.shouldInjectWorkspace = true;
1097
- if (compacted.compactedPersistedMessages > 0) {
1098
- compactedThisTurn = true;
1099
- }
1100
- }
1101
-
1102
730
  // Register confirmation outcome tracker so the agent loop can link
1103
731
  // confirmation decisions to tool_use_ids for persistence.
1104
732
  ctx.onConfirmationOutcome = (requestId, confirmationState, toolUseId) => {
@@ -1165,18 +793,20 @@ export async function runAgentLoopImpl(
1165
793
  }
1166
794
  }
1167
795
 
1168
- // Resolve the channel/interface labels and the guardian flag for this
1169
- // turn. These derive only from the captured turn context and the resolved
1170
- // actor trust class — never from retrieval — so they settle before context
1171
- // assembly.
1172
- const interfaceName =
1173
- capturedTurnInterfaceContext.userMessageInterface ?? undefined;
1174
- const channelName =
1175
- capturedTurnChannelContext?.userMessageChannel ?? undefined;
796
+ // Resolve the guardian flag for this turn. It derives only from the
797
+ // resolved actor trust class — never from retrieval so it settles before
798
+ // context assembly.
1176
799
  const isGuardian =
1177
800
  resolvedInboundActorContext?.trustClass === "guardian" ||
1178
801
  !resolvedInboundActorContext;
1179
802
 
803
+ // Unified `<turn_context>` actor input, included only for non-guardian
804
+ // turns. Resolved once at turn start and threaded per call site (like
805
+ // `modelProfile`) so post-compaction re-injection receives it as an
806
+ // explicit hook input rather than re-deriving it from live state that can
807
+ // flip mid-turn.
808
+ const actorContext = isGuardian ? null : resolvedInboundActorContext;
809
+
1180
810
  // Surface long gaps between user messages so the model can acknowledge
1181
811
  // the absence naturally. Gated at >12h to avoid noisy injection during
1182
812
  // normal back-and-forth turns.
@@ -1196,6 +826,19 @@ export async function runAgentLoopImpl(
1196
826
  }
1197
827
  }
1198
828
 
829
+ // Freeze the turn-start client timezone and long-absence gap on the
830
+ // conversation so `applyRuntimeInjections` sources them from live state —
831
+ // like the channel/voice/transport hints. Frozen here (rather than read
832
+ // live in assembly) because the live `ctx.clientTimezone` is overwritten
833
+ // when a newer message for the same conversation arrives mid-turn, which
834
+ // would otherwise leak a queued message's timezone into the in-flight turn.
835
+ // The `current_time` value is computed fresh at each injection point, so
836
+ // it is not part of this snapshot.
837
+ ctx.currentTurnTemporalSnapshot = {
838
+ clientTimezone: timezoneContext.clientTimezone,
839
+ timeSinceLastMessage,
840
+ };
841
+
1199
842
  // Resolve the effective profile key for this turn and detect changes.
1200
843
  // Only inject model_profile into the turn context when the profile
1201
844
  // changed since the last turn (or on the first turn of a conversation)
@@ -1204,7 +847,7 @@ export async function runAgentLoopImpl(
1204
847
  turnOverrideProfile ??
1205
848
  config.llm.activeProfile ??
1206
849
  resolveDefaultProfileKey("mainAgent", config.llm);
1207
- const lastNotified = turnStartConversation?.lastNotifiedInferenceProfile;
850
+ const lastNotified = ctx.lastNotifiedInferenceProfile;
1208
851
  let modelProfileStr: string | null = null;
1209
852
  if (effectiveProfileKey != null && effectiveProfileKey !== lastNotified) {
1210
853
  const profileEntry = config.llm.profiles?.[effectiveProfileKey];
@@ -1221,479 +864,42 @@ export async function runAgentLoopImpl(
1221
864
  state.pendingNotifiedInferenceProfile = effectiveProfileKey;
1222
865
  }
1223
866
 
1224
- // Memory retrieval — fetches PKB, NOW.md, and memory-graph outputs and
1225
- // persists the retrieval's own side effects (injected-block metadata,
1226
- // recall log, `memory_recalled` event). Runs at the early "prompt
1227
- // submitted, before context assembly" moment because its outputs feed the
1228
- // injection and overflow-reduction transforms below. It is shaped as the
1229
- // `user-prompt-submit-temp` hook handler but invoked directly for now: it
1230
- // must run early, while the canonical late `user-prompt-submit` hook
1231
- // (history repair, title) runs after those transforms, so the two cannot
1232
- // share a fire site until compaction is cleared from the gap between them.
867
+ // Memory retrieval + runtime injection — fetches PKB / NOW.md / memory-graph
868
+ // outputs, persists the retrieval's own side effects (injected-block
869
+ // metadata, recall log, `memory_recalled` event), and assembles the turn's
870
+ // runtime-injection blocks onto the history (persisting those blocks too).
871
+ // Runs at the early "prompt submitted, before context assembly" moment so
872
+ // its injected output is what the agent loop receives. It is shaped as the
873
+ // `user-prompt-submit-temp` hook handler but invoked directly for now,
874
+ // separate from the canonical late `user-prompt-submit` hook (history
875
+ // repair, title) that fires just before the loop.
876
+ // The injection inputs (`mode`, `isNonInteractive`, `modelProfile`,
877
+ // `actorContext`) are resolved once at turn start and threaded in so
878
+ // post-compaction re-injection reuses the same snapshot rather than live
879
+ // state that can flip mid-turn.
1233
880
  const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
881
+ let currentInjectionMode: InjectionMode = "full";
1234
882
  const memoryCtx: MemoryRetrievalHookContext = {
1235
- graphMemory: ctx.graphMemory,
1236
- config: getConfig(),
1237
883
  onEvent,
1238
- isTrustedActor,
1239
884
  conversationId: ctx.conversationId,
1240
885
  userMessageId,
1241
886
  logger: rlog,
1242
- // An external cancel aborts `prepareMemory` instead of letting it run
1243
- // to completion after the turn has already been torn down.
1244
- signal: abortController.signal,
1245
887
  latestMessages: ctx.messages,
888
+ requestId: reqId,
889
+ mode: currentInjectionMode,
890
+ isNonInteractive,
891
+ modelProfile: modelProfileStr,
892
+ actorContext,
1246
893
  };
1247
894
  await userPromptSubmitMemoryRetrieval(memoryCtx);
1248
895
 
1249
- // The retriever owns its side effects (injected-block metadata, recall
1250
- // log, `memory_recalled` event) and records the dense/sparse PKB query
1251
- // pair on the graph handle for the PKB-reminder injector to read back; the
1252
- // loop only reuses the injected message list downstream.
896
+ // The hook owns its side effects (injected-block metadata, recall log,
897
+ // `memory_recalled` event, and the runtime-injection metadata persist) and
898
+ // records the dense/sparse PKB query pair on the graph handle for the
899
+ // PKB-reminder injector to read back; the loop reuses the fully injected
900
+ // message list downstream.
1253
901
  let runMessages = memoryCtx.latestMessages;
1254
902
 
1255
- // Capture wall-clock "now" at its point of use, after the blocking memory
1256
- // retrieval, so the injected `<turn_context>` timestamp reflects current
1257
- // time rather than the moment the turn began.
1258
- const timestamp = formatTurnTimestamp({
1259
- timeZone: timezoneContext.effectiveTimezone,
1260
- });
1261
-
1262
- // Build unified turn context block that replaces the separate temporal,
1263
- // channel, interface, and actor context blocks.
1264
- const baseTurnContext = {
1265
- timestamp,
1266
- interfaceName,
1267
- channelName,
1268
- configuredUserTimezone: timezoneContext.configuredUserTimezone,
1269
- clientTimezone: timezoneContext.clientTimezone,
1270
- detectedTimezone: timezoneContext.detectedTimezone,
1271
- timeSinceLastMessage,
1272
- modelProfile: modelProfileStr,
1273
- };
1274
- const unifiedTurnContextStr = buildUnifiedTurnContextBlock(
1275
- isGuardian
1276
- ? baseTurnContext
1277
- : {
1278
- ...baseTurnContext,
1279
- actorContext: resolvedInboundActorContext,
1280
- },
1281
- );
1282
-
1283
- // The `remember` tool handles scratchpad-style memory writes directly to the graph.
1284
-
1285
- // Subagent status injection — gives the parent LLM visibility into active/completed children.
1286
- // Skipped when this conversation IS a subagent (no nesting) or has no children.
1287
- const subagentStatusBlock = ctx.isSubagent
1288
- ? null
1289
- : buildSubagentStatusBlock(
1290
- getSubagentManager().getChildrenOf(ctx.conversationId),
1291
- );
1292
-
1293
- // For any Slack conversation (channels and DMs alike), build a
1294
- // chronological transcript from the persisted message rows so the
1295
- // model sees one channel-wide view instead of the gateway's per-turn
1296
- // hints. DMs render as a flat sequence (no thread tags), channels
1297
- // include sibling threads.
1298
- const slackConversationForInjection = isSlackConversation
1299
- ? (getConversation(ctx.conversationId) ?? turnStartConversation)
1300
- : turnStartConversation;
1301
- if (isSlackConversation && !slackCompactedThisTurn) {
1302
- slackChronologicalContext ??= loadSlackChronologicalContext(
1303
- ctx.conversationId,
1304
- ctx.channelCapabilities!,
1305
- {
1306
- trustClass: ctx.trustContext?.trustClass,
1307
- contextSummary: slackConversationForInjection?.contextSummary,
1308
- contextCompactedMessageCount:
1309
- slackConversationForInjection?.contextCompactedMessageCount,
1310
- slackContextCompactionWatermarkTs:
1311
- slackConversationForInjection?.slackContextCompactionWatermarkTs,
1312
- },
1313
- );
1314
- }
1315
- const slackChronologicalMessages =
1316
- slackChronologicalContext?.messages ?? null;
1317
-
1318
- // Active-thread focus block: when the inbound user message belongs to
1319
- // a Slack thread, append a non-persisted `<active_thread>` tail block
1320
- // to the final user turn listing the thread's parent + replies. Helps
1321
- // the model orient when the channel transcript is long and
1322
- // interleaved. Replays strip the block via RUNTIME_INJECTION_PREFIXES.
1323
- // DMs short-circuit to null inside `loadSlackActiveThreadFocusBlock`
1324
- // since DMs do not have threads.
1325
- const slackActiveThreadFocusBlock = isSlackConversation
1326
- ? loadSlackActiveThreadFocusBlock(
1327
- ctx.conversationId,
1328
- ctx.channelCapabilities!,
1329
- {
1330
- trustClass: ctx.trustContext?.trustClass,
1331
- contextCompactedMessageCount:
1332
- slackConversationForInjection?.contextCompactedMessageCount,
1333
- slackContextCompactionWatermarkTs:
1334
- slackConversationForInjection?.slackContextCompactionWatermarkTs,
1335
- },
1336
- )
1337
- : null;
1338
-
1339
- state.reducerCompacted = compactedThisTurn;
1340
-
1341
- // memory-v3-live: when on, the provider anchors its long-TTL cache
1342
- // breakpoint on the most recent STABLE user message, since the latest user
1343
- // message now carries the volatile per-turn `<memory>` block the v3
1344
- // injector emits. The matching v2-suppression strip is owned by
1345
- // `applyRuntimeInjections`, which reads the same flag itself. Flag off →
1346
- // bit-for-bit identical to today's v2 path.
1347
- const memoryV3Live = isAssistantFeatureFlagEnabled(
1348
- "memory-v3-live",
1349
- getConfig(),
1350
- );
1351
-
1352
- // Shared injection options — reused whenever we need to re-inject after reduction.
1353
- const injectionOpts = {
1354
- diskPressureContext,
1355
- // Resolved from the conversation's surface state here, where the
1356
- // runtime injector is the only consumer of the active-surface block.
1357
- activeSurface: buildActiveSurfaceContext({
1358
- currentActiveSurfaceId: ctx.currentActiveSurfaceId,
1359
- currentPage: ctx.currentPage,
1360
- surfaceState: ctx.surfaceState,
1361
- }),
1362
- // Resolved here, where the runtime injector is the only consumer of the
1363
- // active-documents block.
1364
- activeDocuments: buildActiveDocuments(ctx.conversationId),
1365
- workspaceTopLevelContext: buildWorkspaceTopLevelContext(
1366
- ctx,
1367
- state.shouldInjectWorkspace,
1368
- ),
1369
- channelCapabilities: ctx.channelCapabilities ?? null,
1370
- channelCommandContext: ctx.commandIntent ?? null,
1371
- unifiedTurnContext: unifiedTurnContextStr,
1372
- voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
1373
- transportHints: ctx.transportHints ?? null,
1374
- isNonInteractive: !isInteractiveResolved,
1375
- isBackgroundConversation: isBackgroundConversationType(
1376
- turnStartConversation?.conversationType,
1377
- ),
1378
- subagentStatusBlock,
1379
- slackChronologicalMessages,
1380
- slackActiveThreadFocusBlock,
1381
- } as const;
1382
-
1383
- let currentInjectionMode: InjectionMode = "full";
1384
-
1385
- // Canonical per-turn TurnContext forwarded to the injector chain. The
1386
- // per-turn injection inputs are built inside `applyRuntimeInjections`
1387
- // from the `injectionOpts` bag; we only need to hand in identity +
1388
- // trust here so third-party injectors see the real turn metadata.
1389
- const injectionTurnCtx = buildPluginTurnContext(ctx, reqId);
1390
-
1391
- const injection = await applyRuntimeInjections(runMessages, {
1392
- ...injectionOpts,
1393
- slackChronologicalMessages: state.reducerCompacted
1394
- ? null
1395
- : injectionOpts.slackChronologicalMessages,
1396
- mode: currentInjectionMode,
1397
- turnContext: injectionTurnCtx,
1398
- });
1399
- runMessages = injection.messages;
1400
-
1401
- // Persist injected blocks in message metadata so they survive conversation
1402
- // reloads (eviction, restart, fork). loadFromDb re-injects from metadata.
1403
- // Only the first call site persists — the overflow-recovery re-entry sites
1404
- // send identical bytes and the tail row may not correspond to
1405
- // `userMessageId`. All blocks are written in a single call to avoid
1406
- // doubling SQLite SELECT+UPDATE work on every turn.
1407
- if (
1408
- injection.blocks.unifiedTurnContext ||
1409
- injection.blocks.pkbSystemReminder ||
1410
- injection.blocks.workspaceBlock ||
1411
- injection.blocks.nowScratchpadBlock ||
1412
- injection.blocks.pkbContextBlock ||
1413
- injection.blocks.memoryV2StaticBlock
1414
- ) {
1415
- try {
1416
- const metadataUpdates: Record<string, unknown> = {};
1417
- if (injection.blocks.unifiedTurnContext) {
1418
- metadataUpdates.turnContextBlock =
1419
- injection.blocks.unifiedTurnContext;
1420
- }
1421
- if (injection.blocks.pkbSystemReminder) {
1422
- metadataUpdates.pkbSystemReminderBlock =
1423
- injection.blocks.pkbSystemReminder;
1424
- }
1425
- if (injection.blocks.workspaceBlock) {
1426
- metadataUpdates.workspaceBlock = injection.blocks.workspaceBlock;
1427
- }
1428
- if (injection.blocks.nowScratchpadBlock) {
1429
- metadataUpdates.nowScratchpadBlock =
1430
- injection.blocks.nowScratchpadBlock;
1431
- }
1432
- if (injection.blocks.pkbContextBlock) {
1433
- metadataUpdates.pkbContextBlock = injection.blocks.pkbContextBlock;
1434
- }
1435
- if (injection.blocks.memoryV2StaticBlock) {
1436
- metadataUpdates.memoryV2StaticBlock =
1437
- injection.blocks.memoryV2StaticBlock;
1438
- }
1439
- updateMessageMetadata(userMessageId, metadataUpdates);
1440
- } catch (err) {
1441
- rlog.warn({ err }, "Failed to persist injection metadata (non-fatal)");
1442
- }
1443
- }
1444
-
1445
- // ── Preflight budget evaluation ──────────────────────────────
1446
- // After runtime injections are applied, estimate the prompt token count
1447
- // and proactively invoke the reducer if already above budget. This avoids
1448
- // a wasted provider round-trip that would just fail with context_too_large.
1449
- const initialContextBudget = resolveCurrentContextBudget();
1450
- const overflowRecovery = initialContextBudget.overflowRecovery;
1451
- const preflightBudget = initialContextBudget.preflightBudget;
1452
- let reducerState: ReducerState | undefined;
1453
-
1454
- const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
1455
- // Canonical calibration key — used by the preflight estimate, the
1456
- // overflow reducer config, and the convergence-path `estimatePromptTokens`
1457
- // call. Matches the key recorded by `handleUsage` for wrapper providers
1458
- // (OpenRouter routing to Anthropic → key is `"anthropic"`).
1459
- const estimationProviderName = getCalibrationProviderKey(ctx.provider);
1460
-
1461
- const preflightTokens = estimatePromptTokensWithTools(
1462
- runMessages,
1463
- ctx.systemPrompt,
1464
- ctx.agentLoop.getResolvedTools(runMessages),
1465
- estimationProviderName,
1466
- );
1467
-
1468
- if (overflowRecovery.enabled && preflightTokens > preflightBudget) {
1469
- rlog.warn(
1470
- {
1471
- phase: "preflight",
1472
- estimatedTokens: preflightTokens,
1473
- budget: preflightBudget,
1474
- },
1475
- "Preflight budget exceeded — running overflow reducer before provider call",
1476
- );
1477
-
1478
- // Overflow reduction runs through the plugin pipeline. The default
1479
- // middleware (`default-overflow-reduce`, registered at bootstrap)
1480
- // contains the historical tier loop — forced compaction → tool-result
1481
- // truncation → media stubbing → injection downgrade — plus the
1482
- // re-inject/re-estimate convergence check. The callbacks below are
1483
- // the orchestrator-specific side effects that the plugin coordinates
1484
- // per iteration (activity emission, compaction application, runtime
1485
- // injection reassembly, token re-estimation). Registered plugins that
1486
- // wrap the `overflowReduce` slot see each iteration through their own
1487
- // middleware `next` callback.
1488
- const messagesForPreflightOverflowReduction =
1489
- slackChronologicalContext?.messages ?? ctx.messages;
1490
- const overflowArgs: OverflowReduceArgs = {
1491
- messages: messagesForPreflightOverflowReduction,
1492
- runMessages,
1493
- systemPrompt: ctx.systemPrompt,
1494
- providerName: estimationProviderName,
1495
- contextWindow: resolveCurrentContextWindowConfig(),
1496
- preflightBudget,
1497
- toolTokenBudget,
1498
- maxAttempts: resolveCurrentContextBudget().overflowRecovery.maxAttempts,
1499
- abortSignal: abortController.signal,
1500
- compactFn: async (msgs, signal, opts) => {
1501
- // Route the reducer's forced-compaction tier through the
1502
- // `compaction` pipeline so registered plugins observe these
1503
- // invocations. Without this, custom compaction middleware only
1504
- // sees the three orchestrator-owned call sites and misses the
1505
- // reducer-initiated forced compactions entirely.
1506
- //
1507
- // Pipeline timeouts must be caught locally — a `PluginTimeoutError`
1508
- // bubbling out of here would abort the overflow-reducer tier loop
1509
- // entirely, skipping fallback tiers (tool-result truncation, media
1510
- // stubbing, injection downgrade) and bypassing circuit-breaker
1511
- // bookkeeping. On timeout, record the failure and return a
1512
- // `compacted: false` result so the reducer falls through to the
1513
- // next tier.
1514
- try {
1515
- return (await runPipeline<CompactionArgs, CompactionResult>(
1516
- "compaction",
1517
- getMiddlewaresFor("compaction"),
1518
- (args) =>
1519
- defaultCompactionTerminal(
1520
- args,
1521
- buildPluginTurnContext(ctx, reqId),
1522
- ),
1523
- {
1524
- messages: msgs,
1525
- signal,
1526
- options: {
1527
- ...(opts ?? {}),
1528
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
1529
- actorTrustClass: resolveTurnActorTrustClass(ctx),
1530
- },
1531
- },
1532
- buildPluginTurnContext(ctx, reqId),
1533
- DEFAULT_TIMEOUTS.compaction,
1534
- )) as Awaited<
1535
- ReturnType<typeof ctx.contextWindowManager.maybeCompact>
1536
- >;
1537
- } catch (err) {
1538
- if (err instanceof PluginTimeoutError) {
1539
- rlog.warn(
1540
- { err, phase: "overflow-reducer-forced-compaction" },
1541
- "Compaction pipeline timed out — falling through to next reducer tier",
1542
- );
1543
- await ctx.agentLoop.compactionCircuit.recordOutcome(
1544
- ctx,
1545
- true,
1546
- onEvent,
1547
- );
1548
- return {
1549
- messages: msgs,
1550
- compacted: false,
1551
- previousEstimatedInputTokens: 0,
1552
- estimatedInputTokens: 0,
1553
- maxInputTokens: 0,
1554
- thresholdTokens: 0,
1555
- compactedMessages: 0,
1556
- compactedPersistedMessages: 0,
1557
- summaryCalls: 0,
1558
- summaryInputTokens: 0,
1559
- summaryOutputTokens: 0,
1560
- summaryModel: "",
1561
- summaryText: "",
1562
- reason: "compaction pipeline timed out",
1563
- };
1564
- }
1565
- throw err;
1566
- }
1567
- },
1568
- emitActivityState: () => {
1569
- ctx.emitActivityState("thinking", "context_compacting", {
1570
- requestId: reqId,
1571
- });
1572
- },
1573
- onCompactionResult: async (result, compactedBasis) => {
1574
- // Track circuit-breaker state whenever the reducer invoked
1575
- // compaction. The reducer's forced_compaction tier uses
1576
- // force:true, so it bypasses the open-circuit check, but we
1577
- // still want failure tracking to detect a run of broken
1578
- // summaries and clear the counter on success. Only track when
1579
- // the summary LLM actually ran — `summaryFailed === undefined`
1580
- // indicates an early return (no eligible messages,
1581
- // truncation-only path, etc.) that shouldn't influence the
1582
- // breaker.
1583
- if (result.summaryFailed !== undefined) {
1584
- await ctx.agentLoop.compactionCircuit.recordOutcome(
1585
- ctx,
1586
- result.summaryFailed,
1587
- onEvent,
1588
- );
1589
- }
1590
- if (result.compacted) {
1591
- await applySuccessfulCompaction(result, compactedBasis);
1592
- state.shouldInjectWorkspace = true;
1593
- }
1594
- },
1595
- reinjectForMode: async (
1596
- reducedMessages,
1597
- mode,
1598
- stepCompacted,
1599
- accumulatedCompacted,
1600
- ) => {
1601
- // Mirror the pre-PR-23 behavior: `ctx.messages` must track the
1602
- // reducer's latest output before re-injection runs, because other
1603
- // sites consulted through `injectionOpts` (`workspaceTopLevelContext`,
1604
- // slack history, etc.) depend on it and `applyCompactionResult`
1605
- // only updates `ctx.messages` on a compaction tier. Assigning here
1606
- // keeps non-compaction tiers (tool-result truncation, media
1607
- // stubbing, injection downgrade) observable to downstream
1608
- // injection assembly on the same turn.
1609
- ctx.messages = reducedMessages;
1610
-
1611
- // When THIS iteration compacted, it stripped the existing
1612
- // memory-static block — so we re-inject current content. A later
1613
- // iteration that only truncates or downgrades must NOT re-force it,
1614
- // or each round would grow the token count.
1615
- // Gate: only the iteration that actually compacted re-injects.
1616
- // (The `<knowledge_base>`, NOW.md, and v2 static `<info>` blocks
1617
- // self-gate inside their injectors on whether they are already
1618
- // present in `reducedMessages`.)
1619
- const injection = await applyRuntimeInjections(reducedMessages, {
1620
- ...injectionOpts,
1621
- workspaceTopLevelContext: buildWorkspaceTopLevelContext(
1622
- ctx,
1623
- state.shouldInjectWorkspace,
1624
- ),
1625
- // Once ANY iteration has compacted `ctx.messages`, the captured
1626
- // `slackChronologicalMessages` snapshot (built from the full
1627
- // persisted transcript) would overwrite the compacted history
1628
- // and undo compaction. Suppress the override from here on —
1629
- // sticky across subsequent non-compacting iterations.
1630
- slackChronologicalMessages: accumulatedCompacted
1631
- ? null
1632
- : injectionOpts.slackChronologicalMessages,
1633
- mode,
1634
- turnContext: buildPluginTurnContext(ctx, reqId),
1635
- });
1636
- let next = injection.messages;
1637
- if (isTrustedActor && mode !== "minimal") {
1638
- const memResult = ctx.graphMemory.reinjectCachedMemory(next);
1639
- next = memResult.runMessages;
1640
- }
1641
- return next;
1642
- },
1643
- estimatePostInjection: (runMsgs) =>
1644
- estimatePromptTokens(runMsgs, ctx.systemPrompt, {
1645
- providerName: estimationProviderName,
1646
- toolTokenBudget,
1647
- }),
1648
- };
1649
-
1650
- const overflowResult = await runPipeline<
1651
- OverflowReduceArgs,
1652
- OverflowReduceResult
1653
- >(
1654
- "overflowReduce",
1655
- getMiddlewaresFor("overflowReduce"),
1656
- // Terminal — only reached when every registered middleware calls
1657
- // `next` and delegates past the innermost layer. The default plugin
1658
- // is a terminal itself (it doesn't call `next`), so in practice
1659
- // this fallback fires only when the default has been explicitly
1660
- // deregistered (tests) and no user plugin replaces it. Strict-fail
1661
- // semantics: throw so the missing terminal surfaces as a visible
1662
- // error instead of silently returning the history untouched.
1663
- async () => {
1664
- throw new PluginExecutionError(
1665
- "overflowReduce pipeline has no terminal handler — every reducer middleware called next() without providing a replacement",
1666
- "overflowReduce",
1667
- );
1668
- },
1669
- overflowArgs,
1670
- buildPluginTurnContext(ctx, reqId),
1671
- DEFAULT_TIMEOUTS.overflowReduce,
1672
- );
1673
-
1674
- ctx.messages = overflowResult.messages;
1675
- runMessages = overflowResult.runMessages;
1676
- currentInjectionMode = overflowResult.injectionMode;
1677
- reducerState = overflowResult.reducerState;
1678
- if (overflowResult.reducerCompacted) {
1679
- state.reducerCompacted = true;
1680
- }
1681
- }
1682
-
1683
- // Replace historical web_search_tool_result blocks with text summaries.
1684
- // The opaque `encrypted_content` tokens Anthropic attaches to each result
1685
- // expire / are route-scoped; replaying a stale token is rejected with
1686
- // `Invalid encrypted_content in search_result block`. Titles + URLs
1687
- // preserve enough context for the model on follow-up turns.
1688
- const webSearchStrip = stripHistoricalWebSearchResults(runMessages);
1689
- if (webSearchStrip.stats.blocksStripped > 0) {
1690
- rlog.info(
1691
- { phase: "pre_run", ...webSearchStrip.stats },
1692
- "Converted historical web_search_tool_result blocks to text summaries",
1693
- );
1694
- runMessages = webSearchStrip.messages;
1695
- }
1696
-
1697
903
  // user-prompt-submit hook: plugins may transform `runMessages` right
1698
904
  // before the agent loop receives them. Fires once per user turn at the
1699
905
  // primary `agentLoop.run` only — the re-entry / retry calls further down
@@ -1718,6 +924,16 @@ export async function runAgentLoopImpl(
1718
924
  );
1719
925
  runMessages = finalUserPromptCtx.latestMessages;
1720
926
 
927
+ // Reducer state, tool-token budget, and calibration provider key consumed
928
+ // by the post-rejection convergence loop further down. The tool-token
929
+ // budget is resolved once per turn (the resolved tool set is stable across
930
+ // the turn); the calibration key matches the key recorded by `handleUsage`
931
+ // for wrapper providers (OpenRouter routing to Anthropic → key is
932
+ // `"anthropic"`).
933
+ let reducerState: ReducerState | undefined;
934
+ const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
935
+ const estimationProviderName = getCalibrationProviderKey(ctx.provider);
936
+
1721
937
  const shouldGenerateTitle = isReplaceableTitle(
1722
938
  getConversation(ctx.conversationId)?.title ?? null,
1723
939
  );
@@ -1750,75 +966,45 @@ export async function runAgentLoopImpl(
1750
966
 
1751
967
  rlog.info({ callSite: turnCallSite }, "Starting agent loop run");
1752
968
 
1753
- // Thread the orchestrator's canonical per-turn context into the agent
1754
- // loop so its internal pipeline invocations (e.g. compaction) see the
1755
- // real conversation identity / trust / contextWindowManager instead of
1756
- // the synthesized `"agent-loop"` placeholder. The loop clones this value
1757
- // and overwrites `turnIndex` with its own tool-use iteration counter.
1758
- const loopTurnCtx = buildPluginTurnContext(ctx, reqId);
1759
-
1760
- // Hook for the loop-owned mid-loop compaction. The agent loop owns the
1761
- // trigger (its budget gate), the `compaction` pipeline call, the result
1762
- // interpretation (circuit-breaker bookkeeping + the exhaustion decision),
1763
- // and the inline continue; this callback bridges the injection state the
1764
- // loop is intentionally blind to. Durable persistence is signalled via
1765
- // events; re-injection stays orchestrator-supplied for now.
1766
- const midLoopCompaction: MidLoopCompaction = {
1767
- postCompactionHook: async ({ history, turnContext }) => {
1768
- // stripInjectionsForCompaction() unconditionally removed the existing
1769
- // memory-static block, so re-inject the current content regardless of
1770
- // whether compaction actually ran. The `<knowledge_base>`, NOW.md, and
1771
- // v2 static `<info>` blocks self-gate inside their injectors on block
1772
- // presence.
1773
- const injection = await postCompactReinject({
1774
- ...injectionOpts,
1775
- workspaceTopLevelContext: buildWorkspaceTopLevelContext(
1776
- ctx,
1777
- state.shouldInjectWorkspace,
1778
- ),
1779
- // Suppress the chronological-transcript snapshot once the reducer
1780
- // has collapsed `ctx.messages`; the captured snapshot reflects the
1781
- // full persisted transcript and would overwrite compaction.
1782
- slackChronologicalMessages: state.reducerCompacted
1783
- ? null
1784
- : injectionOpts.slackChronologicalMessages,
1785
- mode: currentInjectionMode,
1786
- turnContext,
1787
- history,
1788
- isTrustedActor,
1789
- logger: rlog,
1790
- });
1791
- return injection.messages;
1792
- },
1793
- };
969
+ // Trust snapshot the loop forwards to its mid-loop in-place compaction
970
+ // (scoping the compactor's image manifest) and the post-compaction
971
+ // re-injection. Prefers the per-turn snapshot, then the conversation-level
972
+ // context, then the fallback matching the trust the runtime injection
973
+ // assembly resolves for the same turn. The loop's other turn-identity
974
+ // fields self-resolve from its own conversation id.
975
+ const loopTrust =
976
+ ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
1794
977
 
1795
978
  /**
1796
979
  * Shared closure: runs the agent loop with the orchestrator's turn
1797
980
  * context and maps the loop's returned checkpoint pause-reason into the
1798
981
  * orchestrator's yield bookkeeping. Returns the updated history so call
1799
- * sites consume it exactly as before. Pass `compaction` only for the
1800
- * primary run, where the loop compacts in place when its budget gate
1801
- * trips; reruns omit it and keep yielding for budget.
982
+ * sites consume it exactly as before. Pass `compactInPlace` only for the
983
+ * primary run: the loop then runs its budget gate before the first call
984
+ * (subsuming the proactive turn-start compaction) and compacts in place
985
+ * whenever the gate trips. Reruns omit it, skip the first-call gate, and
986
+ * keep yielding for budget.
1802
987
  */
1803
988
  const runAgentLoop = async (
1804
989
  msgs: Message[],
1805
- compaction?: MidLoopCompaction,
990
+ compactInPlace = false,
1806
991
  ): Promise<Message[]> => {
1807
992
  const { history, exitReason, appendedNewMessages, newMessages } =
1808
- await ctx.agentLoop.run(msgs, eventHandler, {
993
+ await ctx.agentLoop.run({
994
+ messages: msgs,
995
+ onEvent: eventHandler,
1809
996
  signal: abortController.signal,
1810
997
  requestId: reqId,
1811
998
  onCheckpoint,
1812
999
  callSite: turnCallSite,
1813
- turnContext: loopTurnCtx,
1000
+ trust: loopTrust,
1814
1001
  overrideProfile: turnOverrideProfile,
1815
1002
  resolveOverrideProfile: resolveCurrentOverrideProfile,
1816
1003
  resolveContextWindow,
1817
- compaction,
1818
- // memory-v3-live: the latest user message carries the volatile v3
1819
- // `<memory>` block, so anchor the provider's long-TTL cache breakpoint
1820
- // on the most recent stable message instead.
1821
- mutableLatestUserMessage: memoryV3Live,
1004
+ compactInPlace,
1005
+ isNonInteractive,
1006
+ modelProfile: modelProfileStr,
1007
+ actorContext,
1822
1008
  });
1823
1009
  lastRunAppendedNewMessages = appendedNewMessages;
1824
1010
  lastRunNewMessages = newMessages;
@@ -1832,7 +1018,7 @@ export async function runAgentLoopImpl(
1832
1018
  return history;
1833
1019
  };
1834
1020
 
1835
- let updatedHistory = await runAgentLoop(runMessages, midLoopCompaction);
1021
+ let updatedHistory = await runAgentLoop(runMessages, true);
1836
1022
 
1837
1023
  rlog.info(
1838
1024
  { resultMessageCount: updatedHistory.length },
@@ -1875,8 +1061,6 @@ export async function runAgentLoopImpl(
1875
1061
  // intentionally deferred until there's a concrete plugin-level use case.
1876
1062
  const retryRepair = deepRepairHistory(updatedHistory);
1877
1063
  runMessages = retryRepair.messages;
1878
- const retryStrip = stripHistoricalWebSearchResults(runMessages);
1879
- runMessages = retryStrip.messages;
1880
1064
  state.orderingErrorDetected = false;
1881
1065
  state.deferredOrderingError = null;
1882
1066
 
@@ -2064,14 +1248,12 @@ export async function runAgentLoopImpl(
2064
1248
  );
2065
1249
  if (emergencyResult.summaryFailed !== undefined) {
2066
1250
  await ctx.agentLoop.compactionCircuit.recordOutcome(
2067
- ctx,
2068
1251
  emergencyResult.summaryFailed,
2069
1252
  onEvent,
2070
1253
  );
2071
1254
  }
2072
1255
  if (emergencyResult.compacted) {
2073
1256
  await applySuccessfulCompaction(emergencyResult, ctx.messages);
2074
- state.shouldInjectWorkspace = true;
2075
1257
  }
2076
1258
  // Clear the overflow flag and re-run the agent loop with
2077
1259
  // the compacted context.
@@ -2119,8 +1301,11 @@ export async function runAgentLoopImpl(
2119
1301
  },
2120
1302
  reducerState,
2121
1303
  (msgs, signal, opts) =>
2122
- ctx.contextWindowManager.maybeCompact(msgs, signal!, {
2123
- ...(opts ?? {}),
1304
+ defaultCompact({
1305
+ manager: ctx.contextWindowManager,
1306
+ messages: msgs,
1307
+ signal,
1308
+ ...((opts ?? {}) as ContextWindowCompactOptions),
2124
1309
  overrideProfile: resolveCurrentOverrideProfile() ?? null,
2125
1310
  actorTrustClass: resolveTurnActorTrustClass(ctx),
2126
1311
  }),
@@ -2140,7 +1325,6 @@ export async function runAgentLoopImpl(
2140
1325
  step.compactionResult.summaryFailed !== undefined
2141
1326
  ) {
2142
1327
  await ctx.agentLoop.compactionCircuit.recordOutcome(
2143
- ctx,
2144
1328
  step.compactionResult.summaryFailed,
2145
1329
  onEvent,
2146
1330
  );
@@ -2151,8 +1335,6 @@ export async function runAgentLoopImpl(
2151
1335
  step.compactionResult,
2152
1336
  convergenceCompactionBasis,
2153
1337
  );
2154
- state.shouldInjectWorkspace = true;
2155
- state.reducerCompacted = true;
2156
1338
  }
2157
1339
 
2158
1340
  // Only re-inject the memory-static block when ctx.messages was
@@ -2161,29 +1343,17 @@ export async function runAgentLoopImpl(
2161
1343
  // blocks self-gate inside their injectors on whether they are already
2162
1344
  // present in `ctx.messages`.)
2163
1345
  const injection = await applyRuntimeInjections(ctx.messages, {
2164
- ...injectionOpts,
2165
- workspaceTopLevelContext: buildWorkspaceTopLevelContext(
2166
- ctx,
2167
- state.shouldInjectWorkspace,
2168
- ),
2169
- slackChronologicalMessages: state.reducerCompacted
2170
- ? null
2171
- : injectionOpts.slackChronologicalMessages,
1346
+ isNonInteractive,
1347
+ modelProfile: modelProfileStr,
1348
+ actorContext,
2172
1349
  mode: currentInjectionMode,
2173
- turnContext: buildPluginTurnContext(ctx, reqId),
1350
+ requestId: reqId,
1351
+ conversationId: ctx.conversationId,
2174
1352
  });
2175
1353
  runMessages = injection.messages;
2176
1354
  if (isTrustedActor && currentInjectionMode !== "minimal") {
2177
1355
  ctx.graphMemory.retrackCachedNodes();
2178
1356
  }
2179
- const convergenceStrip = stripHistoricalWebSearchResults(runMessages);
2180
- if (convergenceStrip.stats.blocksStripped > 0) {
2181
- rlog.info(
2182
- { phase: "convergence", ...convergenceStrip.stats },
2183
- "Converted historical web_search_tool_result blocks to text summaries",
2184
- );
2185
- runMessages = convergenceStrip.messages;
2186
- }
2187
1357
  state.contextTooLargeDetected = false;
2188
1358
  yieldedForBudget = false;
2189
1359
 
@@ -2219,7 +1389,7 @@ export async function runAgentLoopImpl(
2219
1389
  // through to the final graceful-error fallback below.
2220
1390
  if (state.contextTooLargeDetected) {
2221
1391
  const action = resolveOverflowAction({
2222
- overflowRecovery,
1392
+ overflowRecovery: convergenceBudget.overflowRecovery,
2223
1393
  isInteractive: isInteractiveResolved,
2224
1394
  });
2225
1395
 
@@ -2228,71 +1398,24 @@ export async function runAgentLoopImpl(
2228
1398
  ctx.emitActivityState("thinking", "context_compacting", {
2229
1399
  requestId: reqId,
2230
1400
  });
2231
- let emergencyCompact: Awaited<
2232
- ReturnType<typeof ctx.contextWindowManager.maybeCompact>
2233
- > | null = null;
2234
- try {
2235
- emergencyCompact = (await runPipeline<
2236
- CompactionArgs,
2237
- CompactionResult
2238
- >(
2239
- "compaction",
2240
- getMiddlewaresFor("compaction"),
2241
- (args) =>
2242
- defaultCompactionTerminal(
2243
- args,
2244
- buildPluginTurnContext(ctx, reqId),
2245
- ),
2246
- {
2247
- messages: ctx.messages,
2248
- signal: abortController.signal,
2249
- options: {
2250
- force: true,
2251
- minKeepRecentUserTurns: 0,
2252
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
2253
- },
2254
- },
2255
- buildPluginTurnContext(ctx, reqId),
2256
- DEFAULT_TIMEOUTS.compaction,
2257
- )) as Awaited<
2258
- ReturnType<typeof ctx.contextWindowManager.maybeCompact>
2259
- >;
2260
- } catch (err) {
2261
- if (err instanceof PluginTimeoutError) {
2262
- // Emergency compaction timed out. Record the circuit-breaker
2263
- // failure and fall through to the graceful-error path below
2264
- // (the unsuccessful-compaction fallback) rather than hard-
2265
- // failing the turn.
2266
- rlog.warn(
2267
- { err, phase: "emergency-compaction" },
2268
- "Emergency compaction pipeline timed out — continuing with overflow fallback",
2269
- );
2270
- await ctx.agentLoop.compactionCircuit.recordOutcome(
2271
- ctx,
2272
- true,
2273
- onEvent,
2274
- );
2275
- emergencyCompact = null;
2276
- } else {
2277
- throw err;
2278
- }
2279
- }
1401
+ const emergencyCompact = await defaultCompact({
1402
+ manager: ctx.contextWindowManager,
1403
+ messages: ctx.messages,
1404
+ signal: abortController.signal,
1405
+ force: true,
1406
+ minKeepRecentUserTurns: 0,
1407
+ overrideProfile: resolveCurrentOverrideProfile() ?? null,
1408
+ });
2280
1409
  // Only track when the summary LLM actually ran; `force: true`
2281
1410
  // bypasses the auto-threshold gate but not the early-return paths.
2282
- if (
2283
- emergencyCompact &&
2284
- emergencyCompact.summaryFailed !== undefined
2285
- ) {
1411
+ if (emergencyCompact.summaryFailed !== undefined) {
2286
1412
  await ctx.agentLoop.compactionCircuit.recordOutcome(
2287
- ctx,
2288
1413
  emergencyCompact.summaryFailed,
2289
1414
  onEvent,
2290
1415
  );
2291
1416
  }
2292
- if (emergencyCompact?.compacted) {
1417
+ if (emergencyCompact.compacted) {
2293
1418
  await applySuccessfulCompaction(emergencyCompact, ctx.messages);
2294
- state.reducerCompacted = true;
2295
- state.shouldInjectWorkspace = true;
2296
1419
  }
2297
1420
 
2298
1421
  // Only re-inject the memory-static block when ctx.messages was
@@ -2301,29 +1424,17 @@ export async function runAgentLoopImpl(
2301
1424
  // self-gate inside their injectors on whether they are already
2302
1425
  // present in `ctx.messages`.)
2303
1426
  const injection = await applyRuntimeInjections(ctx.messages, {
2304
- ...injectionOpts,
2305
- workspaceTopLevelContext: buildWorkspaceTopLevelContext(
2306
- ctx,
2307
- state.shouldInjectWorkspace,
2308
- ),
2309
- slackChronologicalMessages: state.reducerCompacted
2310
- ? null
2311
- : injectionOpts.slackChronologicalMessages,
1427
+ isNonInteractive,
1428
+ modelProfile: modelProfileStr,
1429
+ actorContext,
2312
1430
  mode: currentInjectionMode,
2313
- turnContext: buildPluginTurnContext(ctx, reqId),
1431
+ requestId: reqId,
1432
+ conversationId: ctx.conversationId,
2314
1433
  });
2315
1434
  runMessages = injection.messages;
2316
1435
  if (isTrustedActor && currentInjectionMode !== "minimal") {
2317
1436
  ctx.graphMemory.retrackCachedNodes();
2318
1437
  }
2319
- const fallbackStrip = stripHistoricalWebSearchResults(runMessages);
2320
- if (fallbackStrip.stats.blocksStripped > 0) {
2321
- rlog.info(
2322
- { phase: "fail_gracefully_compact", ...fallbackStrip.stats },
2323
- "Converted historical web_search_tool_result blocks to text summaries",
2324
- );
2325
- runMessages = fallbackStrip.messages;
2326
- }
2327
1438
  state.contextTooLargeDetected = false;
2328
1439
 
2329
1440
  updatedHistory = await runAgentLoop(runMessages);
@@ -2906,10 +2017,7 @@ export async function runAgentLoopImpl(
2906
2017
  // ── Helper ───────────────────────────────────────────────────────────
2907
2018
 
2908
2019
  function emitUsage(
2909
- ctx: Pick<
2910
- AgentLoopConversationContext,
2911
- "conversationId" | "provider" | "usageStats"
2912
- >,
2020
+ ctx: Pick<Conversation, "conversationId" | "provider" | "usageStats">,
2913
2021
  inputTokens: number,
2914
2022
  outputTokens: number,
2915
2023
  model: string,
@@ -2949,17 +2057,18 @@ function emitUsage(
2949
2057
  }
2950
2058
 
2951
2059
  /**
2952
- * Minimal context shape consumed by `applyCompactionResult`. Both
2953
- * `AgentLoopConversationContext` and `Conversation` satisfy this via structural
2954
- * typing, so the helper can back both the 5 agent-loop auto-compaction sites
2955
- * and the single `forceCompact` user-initiated site.
2060
+ * Minimal context shape consumed by `applyCompactionResult`, satisfied by
2061
+ * `Conversation` via structural typing, so the helper can back both the 5
2062
+ * agent-loop auto-compaction sites and the single `forceCompact`
2063
+ * user-initiated site.
2956
2064
  */
2957
2065
  export interface CompactionApplyContext {
2958
2066
  readonly conversationId: string;
2959
2067
  messages: Message[];
2960
2068
  contextCompactedMessageCount: number;
2961
2069
  contextCompactedAt: number | null;
2962
- pendingPostCompactReinject: boolean;
2070
+ contextSummary: string | null;
2071
+ slackContextCompactionWatermarkTs: string | null;
2963
2072
  readonly graphMemory: ConversationGraphMemory;
2964
2073
  readonly provider: Provider;
2965
2074
  usageStats: UsageStats;
@@ -3006,13 +2115,22 @@ export async function applyCompactionResult(
3006
2115
  } = {},
3007
2116
  ): Promise<void> {
3008
2117
  ctx.messages = result.messages;
3009
- ctx.contextCompactedMessageCount += result.compactedPersistedMessages;
2118
+ // Compaction operates on the in-context history. Untrusted actor views
2119
+ // render that history unsliced (boundary 0); trusted views start past the
2120
+ // already-compacted prefix (the mirrored DB count). Advance from that
2121
+ // in-context boundary rather than the raw mirror so the persisted count
2122
+ // stays consistent with what the new summary represents and never
2123
+ // double-counts an unsliced untrusted view.
2124
+ const inContextCompactedCount = isUntrustedTrustClass(
2125
+ ctx.trustContext?.trustClass,
2126
+ )
2127
+ ? 0
2128
+ : ctx.contextCompactedMessageCount;
2129
+ ctx.contextCompactedMessageCount =
2130
+ inContextCompactedCount + result.compactedPersistedMessages;
2131
+ ctx.contextSummary = result.summaryText;
3010
2132
  const compactedAt = Date.now();
3011
2133
  ctx.contextCompactedAt = compactedAt;
3012
- // Signal to the next agent loop turn that NOW.md / PKB / v2 static blocks
3013
- // were stripped from the tail and need fresh re-injection. Consumed and
3014
- // cleared at the top of the next `runAgentLoopImpl` run.
3015
- ctx.pendingPostCompactReinject = true;
3016
2134
  await ctx.graphMemory.onCompacted(result.compactedPersistedMessages);
3017
2135
  updateConversationContextWindow(
3018
2136
  ctx.conversationId,
@@ -3026,6 +2144,8 @@ export async function applyCompactionResult(
3026
2144
  options.slackContextCompactionWatermarkTs,
3027
2145
  compactedAt,
3028
2146
  );
2147
+ ctx.slackContextCompactionWatermarkTs =
2148
+ options.slackContextCompactionWatermarkTs;
3029
2149
  }
3030
2150
  enqueueAutoAnalysisOnCompaction(
3031
2151
  ctx.conversationId,