@vellumai/assistant 0.8.6 → 0.8.7-dev.202606052118.34cd356

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 (1078) hide show
  1. package/AGENTS.md +4 -4
  2. package/Dockerfile +21 -4
  3. package/bun.lock +13 -4
  4. package/docker-entrypoint.sh +12 -8
  5. package/docker-init-apt-root.sh +3 -1
  6. package/docker-kata-apt-env.sh +3 -1
  7. package/docker-kata-runtime-family.sh +12 -0
  8. package/docs/architecture/memory.md +1 -1
  9. package/docs/plugins.md +110 -83
  10. package/examples/plugins/echo/README.md +13 -12
  11. package/examples/plugins/echo/register.ts +0 -54
  12. package/knip.json +1 -0
  13. package/node_modules/@vellumai/environments/bun.lock +24 -0
  14. package/node_modules/@vellumai/environments/package.json +18 -0
  15. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
  16. package/node_modules/@vellumai/environments/src/index.ts +11 -0
  17. package/node_modules/@vellumai/environments/src/seeds.ts +73 -0
  18. package/node_modules/@vellumai/environments/src/types.ts +70 -0
  19. package/node_modules/@vellumai/environments/tsconfig.json +20 -0
  20. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +11 -0
  21. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +3 -4
  22. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  23. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +13 -8
  24. package/openapi.yaml +6964 -539
  25. package/package.json +8 -4
  26. package/scripts/generate-openapi.ts +88 -54
  27. package/src/__tests__/agent-loop-callsite-precedence.test.ts +42 -80
  28. package/src/__tests__/agent-loop-exit-reason.test.ts +188 -45
  29. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +141 -0
  30. package/src/__tests__/agent-loop-override-profile.test.ts +19 -32
  31. package/src/__tests__/agent-loop-provider-error-recording.test.ts +7 -5
  32. package/src/__tests__/agent-loop-thinking.test.ts +17 -12
  33. package/src/__tests__/agent-loop.test.ts +238 -422
  34. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +6 -2
  35. package/src/__tests__/agent-wake-override-profile.test.ts +22 -40
  36. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  37. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  38. package/src/__tests__/anthropic-provider.test.ts +296 -57
  39. package/src/__tests__/app-builder-skill-instructions.test.ts +22 -0
  40. package/src/__tests__/app-control-flow.test.ts +6 -1
  41. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  42. package/src/__tests__/approval-cascade.test.ts +4 -11
  43. package/src/__tests__/approval-routes-http.test.ts +8 -3
  44. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  45. package/src/__tests__/assistant-event.test.ts +15 -0
  46. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  47. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  48. package/src/__tests__/assistant-stream-state.test.ts +645 -0
  49. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  50. package/src/__tests__/avatar-e2e.test.ts +7 -37
  51. package/src/__tests__/avatar-generator.test.ts +12 -42
  52. package/src/__tests__/avatar-identity-sync.test.ts +28 -3
  53. package/src/__tests__/background-shell-bash.test.ts +3 -7
  54. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  55. package/src/__tests__/btw-routes.test.ts +69 -15
  56. package/src/__tests__/build-persisted-content.test.ts +184 -0
  57. package/src/__tests__/call-pointer-messages.test.ts +5 -3
  58. package/src/__tests__/call-site-routing-provider.test.ts +22 -40
  59. package/src/__tests__/catalog-files.test.ts +1 -0
  60. package/src/__tests__/channel-approval-routes.test.ts +49 -21
  61. package/src/__tests__/channel-approvals.test.ts +4 -2
  62. package/src/__tests__/channel-invite-transport.test.ts +1 -5
  63. package/src/__tests__/channel-readiness-routes.test.ts +0 -4
  64. package/src/__tests__/channel-readiness-slack-remote.test.ts +2 -7
  65. package/src/__tests__/channel-retry-sweep.test.ts +71 -79
  66. package/src/__tests__/clawhub-files.test.ts +1 -0
  67. package/src/__tests__/compaction-circuit.test.ts +258 -0
  68. package/src/__tests__/compaction-direct.test.ts +132 -0
  69. package/src/__tests__/compaction-events.test.ts +5 -17
  70. package/src/__tests__/compaction-trail-store.test.ts +1 -79
  71. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  72. package/src/__tests__/compactor-image-manifest-trust.test.ts +112 -0
  73. package/src/__tests__/computer-use-tools.test.ts +2 -2
  74. package/src/__tests__/config-watcher.test.ts +28 -0
  75. package/src/__tests__/context-search-agent-runner.test.ts +6 -3
  76. package/src/__tests__/context-token-estimator.test.ts +34 -0
  77. package/src/__tests__/context-window-manager-compact-retry.test.ts +291 -0
  78. package/src/__tests__/conversation-abort-tool-results.test.ts +70 -25
  79. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +9 -7
  80. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -34
  81. package/src/__tests__/conversation-agent-loop-overflow.test.ts +476 -963
  82. package/src/__tests__/conversation-agent-loop.test.ts +823 -1321
  83. package/src/__tests__/conversation-analysis-routes.test.ts +7 -3
  84. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  85. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  86. package/src/__tests__/conversation-clear-safety.test.ts +20 -10
  87. package/src/__tests__/conversation-confirmation-signals.test.ts +15 -45
  88. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  89. package/src/__tests__/conversation-disk-view.test.ts +10 -17
  90. package/src/__tests__/conversation-fork-crud.test.ts +86 -172
  91. package/src/__tests__/conversation-fork-route.test.ts +16 -14
  92. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  93. package/src/__tests__/conversation-init.benchmark.test.ts +6 -6
  94. package/src/__tests__/conversation-lifecycle.test.ts +3 -2
  95. package/src/__tests__/conversation-load-history-repair.test.ts +3 -2
  96. package/src/__tests__/conversation-load-history-stripped.test.ts +1 -1
  97. package/src/__tests__/conversation-message-sync-tags.test.ts +3 -4
  98. package/src/__tests__/conversation-pairing.test.ts +10 -7
  99. package/src/__tests__/conversation-pre-run-repair.test.ts +1 -1
  100. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -0
  101. package/src/__tests__/conversation-process-callsite.test.ts +27 -30
  102. package/src/__tests__/conversation-provider-retry-repair.test.ts +80 -51
  103. package/src/__tests__/conversation-queue.test.ts +272 -164
  104. package/src/__tests__/conversation-routes-disk-view.test.ts +6 -2
  105. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  106. package/src/__tests__/conversation-routes-slash-commands.test.ts +8 -7
  107. package/src/__tests__/conversation-runtime-assembly.test.ts +317 -313
  108. package/src/__tests__/conversation-runtime-workspace.test.ts +114 -36
  109. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  110. package/src/__tests__/conversation-slash-queue.test.ts +42 -31
  111. package/src/__tests__/conversation-slash-unknown.test.ts +13 -15
  112. package/src/__tests__/conversation-speed-override.test.ts +8 -22
  113. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  114. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +90 -15
  115. package/src/__tests__/conversation-surfaces-app-control.test.ts +32 -4
  116. package/src/__tests__/conversation-surfaces-state-update.test.ts +5 -2
  117. package/src/__tests__/conversation-surfaces-table-action.test.ts +6 -15
  118. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  119. package/src/__tests__/conversation-title-service.test.ts +135 -2
  120. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +23 -11
  121. package/src/__tests__/conversation-unread-route.test.ts +14 -2
  122. package/src/__tests__/conversation-usage.test.ts +0 -2
  123. package/src/__tests__/conversation-wipe.test.ts +1 -1
  124. package/src/__tests__/conversation-workspace-cache-state.test.ts +20 -17
  125. package/src/__tests__/conversation-workspace-injection.test.ts +114 -23
  126. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +34 -13
  127. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  128. package/src/__tests__/credential-execution-tools.test.ts +1 -2
  129. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  130. package/src/__tests__/cross-provider-web-search.test.ts +220 -3
  131. package/src/__tests__/cu-unified-flow.test.ts +26 -1
  132. package/src/__tests__/db-acp-history.test.ts +101 -0
  133. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -0
  134. package/src/__tests__/disk-pressure-guard.test.ts +66 -0
  135. package/src/__tests__/disk-pressure-routes.test.ts +9 -2
  136. package/src/__tests__/dm-persistence.test.ts +12 -3
  137. package/src/__tests__/dynamic-page-surface.test.ts +99 -0
  138. package/src/__tests__/edit-propagation.test.ts +1 -2
  139. package/src/__tests__/empty-response-hook.test.ts +304 -0
  140. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  141. package/src/__tests__/file-write-tool.test.ts +63 -0
  142. package/src/__tests__/filing-service.test.ts +2 -2
  143. package/src/__tests__/first-greeting.test.ts +55 -14
  144. package/src/__tests__/gemini-image-service.test.ts +13 -0
  145. package/src/__tests__/gemini-inline-media.test.ts +78 -0
  146. package/src/__tests__/gemini-provider.test.ts +351 -28
  147. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  148. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  149. package/src/__tests__/guardian-routing-state.test.ts +60 -71
  150. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +10 -8
  151. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  152. package/src/__tests__/heartbeat-service.test.ts +3 -1
  153. package/src/__tests__/helpers/mock-provider.ts +110 -0
  154. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  155. package/src/__tests__/history-repair-hook.test.ts +162 -0
  156. package/src/__tests__/history-repair-observability.test.ts +1 -1
  157. package/src/__tests__/history-repair.test.ts +2 -1
  158. package/src/__tests__/host-app-control-proxy.test.ts +2 -0
  159. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  160. package/src/__tests__/host-cu-proxy.test.ts +2 -0
  161. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  162. package/src/__tests__/host-file-edit-tool.test.ts +4 -2
  163. package/src/__tests__/host-file-proxy.test.ts +31 -0
  164. package/src/__tests__/host-file-read-tool.test.ts +4 -2
  165. package/src/__tests__/host-file-write-tool.test.ts +9 -3
  166. package/src/__tests__/host-proxy-preactivation.test.ts +53 -14
  167. package/src/__tests__/host-shell-tool.test.ts +9 -4
  168. package/src/__tests__/http-user-message-parity.test.ts +2 -2
  169. package/src/__tests__/identity-intro-cache.test.ts +47 -114
  170. package/src/__tests__/identity-routes.test.ts +248 -7
  171. package/src/__tests__/inbound-slack-persistence.test.ts +12 -3
  172. package/src/__tests__/injector-background-turn.test.ts +3 -9
  173. package/src/__tests__/injector-chain.test.ts +139 -275
  174. package/src/__tests__/injector-disk-pressure.test.ts +75 -41
  175. package/src/__tests__/injector-document-comments.test.ts +3 -3
  176. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  177. package/src/__tests__/injector-v3-suppression.test.ts +214 -0
  178. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  179. package/src/__tests__/list-messages-attachments.test.ts +7 -8
  180. package/src/__tests__/list-messages-hidden-metadata.test.ts +55 -15
  181. package/src/__tests__/list-messages-page-latest.test.ts +60 -1
  182. package/src/__tests__/list-messages-tool-merge.test.ts +56 -6
  183. package/src/__tests__/llm-request-log-turn-query.test.ts +42 -86
  184. package/src/__tests__/llm-resolver.test.ts +23 -47
  185. package/src/__tests__/llm-usage-store.test.ts +268 -1
  186. package/src/__tests__/log-export-routes.test.ts +59 -0
  187. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -8
  188. package/src/__tests__/mcp-auth-routes.test.ts +15 -10
  189. package/src/__tests__/mcp-health-check.test.ts +18 -13
  190. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  191. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  192. package/src/__tests__/messaging-send-tool.test.ts +8 -4
  193. package/src/__tests__/migration-export-http.test.ts +12 -12
  194. package/src/__tests__/migration-import-commit-http.test.ts +8 -8
  195. package/src/__tests__/migration-import-preflight-http.test.ts +7 -7
  196. package/src/__tests__/migration-validate-http.test.ts +3 -3
  197. package/src/__tests__/native-web-search.test.ts +205 -20
  198. package/src/__tests__/notification-decision-identity.test.ts +9 -18
  199. package/src/__tests__/notification-decision-recipient-context.test.ts +3 -6
  200. package/src/__tests__/oauth-commands-routes.test.ts +1 -1
  201. package/src/__tests__/onboarding-template-contract.test.ts +12 -0
  202. package/src/__tests__/openai-image-service.test.ts +17 -0
  203. package/src/__tests__/openai-provider.test.ts +97 -71
  204. package/src/__tests__/openai-responses-provider.test.ts +21 -77
  205. package/src/__tests__/outbound-slack-persistence.test.ts +2 -1
  206. package/src/__tests__/{overflow-reduce-pipeline.test.ts → overflow-reduction-loop.test.ts} +64 -286
  207. package/src/__tests__/parallel-tool.benchmark.test.ts +24 -36
  208. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  209. package/src/__tests__/persistence-secret-redaction.test.ts +3 -1
  210. package/src/__tests__/pipeline-runner.test.ts +31 -43
  211. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  212. package/src/__tests__/plugin-bootstrap.test.ts +62 -51
  213. package/src/__tests__/plugin-registry.test.ts +0 -27
  214. package/src/__tests__/plugin-route-contribution.test.ts +6 -16
  215. package/src/__tests__/plugin-skill-contribution.test.ts +7 -17
  216. package/src/__tests__/plugin-tool-contribution.test.ts +10 -26
  217. package/src/__tests__/plugin-types.test.ts +8 -173
  218. package/src/__tests__/prechat-onboarding-contract.test.ts +23 -0
  219. package/src/__tests__/process-message-background-slack.test.ts +17 -16
  220. package/src/__tests__/process-message-display-content.test.ts +36 -44
  221. package/src/__tests__/provider-commit-message-generator.test.ts +19 -14
  222. package/src/__tests__/provider-error-scenarios.test.ts +7 -6
  223. package/src/__tests__/provider-platform-proxy-integration.test.ts +3 -8
  224. package/src/__tests__/provider-send-message-override-profile.test.ts +9 -25
  225. package/src/__tests__/provider-streaming.benchmark.test.ts +12 -22
  226. package/src/__tests__/provider-usage-tracking.test.ts +0 -6
  227. package/src/__tests__/ratelimit.test.ts +9 -4
  228. package/src/__tests__/reaction-persistence.test.ts +1 -1
  229. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  230. package/src/__tests__/relay-server.test.ts +20 -13
  231. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  232. package/src/__tests__/retry-openrouter-only-normalization.test.ts +5 -8
  233. package/src/__tests__/retry-thinking-tool-choice.test.ts +10 -13
  234. package/src/__tests__/retry-verbosity-normalization.test.ts +5 -8
  235. package/src/__tests__/runtime-events-sse-reconnect.test.ts +390 -0
  236. package/src/__tests__/schedule-routes.test.ts +683 -12
  237. package/src/__tests__/schedule-store.test.ts +108 -0
  238. package/src/__tests__/schedule-tools.test.ts +160 -0
  239. package/src/__tests__/secret-ingress-http.test.ts +2 -2
  240. package/src/__tests__/secret-prompt-log-hygiene.test.ts +11 -7
  241. package/src/__tests__/secret-prompter-channel-fallback.test.ts +11 -9
  242. package/src/__tests__/secret-response-routing.test.ts +13 -11
  243. package/src/__tests__/send-endpoint-busy.test.ts +6 -2
  244. package/src/__tests__/server-history-render.test.ts +314 -1
  245. package/src/__tests__/shell-observability.test.ts +249 -0
  246. package/src/__tests__/skill-feature-flags-integration.test.ts +44 -11
  247. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  248. package/src/__tests__/skill-load-feature-flag.test.ts +10 -10
  249. package/src/__tests__/skills-files-catalog-fallback.test.ts +10 -0
  250. package/src/__tests__/skillssh-files.test.ts +1 -0
  251. package/src/__tests__/starter-task-flow.test.ts +6 -6
  252. package/src/__tests__/strip-memory-injections.test.ts +102 -14
  253. package/src/__tests__/subagent-call-site-routing.test.ts +3 -3
  254. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  255. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  256. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  257. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  258. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  259. package/src/__tests__/suggestion-routes.test.ts +3 -3
  260. package/src/__tests__/sync-message-contract.test.ts +19 -16
  261. package/src/__tests__/system-prompt.test.ts +74 -0
  262. package/src/__tests__/task-scheduler.test.ts +162 -1
  263. package/src/__tests__/terminal-tools.test.ts +9 -25
  264. package/src/__tests__/thread-backfill.test.ts +4 -9
  265. package/src/__tests__/title-generate-hook.test.ts +319 -0
  266. package/src/__tests__/tool-error-hook.test.ts +278 -0
  267. package/src/__tests__/tool-preview-lifecycle.test.ts +481 -16
  268. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  269. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  270. package/src/__tests__/tool-result-truncation.test.ts +1 -1
  271. package/src/__tests__/tools-audio-read.test.ts +113 -0
  272. package/src/__tests__/turn-boundary-resolution.test.ts +44 -84
  273. package/src/__tests__/turn-events-store.test.ts +11 -7
  274. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  275. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  276. package/src/__tests__/usage-routes.test.ts +285 -1
  277. package/src/__tests__/user-plugin-loader.test.ts +2 -2
  278. package/src/__tests__/voice-scoped-grant-consumer.test.ts +8 -6
  279. package/src/__tests__/voice-session-bridge.test.ts +19 -10
  280. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  281. package/src/acp/__tests__/agent-process.test.ts +161 -0
  282. package/src/acp/__tests__/client-handler.test.ts +40 -0
  283. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  284. package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
  285. package/src/acp/__tests__/prepare-agent-env.test.ts +143 -31
  286. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  287. package/src/acp/__tests__/session-manager-resume.test.ts +695 -0
  288. package/src/acp/agent-process.ts +61 -1
  289. package/src/acp/auto-install.test.ts +125 -0
  290. package/src/acp/auto-install.ts +174 -0
  291. package/src/acp/client-handler.ts +31 -0
  292. package/src/acp/feature-gate.test.ts +48 -0
  293. package/src/acp/feature-gate.ts +34 -0
  294. package/src/acp/prepare-agent-env.ts +52 -11
  295. package/src/acp/resolve-agent.test.ts +147 -6
  296. package/src/acp/resolve-agent.ts +81 -7
  297. package/src/acp/resume-hint.ts +22 -0
  298. package/src/acp/session-manager.ts +487 -71
  299. package/src/agent/compaction-circuit.ts +98 -0
  300. package/src/agent/loop.ts +651 -450
  301. package/src/api/README.md +19 -17
  302. package/src/api/constants/tool-execution.ts +21 -0
  303. package/src/api/events/assistant-activity-state.ts +75 -0
  304. package/src/api/events/assistant-outbound-attachment.ts +25 -27
  305. package/src/api/events/assistant-text-delta.ts +6 -8
  306. package/src/api/events/assistant-thinking-delta.ts +33 -0
  307. package/src/api/events/assistant-turn-start.ts +5 -7
  308. package/src/api/events/avatar-updated.ts +24 -0
  309. package/src/api/events/compaction-circuit-closed.ts +26 -0
  310. package/src/api/events/compaction-circuit-open.ts +28 -0
  311. package/src/api/events/confirmation-request.ts +114 -0
  312. package/src/api/events/contact-request.ts +33 -0
  313. package/src/api/events/conversation-error.ts +77 -0
  314. package/src/api/events/conversation-list-invalidated.ts +38 -0
  315. package/src/api/events/conversation-title-updated.ts +24 -0
  316. package/src/api/events/disk-pressure-status-changed.ts +61 -0
  317. package/src/api/events/document-comment-created.ts +24 -28
  318. package/src/api/events/document-comment-deleted.ts +6 -8
  319. package/src/api/events/document-comment-reopened.ts +6 -8
  320. package/src/api/events/document-comment-resolved.ts +8 -10
  321. package/src/api/events/document-editor-update.ts +27 -0
  322. package/src/api/events/error.ts +32 -0
  323. package/src/api/events/generation-cancelled.ts +4 -6
  324. package/src/api/events/generation-handoff.ts +13 -15
  325. package/src/api/events/home-feed-updated.ts +26 -0
  326. package/src/api/events/identity-changed.ts +32 -0
  327. package/src/api/events/interaction-resolved.ts +50 -0
  328. package/src/api/events/message-complete.ts +10 -12
  329. package/src/api/events/message-dequeued.ts +21 -0
  330. package/src/api/events/message-queued-deleted.ts +23 -0
  331. package/src/api/events/message-queued.ts +22 -0
  332. package/src/api/events/message-request-complete.ts +29 -0
  333. package/src/api/events/navigate-settings.ts +20 -0
  334. package/src/api/events/notification-intent.ts +33 -0
  335. package/src/api/events/open-url.ts +6 -8
  336. package/src/api/events/question-request.ts +67 -0
  337. package/src/api/events/relationship-state-updated.ts +4 -6
  338. package/src/api/events/secret-request.ts +42 -0
  339. package/src/api/events/subagent-event.ts +79 -0
  340. package/src/api/events/subagent-spawned.ts +40 -0
  341. package/src/api/events/subagent-status-changed.ts +65 -0
  342. package/src/api/events/sync-changed.ts +29 -0
  343. package/src/api/events/tool-output-chunk.ts +45 -0
  344. package/src/api/events/tool-result.ts +129 -0
  345. package/src/api/events/tool-use-preview-start.ts +32 -0
  346. package/src/api/events/tool-use-start.ts +8 -10
  347. package/src/api/events/trace-event.ts +69 -0
  348. package/src/api/events/turn-profile-auto-routed.ts +28 -0
  349. package/src/api/events/ui-surface-complete.ts +30 -0
  350. package/src/api/events/ui-surface-dismiss.ts +22 -0
  351. package/src/api/events/ui-surface-show.ts +67 -0
  352. package/src/api/events/ui-surface-update.ts +26 -0
  353. package/src/api/events/usage-update.ts +34 -0
  354. package/src/api/events/user-message-echo.ts +35 -0
  355. package/src/api/index.ts +389 -0
  356. package/src/api/requests/dictation.ts +45 -0
  357. package/src/api/responses/conversation-message.ts +374 -0
  358. package/src/api/responses/disk-pressure-status.ts +26 -0
  359. package/src/api/responses/home.ts +217 -0
  360. package/src/api/responses/llm-context-response.ts +2 -0
  361. package/src/api/responses/memory-v3-selection-log.ts +50 -0
  362. package/src/api/responses/subagent-detail.ts +48 -0
  363. package/src/approvals/guardian-decision-primitive.ts +7 -15
  364. package/src/approvals/guardian-request-resolvers.ts +7 -10
  365. package/src/avatar/__tests__/avatar-manifest.test.ts +236 -0
  366. package/src/avatar/__tests__/avatar-store.test.ts +198 -0
  367. package/src/avatar/avatar-manifest.ts +195 -0
  368. package/src/avatar/avatar-store.ts +113 -0
  369. package/src/avatar/traits-png-sync.ts +8 -2
  370. package/src/background-wake/next-wake.test.ts +31 -1
  371. package/src/background-wake/next-wake.ts +5 -1
  372. package/src/calls/call-conversation-messages.ts +6 -4
  373. package/src/calls/guardian-action-sweep.ts +6 -4
  374. package/src/calls/relay-server.ts +12 -8
  375. package/src/calls/voice-session-bridge.ts +13 -27
  376. package/src/cli/commands/__tests__/memory-v3.test.ts +245 -0
  377. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  378. package/src/cli/commands/avatar.ts +17 -11
  379. package/src/cli/commands/conversations.ts +15 -1
  380. package/src/cli/commands/db/__tests__/repair.test.ts +540 -0
  381. package/src/cli/commands/db/__tests__/status.test.ts +253 -0
  382. package/src/cli/commands/db/format.ts +48 -0
  383. package/src/cli/commands/db/index.ts +29 -0
  384. package/src/cli/commands/db/repair-step-conversation-backfill.ts +345 -0
  385. package/src/cli/commands/db/repair-step-integrity.ts +146 -0
  386. package/src/cli/commands/db/repair-steps.ts +164 -0
  387. package/src/cli/commands/db/repair.ts +141 -0
  388. package/src/cli/commands/db/status.ts +366 -0
  389. package/src/cli/commands/memory-v3.ts +159 -445
  390. package/src/cli/commands/notifications.ts +112 -60
  391. package/src/cli/lib/cli-colors.ts +24 -6
  392. package/src/cli/program.ts +4 -5
  393. package/src/config/__tests__/feature-flag-registry-guard.test.ts +4 -4
  394. package/src/config/acp-defaults.test.ts +10 -0
  395. package/src/config/acp-defaults.ts +6 -0
  396. package/src/config/assistant-feature-flags.ts +24 -13
  397. package/src/config/bundled-skills/acp/SKILL.md +64 -30
  398. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  399. package/src/config/bundled-skills/app-builder/SKILL.md +224 -387
  400. package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
  401. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  402. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  403. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  404. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  405. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  406. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  407. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  408. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  409. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  410. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  411. package/src/config/bundled-skills/media-processing/services/reduce.ts +6 -9
  412. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  413. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +7 -2
  414. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  415. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  416. package/src/config/bundled-tool-registry.ts +2 -0
  417. package/src/config/call-site-defaults.ts +2 -7
  418. package/src/config/feature-flag-cache.ts +3 -3
  419. package/src/config/feature-flag-registry.json +68 -12
  420. package/src/config/schemas/__tests__/memory-v2.test.ts +2 -226
  421. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  422. package/src/config/schemas/call-site-catalog.ts +8 -15
  423. package/src/config/schemas/heartbeat.ts +9 -0
  424. package/src/config/schemas/llm.ts +3 -3
  425. package/src/config/schemas/memory-lifecycle.ts +24 -0
  426. package/src/config/schemas/memory-v2.ts +8 -253
  427. package/src/config/schemas/memory-v3.ts +47 -0
  428. package/src/config/schemas/memory.ts +6 -1
  429. package/src/config/schemas/platform.ts +8 -0
  430. package/src/config/schemas/timeouts.ts +3 -1
  431. package/src/config/seed-inference-profiles.ts +2 -2
  432. package/src/config/skills.ts +13 -0
  433. package/src/context/compactor.ts +55 -32
  434. package/src/context/strip-injections.ts +128 -0
  435. package/src/context/token-estimator.ts +42 -0
  436. package/src/context/tool-result-truncation.ts +1 -66
  437. package/src/context/window-manager.ts +141 -26
  438. package/src/credential-execution/executable-discovery.ts +16 -0
  439. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  440. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +2 -2
  441. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  442. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  443. package/src/daemon/__tests__/web-search-status-text.test.ts +10 -6
  444. package/src/daemon/approval-generators.ts +4 -4
  445. package/src/daemon/assistant-attachments.ts +1 -1
  446. package/src/daemon/config-watcher.ts +7 -1
  447. package/src/daemon/context-overflow-reducer.ts +0 -1
  448. package/src/daemon/conversation-agent-loop-handlers.ts +793 -215
  449. package/src/daemon/conversation-agent-loop.ts +487 -1478
  450. package/src/daemon/conversation-error.ts +7 -7
  451. package/src/daemon/conversation-history.ts +27 -10
  452. package/src/daemon/conversation-launch.ts +4 -8
  453. package/src/daemon/conversation-lifecycle.ts +13 -42
  454. package/src/daemon/conversation-messaging.ts +8 -9
  455. package/src/daemon/conversation-notifiers.ts +7 -5
  456. package/src/daemon/conversation-process.ts +109 -93
  457. package/src/daemon/conversation-registry.ts +159 -0
  458. package/src/daemon/conversation-runtime-assembly.ts +209 -382
  459. package/src/daemon/conversation-slash.ts +6 -25
  460. package/src/daemon/conversation-store.ts +15 -95
  461. package/src/daemon/conversation-surfaces.ts +277 -73
  462. package/src/daemon/conversation-tool-setup.ts +5 -29
  463. package/src/daemon/conversation-workspace.ts +17 -0
  464. package/src/daemon/conversation.ts +123 -146
  465. package/src/daemon/daemon-skill-host.ts +2 -6
  466. package/src/daemon/disk-pressure-guard.ts +35 -29
  467. package/src/daemon/external-plugins-bootstrap.ts +53 -32
  468. package/src/daemon/first-greeting.ts +26 -4
  469. package/src/daemon/guardian-action-generators.ts +2 -2
  470. package/src/daemon/handlers/config-a2a.ts +51 -36
  471. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  472. package/src/daemon/handlers/config-telegram.ts +16 -2
  473. package/src/daemon/handlers/conversations.ts +9 -23
  474. package/src/daemon/handlers/shared.ts +158 -82
  475. package/src/daemon/handlers/skills.ts +53 -20
  476. package/src/daemon/host-app-control-proxy.ts +54 -1
  477. package/src/daemon/host-cu-proxy.ts +46 -22
  478. package/src/daemon/host-file-proxy.ts +25 -1
  479. package/src/daemon/host-proxy-preactivation.ts +25 -6
  480. package/src/daemon/lifecycle.ts +53 -55
  481. package/src/daemon/message-protocol.ts +2 -3
  482. package/src/daemon/message-provenance.ts +49 -0
  483. package/src/daemon/message-types/apps.ts +1 -29
  484. package/src/daemon/message-types/contacts.ts +3 -20
  485. package/src/daemon/message-types/conversations.ts +13 -111
  486. package/src/daemon/message-types/documents.ts +3 -9
  487. package/src/daemon/message-types/home.ts +4 -17
  488. package/src/daemon/message-types/integrations.ts +2 -6
  489. package/src/daemon/message-types/messages.ts +37 -400
  490. package/src/daemon/message-types/notifications.ts +2 -32
  491. package/src/daemon/message-types/settings.ts +3 -8
  492. package/src/daemon/message-types/skills.ts +4 -0
  493. package/src/daemon/message-types/surfaces.ts +138 -3
  494. package/src/daemon/message-types/sync.ts +12 -25
  495. package/src/daemon/message-types/workspace.ts +3 -11
  496. package/src/daemon/now-scratchpad.ts +21 -0
  497. package/src/daemon/orphan-reaper.test.ts +210 -0
  498. package/src/daemon/orphan-reaper.ts +240 -0
  499. package/src/daemon/overflow-reduction-loop.ts +230 -0
  500. package/src/daemon/persist-unsendable-image.ts +117 -0
  501. package/src/daemon/process-message.ts +50 -49
  502. package/src/daemon/server.ts +14 -0
  503. package/src/daemon/tool-side-effects.ts +10 -7
  504. package/src/daemon/trace-emitter.ts +6 -4
  505. package/src/daemon/trust-context.ts +32 -0
  506. package/src/daemon/wake-target-adapter.ts +14 -2
  507. package/src/heartbeat/__tests__/heartbeat-service.test.ts +6 -1
  508. package/src/heartbeat/heartbeat-run-store.ts +54 -1
  509. package/src/heartbeat/heartbeat-service.ts +42 -0
  510. package/src/home/feed-types.ts +36 -221
  511. package/src/home/home-greeting-cache.ts +24 -1
  512. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  513. package/src/ipc/__tests__/email-ipc.test.ts +0 -9
  514. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  515. package/src/ipc/gateway-client.test.ts +2 -2
  516. package/src/ipc/gateway-client.ts +3 -3
  517. package/src/ipc/routes/__tests__/route-adapter.test.ts +244 -0
  518. package/src/ipc/routes/route-adapter.ts +45 -6
  519. package/src/ipc/skill-routes/__tests__/memory.test.ts +33 -9
  520. package/src/ipc/skill-routes/__tests__/providers.test.ts +10 -10
  521. package/src/ipc/skill-routes/__tests__/registries.test.ts +28 -18
  522. package/src/ipc/skill-routes/memory.ts +29 -14
  523. package/src/ipc/skill-routes/providers.ts +5 -6
  524. package/src/ipc/skill-routes/registries.ts +13 -61
  525. package/src/live-voice/__tests__/live-voice-archive.test.ts +24 -11
  526. package/src/media/gemini-image-service.ts +15 -0
  527. package/src/media/openai-image-service.ts +14 -0
  528. package/src/media/types.ts +34 -0
  529. package/src/memory/__tests__/conversation-queries.test.ts +192 -8
  530. package/src/memory/__tests__/db-maintenance.test.ts +128 -0
  531. package/src/memory/__tests__/jobs-store-job-classes.test.ts +5 -4
  532. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  533. package/src/memory/__tests__/memory-retrospective-job.test.ts +10 -6
  534. package/src/memory/__tests__/memory-v3-selections-migration.test.ts +103 -0
  535. package/src/memory/auth-fallback-events-store.ts +94 -0
  536. package/src/memory/context-search/agent-runner.ts +2 -4
  537. package/src/memory/conversation-crud.ts +39 -8
  538. package/src/memory/conversation-queries.ts +78 -22
  539. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  540. package/src/memory/conversation-title-service.ts +65 -41
  541. package/src/memory/db-init.ts +14 -0
  542. package/src/memory/db-maintenance.ts +18 -2
  543. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  544. package/src/memory/graph/consolidation.ts +8 -11
  545. package/src/memory/graph/conversation-graph-memory.ts +106 -8
  546. package/src/memory/graph/extraction.ts +6 -9
  547. package/src/memory/graph/narrative.ts +2 -2
  548. package/src/memory/graph/pattern-scan.ts +2 -2
  549. package/src/memory/graph/retriever.ts +20 -26
  550. package/src/memory/graph/tools.ts +4 -4
  551. package/src/memory/job-handlers/conversation-starters.ts +45 -34
  552. package/src/memory/job-handlers/summarization.ts +1 -2
  553. package/src/memory/jobs-store.ts +36 -1
  554. package/src/memory/jobs-worker.ts +82 -43
  555. package/src/memory/llm-request-log-source-clickhouse.ts +5 -31
  556. package/src/memory/llm-request-log-source-local.ts +0 -11
  557. package/src/memory/llm-request-log-source.ts +9 -25
  558. package/src/memory/llm-request-log-store.ts +0 -41
  559. package/src/memory/llm-usage-store.ts +234 -50
  560. package/src/memory/memory-marker.ts +17 -0
  561. package/src/memory/memory-retrospective-job.ts +6 -2
  562. package/src/memory/memory-v2-activation-log-store.ts +1 -83
  563. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  564. package/src/memory/migrations/267-llm-usage-events-add-assistant-version.ts +46 -0
  565. package/src/memory/migrations/268-add-memory-v3-selections.ts +28 -0
  566. package/src/memory/migrations/269-schedule-script-timeout.ts +11 -0
  567. package/src/memory/migrations/270-messages-role-created-at-index.ts +18 -0
  568. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  569. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  570. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  571. package/src/memory/migrations/__tests__/267-llm-usage-events-add-assistant-version.test.ts +117 -0
  572. package/src/memory/migrations/index.ts +7 -0
  573. package/src/memory/pkb/autoinject.ts +61 -0
  574. package/src/memory/pkb/context.ts +50 -0
  575. package/src/memory/pkb/types.ts +14 -0
  576. package/src/memory/schedule-attribution-sql.ts +104 -0
  577. package/src/memory/schema/acp.ts +4 -0
  578. package/src/memory/schema/infrastructure.ts +27 -0
  579. package/src/memory/usage-grouped-buckets.ts +6 -1
  580. package/src/memory/v2/__tests__/consolidation-job.test.ts +125 -1
  581. package/src/memory/v2/__tests__/migration.test.ts +11 -3
  582. package/src/memory/v2/__tests__/page-index.test.ts +37 -1
  583. package/src/memory/v2/__tests__/router.test.ts +14 -4
  584. package/src/memory/v2/__tests__/sweep-job.test.ts +6 -5
  585. package/src/memory/v2/backfill-jobs.ts +6 -0
  586. package/src/memory/v2/consolidation-job.ts +99 -10
  587. package/src/memory/v2/migration.ts +5 -3
  588. package/src/memory/v2/page-index.ts +11 -0
  589. package/src/memory/v2/router.ts +8 -11
  590. package/src/memory/v2/sweep-job.ts +8 -11
  591. package/src/memory/v2/types.ts +1 -0
  592. package/src/messaging/providers/slack/render-transcript.test.ts +1 -1
  593. package/src/messaging/providers/slack/render-transcript.ts +2 -2
  594. package/src/messaging/style-analyzer.ts +8 -11
  595. package/src/notifications/conversation-pairing.ts +8 -13
  596. package/src/notifications/decision-engine.ts +16 -16
  597. package/src/notifications/home-feed-side-effect.ts +12 -1
  598. package/src/notifications/preference-extractor.ts +11 -14
  599. package/src/permissions/prompter.ts +46 -36
  600. package/src/permissions/question-prompter.test.ts +35 -26
  601. package/src/permissions/question-prompter.ts +6 -10
  602. package/src/plugin-api/constants.ts +4 -0
  603. package/src/plugin-api/index.ts +10 -1
  604. package/src/plugin-api/types.ts +176 -4
  605. package/src/plugins/defaults/compaction/compact.ts +59 -0
  606. package/src/plugins/defaults/compaction/package.json +15 -0
  607. package/src/plugins/defaults/compaction/register.ts +24 -0
  608. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  609. package/src/plugins/defaults/empty-response/package.json +15 -0
  610. package/src/plugins/defaults/empty-response/register.ts +23 -0
  611. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +35 -0
  612. package/src/plugins/defaults/history-repair/package.json +15 -0
  613. package/src/plugins/defaults/history-repair/register.ts +24 -0
  614. package/src/{daemon/history-repair.ts → plugins/defaults/history-repair/terminal.ts} +48 -35
  615. package/src/plugins/defaults/index.ts +22 -49
  616. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
  617. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  618. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  619. package/src/plugins/defaults/{injectors.ts → memory-retrieval/injectors.ts} +295 -112
  620. package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +242 -0
  621. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +118 -0
  622. package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +39 -0
  623. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +36 -0
  624. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +37 -0
  625. package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +219 -0
  626. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +330 -0
  627. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +288 -0
  628. package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +107 -0
  629. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +436 -0
  630. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
  631. package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +274 -0
  632. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +61 -0
  633. package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +332 -0
  634. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +179 -0
  635. package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +470 -0
  636. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +432 -0
  637. package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +168 -0
  638. package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +192 -0
  639. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +54 -0
  640. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +106 -0
  641. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +44 -0
  642. package/src/plugins/defaults/memory-v3-shadow/assign.ts +268 -0
  643. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +124 -0
  644. package/src/plugins/defaults/memory-v3-shadow/core.ts +26 -0
  645. package/src/plugins/defaults/memory-v3-shadow/data/README.md +84 -0
  646. package/src/plugins/defaults/memory-v3-shadow/data/assignments.json +5 -0
  647. package/src/plugins/defaults/memory-v3-shadow/data/core.json +1 -0
  648. package/src/plugins/defaults/memory-v3-shadow/data/leaves/domain-a/topic-x.md +9 -0
  649. package/src/plugins/defaults/memory-v3-shadow/data/leaves/domain-a/topic-y.md +9 -0
  650. package/src/plugins/defaults/memory-v3-shadow/data/leaves/domain-b/topic-z.md +9 -0
  651. package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
  652. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  653. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  654. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  655. package/src/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
  656. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +314 -0
  657. package/src/plugins/defaults/memory-v3-shadow/needle.ts +115 -0
  658. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +126 -0
  659. package/src/plugins/defaults/memory-v3-shadow/package.json +15 -0
  660. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +34 -0
  661. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
  662. package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +523 -0
  663. package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
  664. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +32 -0
  665. package/src/plugins/defaults/memory-v3-shadow/router.ts +190 -0
  666. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +84 -0
  667. package/src/plugins/defaults/memory-v3-shadow/selector.ts +226 -0
  668. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +349 -0
  669. package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +209 -0
  670. package/src/plugins/defaults/memory-v3-shadow/tree.ts +174 -0
  671. package/src/plugins/defaults/memory-v3-shadow/types.ts +59 -0
  672. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +88 -0
  673. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  674. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  675. package/src/plugins/defaults/title-generate/package.json +15 -0
  676. package/src/plugins/defaults/title-generate/register.ts +35 -0
  677. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  678. package/src/plugins/defaults/tool-error/package.json +15 -0
  679. package/src/plugins/defaults/tool-error/register.ts +23 -0
  680. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  681. package/src/plugins/defaults/tool-result-truncate/package.json +15 -0
  682. package/src/plugins/defaults/tool-result-truncate/register.ts +24 -0
  683. package/src/plugins/defaults/tool-result-truncate/terminal.ts +132 -0
  684. package/src/plugins/external-plugin-loader.ts +2 -2
  685. package/src/plugins/pipeline.ts +8 -35
  686. package/src/plugins/registry.ts +8 -25
  687. package/src/plugins/types.ts +62 -721
  688. package/src/plugins/user-loader.ts +4 -3
  689. package/src/proactive-artifact/aux-message-injector.ts +4 -5
  690. package/src/proactive-artifact/job.test.ts +28 -21
  691. package/src/proactive-artifact/job.ts +3 -1
  692. package/src/prompts/__tests__/system-prompt.test.ts +42 -0
  693. package/src/prompts/sections.ts +20 -7
  694. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +64 -0
  695. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +2 -2
  696. package/src/prompts/templates/BOOTSTRAP.md +7 -3
  697. package/src/prompts/templates/system-sections.ts +21 -0
  698. package/src/providers/__tests__/retry-callsite.test.ts +25 -25
  699. package/src/providers/__tests__/satellite-connection-routing.test.ts +7 -21
  700. package/src/providers/anthropic/client.ts +61 -34
  701. package/src/providers/call-site-routing.ts +1 -9
  702. package/src/providers/gemini/client.ts +152 -34
  703. package/src/providers/gemini/inline-media.ts +74 -0
  704. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -2
  705. package/src/providers/openai/chat-completions-provider.ts +45 -4
  706. package/src/providers/openai/responses-provider.ts +1 -4
  707. package/src/providers/openrouter/client.ts +2 -6
  708. package/src/providers/placeholder-sentinels.ts +35 -0
  709. package/src/providers/provider-send-message.ts +6 -6
  710. package/src/providers/ratelimit.ts +1 -9
  711. package/src/providers/retry.ts +0 -5
  712. package/src/providers/types.ts +11 -2
  713. package/src/providers/usage-tracking.ts +1 -9
  714. package/src/runtime/__tests__/agent-wake.test.ts +141 -32
  715. package/src/runtime/__tests__/background-job-runner.test.ts +1 -3
  716. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  717. package/src/runtime/agent-wake.ts +95 -23
  718. package/src/runtime/assistant-event-hub.ts +38 -8
  719. package/src/runtime/assistant-stream-state.ts +368 -0
  720. package/src/runtime/auth/__tests__/guard-tests.test.ts +75 -109
  721. package/src/runtime/auth/__tests__/route-policy.test.ts +153 -170
  722. package/src/runtime/auth/route-policy.ts +42 -1079
  723. package/src/runtime/background-job-runner.ts +1 -4
  724. package/src/runtime/btw-sidechain.ts +3 -1
  725. package/src/runtime/channel-approvals.ts +4 -15
  726. package/src/runtime/channel-invite-transport.ts +5 -6
  727. package/src/runtime/channel-readiness-service.ts +2 -5
  728. package/src/runtime/channel-retry-sweep.ts +12 -16
  729. package/src/runtime/http-router.ts +35 -43
  730. package/src/runtime/http-types.ts +23 -71
  731. package/src/runtime/interactive-ui.ts +1 -1
  732. package/src/runtime/invite-instruction-generator.ts +3 -3
  733. package/src/runtime/pending-interactions.ts +3 -2
  734. package/src/runtime/routes/__tests__/acp-routes.test.ts +253 -55
  735. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +565 -0
  736. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  737. package/src/runtime/routes/__tests__/content-source-routes.test.ts +4 -4
  738. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +62 -32
  739. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +237 -0
  740. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  741. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +13 -22
  742. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  743. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +7 -2
  744. package/src/runtime/routes/__tests__/sanity-routes.test.ts +6 -6
  745. package/src/runtime/routes/__tests__/stt-routes.test.ts +3 -3
  746. package/src/runtime/routes/__tests__/suggest-trust-rule-routes.test.ts +5 -2
  747. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  748. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  749. package/src/runtime/routes/__tests__/tts-routes.test.ts +9 -5
  750. package/src/runtime/routes/acp-routes.test.ts +186 -100
  751. package/src/runtime/routes/acp-routes.ts +110 -35
  752. package/src/runtime/routes/app-management-routes.ts +93 -131
  753. package/src/runtime/routes/app-routes.ts +38 -20
  754. package/src/runtime/routes/approval-routes.ts +17 -5
  755. package/src/runtime/routes/attachment-routes.ts +51 -16
  756. package/src/runtime/routes/audio-routes.ts +1 -0
  757. package/src/runtime/routes/audit-routes.ts +5 -0
  758. package/src/runtime/routes/auth-routes.ts +5 -0
  759. package/src/runtime/routes/avatar-routes.ts +264 -59
  760. package/src/runtime/routes/background-tool-routes.ts +9 -0
  761. package/src/runtime/routes/background-wake-routes.ts +13 -3
  762. package/src/runtime/routes/backup-routes.ts +45 -0
  763. package/src/runtime/routes/bookmark-routes.ts +13 -0
  764. package/src/runtime/routes/brain-graph-routes.ts +9 -0
  765. package/src/runtime/routes/browser-routes.ts +6 -1
  766. package/src/runtime/routes/browser-tabs-routes.ts +11 -10
  767. package/src/runtime/routes/btw-routes.ts +34 -24
  768. package/src/runtime/routes/cache-routes.ts +13 -0
  769. package/src/runtime/routes/call-routes.ts +21 -10
  770. package/src/runtime/routes/channel-availability-routes.ts +5 -1
  771. package/src/runtime/routes/channel-readiness-routes.ts +37 -4
  772. package/src/runtime/routes/channel-route-definitions.ts +21 -0
  773. package/src/runtime/routes/channel-verification-routes.ts +21 -0
  774. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +9 -2
  775. package/src/runtime/routes/client-routes.ts +9 -0
  776. package/src/runtime/routes/consolidation-routes.ts +133 -25
  777. package/src/runtime/routes/contact-prompt-routes.ts +9 -0
  778. package/src/runtime/routes/contact-routes.ts +90 -23
  779. package/src/runtime/routes/content-source-routes.ts +5 -1
  780. package/src/runtime/routes/conversation-analysis-routes.ts +5 -1
  781. package/src/runtime/routes/conversation-attention-routes.ts +5 -0
  782. package/src/runtime/routes/conversation-cli-routes.ts +54 -7
  783. package/src/runtime/routes/conversation-compaction-routes.ts +54 -25
  784. package/src/runtime/routes/conversation-list-routes.ts +81 -12
  785. package/src/runtime/routes/conversation-management-routes.ts +57 -14
  786. package/src/runtime/routes/conversation-query-routes.ts +90 -41
  787. package/src/runtime/routes/conversation-routes.ts +446 -204
  788. package/src/runtime/routes/conversation-starter-routes.ts +35 -20
  789. package/src/runtime/routes/conversations-import-routes.ts +30 -8
  790. package/src/runtime/routes/credential-prompt-routes.ts +5 -0
  791. package/src/runtime/routes/credential-routes.ts +25 -6
  792. package/src/runtime/routes/debug-bash-routes.ts +5 -0
  793. package/src/runtime/routes/debug-routes.ts +11 -2
  794. package/src/runtime/routes/defer-routes.ts +13 -0
  795. package/src/runtime/routes/diagnostics-routes.ts +37 -46
  796. package/src/runtime/routes/disk-pressure-routes.ts +17 -31
  797. package/src/runtime/routes/document-comments-routes.ts +46 -27
  798. package/src/runtime/routes/documents-routes.ts +25 -10
  799. package/src/runtime/routes/domain-routes.ts +98 -51
  800. package/src/runtime/routes/email-routes.ts +33 -0
  801. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  802. package/src/runtime/routes/events-routes.ts +107 -8
  803. package/src/runtime/routes/filing-routes.ts +9 -4
  804. package/src/runtime/routes/gateway-log-routes.ts +31 -4
  805. package/src/runtime/routes/global-search-routes.ts +53 -50
  806. package/src/runtime/routes/group-routes.ts +21 -5
  807. package/src/runtime/routes/guardian-action-routes.ts +9 -0
  808. package/src/runtime/routes/guardian-approval-interception.ts +0 -31
  809. package/src/runtime/routes/heartbeat-routes.ts +57 -21
  810. package/src/runtime/routes/home-feed-routes.ts +23 -19
  811. package/src/runtime/routes/home-state-routes.ts +8 -40
  812. package/src/runtime/routes/host-app-control-routes.ts +6 -1
  813. package/src/runtime/routes/host-bash-routes.ts +5 -0
  814. package/src/runtime/routes/host-browser-routes.ts +13 -0
  815. package/src/runtime/routes/host-cu-routes.ts +6 -1
  816. package/src/runtime/routes/host-file-routes.ts +26 -6
  817. package/src/runtime/routes/host-transfer-routes.ts +13 -2
  818. package/src/runtime/routes/http-adapter.ts +1 -2
  819. package/src/runtime/routes/identity-intro-cache.ts +28 -40
  820. package/src/runtime/routes/identity-routes.ts +236 -20
  821. package/src/runtime/routes/image-generation-routes.ts +45 -2
  822. package/src/runtime/routes/inbound-message-handler.ts +16 -12
  823. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +0 -12
  824. package/src/runtime/routes/inbound-stages/background-dispatch.ts +15 -19
  825. package/src/runtime/routes/index.ts +2 -0
  826. package/src/runtime/routes/inference-profile-session-routes.ts +13 -3
  827. package/src/runtime/routes/inference-provider-connection-routes.ts +21 -5
  828. package/src/runtime/routes/inference-send-routes.ts +11 -11
  829. package/src/runtime/routes/integrations/a2a.ts +32 -7
  830. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  831. package/src/runtime/routes/integrations/slack/channel.ts +23 -3
  832. package/src/runtime/routes/integrations/slack/share.ts +36 -8
  833. package/src/runtime/routes/integrations/telegram.ts +34 -9
  834. package/src/runtime/routes/integrations/twilio.ts +77 -7
  835. package/src/runtime/routes/integrations/vercel.ts +3 -3
  836. package/src/runtime/routes/internal-oauth-routes.ts +5 -0
  837. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  838. package/src/runtime/routes/internal-twilio-routes.ts +13 -0
  839. package/src/runtime/routes/llm-call-sites-routes.ts +39 -4
  840. package/src/runtime/routes/log-export-routes.ts +36 -10
  841. package/src/runtime/routes/mcp-auth-routes.ts +25 -0
  842. package/src/runtime/routes/memory-item-routes.ts +21 -10
  843. package/src/runtime/routes/memory-v2-routes.ts +105 -44
  844. package/src/runtime/routes/memory-v3-routes.ts +306 -408
  845. package/src/runtime/routes/migration-rollback-routes.ts +5 -1
  846. package/src/runtime/routes/migration-routes.ts +29 -0
  847. package/src/runtime/routes/notification-routes.ts +17 -1
  848. package/src/runtime/routes/oauth-apps.ts +99 -23
  849. package/src/runtime/routes/oauth-commands-routes.ts +37 -14
  850. package/src/runtime/routes/oauth-connect-routes.ts +9 -0
  851. package/src/runtime/routes/oauth-lifecycle-routes.ts +5 -1
  852. package/src/runtime/routes/oauth-providers.ts +79 -15
  853. package/src/runtime/routes/platform-routes.ts +102 -5
  854. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +9 -6
  855. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +37 -16
  856. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +7 -3
  857. package/src/runtime/routes/playground/__tests__/state.test.ts +10 -3
  858. package/src/runtime/routes/playground/force-compact.ts +2 -2
  859. package/src/runtime/routes/playground/helpers.ts +1 -2
  860. package/src/runtime/routes/playground/inject-failures.ts +13 -8
  861. package/src/runtime/routes/playground/reset-circuit.ts +14 -9
  862. package/src/runtime/routes/playground/seed-conversation.ts +1 -1
  863. package/src/runtime/routes/playground/seeded-conversations.ts +3 -3
  864. package/src/runtime/routes/playground/state.ts +4 -3
  865. package/src/runtime/routes/plugins-routes.ts +22 -19
  866. package/src/runtime/routes/profiler-routes.ts +17 -4
  867. package/src/runtime/routes/ps-routes.ts +5 -0
  868. package/src/runtime/routes/publish-routes.ts +13 -3
  869. package/src/runtime/routes/question-routes.ts +5 -0
  870. package/src/runtime/routes/recording-routes.ts +25 -12
  871. package/src/runtime/routes/rename-conversation-routes.ts +10 -0
  872. package/src/runtime/routes/sanity-routes.ts +9 -2
  873. package/src/runtime/routes/schedule-routes.ts +288 -88
  874. package/src/runtime/routes/secret-routes.ts +31 -6
  875. package/src/runtime/routes/sequence-routes.ts +33 -0
  876. package/src/runtime/routes/settings-routes.ts +65 -19
  877. package/src/runtime/routes/skills-routes.ts +166 -73
  878. package/src/runtime/routes/slack-channel-routes.ts +5 -0
  879. package/src/runtime/routes/stt-routes.ts +13 -6
  880. package/src/runtime/routes/subagents-routes.ts +24 -18
  881. package/src/runtime/routes/suggest-trust-rule-routes.ts +7 -2
  882. package/src/runtime/routes/surface-action-routes.ts +9 -0
  883. package/src/runtime/routes/surface-content-routes.ts +10 -2
  884. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  885. package/src/runtime/routes/task-routes.ts +37 -0
  886. package/src/runtime/routes/telemetry-routes.ts +9 -0
  887. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  888. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  889. package/src/runtime/routes/trace-event-routes.ts +42 -1
  890. package/src/runtime/routes/trust-rules-routes.ts +31 -2
  891. package/src/runtime/routes/tts-routes.ts +48 -6
  892. package/src/runtime/routes/types.ts +83 -16
  893. package/src/runtime/routes/ui-request-routes.ts +5 -0
  894. package/src/runtime/routes/upgrade-broadcast-routes.ts +5 -0
  895. package/src/runtime/routes/usage-routes.ts +118 -42
  896. package/src/runtime/routes/user-routes-cli.ts +9 -0
  897. package/src/runtime/routes/user-routes.ts +5 -1
  898. package/src/runtime/routes/wake-conversation-routes.ts +5 -0
  899. package/src/runtime/routes/watcher-routes.ts +21 -0
  900. package/src/runtime/routes/webhook-routes.ts +50 -2
  901. package/src/runtime/routes/wipe-conversation-routes.ts +5 -0
  902. package/src/runtime/routes/work-items-routes.ts +49 -23
  903. package/src/runtime/routes/workspace-commit-routes.ts +5 -0
  904. package/src/runtime/routes/workspace-routes.test.ts +42 -0
  905. package/src/runtime/routes/workspace-routes.ts +124 -9
  906. package/src/runtime/services/__tests__/analyze-conversation.test.ts +8 -4
  907. package/src/runtime/services/analyze-conversation.ts +5 -8
  908. package/src/runtime/services/conversation-serializer.ts +24 -2
  909. package/src/runtime/sync/resource-sync-events.ts +16 -2
  910. package/src/runtime/sync/sync-publisher.ts +2 -2
  911. package/src/schedule/run-script.ts +28 -3
  912. package/src/schedule/schedule-store.ts +28 -1
  913. package/src/schedule/schedule-usage-store.ts +83 -0
  914. package/src/schedule/scheduler.ts +15 -6
  915. package/src/signals/cancel.ts +2 -4
  916. package/src/signals/user-message.ts +5 -8
  917. package/src/skills/catalog-files.ts +4 -1
  918. package/src/skills/catalog-install.ts +3 -0
  919. package/src/skills/categories-cache.ts +118 -0
  920. package/src/skills/clawhub-files.ts +1 -0
  921. package/src/skills/skillssh-files.ts +1 -0
  922. package/src/subagent/manager.ts +20 -11
  923. package/src/telemetry/types.ts +55 -1
  924. package/src/telemetry/usage-telemetry-reporter.test.ts +250 -4
  925. package/src/telemetry/usage-telemetry-reporter.ts +88 -2
  926. package/src/tools/acp/context.ts +20 -0
  927. package/src/tools/acp/list-agents.test.ts +7 -1
  928. package/src/tools/acp/spawn.test.ts +198 -93
  929. package/src/tools/acp/spawn.ts +32 -70
  930. package/src/tools/acp/steer.test.ts +105 -8
  931. package/src/tools/acp/steer.ts +48 -17
  932. package/src/tools/apps/definitions.ts +8 -4
  933. package/src/tools/apps/executors.ts +13 -8
  934. package/src/tools/ask-question/ask-question-tool.test.ts +120 -105
  935. package/src/tools/ask-question/ask-question-tool.ts +85 -90
  936. package/src/tools/computer-use/definitions.ts +28 -24
  937. package/src/tools/credential-execution/make-authenticated-request.ts +56 -51
  938. package/src/tools/credential-execution/manage-secure-command-tool.ts +2 -2
  939. package/src/tools/credential-execution/run-authenticated-command.ts +82 -77
  940. package/src/tools/credentials/vault.ts +112 -111
  941. package/src/tools/execution-target.ts +1 -1
  942. package/src/tools/execution-timeout.ts +3 -4
  943. package/src/tools/executor.ts +1 -53
  944. package/src/tools/filesystem/edit.ts +45 -42
  945. package/src/tools/filesystem/list.ts +33 -30
  946. package/src/tools/filesystem/read.ts +54 -35
  947. package/src/tools/filesystem/write.ts +69 -32
  948. package/src/tools/host-filesystem/edit.ts +44 -42
  949. package/src/tools/host-filesystem/read.ts +49 -35
  950. package/src/tools/host-filesystem/transfer.ts +121 -108
  951. package/src/tools/host-filesystem/write.ts +33 -31
  952. package/src/tools/host-terminal/host-shell.ts +50 -48
  953. package/src/tools/memory/register.ts +23 -24
  954. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  955. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  956. package/src/tools/network/web-fetch.ts +49 -46
  957. package/src/tools/network/web-search-error.test.ts +248 -0
  958. package/src/tools/network/web-search-error.ts +267 -0
  959. package/src/tools/network/web-search.ts +223 -61
  960. package/src/tools/registry.ts +39 -16
  961. package/src/tools/schedule/create.ts +13 -0
  962. package/src/tools/schedule/update.ts +16 -0
  963. package/src/tools/shared/filesystem/audio-read.ts +122 -0
  964. package/src/tools/shared/filesystem/image-read.ts +1 -1
  965. package/src/tools/skills/execute.ts +34 -31
  966. package/src/tools/skills/load.ts +29 -23
  967. package/src/tools/subagent/notify-parent.ts +35 -32
  968. package/src/tools/subagent/spawn.ts +2 -4
  969. package/src/tools/system/avatar-generator.ts +13 -22
  970. package/src/tools/system/request-permission.ts +30 -27
  971. package/src/tools/terminal/safe-env.ts +10 -1
  972. package/src/tools/terminal/shell.ts +190 -61
  973. package/src/tools/tool-defaults.ts +20 -9
  974. package/src/tools/tool-manifest.ts +4 -4
  975. package/src/tools/types.ts +74 -23
  976. package/src/tools/ui-surface/definitions.ts +99 -10
  977. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  978. package/src/tts/provider-catalog.ts +76 -1
  979. package/src/usage/types.ts +10 -0
  980. package/src/util/errors.ts +2 -2
  981. package/src/util/map-limit.ts +27 -0
  982. package/src/util/mutex.ts +47 -0
  983. package/src/util/platform.ts +15 -12
  984. package/src/work-items/work-item-runner.ts +7 -2
  985. package/src/workspace/git-service.ts +1 -42
  986. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +7 -20
  987. package/src/workspace/migrations/092-backfill-v3-leaves.ts +169 -0
  988. package/src/workspace/migrations/093-backfill-leaf-ids.ts +144 -0
  989. package/src/workspace/migrations/094-seed-avatar-manifest.ts +155 -0
  990. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  991. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  992. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -0
  993. package/src/workspace/migrations/__tests__/094-seed-avatar-manifest.test.ts +136 -0
  994. package/src/workspace/migrations/__tests__/backfill-leaf-ids.test.ts +175 -0
  995. package/src/workspace/migrations/__tests__/backfill-v3-leaves.test.ts +124 -0
  996. package/src/workspace/migrations/registry.ts +12 -0
  997. package/src/workspace/provider-commit-message-generator.ts +15 -17
  998. package/tsconfig.json +4 -1
  999. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  1000. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  1001. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  1002. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -262
  1003. package/src/__tests__/empty-response-pipeline.test.ts +0 -301
  1004. package/src/__tests__/history-repair-pipeline.test.ts +0 -396
  1005. package/src/__tests__/llm-call-pipeline.test.ts +0 -281
  1006. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  1007. package/src/__tests__/persistence-pipeline.test.ts +0 -514
  1008. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  1009. package/src/__tests__/token-estimate-pipeline.test.ts +0 -481
  1010. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  1011. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  1012. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -344
  1013. package/src/cli/commands/__tests__/memory-v3-render.test.ts +0 -340
  1014. package/src/cli/commands/memory-v3-render.ts +0 -491
  1015. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  1016. package/src/daemon/message-types/disk-pressure.ts +0 -9
  1017. package/src/email/feature-gate.ts +0 -23
  1018. package/src/gallery/default-gallery.ts +0 -1359
  1019. package/src/gallery/gallery-manifest.ts +0 -28
  1020. package/src/memory/v3/__tests__/coactivation-store.test.ts +0 -422
  1021. package/src/memory/v3/__tests__/consolidation-job.test.ts +0 -466
  1022. package/src/memory/v3/__tests__/coretrieval-seed.test.ts +0 -270
  1023. package/src/memory/v3/__tests__/edge-learning-job.test.ts +0 -324
  1024. package/src/memory/v3/__tests__/edges.test.ts +0 -706
  1025. package/src/memory/v3/__tests__/filter.test.ts +0 -560
  1026. package/src/memory/v3/__tests__/gate.test.ts +0 -637
  1027. package/src/memory/v3/__tests__/index-composition.test.ts +0 -291
  1028. package/src/memory/v3/__tests__/loop.test.ts +0 -775
  1029. package/src/memory/v3/__tests__/retriever.test.ts +0 -226
  1030. package/src/memory/v3/__tests__/scouts.test.ts +0 -489
  1031. package/src/memory/v3/__tests__/shadow-diff.test.ts +0 -225
  1032. package/src/memory/v3/__tests__/shadow-middleware.test.ts +0 -398
  1033. package/src/memory/v3/__tests__/system-prompts.test.ts +0 -154
  1034. package/src/memory/v3/__tests__/traversal.test.ts +0 -508
  1035. package/src/memory/v3/__tests__/tree-index.test.ts +0 -280
  1036. package/src/memory/v3/__tests__/tree-store.test.ts +0 -529
  1037. package/src/memory/v3/__tests__/tree-walk.test.ts +0 -784
  1038. package/src/memory/v3/__tests__/validate.test.ts +0 -277
  1039. package/src/memory/v3/auto-edges.ts +0 -223
  1040. package/src/memory/v3/coactivation-store.ts +0 -124
  1041. package/src/memory/v3/consolidation-job.ts +0 -323
  1042. package/src/memory/v3/coretrieval-seed.ts +0 -240
  1043. package/src/memory/v3/edge-learning-job.ts +0 -160
  1044. package/src/memory/v3/edges.ts +0 -286
  1045. package/src/memory/v3/filter.ts +0 -286
  1046. package/src/memory/v3/gate.ts +0 -349
  1047. package/src/memory/v3/index-composition.ts +0 -126
  1048. package/src/memory/v3/llm-capture.ts +0 -46
  1049. package/src/memory/v3/loop.ts +0 -430
  1050. package/src/memory/v3/maintenance.ts +0 -144
  1051. package/src/memory/v3/prompt-context.ts +0 -33
  1052. package/src/memory/v3/prompts/consolidation.ts +0 -458
  1053. package/src/memory/v3/prompts/system-prompts.ts +0 -196
  1054. package/src/memory/v3/retriever.ts +0 -33
  1055. package/src/memory/v3/scouts.ts +0 -431
  1056. package/src/memory/v3/shadow-diff.ts +0 -287
  1057. package/src/memory/v3/shadow-middleware.ts +0 -347
  1058. package/src/memory/v3/traversal.ts +0 -211
  1059. package/src/memory/v3/tree-index.ts +0 -237
  1060. package/src/memory/v3/tree-store.ts +0 -394
  1061. package/src/memory/v3/tree-walk.ts +0 -356
  1062. package/src/memory/v3/types.ts +0 -65
  1063. package/src/memory/v3/validate.ts +0 -323
  1064. package/src/plugins/defaults/circuit-breaker.ts +0 -141
  1065. package/src/plugins/defaults/compaction.ts +0 -141
  1066. package/src/plugins/defaults/empty-response.ts +0 -124
  1067. package/src/plugins/defaults/history-repair.ts +0 -83
  1068. package/src/plugins/defaults/llm-call.ts +0 -77
  1069. package/src/plugins/defaults/memory-retrieval.ts +0 -219
  1070. package/src/plugins/defaults/overflow-reduce.ts +0 -185
  1071. package/src/plugins/defaults/persistence.ts +0 -146
  1072. package/src/plugins/defaults/title-generate.ts +0 -90
  1073. package/src/plugins/defaults/token-estimate.ts +0 -101
  1074. package/src/plugins/defaults/tool-error.ts +0 -119
  1075. package/src/plugins/defaults/tool-execute.ts +0 -87
  1076. package/src/plugins/defaults/tool-result-truncate.ts +0 -84
  1077. package/src/runtime/routes/__tests__/memory-v3-simulate-params.test.ts +0 -35
  1078. package/src/skills/category-inference.ts +0 -111
@@ -7,8 +7,6 @@
7
7
  * runAgentLoop method here via the AgentLoopConversationContext interface.
8
8
  */
9
9
 
10
- import { join } from "node:path";
11
-
12
10
  import { v4 as uuid } from "uuid";
13
11
 
14
12
  import { optimizeImageForTransport } from "../agent/image-optimize.js";
@@ -17,7 +15,7 @@ import type {
17
15
  AgentLoop,
18
16
  AgentLoopExitReason,
19
17
  CheckpointDecision,
20
- CheckpointInfo,
18
+ MidLoopCompaction,
21
19
  } from "../agent/loop.js";
22
20
  import { createAssistantMessage } from "../agent/message-types.js";
23
21
  import type {
@@ -26,6 +24,7 @@ import type {
26
24
  TurnChannelContext,
27
25
  TurnInterfaceContext,
28
26
  } from "../channels/types.js";
27
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
29
28
  import {
30
29
  contextWindowConfigFromEffective,
31
30
  type EffectiveContextWindow,
@@ -45,10 +44,13 @@ import {
45
44
  } from "../context/post-turn-tool-result-truncation.js";
46
45
  import {
47
46
  estimatePromptTokens,
47
+ estimatePromptTokensWithTools,
48
48
  getCalibrationProviderKey,
49
49
  } from "../context/token-estimator.js";
50
- import type { ContextWindowManager } from "../context/window-manager.js";
51
- import { getDocumentsForConversation } from "../documents/document-store.js";
50
+ import type {
51
+ ContextWindowCompactOptions,
52
+ ContextWindowManager,
53
+ } from "../context/window-manager.js";
52
54
  import type { ToolProfiler } from "../events/tool-profiling-listener.js";
53
55
  import { writeRelationshipState } from "../home/relationship-state-writer.js";
54
56
  import {
@@ -56,9 +58,9 @@ import {
56
58
  setSentryConversationContext,
57
59
  } from "../instrument.js";
58
60
  import { commitAppTurnChanges } from "../memory/app-git-service.js";
59
- import { getApp, listAppFiles, resolveAppDir } from "../memory/app-store.js";
60
61
  import { enqueueAutoAnalysisOnCompaction } from "../memory/auto-analysis-enqueue.js";
61
62
  import {
63
+ addMessage,
62
64
  deleteMessageById,
63
65
  getConversation,
64
66
  getConversationOriginChannel,
@@ -67,86 +69,40 @@ import {
67
69
  getLastUserTimestampBefore,
68
70
  getMessageById,
69
71
  provenanceFromTrustContext,
70
- setConversationHistoryStrippedAt,
71
- setLastNotifiedInferenceProfile,
72
72
  updateConversationContextWindow,
73
73
  updateConversationSlackContextWatermark,
74
+ updateMessageMetadata,
74
75
  } from "../memory/conversation-crud.js";
75
76
  import { getResolvedConversationDirPath } from "../memory/conversation-directories.js";
76
77
  import { syncMessageToDisk } from "../memory/conversation-disk-view.js";
77
- import {
78
- isReplaceableTitle,
79
- queueRegenerateConversationTitle,
80
- } from "../memory/conversation-title-service.js";
78
+ import { isReplaceableTitle } from "../memory/conversation-title-service.js";
81
79
  import { isBackgroundConversationType } from "../memory/conversation-types.js";
82
80
  import type { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
83
81
  import {
84
82
  backfillMessageIdOnLogs,
85
83
  recordSyntheticAgentErrorMessageLog,
86
84
  } from "../memory/llm-request-log-store.js";
87
- import { recordMemoryRecallLog } from "../memory/memory-recall-log-store.js";
88
85
  import { enqueueMemoryRetrospectiveOnCompaction } from "../memory/memory-retrospective-enqueue.js";
89
- import { PKB_WORKSPACE_SCOPE } from "../memory/pkb/types.js";
90
- import type { QdrantSparseVector } from "../memory/qdrant-client.js";
91
- import {
92
- readMemoryV2StaticContent,
93
- shouldExposePersonalMemory,
94
- } from "../memory/v2/static-context.js";
95
86
  import type { PermissionPrompter } from "../permissions/prompter.js";
96
87
  import { HOOKS } from "../plugin-api/constants.js";
97
88
  import type { UserPromptSubmitContext } from "../plugin-api/types.js";
98
- import { defaultCompactionTerminal } from "../plugins/defaults/compaction.js";
99
- import { defaultHistoryRepairTerminal } from "../plugins/defaults/history-repair.js";
100
- import {
101
- asDefaultGraphPayload,
102
- type DefaultMemoryRetrievalDeps,
103
- type GraphMemoryPayload,
104
- runDefaultMemoryRetrieval,
105
- } from "../plugins/defaults/memory-retrieval.js";
106
- import { defaultPersistenceTerminal } from "../plugins/defaults/persistence.js";
107
- import { defaultTitleGenerateTerminal } from "../plugins/defaults/title-generate.js";
108
- import { defaultTokenEstimateTerminal } from "../plugins/defaults/token-estimate.js";
109
- import { DEFAULT_TIMEOUTS, runHook, runPipeline } from "../plugins/pipeline.js";
110
- import { getMiddlewaresFor } from "../plugins/registry.js";
111
- import type {
112
- CircuitBreakerArgs,
113
- CircuitBreakerResult,
114
- CompactionArgs,
115
- CompactionResult,
116
- EstimateArgs,
117
- EstimateResult,
118
- HistoryRepairArgs,
119
- HistoryRepairResult,
120
- MemoryArgs,
121
- MemoryResult,
122
- OverflowReduceArgs,
123
- OverflowReduceResult,
124
- PersistAddResult,
125
- PersistArgs,
126
- PersistResult,
127
- TurnContext as PluginTurnContext,
128
- } from "../plugins/types.js";
129
- import { PluginExecutionError, PluginTimeoutError } from "../plugins/types.js";
130
- import {
131
- hasProactiveArtifactCompleted,
132
- runProactiveArtifactJob,
133
- tryClaimProactiveArtifactTrigger,
134
- } from "../proactive-artifact/index.js";
135
- import type {
136
- ContentBlock,
137
- Message,
138
- ToolDefinition,
139
- } from "../providers/types.js";
89
+ import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
90
+ import { deepRepairHistory } from "../plugins/defaults/history-repair/terminal.js";
91
+ import postCompactReinject from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
92
+ import userPromptSubmitMemoryRetrieval, {
93
+ type MemoryRetrievalHookContext,
94
+ } from "../plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.js";
95
+ import { runHook } from "../plugins/pipeline.js";
96
+ import type { TurnContext as PluginTurnContext } from "../plugins/types.js";
97
+ import type { ContentBlock, Message } from "../providers/types.js";
140
98
  import type { Provider } from "../providers/types.js";
141
99
  import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
142
100
  import { broadcastMessage } from "../runtime/assistant-event-hub.js";
143
101
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
144
102
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
145
- import { redactSecrets } from "../security/secret-scanner.js";
146
103
  import { getSubagentManager } from "../subagent/index.js";
147
104
  import type { UsageActor } from "../usage/actors.js";
148
105
  import { getLogger } from "../util/logger.js";
149
- import { getWorkspaceDir } from "../util/platform.js";
150
106
  import { timeAgo } from "../util/time.js";
151
107
  import { truncate } from "../util/truncate.js";
152
108
  import { getWorkspaceGitService } from "../workspace/git-service.js";
@@ -155,7 +111,6 @@ import {
155
111
  type AssistantAttachmentDraft,
156
112
  cleanAssistantContent,
157
113
  } from "./assistant-attachments.js";
158
- import { cleanupBootstrapAfterTurnThreshold } from "./bootstrap-turn-cleanup.js";
159
114
  import { resolveOverflowAction } from "./context-overflow-policy.js";
160
115
  import {
161
116
  createInitialReducerState,
@@ -166,6 +121,8 @@ import {
166
121
  createEventHandlerState,
167
122
  dispatchAgentEvent,
168
123
  type EventHandlerDeps,
124
+ finalizePendingToolResultRow,
125
+ markHistoryStrippedBestEffort,
169
126
  } from "./conversation-agent-loop-handlers.js";
170
127
  import {
171
128
  approveHostAttachmentRead,
@@ -181,7 +138,6 @@ import { raceWithTimeout } from "./conversation-media-retry.js";
181
138
  import type { MessageQueue } from "./conversation-queue-manager.js";
182
139
  import type { QueueDrainReason } from "./conversation-queue-manager.js";
183
140
  import type {
184
- ActiveSurfaceContext,
185
141
  ChannelCapabilities,
186
142
  InboundActorContext,
187
143
  InjectionMode,
@@ -190,8 +146,6 @@ import {
190
146
  applyRuntimeInjections,
191
147
  buildSubagentStatusBlock,
192
148
  buildUnifiedTurnContextBlock,
193
- findLastInjectedNowContent,
194
- getPkbAutoInjectList,
195
149
  getSlackCompactionWatermarkForPrefix,
196
150
  inboundActorContextFromTrust,
197
151
  inboundActorContextFromTrustContext,
@@ -202,7 +156,6 @@ import {
202
156
  } from "./conversation-runtime-assembly.js";
203
157
  import type { SkillProjectionCache } from "./conversation-skill-tools.js";
204
158
  import { markSurfaceCompleted } from "./conversation-surfaces.js";
205
- import { resolveTrustClass } from "./conversation-tool-setup.js";
206
159
  import { recordUsage } from "./conversation-usage.js";
207
160
  import {
208
161
  formatTurnTimestamp,
@@ -210,47 +163,28 @@ import {
210
163
  } from "./date-context.js";
211
164
  import { getDiskPressureStatus } from "./disk-pressure-guard.js";
212
165
  import { classifyDiskPressureTurnPolicy } from "./disk-pressure-policy.js";
213
- import { deepRepairHistory } from "./history-repair.js";
214
166
  import type {
215
- DynamicPageSurfaceData,
216
167
  ServerMessage,
217
168
  SurfaceData,
218
169
  SurfaceType,
219
170
  UsageStats,
220
171
  } from "./message-protocol.js";
221
- import type { MemoryRecalled } from "./message-types/memory.js";
222
172
  import type { ConfirmationStateChanged } from "./message-types/messages.js";
223
- import { conversationMetadataSyncTag } from "./message-types/sync.js";
173
+ import {
174
+ type OverflowReduceArgs,
175
+ runOverflowReductionLoop,
176
+ } from "./overflow-reduction-loop.js";
224
177
  import { parseActualTokensFromError } from "./parse-actual-tokens-from-error.js";
178
+ import {
179
+ persistUnsendableImageDowngrades,
180
+ UNSENDABLE_IMAGE_NOTE,
181
+ } from "./persist-unsendable-image.js";
225
182
  import type { TraceEmitter } from "./trace-emitter.js";
226
- import type { TrustContext } from "./trust-context.js";
183
+ import { resolveTrustClass, type TrustContext } from "./trust-context.js";
227
184
  import { stripHistoricalWebSearchResults } from "./web-search-history.js";
228
185
 
229
186
  const log = getLogger("conversation-agent-loop");
230
187
 
231
- /**
232
- * Best-effort persistence of the history-stripped marker after an
233
- * injection-strip event (compaction / overflow recovery). The marker is a
234
- * durability hint, not turn-critical state — a transient SQLite write failure
235
- * (SQLITE_BUSY, disk-full, read-only FS) must not abort the turn. Logs a
236
- * warning and continues on failure, preserving the long-standing non-fatal
237
- * contract for this metadata write.
238
- */
239
- function markHistoryStrippedBestEffort(
240
- conversationId: string,
241
- strippedAt: number,
242
- logger: ReturnType<typeof getLogger>,
243
- ): void {
244
- try {
245
- setConversationHistoryStrippedAt(conversationId, strippedAt);
246
- } catch (err) {
247
- logger.warn(
248
- { err },
249
- "Failed to persist history-stripped marker after compaction strip (non-fatal)",
250
- );
251
- }
252
- }
253
-
254
188
  const DISK_PRESSURE_ERROR_CODE = "DISK_SPACE_CRITICAL" as const;
255
189
  const DISK_PRESSURE_ERROR_CATEGORY = "disk_pressure";
256
190
 
@@ -276,163 +210,15 @@ function formatDiskPressureBlockedMessage(): string {
276
210
  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.";
277
211
  }
278
212
 
279
- // ── Compaction circuit-breaker pipeline helpers ─────────────────────
280
- //
281
- // The circuit-breaker behavior (3 consecutive summary-LLM failures trips a
282
- // 1-hour cooldown) is now implemented by the `circuitBreaker` plugin
283
- // pipeline. The default plugin (`plugins/defaults/circuit-breaker.ts`)
284
- // replicates the legacy threshold/cooldown constants and event-emission
285
- // semantics exactly — it operates on the `consecutiveCompactionFailures` /
286
- // `compactionCircuitOpenUntil` fields the conversation still owns so the
287
- // dev-only playground routes (`POST /playground/reset-compaction-circuit`,
288
- // `POST /playground/inject-compaction-failures`) continue to read and
289
- // mutate those fields directly.
290
- //
291
- // The helpers below build the pipeline inputs and invoke the runner. They
292
- // are the sole entry points the rest of the daemon uses to query or update
293
- // the compaction circuit.
294
-
295
- /** Circuit-breaker key for a specific conversation's compaction pipeline. */
296
- function compactionCircuitKey(conversationId: string): string {
297
- return `compaction:${conversationId}`;
298
- }
299
-
300
- /**
301
- * Build the minimal {@link TurnContext} the pipeline runner requires. Called
302
- * both from inside the agent loop (where turn identifiers are available) and
303
- * from non-turn invocations like `Conversation.forceCompact` (which falls
304
- * back to stable placeholders so the runner's log records still carry the
305
- * conversation identifier).
306
- */
307
- function buildCircuitTurnContext(ctx: {
308
- readonly conversationId: string;
309
- currentRequestId?: string;
310
- currentTurnTrustContext?: TrustContext;
311
- trustContext?: TrustContext;
312
- turnCount: number;
313
- }): PluginTurnContext {
314
- const trust: TrustContext =
315
- ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
316
- return {
317
- requestId: ctx.currentRequestId ?? "circuit-breaker",
318
- conversationId: ctx.conversationId,
319
- turnIndex: ctx.turnCount,
320
- trust,
321
- };
322
- }
323
-
324
- /**
325
- * Run the `circuitBreaker` pipeline for the compaction circuit on this
326
- * conversation. When `outcome` is provided, state is updated (and transition
327
- * events emit via `onEvent`); when omitted the call is query-only.
328
- *
329
- * Returns the post-call decision from the pipeline. Callers gate auto-paths
330
- * on `!result.open` and admit forced paths regardless of the decision.
331
- */
332
- async function runCompactionCircuitPipeline(
333
- ctx: {
334
- readonly conversationId: string;
335
- consecutiveCompactionFailures: number;
336
- compactionCircuitOpenUntil: number | null;
337
- currentRequestId?: string;
338
- currentTurnTrustContext?: TrustContext;
339
- trustContext?: TrustContext;
340
- turnCount: number;
341
- },
342
- args: {
343
- outcome?: "success" | "failure";
344
- onEvent?: (msg: ServerMessage) => void;
345
- },
346
- ): Promise<CircuitBreakerResult> {
347
- const turnContext = buildCircuitTurnContext(ctx);
348
- return runPipeline<CircuitBreakerArgs, CircuitBreakerResult>(
349
- "circuitBreaker",
350
- getMiddlewaresFor("circuitBreaker"),
351
- async (terminalArgs) => {
352
- // No plugin in the chain produced a decision. This should be
353
- // unreachable in production because the default plugin registers a
354
- // `circuitBreaker` middleware that always returns a decision, but we
355
- // defensively derive the state here so test setups that intentionally
356
- // omit the default plugin still get a sensible response.
357
- const openUntil = terminalArgs.state.compactionCircuitOpenUntil;
358
- const now = Date.now();
359
- if (openUntil !== null && now < openUntil) {
360
- return { open: true, cooldownRemainingMs: openUntil - now };
361
- }
362
- return { open: false };
363
- },
364
- {
365
- key: compactionCircuitKey(ctx.conversationId),
366
- // Pass the ctx directly as the mutable state container. The
367
- // `CircuitBreakerArgs.state` shape deliberately matches the subset of
368
- // fields the conversation owns so plugins mutate the same object the
369
- // playground routes read and write.
370
- state: ctx,
371
- ...(args.outcome !== undefined ? { outcome: args.outcome } : {}),
372
- ...(args.onEvent ? { onEvent: args.onEvent } : {}),
373
- },
374
- turnContext,
375
- DEFAULT_TIMEOUTS.circuitBreaker,
376
- );
377
- }
378
-
379
- /**
380
- * Query-only: is the compaction circuit breaker currently open for this
381
- * conversation? Thin wrapper around {@link runCompactionCircuitPipeline}
382
- * with no outcome. Async because the pipeline runner is async, but the
383
- * default plugin resolves synchronously on its microtask.
384
- */
385
- async function isCompactionCircuitOpen(ctx: {
386
- readonly conversationId: string;
387
- consecutiveCompactionFailures: number;
388
- compactionCircuitOpenUntil: number | null;
389
- currentRequestId?: string;
390
- currentTurnTrustContext?: TrustContext;
391
- trustContext?: TrustContext;
392
- turnCount: number;
393
- }): Promise<boolean> {
394
- const decision = await runCompactionCircuitPipeline(ctx, {});
395
- return decision.open;
396
- }
397
-
398
- /**
399
- * Update the compaction circuit breaker with the outcome of a `maybeCompact`
400
- * call and emit any transition event. A `summaryFailed` value of `undefined`
401
- * means the summary LLM never ran (early return) — callers must guard with
402
- * `summaryFailed !== undefined` before invoking this helper so early-return
403
- * paths don't silently reset the 3-strike counter.
404
- *
405
- * The default plugin handles threshold-based tripping and cooldown reset;
406
- * see `plugins/defaults/circuit-breaker.ts` for the canonical semantics.
407
- */
408
- export async function trackCompactionOutcome(
409
- ctx: {
410
- readonly conversationId: string;
411
- consecutiveCompactionFailures: number;
412
- compactionCircuitOpenUntil: number | null;
413
- currentRequestId?: string;
414
- currentTurnTrustContext?: TrustContext;
415
- trustContext?: TrustContext;
416
- turnCount: number;
417
- },
418
- summaryFailed: boolean,
419
- onEvent: (msg: ServerMessage) => void,
420
- ): Promise<void> {
421
- await runCompactionCircuitPipeline(ctx, {
422
- outcome: summaryFailed ? "failure" : "success",
423
- onEvent,
424
- });
425
- }
426
-
427
213
  // ── Plugin pipeline helpers ──────────────────────────────────────────
428
214
  //
429
215
  // Canonical {@link PluginTurnContext} builder threaded into every
430
- // `runPipeline` call inside `runAgentLoopImpl`. The orchestrator composes
216
+ // `runHook` call inside `runAgentLoopImpl`. The orchestrator composes
431
217
  // the context on demand at each call site from ambient state rather than
432
218
  // carrying a persistent `TurnContext` instance across the turn.
433
219
 
434
220
  /**
435
- * Synthetic fallback trust context used when the orchestrator fires a pipeline
221
+ * Synthetic fallback trust context used when the orchestrator fires a hook
436
222
  * before the per-turn trust snapshot has been captured (e.g. invocations that
437
223
  * bypass `processMessage` / `drainQueue`). We bias to `unknown` rather than
438
224
  * `guardian` so a missing snapshot cannot accidentally grant elevated trust
@@ -444,14 +230,14 @@ const FALLBACK_TURN_TRUST: TrustContext = {
444
230
  };
445
231
 
446
232
  /**
447
- * Build the {@link TurnContext} passed to {@link runPipeline}.
233
+ * Build the {@link TurnContext} passed to {@link runHook}.
448
234
  *
449
- * Canonical source of truth for every pipeline call site inside the agent
450
- * loop. Every `runPipeline` invocation in `runAgentLoopImpl` (and in the
235
+ * Canonical source of truth for every hook call site inside the agent
236
+ * loop. Every `runHook` invocation in `runAgentLoopImpl` (and in the
451
237
  * handlers that share its ambient state) must route through this helper
452
238
  * rather than constructing a `TurnContext` literal inline — this keeps
453
239
  * `turnIndex`, trust resolution, and the `contextWindowManager` attachment
454
- * consistent across pipeline slots, which in turn keeps structured logs
240
+ * consistent across hooks, which in turn keeps structured logs
455
241
  * filtered by `conversationId`/`turnIndex` coherent across slots.
456
242
  *
457
243
  * Behavior:
@@ -463,9 +249,9 @@ const FALLBACK_TURN_TRUST: TrustContext = {
463
249
  * level context, then {@link FALLBACK_TURN_TRUST}. The cascade matches
464
250
  * the one inside the orchestrator's inline injection assembly so
465
251
  * middleware reads the same trust class the runtime sees.
466
- * - `contextWindowManager` is attached unconditionally. Pipelines that
467
- * don't need it can ignore it; the default compaction plugin reads it
468
- * via the typed optional field on `TurnContext`.
252
+ * - `contextWindowManager` is attached unconditionally. Hooks that
253
+ * don't need it can ignore it; it remains available via the typed
254
+ * optional field on `TurnContext`.
469
255
  */
470
256
  function buildPluginTurnContext(
471
257
  ctx: AgentLoopConversationContext,
@@ -479,17 +265,62 @@ function buildPluginTurnContext(
479
265
  turnIndex: ctx.turnCount,
480
266
  trust,
481
267
  contextWindowManager: ctx.contextWindowManager,
268
+ callSite: ctx.currentCallSite,
482
269
  };
483
270
  }
484
271
 
272
+ /**
273
+ * Trust class of the actor whose turn is in progress, for the compactor's
274
+ * image manifest filter. Prefers the turn-start snapshot
275
+ * ({@link AgentLoopConversationContext.currentTurnTrustContext}) over the live
276
+ * trust context so compaction running in a later tool iteration can't pick up
277
+ * a concurrent request's actor.
278
+ */
279
+ function resolveTurnActorTrustClass(
280
+ ctx: AgentLoopConversationContext,
281
+ ): TrustContext["trustClass"] | undefined {
282
+ return (ctx.currentTurnTrustContext ?? ctx.trustContext)?.trustClass;
283
+ }
284
+
485
285
  // ── Context Interface ────────────────────────────────────────────────
486
286
 
287
+ /**
288
+ * Per-surface entry tracked on the current turn. Inline shape kept stable so
289
+ * routes and persistence helpers can consume it via a named import instead of
290
+ * `infer`-extracting from {@link AgentLoopConversationContext}.
291
+ */
292
+ export interface AssistantSurface {
293
+ surfaceId: string;
294
+ surfaceType: SurfaceType;
295
+ title?: string;
296
+ data: SurfaceData;
297
+ actions?: Array<{
298
+ id: string;
299
+ label: string;
300
+ style?: string;
301
+ data?: Record<string, unknown>;
302
+ }>;
303
+ display?: string;
304
+ persistent?: boolean;
305
+ /** Id of the tool call that produced this surface (the `ui_show` proxy tool). Persisted so app previews can gate on the tool result's arrival rather than whole-turn streaming state. */
306
+ toolCallId?: string;
307
+ }
308
+
487
309
  export interface AgentLoopConversationContext {
488
310
  readonly conversationId: string;
489
311
  messages: Message[];
490
- processing: boolean;
312
+ isProcessing(): boolean;
313
+ setProcessing(value: boolean): void;
491
314
  abortController: AbortController | null;
492
315
  currentRequestId?: string;
316
+ /**
317
+ * The {@link LLMCallSite} of the in-flight turn, set at turn start from
318
+ * `options?.callSite ?? "mainAgent"`. Read by {@link buildPluginTurnContext}
319
+ * so pipeline/injector plugins can tell the main reply apart from
320
+ * background agent-loop work (compaction, subagents, …) on this same
321
+ * conversation. Per-turn mutable, mirroring {@link currentRequestId}.
322
+ */
323
+ currentCallSite?: LLMCallSite;
493
324
 
494
325
  readonly agentLoop: AgentLoop;
495
326
  readonly provider: Provider;
@@ -507,10 +338,6 @@ export interface AgentLoopConversationContext {
507
338
  * happened just before this turn).
508
339
  */
509
340
  pendingPostCompactReinject: boolean;
510
- /** Tracks consecutive compaction failures (summary LLM call threw). */
511
- consecutiveCompactionFailures: number;
512
- /** Timestamp (ms since epoch) until which the circuit breaker is open. */
513
- compactionCircuitOpenUntil: number | null;
514
341
 
515
342
  readonly graphMemory: ConversationGraphMemory;
516
343
 
@@ -533,24 +360,9 @@ export interface AgentLoopConversationContext {
533
360
  pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
534
361
  surfaceActionRequestIds: Set<string>;
535
362
  approvedViaPromptThisTurn?: boolean;
536
- currentTurnSurfaces: Array<{
537
- surfaceId: string;
538
- surfaceType: SurfaceType;
539
- title?: string;
540
- data: SurfaceData;
541
- actions?: Array<{
542
- id: string;
543
- label: string;
544
- style?: string;
545
- data?: Record<string, unknown>;
546
- }>;
547
- display?: string;
548
- persistent?: boolean;
549
- }>;
363
+ currentTurnSurfaces: AssistantSurface[];
550
364
 
551
365
  workingDir: string;
552
- workspaceTopLevelContext: string | null;
553
- workspaceTopLevelDirty: boolean;
554
366
  channelCapabilities?: ChannelCapabilities;
555
367
  /** Per-turn snapshot of trustContext, frozen at message-processing start. */
556
368
  currentTurnTrustContext?: TrustContext;
@@ -624,9 +436,11 @@ export interface AgentLoopConversationContext {
624
436
  | "message_complete"
625
437
  | "generation_cancelled"
626
438
  | "error_terminal",
627
- anchor?: "assistant_turn" | "user_turn" | "global",
628
- requestId?: string,
629
- statusText?: string,
439
+ options?: {
440
+ anchor?: "assistant_turn" | "user_turn" | "global";
441
+ requestId?: string;
442
+ statusText?: string;
443
+ },
630
444
  ): void;
631
445
  emitConfirmationStateChanged(
632
446
  params: ConfirmationStateChanged extends {
@@ -644,14 +458,12 @@ export interface AgentLoopConversationContext {
644
458
  onConfirmationOutcome?: (
645
459
  requestId: string,
646
460
  state: string,
647
- toolName?: string,
648
461
  toolUseId?: string,
649
462
  ) => void;
650
463
 
651
464
  getWorkspaceGitService?: (workspaceDir: string) => GitServiceInitializer;
652
465
  commitTurnChanges?: typeof commitTurnChanges;
653
466
 
654
- refreshWorkspaceTopLevelContextIfNeeded(): void;
655
467
  markWorkspaceTopLevelDirty(): void;
656
468
  getQueueDepth(): number;
657
469
  hasQueuedMessages(): boolean;
@@ -712,6 +524,13 @@ export async function runAgentLoopImpl(
712
524
  });
713
525
  let yieldedForHandoff = false;
714
526
  let yieldedForBudget = false;
527
+ // Whether the most recent agent-loop run produced at least one new assistant
528
+ // message — the loop's own forward-progress signal, used by the ordering
529
+ // retry gate and the overflow convergence fold.
530
+ let lastRunAppendedNewMessages = false;
531
+ // The messages the most recent agent-loop run appended on top of its base —
532
+ // the loop's own new-output boundary, persisted as this turn's new messages.
533
+ let lastRunNewMessages: Message[] = [];
715
534
  let pendingCheckpointYield: "budget" | "handoff" | null = null;
716
535
  // Captured when the auto_compress_latest_turn rerun yields at the mid-loop
717
536
  // budget checkpoint. SSE emission happens immediately at the detection site;
@@ -730,6 +549,9 @@ export async function runAgentLoopImpl(
730
549
  // `resolveCallSiteConfig`, picking up any user overrides under
731
550
  // `llm.callSites.mainAgent` (falling back to `llm.default` when absent).
732
551
  const turnCallSite: LLMCallSite = options?.callSite ?? "mainAgent";
552
+ // Expose the turn's call site to plugin pipeline/injector contexts (read by
553
+ // buildPluginTurnContext) so plugins can scope behaviour to the main reply.
554
+ ctx.currentCallSite = turnCallSite;
733
555
 
734
556
  // Read the conversation row once for both the override-profile derivation
735
557
  // below and the title-replaceability check at turn start. Later reads in
@@ -859,6 +681,25 @@ export async function runAgentLoopImpl(
859
681
  preflightBudget: Math.floor(providerMaxTokens * (1 - safetyMargin)),
860
682
  };
861
683
  };
684
+ /**
685
+ * The agent loop's window into the orchestrator's current effective
686
+ * context window. The loop reads `maxInputTokens` for tool-result
687
+ * truncation and `overflowRecovery` for its mid-loop budget gate, applying
688
+ * the long-history safety-margin bump itself off its own running history.
689
+ * Resolved fresh on each access so a mid-turn profile change is reflected.
690
+ */
691
+ const resolveContextWindow = (): {
692
+ maxInputTokens: number;
693
+ overflowRecovery: { enabled: boolean; safetyMarginRatio: number };
694
+ } => {
695
+ refreshCurrentProfileState();
696
+ const { enabled, safetyMarginRatio } =
697
+ currentEffectiveContextWindow.overflowRecovery;
698
+ return {
699
+ maxInputTokens: currentEffectiveContextWindow.maxInputTokens,
700
+ overflowRecovery: { enabled, safetyMarginRatio },
701
+ };
702
+ };
862
703
 
863
704
  // Initial value for `createToolExecutor` to read into
864
705
  // `ToolContext.overrideProfile`. `resolveCurrentOverrideProfile` refreshes
@@ -924,10 +765,6 @@ export async function runAgentLoopImpl(
924
765
  : null,
925
766
  },
926
767
  );
927
- const diskPressureContext =
928
- diskPressureDecision.action === "allow-cleanup-mode"
929
- ? { cleanupModeActive: true }
930
- : null;
931
768
  ctx.diskPressureCleanupModeActive =
932
769
  diskPressureDecision.action === "allow-cleanup-mode";
933
770
 
@@ -968,7 +805,10 @@ export async function runAgentLoopImpl(
968
805
  { reason: diskPressureDecision.reason },
969
806
  "Blocked turn during disk pressure cleanup mode",
970
807
  );
971
- ctx.emitActivityState("idle", "error_terminal", "global", reqId);
808
+ ctx.emitActivityState("idle", "error_terminal", {
809
+ anchor: "global",
810
+ requestId: reqId,
811
+ });
972
812
  ctx.traceEmitter.emit("request_error", message, {
973
813
  requestId: reqId,
974
814
  status: "error",
@@ -1027,55 +867,6 @@ export async function runAgentLoopImpl(
1027
867
  }
1028
868
  }
1029
869
 
1030
- // Generate title early — the user message alone is sufficient context.
1031
- // Firing before the main LLM call removes the delay of waiting for the
1032
- // full assistant response. The second-pass regeneration at turn 3 will
1033
- // refine the title with more context.
1034
- // No abort signal — title generation should complete even if the user
1035
- // cancels the response, since the user message is already persisted.
1036
- // Deferred via setTimeout so the main agent loop LLM call enqueues
1037
- // first, avoiding rate-limit slot contention on strict configs.
1038
- if (isReplaceableTitle(turnStartConversation?.title ?? null)) {
1039
- // TurnContext routed through the canonical builder so the pipeline's
1040
- // log record reports the same `conversationId`/`turnIndex` shape as
1041
- // every other slot in this turn. Title generation does not depend on
1042
- // the context-window manager attached by the builder, but sharing the
1043
- // builder keeps the invariant enforced in one place.
1044
- const titlePipelineCtx = buildPluginTurnContext(ctx, reqId);
1045
- const titleArgs = {
1046
- conversationId: ctx.conversationId,
1047
- provider: ctx.provider,
1048
- userMessage: options?.titleText ?? content,
1049
- onTitleUpdated: (title: string) => {
1050
- onEvent({
1051
- type: "conversation_title_updated",
1052
- conversationId: ctx.conversationId,
1053
- title,
1054
- });
1055
- onEvent({
1056
- type: "sync_changed",
1057
- tags: [conversationMetadataSyncTag(ctx.conversationId)],
1058
- });
1059
- },
1060
- };
1061
- setTimeout(() => {
1062
- runPipeline(
1063
- "titleGenerate",
1064
- getMiddlewaresFor("titleGenerate"),
1065
- defaultTitleGenerateTerminal,
1066
- titleArgs,
1067
- titlePipelineCtx,
1068
- DEFAULT_TIMEOUTS.titleGenerate,
1069
- ).catch((err) => {
1070
- // Fire-and-forget — keep previous non-propagating semantics.
1071
- // queueGenerateConversationTitle already swallows internal
1072
- // errors; this catch covers pipeline-layer errors (timeouts,
1073
- // middleware throws) without surfacing them to the agent loop.
1074
- rlog.warn({ err }, "titleGenerate pipeline failed (non-fatal)");
1075
- });
1076
- }, 0);
1077
- }
1078
-
1079
870
  const isFirstMessage = ctx.messages.length === 1;
1080
871
  // Promote a pending post-compaction re-inject signal (e.g. from `/compact`)
1081
872
  // into `compactedThisTurn` so NOW.md / PKB / v2 static blocks land on this
@@ -1083,7 +874,6 @@ export async function runAgentLoopImpl(
1083
874
  // so this fires exactly once per `/compact` event.
1084
875
  const consumedPostCompactReinject = ctx.pendingPostCompactReinject;
1085
876
  ctx.pendingPostCompactReinject = false;
1086
- let shouldInjectWorkspace = isFirstMessage || consumedPostCompactReinject;
1087
877
  let compactedThisTurn = consumedPostCompactReinject;
1088
878
  let slackCompactedThisTurn = false;
1089
879
  const isSlackConversation = ctx.channelCapabilities?.channel === "slack";
@@ -1220,74 +1010,42 @@ export async function runAgentLoopImpl(
1220
1010
  );
1221
1011
  // Skip auto-compaction while the circuit breaker is open. Force paths
1222
1012
  // and user-initiated /compact bypass this check.
1223
- const autoCompactAllowed = !(await isCompactionCircuitOpen(ctx));
1013
+ const autoCompactAllowed =
1014
+ !(await ctx.agentLoop.compactionCircuit.isOpen());
1224
1015
  if (compactCheck.needed && autoCompactAllowed) {
1225
- ctx.emitActivityState(
1226
- "thinking",
1227
- "context_compacting",
1228
- "assistant_turn",
1229
- reqId,
1230
- );
1016
+ ctx.emitActivityState("thinking", "context_compacting", {
1017
+ requestId: reqId,
1018
+ });
1231
1019
  }
1232
- const compactionOptions = {
1233
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1234
- precomputedEstimate: compactCheck.estimatedTokens,
1235
- conversationOriginChannel:
1236
- getConversationOriginChannel(ctx.conversationId) ?? undefined,
1237
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
1238
- };
1239
1020
  let compacted: Awaited<
1240
1021
  ReturnType<typeof ctx.contextWindowManager.maybeCompact>
1241
1022
  > | null = null;
1242
1023
  if (autoCompactAllowed) {
1243
- try {
1244
- compacted = (await runPipeline<CompactionArgs, CompactionResult>(
1245
- "compaction",
1246
- getMiddlewaresFor("compaction"),
1247
- (args) =>
1248
- defaultCompactionTerminal(args, buildPluginTurnContext(ctx, reqId)),
1249
- {
1250
- messages: messagesForStartOfTurnCompaction,
1251
- signal: abortController.signal,
1252
- options: compactionOptions,
1253
- },
1254
- buildPluginTurnContext(ctx, reqId),
1255
- DEFAULT_TIMEOUTS.compaction,
1256
- )) as Awaited<ReturnType<typeof ctx.contextWindowManager.maybeCompact>>;
1257
- } catch (err) {
1258
- if (err instanceof PluginTimeoutError) {
1259
- // Pipeline exceeded its budget. Record the failure so the circuit
1260
- // breaker tracks consecutive timeouts (it trips after three),
1261
- // then degrade gracefully by skipping compaction this turn —
1262
- // the turn proceeds with the un-compacted history rather than
1263
- // hard-failing. The inner summary call has been aborted by the
1264
- // runner's signal-linking, so updateSummary's local fallback
1265
- // also ran before this catch block is reached.
1266
- rlog.warn(
1267
- { err, phase: "start-of-turn-compaction" },
1268
- "Compaction pipeline timed out — skipping compaction this turn",
1269
- );
1270
- await trackCompactionOutcome(ctx, true, onEvent);
1271
- compacted = null;
1272
- } else {
1273
- throw err;
1274
- }
1275
- }
1024
+ compacted = await defaultCompact({
1025
+ manager: ctx.contextWindowManager,
1026
+ messages: messagesForStartOfTurnCompaction,
1027
+ signal: abortController.signal,
1028
+ precomputedEstimate: compactCheck.estimatedTokens,
1029
+ overrideProfile: resolveCurrentOverrideProfile() ?? null,
1030
+ actorTrustClass: resolveTurnActorTrustClass(ctx),
1031
+ });
1276
1032
  }
1277
1033
  // Only track circuit-breaker state when a summary LLM call actually ran.
1278
1034
  // `summaryFailed` is `undefined` on early returns (compaction disabled,
1279
- // below threshold, cooldown active, no eligible messages, truncation-only
1035
+ // below threshold, no eligible messages, truncation-only
1280
1036
  // path) — treating those as "successful" compactions would silently reset
1281
1037
  // the 3-strike counter and break the invariant.
1282
1038
  if (compacted && compacted.summaryFailed !== undefined) {
1283
- await trackCompactionOutcome(ctx, compacted.summaryFailed, onEvent);
1039
+ await ctx.agentLoop.compactionCircuit.recordOutcome(
1040
+ compacted.summaryFailed,
1041
+ onEvent,
1042
+ );
1284
1043
  }
1285
1044
  if (compacted?.compacted) {
1286
1045
  await applySuccessfulCompaction(
1287
1046
  compacted,
1288
1047
  messagesForStartOfTurnCompaction,
1289
1048
  );
1290
- shouldInjectWorkspace = true;
1291
1049
  if (compacted.compactedPersistedMessages > 0) {
1292
1050
  compactedThisTurn = true;
1293
1051
  }
@@ -1295,12 +1053,7 @@ export async function runAgentLoopImpl(
1295
1053
 
1296
1054
  // Register confirmation outcome tracker so the agent loop can link
1297
1055
  // confirmation decisions to tool_use_ids for persistence.
1298
- ctx.onConfirmationOutcome = (
1299
- requestId,
1300
- confirmationState,
1301
- toolName,
1302
- toolUseId,
1303
- ) => {
1056
+ ctx.onConfirmationOutcome = (requestId, confirmationState, toolUseId) => {
1304
1057
  if (confirmationState === "pending") {
1305
1058
  // Use the toolUseId passed from the prompter (which knows which tool
1306
1059
  // requested confirmation) instead of the ambient state.currentToolUseId,
@@ -1317,7 +1070,7 @@ export async function runAgentLoopImpl(
1317
1070
  const resolvedId =
1318
1071
  state.requestIdToToolUseId.get(requestId) ?? toolUseId;
1319
1072
  if (resolvedId) {
1320
- const name = state.toolUseIdToName.get(resolvedId) ?? toolName ?? "";
1073
+ const name = state.toolUseIdToName.get(resolvedId) ?? "";
1321
1074
  // Build a friendly label from the tool name
1322
1075
  const label =
1323
1076
  TOOL_FRIENDLY_LABEL[name] ??
@@ -1330,213 +1083,10 @@ export async function runAgentLoopImpl(
1330
1083
  }
1331
1084
  };
1332
1085
 
1333
- let runMessages = ctx.messages;
1334
-
1335
- // Memory retrieval pipeline fetches PKB, NOW.md, and memory-graph
1336
- // outputs through a single `memoryRetrieval` pipeline. Plugins may
1337
- // replace the terminal behavior by registering a middleware that
1338
- // short-circuits with its own `MemoryResult`; the default terminal
1339
- // below runs `runDefaultMemoryRetrieval` which reproduces the prior
1340
- // in-lined behavior (PKB/NOW reads + gated graph call).
1341
- const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
1342
- // Canonical builder — pulls trust from per-turn snapshot, then
1343
- // conversation-level, then the synthetic fallback. Memory retrieval
1344
- // does not need the context-window handle the builder attaches, but
1345
- // keeping every call site on one helper is load-bearing for log
1346
- // coherence across pipeline slots.
1347
- const memoryPluginTurnCtx = buildPluginTurnContext(ctx, reqId);
1348
- const memoryArgs: MemoryArgs = {
1349
- conversationId: ctx.conversationId,
1350
- trustContext: ctx.trustContext,
1351
- turnIndex: ctx.turnCount,
1352
- // Pass the abort signal via `args` (not `deps`) so the pipeline
1353
- // runner's `linkAbortSignal` can swap it for a signal linked to the
1354
- // pipeline's internal controller — on a plugin-set timeout or
1355
- // external cancel, the linked signal aborts and `prepareMemory`
1356
- // stops mutating graph state / emitting events after the pipeline
1357
- // has already errored.
1358
- signal: abortController.signal,
1359
- };
1360
- const memoryDeps: DefaultMemoryRetrievalDeps = {
1361
- messages: ctx.messages,
1362
- graphMemory: ctx.graphMemory,
1363
- config: getConfig(),
1364
- onEvent,
1365
- isTrustedActor,
1366
- };
1367
- const memoryResult: MemoryResult = await runPipeline(
1368
- "memoryRetrieval",
1369
- getMiddlewaresFor("memoryRetrieval"),
1370
- (args) => runDefaultMemoryRetrieval(args, memoryDeps),
1371
- memoryArgs,
1372
- memoryPluginTurnCtx,
1373
- DEFAULT_TIMEOUTS.memoryRetrieval,
1374
- );
1375
-
1376
- // Consume the memory-graph block when the default retriever emitted
1377
- // one. Custom plugins that substitute their own blocks without the
1378
- // default discriminator are expected to handle their own side effects
1379
- // (event emission, metric persistence) inside their middleware; this
1380
- // block short-circuits to the original no-op behavior in that case.
1381
- const defaultGraphPayload: GraphMemoryPayload | null =
1382
- asDefaultGraphPayload(memoryResult.memoryGraphBlocks);
1383
- let pkbQueryVector: number[] | undefined;
1384
- let pkbSparseVector: QdrantSparseVector | undefined;
1385
- if (defaultGraphPayload) {
1386
- const graphResult = defaultGraphPayload.result;
1387
- runMessages = graphResult.runMessages;
1388
- // Select dense+sparse as a matched pair so RRF fusion combines two
1389
- // signals aligned to the same query text:
1390
- // 1. Context-load with a user query: user-query dense + user-query
1391
- // sparse — the cleanest pairing.
1392
- // 2. Otherwise (context-load without a user query, or per-turn):
1393
- // whatever `queryVector` / `sparseVector` the retriever produced,
1394
- // which are themselves co-aligned (both summary-derived in
1395
- // context-load, both user-last-message-derived in per-turn).
1396
- // Never pair a user-query dense with a summary-aligned sparse.
1397
- if (graphResult.userQueryVector) {
1398
- pkbQueryVector = graphResult.userQueryVector;
1399
- pkbSparseVector = graphResult.userQuerySparseVector;
1400
- } else {
1401
- pkbQueryVector = graphResult.queryVector;
1402
- pkbSparseVector = graphResult.sparseVector;
1403
- }
1404
-
1405
- // Persist the injected block text in message metadata so it survives
1406
- // conversation reloads (eviction, restart, fork). loadFromDb re-injects
1407
- // from metadata. Routed through the `persistence` pipeline so plugins
1408
- // can observe or override metadata updates alongside add/delete.
1409
- if (graphResult.injectedBlockText) {
1410
- try {
1411
- await runPipeline<PersistArgs, PersistResult>(
1412
- "persistence",
1413
- getMiddlewaresFor("persistence"),
1414
- defaultPersistenceTerminal,
1415
- {
1416
- op: "update",
1417
- messageId: userMessageId,
1418
- updates: {
1419
- memoryInjectedBlock: graphResult.injectedBlockText,
1420
- },
1421
- },
1422
- buildPluginTurnContext(ctx, reqId),
1423
- DEFAULT_TIMEOUTS.persistence,
1424
- );
1425
- } catch (err) {
1426
- rlog.warn(
1427
- { err },
1428
- "Failed to persist memory injection to metadata (non-fatal)",
1429
- );
1430
- }
1431
- }
1432
-
1433
- const m = graphResult.metrics;
1434
-
1435
- try {
1436
- recordMemoryRecallLog({
1437
- conversationId: ctx.conversationId,
1438
- enabled: true,
1439
- degraded: false,
1440
- provider: m?.embeddingProvider ?? undefined,
1441
- model: m?.embeddingModel ?? undefined,
1442
- semanticHits: m?.semanticHits ?? 0,
1443
- mergedCount: m?.mergedCount ?? 0,
1444
- selectedCount: m?.selectedCount ?? 0,
1445
- tier1Count: m?.tier1Count ?? 0,
1446
- tier2Count: m?.tier2Count ?? 0,
1447
- hybridSearchLatencyMs: m?.hybridSearchLatencyMs ?? 0,
1448
- sparseVectorUsed: m?.sparseVectorUsed ?? false,
1449
- injectedTokens: graphResult.injectedTokens,
1450
- latencyMs: graphResult.latencyMs,
1451
- topCandidatesJson: (m?.topCandidates ?? []).map((c) => ({
1452
- key: c.nodeId,
1453
- type: c.type,
1454
- kind: "graph",
1455
- finalScore: c.score,
1456
- semantic: c.semanticSimilarity,
1457
- recency: c.recencyBoost,
1458
- })),
1459
- injectedText: graphResult.injectedBlockText ?? undefined,
1460
- reason: `graph:${graphResult.mode}`,
1461
- queryContext: m?.queryContext ?? undefined,
1462
- });
1463
- } catch (err) {
1464
- log.warn({ err }, "Failed to persist memory recall log (non-fatal)");
1465
- }
1466
-
1467
- if (m) {
1468
- const memoryRecalledEvent: MemoryRecalled = {
1469
- type: "memory_recalled",
1470
- provider: m.embeddingProvider ?? "unknown",
1471
- model: m.embeddingModel ?? "unknown",
1472
- semanticHits: m.semanticHits,
1473
- mergedCount: m.mergedCount,
1474
- selectedCount: m.selectedCount,
1475
- tier1Count: m.tier1Count,
1476
- tier2Count: m.tier2Count,
1477
- hybridSearchLatencyMs: m.hybridSearchLatencyMs,
1478
- sparseVectorUsed: m.sparseVectorUsed,
1479
- injectedTokens: graphResult.injectedTokens,
1480
- latencyMs: graphResult.latencyMs,
1481
- topCandidates: m.topCandidates.map((c) => ({
1482
- key: c.nodeId,
1483
- type: c.type,
1484
- kind: "graph",
1485
- finalScore: c.score,
1486
- semantic: c.semanticSimilarity,
1487
- recency: c.recencyBoost,
1488
- })),
1489
- };
1490
- onEvent(memoryRecalledEvent);
1491
- }
1492
- }
1493
-
1494
- // Build active surface context
1495
- let activeSurface: ActiveSurfaceContext | null = null;
1496
- if (ctx.currentActiveSurfaceId) {
1497
- const stored = ctx.surfaceState.get(ctx.currentActiveSurfaceId);
1498
- if (stored && stored.surfaceType === "dynamic_page") {
1499
- const data = stored.data as DynamicPageSurfaceData;
1500
- activeSurface = {
1501
- surfaceId: ctx.currentActiveSurfaceId,
1502
- html: data.html,
1503
- currentPage: ctx.currentPage,
1504
- };
1505
- if (data.appId) {
1506
- const app = getApp(data.appId);
1507
- if (app) {
1508
- activeSurface.appId = app.id;
1509
- activeSurface.appName = app.name;
1510
- activeSurface.appDirName = resolveAppDir(app.id).dirName;
1511
- activeSurface.appSchemaJson = app.schemaJson;
1512
- activeSurface.appFiles = listAppFiles(app.id);
1513
- if (app.pages && Object.keys(app.pages).length > 0) {
1514
- activeSurface.appPages = app.pages;
1515
- }
1516
- }
1517
- }
1518
- }
1519
- }
1520
-
1521
- // Query active documents for this conversation so the injector chain
1522
- // can surface them to the assistant (prevents duplicate document_create
1523
- // calls when existing documents should be targeted with document_update).
1524
- const conversationDocs = getDocumentsForConversation(ctx.conversationId);
1525
- const activeDocuments =
1526
- conversationDocs.length > 0
1527
- ? conversationDocs.map((d) => ({
1528
- surfaceId: d.surfaceId,
1529
- title: d.title,
1530
- wordCount: d.wordCount,
1531
- updatedAt: d.updatedAt,
1532
- }))
1533
- : null;
1534
-
1535
- ctx.refreshWorkspaceTopLevelContextIfNeeded();
1536
-
1537
- // Compute fresh turn timestamp for date grounding.
1538
- // Absolute "now" is always anchored to assistant host clock, while local
1539
- // date semantics prefer configured user timezone, then device timezones.
1086
+ // Resolve the turn's timezone cascade up front. It depends only on config
1087
+ // and the inbound request — never on retrieval output — so it can be
1088
+ // settled before context assembly. Local date semantics prefer the
1089
+ // configured user timezone, then device timezones, then the host clock.
1540
1090
  const hostTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1541
1091
  const timezoneContext = resolveTurnTimezoneContext({
1542
1092
  configuredUserTimeZone: config.ui.userTimezone ?? null,
@@ -1544,9 +1094,6 @@ export async function runAgentLoopImpl(
1544
1094
  detectedTimezone: config.ui.detectedTimezone ?? null,
1545
1095
  hostTimeZone,
1546
1096
  });
1547
- const timestamp = formatTurnTimestamp({
1548
- timeZone: timezoneContext.effectiveTimezone,
1549
- });
1550
1097
 
1551
1098
  // Resolve the inbound actor context for the unified <turn_context> block.
1552
1099
  // When the conversation carries enough identity info, use the unified
@@ -1570,8 +1117,10 @@ export async function runAgentLoopImpl(
1570
1117
  }
1571
1118
  }
1572
1119
 
1573
- // Build unified turn context block that replaces the separate temporal,
1574
- // channel, interface, and actor context blocks.
1120
+ // Resolve the channel/interface labels and the guardian flag for this
1121
+ // turn. These derive only from the captured turn context and the resolved
1122
+ // actor trust class — never from retrieval — so they settle before context
1123
+ // assembly.
1575
1124
  const interfaceName =
1576
1125
  capturedTurnInterfaceContext.userMessageInterface ?? undefined;
1577
1126
  const channelName =
@@ -1616,9 +1165,54 @@ export async function runAgentLoopImpl(
1616
1165
  });
1617
1166
  const label = profileEntry?.label ?? effectiveProfileKey;
1618
1167
  modelProfileStr = resolved.model ? `${label} (${resolved.model})` : label;
1619
- setLastNotifiedInferenceProfile(ctx.conversationId, effectiveProfileKey);
1168
+ // Record the notification for persistence on delivery rather than here:
1169
+ // the model only "learns" the profile once it receives this turn
1170
+ // context, signalled by the first `message_complete`. Persisting inline
1171
+ // would mark the profile notified even if the turn is cancelled or fails
1172
+ // before the model ever sees the notice.
1173
+ state.pendingNotifiedInferenceProfile = effectiveProfileKey;
1620
1174
  }
1621
1175
 
1176
+ // Memory retrieval — fetches PKB, NOW.md, and memory-graph outputs and
1177
+ // persists the retrieval's own side effects (injected-block metadata,
1178
+ // recall log, `memory_recalled` event). Runs at the early "prompt
1179
+ // submitted, before context assembly" moment because its outputs feed the
1180
+ // injection and overflow-reduction transforms below. It is shaped as the
1181
+ // `user-prompt-submit-temp` hook handler but invoked directly for now: it
1182
+ // must run early, while the canonical late `user-prompt-submit` hook
1183
+ // (history repair, title) runs after those transforms, so the two cannot
1184
+ // share a fire site until compaction is cleared from the gap between them.
1185
+ const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
1186
+ const memoryCtx: MemoryRetrievalHookContext = {
1187
+ graphMemory: ctx.graphMemory,
1188
+ config: getConfig(),
1189
+ onEvent,
1190
+ isTrustedActor,
1191
+ conversationId: ctx.conversationId,
1192
+ userMessageId,
1193
+ logger: rlog,
1194
+ // An external cancel aborts `prepareMemory` instead of letting it run
1195
+ // to completion after the turn has already been torn down.
1196
+ signal: abortController.signal,
1197
+ latestMessages: ctx.messages,
1198
+ };
1199
+ await userPromptSubmitMemoryRetrieval(memoryCtx);
1200
+
1201
+ // The retriever owns its side effects (injected-block metadata, recall
1202
+ // log, `memory_recalled` event) and records the dense/sparse PKB query
1203
+ // pair on the graph handle for the PKB-reminder injector to read back; the
1204
+ // loop only reuses the injected message list downstream.
1205
+ let runMessages = memoryCtx.latestMessages;
1206
+
1207
+ // Capture wall-clock "now" at its point of use, after the blocking memory
1208
+ // retrieval, so the injected `<turn_context>` timestamp reflects current
1209
+ // time rather than the moment the turn began.
1210
+ const timestamp = formatTurnTimestamp({
1211
+ timeZone: timezoneContext.effectiveTimezone,
1212
+ });
1213
+
1214
+ // Build unified turn context block that replaces the separate temporal,
1215
+ // channel, interface, and actor context blocks.
1622
1216
  const baseTurnContext = {
1623
1217
  timestamp,
1624
1218
  interfaceName,
@@ -1640,64 +1234,6 @@ export async function runAgentLoopImpl(
1640
1234
 
1641
1235
  // The `remember` tool handles scratchpad-style memory writes directly to the graph.
1642
1236
 
1643
- // Personal-memory trust gate: PKB, NOW.md, and v2 static blocks all
1644
- // hold private user content. Block exposure to non-guardian actors
1645
- // arriving over a remote channel; internal/local flows pass through.
1646
- // See `shouldExposePersonalMemory` for the threat model.
1647
- const personalMemoryAllowed = shouldExposePersonalMemory({
1648
- sourceChannel: ctx.trustContext?.sourceChannel,
1649
- isTrustedActor,
1650
- });
1651
-
1652
- // Inject NOW.md and PKB content only on the first turn (or after
1653
- // compaction re-strips them). Old injections persist in history and
1654
- // are never stripped on normal turns — this preserves the cached prefix.
1655
- // PKB/NOW content is sourced from the `memoryRetrieval` pipeline above
1656
- // so plugins can override either source without touching the agent loop.
1657
- // NOW.md injection can be disabled via `memory.retrieval.scratchpadInjection.enabled`.
1658
- const scratchpadInjectionEnabled =
1659
- getConfig().memory.retrieval.scratchpadInjection.enabled;
1660
- const currentNowContent =
1661
- personalMemoryAllowed && scratchpadInjectionEnabled
1662
- ? memoryResult.nowContent
1663
- : null;
1664
- const shouldInjectNowAndPkb = isFirstMessage || compactedThisTurn;
1665
- const nowScratchpad = shouldInjectNowAndPkb ? currentNowContent : null;
1666
-
1667
- const currentPkbContent = personalMemoryAllowed
1668
- ? memoryResult.pkbContent
1669
- : null;
1670
- const pkbContext = shouldInjectNowAndPkb ? currentPkbContent : null;
1671
- const pkbActive = currentPkbContent !== null;
1672
-
1673
- // V2 static memory block (essentials/threads/recent/buffer).
1674
- // `currentMemoryV2Static` is the trust-gated content reused by every
1675
- // re-injection path — it stays non-null on non-full-mode turns so
1676
- // that mid-turn reducer compaction (which strips the prior `<info>`
1677
- // block) can restore the freshest content. `memoryV2Static` is the
1678
- // first-turn / post-compaction cadence-gated value for initial
1679
- // injection only. `readMemoryV2StaticContent` self-gates on the v2
1680
- // flag + config and returns null when v2 is off.
1681
- const currentMemoryV2Static = personalMemoryAllowed
1682
- ? readMemoryV2StaticContent()
1683
- : null;
1684
- const memoryV2Static = shouldInjectNowAndPkb ? currentMemoryV2Static : null;
1685
-
1686
- // PKB relevance-hint inputs. Resolved once per turn and reused across
1687
- // re-injections so post-compaction rebuilds pick up fresh hints against
1688
- // the updated conversation history.
1689
- const pkbRoot = pkbActive ? join(getWorkspaceDir(), "pkb") : undefined;
1690
- const pkbAutoInjectList = pkbRoot
1691
- ? getPkbAutoInjectList(pkbRoot)
1692
- : undefined;
1693
- // Pass `ctx` directly — `PkbContextConversation` is structural and
1694
- // `getInContextPkbPaths` re-reads `conversation.messages` on each call,
1695
- // so post-compaction re-injects see the updated history.
1696
- const pkbConversation = pkbActive ? ctx : undefined;
1697
- // PKB points live under a single workspace sentinel scope.
1698
- // See `PKB_WORKSPACE_SCOPE` for why.
1699
- const pkbScopeId = pkbActive ? PKB_WORKSPACE_SCOPE : undefined;
1700
-
1701
1237
  // Subagent status injection — gives the parent LLM visibility into active/completed children.
1702
1238
  // Skipped when this conversation IS a subagent (no nesting) or has no children.
1703
1239
  const subagentStatusBlock = ctx.isSubagent
@@ -1752,36 +1288,23 @@ export async function runAgentLoopImpl(
1752
1288
  )
1753
1289
  : null;
1754
1290
 
1755
- // Guards the chronological-transcript override on re-injection after
1756
- // the reducer compacts `ctx.messages`. The captured transcript is the
1757
- // full persisted history; blindly replaying it on every re-inject would
1758
- // overwrite the reducer's compacted messages and undo compaction. Flip
1759
- // to `true` after any compaction so subsequent re-injections fall back
1760
- // to the reduced `ctx.messages`.
1761
- let reducerCompacted = compactedThisTurn;
1291
+ state.reducerCompacted = compactedThisTurn;
1292
+
1293
+ // memory-v3-live: when on, the provider anchors its long-TTL cache
1294
+ // breakpoint on the most recent STABLE user message, since the latest user
1295
+ // message now carries the volatile per-turn `<memory>` block the v3
1296
+ // injector emits. The matching v2-suppression strip is owned by
1297
+ // `applyRuntimeInjections`, which reads the same flag itself. Flag off →
1298
+ // bit-for-bit identical to today's v2 path.
1299
+ const memoryV3Live = isAssistantFeatureFlagEnabled(
1300
+ "memory-v3-live",
1301
+ getConfig(),
1302
+ );
1762
1303
 
1763
1304
  // Shared injection options — reused whenever we need to re-inject after reduction.
1764
1305
  const injectionOpts = {
1765
- diskPressureContext,
1766
- activeSurface,
1767
- activeDocuments,
1768
- workspaceTopLevelContext: shouldInjectWorkspace
1769
- ? ctx.workspaceTopLevelContext
1770
- : null,
1771
- channelCapabilities: ctx.channelCapabilities ?? null,
1772
1306
  channelCommandContext: ctx.commandIntent ?? null,
1773
1307
  unifiedTurnContext: unifiedTurnContextStr,
1774
- pkbContext,
1775
- pkbActive,
1776
- pkbQueryVector,
1777
- pkbSparseVector,
1778
- pkbScopeId,
1779
- pkbConversation,
1780
- pkbAutoInjectList,
1781
- pkbRoot,
1782
- pkbWorkingDir: pkbActive ? ctx.workingDir : undefined,
1783
- memoryV2Static,
1784
- nowScratchpad,
1785
1308
  voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
1786
1309
  transportHints: ctx.transportHints ?? null,
1787
1310
  isNonInteractive: !isInteractiveResolved,
@@ -1803,7 +1326,7 @@ export async function runAgentLoopImpl(
1803
1326
 
1804
1327
  const injection = await applyRuntimeInjections(runMessages, {
1805
1328
  ...injectionOpts,
1806
- slackChronologicalMessages: reducerCompacted
1329
+ slackChronologicalMessages: state.reducerCompacted
1807
1330
  ? null
1808
1331
  : injectionOpts.slackChronologicalMessages,
1809
1332
  mode: currentInjectionMode,
@@ -1849,18 +1372,7 @@ export async function runAgentLoopImpl(
1849
1372
  metadataUpdates.memoryV2StaticBlock =
1850
1373
  injection.blocks.memoryV2StaticBlock;
1851
1374
  }
1852
- await runPipeline<PersistArgs, PersistResult>(
1853
- "persistence",
1854
- getMiddlewaresFor("persistence"),
1855
- defaultPersistenceTerminal,
1856
- {
1857
- op: "update",
1858
- messageId: userMessageId,
1859
- updates: metadataUpdates,
1860
- },
1861
- buildPluginTurnContext(ctx, reqId),
1862
- DEFAULT_TIMEOUTS.persistence,
1863
- );
1375
+ updateMessageMetadata(userMessageId, metadataUpdates);
1864
1376
  } catch (err) {
1865
1377
  rlog.warn({ err }, "Failed to persist injection metadata (non-fatal)");
1866
1378
  }
@@ -1876,51 +1388,18 @@ export async function runAgentLoopImpl(
1876
1388
  let reducerState: ReducerState | undefined;
1877
1389
 
1878
1390
  const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
1879
- // Canonical calibration key — passed to the `tokenEstimate` pipeline for
1880
- // every preflight/mid-loop estimate, the overflow reducer config, and the
1881
- // convergence-path `estimatePromptTokens` call. Matches the key recorded
1882
- // by `handleUsage` for wrapper providers (OpenRouter routing to
1883
- // Anthropic → key is `"anthropic"`).
1391
+ // Canonical calibration key — used by the preflight estimate, the
1392
+ // overflow reducer config, and the convergence-path `estimatePromptTokens`
1393
+ // call. Matches the key recorded by `handleUsage` for wrapper providers
1394
+ // (OpenRouter routing to Anthropic key is `"anthropic"`).
1884
1395
  const estimationProviderName = getCalibrationProviderKey(ctx.provider);
1885
1396
 
1886
- // Shared `TurnContext` for every `tokenEstimate` pipeline invocation in
1887
- // this turn. The pipeline is the extension point for plugins that want
1888
- // to substitute an alternate estimator (e.g. provider-native tokenization)
1889
- // without touching orchestrator code.
1890
- //
1891
- // Routed through the canonical builder — `turnIndex` is `ctx.turnCount`,
1892
- // trust cascades through per-turn/conversation-level/fallback, and the
1893
- // context-window handle rides along so any middleware that wants to
1894
- // reuse the manager (e.g. to compute compaction-aware estimates) can.
1895
- const pipelineTurnCtx = buildPluginTurnContext(ctx, reqId);
1896
-
1897
- const runTokenEstimatePipeline = (
1898
- history: Message[],
1899
- ): Promise<EstimateResult> =>
1900
- runPipeline<EstimateArgs, EstimateResult>(
1901
- "tokenEstimate",
1902
- getMiddlewaresFor("tokenEstimate"),
1903
- defaultTokenEstimateTerminal,
1904
- {
1905
- // Shallow-frozen copies so a misbehaving middleware that mutates
1906
- // `args.history` or `args.tools` in place (e.g. trims the array
1907
- // before calling next) can't silently strip prompt context from
1908
- // the orchestrator's live `runMessages` / resolved-tools arrays.
1909
- // TypeScript `readonly` on `EstimateArgs` does not prevent
1910
- // `push`/`splice` at runtime; the frozen wrapper throws in strict
1911
- // mode and isolates any mutation attempts from the call-site state.
1912
- history: Object.freeze([...history]) as Message[],
1913
- systemPrompt: ctx.systemPrompt,
1914
- tools: Object.freeze([
1915
- ...ctx.agentLoop.getResolvedTools(history),
1916
- ]) as ToolDefinition[],
1917
- providerName: estimationProviderName,
1918
- },
1919
- pipelineTurnCtx,
1920
- DEFAULT_TIMEOUTS.tokenEstimate,
1921
- );
1922
-
1923
- const preflightTokens = await runTokenEstimatePipeline(runMessages);
1397
+ const preflightTokens = estimatePromptTokensWithTools(
1398
+ runMessages,
1399
+ ctx.systemPrompt,
1400
+ ctx.agentLoop.getResolvedTools(runMessages),
1401
+ estimationProviderName,
1402
+ );
1924
1403
 
1925
1404
  if (overflowRecovery.enabled && preflightTokens > preflightBudget) {
1926
1405
  rlog.warn(
@@ -1932,16 +1411,12 @@ export async function runAgentLoopImpl(
1932
1411
  "Preflight budget exceeded — running overflow reducer before provider call",
1933
1412
  );
1934
1413
 
1935
- // Overflow reduction runs through the plugin pipeline. The default
1936
- // middleware (`default-overflow-reduce`, registered at bootstrap)
1937
- // contains the historical tier loop forced compaction → tool-result
1938
- // truncation media stubbing injection downgrade — plus the
1939
- // re-inject/re-estimate convergence check. The callbacks below are
1940
- // the orchestrator-specific side effects that the plugin coordinates
1941
- // per iteration (activity emission, compaction application, runtime
1942
- // injection reassembly, token re-estimation). Registered plugins that
1943
- // wrap the `overflowReduce` slot see each iteration through their own
1944
- // middleware `next` callback.
1414
+ // `runOverflowReductionLoop` drives the tier loop forced compaction
1415
+ // tool-result truncation media stubbing → injection downgrade — plus
1416
+ // the re-inject/re-estimate convergence check. The callbacks below are
1417
+ // the orchestrator-specific side effects it coordinates per iteration
1418
+ // (activity emission, compaction application, runtime injection
1419
+ // reassembly, token re-estimation).
1945
1420
  const messagesForPreflightOverflowReduction =
1946
1421
  slackChronologicalContext?.messages ?? ctx.messages;
1947
1422
  const overflowArgs: OverflowReduceArgs = {
@@ -1955,75 +1430,23 @@ export async function runAgentLoopImpl(
1955
1430
  maxAttempts: resolveCurrentContextBudget().overflowRecovery.maxAttempts,
1956
1431
  abortSignal: abortController.signal,
1957
1432
  compactFn: async (msgs, signal, opts) => {
1958
- // Route the reducer's forced-compaction tier through the
1959
- // `compaction` pipeline so registered plugins observe these
1960
- // invocations. Without this, custom compaction middleware only
1961
- // sees the three orchestrator-owned call sites and misses the
1962
- // reducer-initiated forced compactions entirely.
1963
- //
1964
- // Pipeline timeouts must be caught locally — a `PluginTimeoutError`
1965
- // bubbling out of here would abort the overflow-reducer tier loop
1966
- // entirely, skipping fallback tiers (tool-result truncation, media
1967
- // stubbing, injection downgrade) and bypassing circuit-breaker
1968
- // bookkeeping. On timeout, record the failure and return a
1969
- // `compacted: false` result so the reducer falls through to the
1970
- // next tier.
1971
- try {
1972
- return (await runPipeline<CompactionArgs, CompactionResult>(
1973
- "compaction",
1974
- getMiddlewaresFor("compaction"),
1975
- (args) =>
1976
- defaultCompactionTerminal(
1977
- args,
1978
- buildPluginTurnContext(ctx, reqId),
1979
- ),
1980
- {
1981
- messages: msgs,
1982
- signal,
1983
- options: {
1984
- ...(opts ?? {}),
1985
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
1986
- },
1987
- },
1988
- buildPluginTurnContext(ctx, reqId),
1989
- DEFAULT_TIMEOUTS.compaction,
1990
- )) as Awaited<
1991
- ReturnType<typeof ctx.contextWindowManager.maybeCompact>
1992
- >;
1993
- } catch (err) {
1994
- if (err instanceof PluginTimeoutError) {
1995
- rlog.warn(
1996
- { err, phase: "overflow-reducer-forced-compaction" },
1997
- "Compaction pipeline timed out — falling through to next reducer tier",
1998
- );
1999
- await trackCompactionOutcome(ctx, true, onEvent);
2000
- return {
2001
- messages: msgs,
2002
- compacted: false,
2003
- previousEstimatedInputTokens: 0,
2004
- estimatedInputTokens: 0,
2005
- maxInputTokens: 0,
2006
- thresholdTokens: 0,
2007
- compactedMessages: 0,
2008
- compactedPersistedMessages: 0,
2009
- summaryCalls: 0,
2010
- summaryInputTokens: 0,
2011
- summaryOutputTokens: 0,
2012
- summaryModel: "",
2013
- summaryText: "",
2014
- reason: "compaction pipeline timed out",
2015
- };
2016
- }
2017
- throw err;
2018
- }
1433
+ // Delegate the reducer's forced-compaction tier to the default
1434
+ // compaction plugin, overlaying the turn's resolved inference
1435
+ // profile and actor trust class onto the reducer-supplied options.
1436
+ const reducerOptions = (opts ?? {}) as ContextWindowCompactOptions;
1437
+ return defaultCompact({
1438
+ manager: ctx.contextWindowManager,
1439
+ messages: msgs,
1440
+ signal,
1441
+ ...reducerOptions,
1442
+ overrideProfile: resolveCurrentOverrideProfile() ?? null,
1443
+ actorTrustClass: resolveTurnActorTrustClass(ctx),
1444
+ });
2019
1445
  },
2020
1446
  emitActivityState: () => {
2021
- ctx.emitActivityState(
2022
- "thinking",
2023
- "context_compacting",
2024
- "assistant_turn",
2025
- reqId,
2026
- );
1447
+ ctx.emitActivityState("thinking", "context_compacting", {
1448
+ requestId: reqId,
1449
+ });
2027
1450
  },
2028
1451
  onCompactionResult: async (result, compactedBasis) => {
2029
1452
  // Track circuit-breaker state whenever the reducer invoked
@@ -2036,11 +1459,13 @@ export async function runAgentLoopImpl(
2036
1459
  // truncation-only path, etc.) that shouldn't influence the
2037
1460
  // breaker.
2038
1461
  if (result.summaryFailed !== undefined) {
2039
- await trackCompactionOutcome(ctx, result.summaryFailed, onEvent);
1462
+ await ctx.agentLoop.compactionCircuit.recordOutcome(
1463
+ result.summaryFailed,
1464
+ onEvent,
1465
+ );
2040
1466
  }
2041
1467
  if (result.compacted) {
2042
1468
  await applySuccessfulCompaction(result, compactedBasis);
2043
- shouldInjectWorkspace = true;
2044
1469
  }
2045
1470
  },
2046
1471
  reinjectForMode: async (
@@ -2051,27 +1476,25 @@ export async function runAgentLoopImpl(
2051
1476
  ) => {
2052
1477
  // Mirror the pre-PR-23 behavior: `ctx.messages` must track the
2053
1478
  // reducer's latest output before re-injection runs, because other
2054
- // sites consulted through `injectionOpts` (`workspaceTopLevelContext`,
2055
- // slack history, etc.) depend on it and `applyCompactionResult`
2056
- // only updates `ctx.messages` on a compaction tier. Assigning here
1479
+ // sites consulted through `injectionOpts` (slack history, etc.) and
1480
+ // the injectors' own message-presence scans depend on it, and
1481
+ // `applyCompactionResult` only updates `ctx.messages` on a
1482
+ // compaction tier. Assigning here
2057
1483
  // keeps non-compaction tiers (tool-result truncation, media
2058
1484
  // stubbing, injection downgrade) observable to downstream
2059
1485
  // injection assembly on the same turn.
2060
1486
  ctx.messages = reducedMessages;
2061
1487
 
2062
- // When THIS iteration compacted, it stripped existing NOW.md /
2063
- // PKB blocks — so we re-inject current content. A later iteration
2064
- // that only truncates or downgrades must NOT re-force PKB/NOW,
1488
+ // When THIS iteration compacted, it stripped the existing
1489
+ // memory-static block — so we re-inject current content. A later
1490
+ // iteration that only truncates or downgrades must NOT re-force it,
2065
1491
  // or each round would grow the token count.
2066
1492
  // Gate: only the iteration that actually compacted re-injects.
1493
+ // (The `<knowledge_base>`, NOW.md, and v2 static `<info>` blocks
1494
+ // self-gate inside their injectors on whether they are already
1495
+ // present in `reducedMessages`.)
2067
1496
  const injection = await applyRuntimeInjections(reducedMessages, {
2068
1497
  ...injectionOpts,
2069
- ...(stepCompacted && { pkbContext: currentPkbContent }),
2070
- ...(stepCompacted && { memoryV2Static: currentMemoryV2Static }),
2071
- ...(stepCompacted && { nowScratchpad: currentNowContent }),
2072
- workspaceTopLevelContext: shouldInjectWorkspace
2073
- ? ctx.workspaceTopLevelContext
2074
- : null,
2075
1498
  // Once ANY iteration has compacted `ctx.messages`, the captured
2076
1499
  // `slackChronologicalMessages` snapshot (built from the full
2077
1500
  // persisted transcript) would overwrite the compacted history
@@ -2097,86 +1520,14 @@ export async function runAgentLoopImpl(
2097
1520
  }),
2098
1521
  };
2099
1522
 
2100
- const overflowResult = await runPipeline<
2101
- OverflowReduceArgs,
2102
- OverflowReduceResult
2103
- >(
2104
- "overflowReduce",
2105
- getMiddlewaresFor("overflowReduce"),
2106
- // Terminal — only reached when every registered middleware calls
2107
- // `next` and delegates past the innermost layer. The default plugin
2108
- // is a terminal itself (it doesn't call `next`), so in practice
2109
- // this fallback fires only when the default has been explicitly
2110
- // deregistered (tests) and no user plugin replaces it. Strict-fail
2111
- // semantics: throw so the missing terminal surfaces as a visible
2112
- // error instead of silently returning the history untouched.
2113
- async () => {
2114
- throw new PluginExecutionError(
2115
- "overflowReduce pipeline has no terminal handler — every reducer middleware called next() without providing a replacement",
2116
- "overflowReduce",
2117
- );
2118
- },
2119
- overflowArgs,
2120
- buildPluginTurnContext(ctx, reqId),
2121
- DEFAULT_TIMEOUTS.overflowReduce,
2122
- );
1523
+ const overflowResult = await runOverflowReductionLoop(overflowArgs);
2123
1524
 
2124
1525
  ctx.messages = overflowResult.messages;
2125
1526
  runMessages = overflowResult.runMessages;
2126
1527
  currentInjectionMode = overflowResult.injectionMode;
2127
1528
  reducerState = overflowResult.reducerState;
2128
1529
  if (overflowResult.reducerCompacted) {
2129
- reducerCompacted = true;
2130
- }
2131
- }
2132
-
2133
- // Pre-run repair — routed through the `historyRepair` plugin pipeline so
2134
- // plugins can observe or override repair behavior. The default plugin's
2135
- // middleware is a passthrough; the actual repair runs in the terminal
2136
- // (`defaultHistoryRepairTerminal`).
2137
- let preRepairMessages = runMessages;
2138
- let preRunRepair: HistoryRepairResult | null = null;
2139
- try {
2140
- preRunRepair = await runPipeline<HistoryRepairArgs, HistoryRepairResult>(
2141
- "historyRepair",
2142
- getMiddlewaresFor("historyRepair"),
2143
- async (args) => defaultHistoryRepairTerminal(args),
2144
- { history: runMessages, provider: ctx.provider.name },
2145
- buildPluginTurnContext(ctx, reqId),
2146
- DEFAULT_TIMEOUTS.historyRepair,
2147
- );
2148
- } catch (err) {
2149
- if (err instanceof PluginTimeoutError) {
2150
- // Pipeline exceeded its budget — likely a misbehaving third-party
2151
- // middleware. Degrade gracefully by proceeding with the un-repaired
2152
- // history rather than turn-fatal-erroring; un-repaired history is
2153
- // strictly better than no turn at all, and the provider call itself
2154
- // will still error visibly if the drift is unrecoverable.
2155
- rlog.warn(
2156
- { err, phase: "pre_run" },
2157
- "historyRepair pipeline timed out — proceeding with un-repaired history",
2158
- );
2159
- } else {
2160
- throw err;
2161
- }
2162
- }
2163
- if (preRunRepair !== null) {
2164
- // Always adopt the pipeline's output history — a user `historyRepair`
2165
- // middleware may rewrite `messages` (e.g. provider-specific
2166
- // normalization) without incrementing any of the built-in repair
2167
- // counters. Gating the assignment on `stats` would silently discard
2168
- // those edits and send the un-rewritten history to the provider.
2169
- runMessages = preRunRepair.messages;
2170
- if (
2171
- preRunRepair.stats.assistantToolResultsMigrated > 0 ||
2172
- preRunRepair.stats.missingToolResultsInserted > 0 ||
2173
- preRunRepair.stats.orphanToolResultsDowngraded > 0 ||
2174
- preRunRepair.stats.consecutiveSameRoleMerged > 0
2175
- ) {
2176
- rlog.warn(
2177
- { phase: "pre_run", ...preRunRepair.stats },
2178
- "Repaired runtime history before provider call",
2179
- );
1530
+ state.reducerCompacted = true;
2180
1531
  }
2181
1532
  }
2182
1533
 
@@ -2195,22 +1546,22 @@ export async function runAgentLoopImpl(
2195
1546
  }
2196
1547
 
2197
1548
  // user-prompt-submit hook: plugins may transform `runMessages` right
2198
- // before the agent loop receives them. Fires once per user turn at
2199
- // the primary `agentLoop.run` only — the re-entry / retry calls
2200
- // further down in this function do not refire it (they're not new
2201
- // user submissions). Plugins may mutate `ctx.latestMessages` in place
2202
- // OR return a new context with a fresh array; `runHook` forwards
2203
- // whichever the chain settles on. Order is plugin registration order.
1549
+ // before the agent loop receives them. Fires once per user turn at the
1550
+ // primary `agentLoop.run` only — the re-entry / retry calls further down
1551
+ // in this function do not refire it (they're not new user submissions).
1552
+ // Plugins may mutate `ctx.latestMessages` in place OR return a new
1553
+ // context with a fresh array; `runHook` forwards whichever the chain
1554
+ // settles on. Order is plugin registration order.
2204
1555
  //
2205
- // Fires BEFORE `preRunHistoryLength` is captured so the boundary
2206
- // between pre-existing and hook-emitted messages consumed by the
2207
- // ordering-error retry gate, the post-run reconcile loop, and the
2208
- // new-message extraction for persistence — reflects exactly what
2209
- // `agentLoop.run` receives.
1556
+ // Fires BEFORE the agent loop runs so the hook-emitted messages are part
1557
+ // of the loop's input; the loop then reports its own appended output via
1558
+ // `AgentLoopRunResult.newMessages`, which is what persistence consumes.
2210
1559
  const userPromptCtx: UserPromptSubmitContext = {
2211
1560
  conversationId: ctx.conversationId,
1561
+ prompt: options?.titleText ?? content,
2212
1562
  originalMessages: ctx.messages,
2213
1563
  latestMessages: runMessages,
1564
+ logger: rlog,
2214
1565
  };
2215
1566
  const finalUserPromptCtx = await runHook(
2216
1567
  HOOKS.USER_PROMPT_SUBMIT,
@@ -2218,8 +1569,6 @@ export async function runAgentLoopImpl(
2218
1569
  );
2219
1570
  runMessages = finalUserPromptCtx.latestMessages;
2220
1571
 
2221
- let preRunHistoryLength = runMessages.length;
2222
-
2223
1572
  const shouldGenerateTitle = isReplaceableTitle(
2224
1573
  getConversation(ctx.conversationId)?.title ?? null,
2225
1574
  );
@@ -2233,42 +1582,18 @@ export async function runAgentLoopImpl(
2233
1582
  rlog,
2234
1583
  turnChannelContext: capturedTurnChannelContext,
2235
1584
  turnInterfaceContext: capturedTurnInterfaceContext,
1585
+ applyCompaction: applySuccessfulCompaction,
2236
1586
  };
2237
- const eventHandler = (event: AgentEvent) =>
1587
+ const eventHandler = (event: AgentEvent): Promise<void> =>
2238
1588
  dispatchAgentEvent(state, deps, event);
2239
1589
  emitTerminalExit = async (reason: AgentLoopExitReason): Promise<void> => {
2240
1590
  await eventHandler({ type: "agent_loop_exit", reason });
2241
1591
  };
2242
1592
 
2243
- const onCheckpoint = async (
2244
- checkpoint: CheckpointInfo,
2245
- ): Promise<CheckpointDecision> => {
2246
- state.currentTurnToolNames = [];
2247
-
1593
+ const onCheckpoint = async (): Promise<CheckpointDecision> => {
2248
1594
  if (ctx.canHandoffAtCheckpoint()) {
2249
- yieldedForHandoff = true;
2250
- pendingCheckpointYield = "handoff";
2251
- return "yield";
1595
+ return "handoff";
2252
1596
  }
2253
-
2254
- // Mid-loop token budget check: estimate current context size and
2255
- // yield if we're approaching the preflight budget. This lets the
2256
- // conversation-agent-loop run compaction before the provider rejects.
2257
- if (overflowRecovery.enabled) {
2258
- const midLoopThreshold =
2259
- resolveCurrentContextBudget().preflightBudget * 0.85;
2260
- const estimated = await runTokenEstimatePipeline(checkpoint.history);
2261
- if (estimated > midLoopThreshold) {
2262
- rlog.warn(
2263
- { phase: "mid-loop", estimated, threshold: midLoopThreshold },
2264
- "Token estimate approaching budget — yielding for compaction",
2265
- );
2266
- yieldedForBudget = true;
2267
- pendingCheckpointYield = "budget";
2268
- return "yield";
2269
- }
2270
- }
2271
-
2272
1597
  return "continue";
2273
1598
  };
2274
1599
 
@@ -2277,26 +1602,83 @@ export async function runAgentLoopImpl(
2277
1602
  rlog.info({ callSite: turnCallSite }, "Starting agent loop run");
2278
1603
 
2279
1604
  // Thread the orchestrator's canonical per-turn context into the agent
2280
- // loop so its internal pipeline invocations (llmCall, emptyResponse,
2281
- // toolError, toolResultTruncate, toolExecute) see the real
2282
- // conversation identity / trust / contextWindowManager instead of the
2283
- // synthesized `"agent-loop"` placeholder. The loop clones this value
1605
+ // loop so its internal pipeline invocations (e.g. compaction) see the
1606
+ // real conversation identity / trust / contextWindowManager instead of
1607
+ // the synthesized `"agent-loop"` placeholder. The loop clones this value
2284
1608
  // and overwrites `turnIndex` with its own tool-use iteration counter.
2285
1609
  const loopTurnCtx = buildPluginTurnContext(ctx, reqId);
2286
1610
 
2287
- let updatedHistory = await ctx.agentLoop.run(
2288
- runMessages,
2289
- eventHandler,
2290
- abortController.signal,
2291
- reqId,
2292
- onCheckpoint,
2293
- turnCallSite,
2294
- loopTurnCtx,
2295
- turnOverrideProfile,
2296
- resolveCurrentMaxInputTokens(),
2297
- resolveCurrentOverrideProfile,
2298
- resolveCurrentMaxInputTokens,
2299
- );
1611
+ // Hook for the loop-owned mid-loop compaction. The agent loop owns the
1612
+ // trigger (its budget gate), the `compaction` pipeline call, the result
1613
+ // interpretation (circuit-breaker bookkeeping + the exhaustion decision),
1614
+ // and the inline continue; this callback bridges the injection state the
1615
+ // loop is intentionally blind to. Durable persistence is signalled via
1616
+ // events; re-injection stays orchestrator-supplied for now.
1617
+ const midLoopCompaction: MidLoopCompaction = {
1618
+ postCompactionHook: async ({ history, turnContext }) => {
1619
+ // stripInjectionsForCompaction() unconditionally removed the existing
1620
+ // memory-static block, so re-inject the current content regardless of
1621
+ // whether compaction actually ran. The `<knowledge_base>`, NOW.md, and
1622
+ // v2 static `<info>` blocks self-gate inside their injectors on block
1623
+ // presence.
1624
+ const injection = await postCompactReinject({
1625
+ ...injectionOpts,
1626
+ // Suppress the chronological-transcript snapshot once the reducer
1627
+ // has collapsed `ctx.messages`; the captured snapshot reflects the
1628
+ // full persisted transcript and would overwrite compaction.
1629
+ slackChronologicalMessages: state.reducerCompacted
1630
+ ? null
1631
+ : injectionOpts.slackChronologicalMessages,
1632
+ mode: currentInjectionMode,
1633
+ turnContext,
1634
+ history,
1635
+ logger: rlog,
1636
+ });
1637
+ return injection.messages;
1638
+ },
1639
+ };
1640
+
1641
+ /**
1642
+ * Shared closure: runs the agent loop with the orchestrator's turn
1643
+ * context and maps the loop's returned checkpoint pause-reason into the
1644
+ * orchestrator's yield bookkeeping. Returns the updated history so call
1645
+ * sites consume it exactly as before. Pass `compaction` only for the
1646
+ * primary run, where the loop compacts in place when its budget gate
1647
+ * trips; reruns omit it and keep yielding for budget.
1648
+ */
1649
+ const runAgentLoop = async (
1650
+ msgs: Message[],
1651
+ compaction?: MidLoopCompaction,
1652
+ ): Promise<Message[]> => {
1653
+ const { history, exitReason, appendedNewMessages, newMessages } =
1654
+ await ctx.agentLoop.run(msgs, eventHandler, {
1655
+ signal: abortController.signal,
1656
+ requestId: reqId,
1657
+ onCheckpoint,
1658
+ callSite: turnCallSite,
1659
+ turnContext: loopTurnCtx,
1660
+ overrideProfile: turnOverrideProfile,
1661
+ resolveOverrideProfile: resolveCurrentOverrideProfile,
1662
+ resolveContextWindow,
1663
+ compaction,
1664
+ // memory-v3-live: the latest user message carries the volatile v3
1665
+ // `<memory>` block, so anchor the provider's long-TTL cache breakpoint
1666
+ // on the most recent stable message instead.
1667
+ mutableLatestUserMessage: memoryV3Live,
1668
+ });
1669
+ lastRunAppendedNewMessages = appendedNewMessages;
1670
+ lastRunNewMessages = newMessages;
1671
+ if (exitReason === "handoff") {
1672
+ yieldedForHandoff = true;
1673
+ pendingCheckpointYield = "handoff";
1674
+ } else if (exitReason === "budget") {
1675
+ yieldedForBudget = true;
1676
+ pendingCheckpointYield = "budget";
1677
+ }
1678
+ return history;
1679
+ };
1680
+
1681
+ let updatedHistory = await runAgentLoop(runMessages, midLoopCompaction);
2300
1682
 
2301
1683
  rlog.info(
2302
1684
  { resultMessageCount: updatedHistory.length },
@@ -2308,211 +1690,43 @@ export async function runAgentLoopImpl(
2308
1690
  pendingCheckpointYield = null;
2309
1691
  }
2310
1692
 
2311
- // ── Proactive mid-loop compaction ───────────────────────────────
2312
- // When the agent loop yielded because the token budget check in
2313
- // onCheckpoint detected approaching limits, run compaction on the
2314
- // accumulated history and re-enter the agent loop. This is distinct
2315
- // from the reactive convergence loop below that fires after a
2316
- // provider rejection — here we compact *before* hitting the limit.
2317
- let midLoopCompactAttempts = 0;
2318
- while (
2319
- yieldedForBudget &&
2320
- midLoopCompactAttempts <
2321
- resolveCurrentContextBudget().overflowRecovery.maxAttempts &&
2322
- !state.contextTooLargeDetected &&
2323
- !abortController.signal.aborted
2324
- ) {
2325
- midLoopCompactAttempts++;
2326
- yieldedForBudget = false;
2327
- pendingCheckpointYield = null;
2328
-
2329
- rlog.info(
2330
- { phase: "mid-loop-compact" },
2331
- "Running compaction after checkpoint yield",
2332
- );
2333
-
2334
- // Strip injected context from updated history before compacting,
2335
- // so we compact the "raw" persistent messages.
2336
- const rawHistory = stripInjectionsForCompaction(updatedHistory);
2337
- ctx.messages = rawHistory;
2338
- markHistoryStrippedBestEffort(ctx.conversationId, Date.now(), rlog);
2339
-
2340
- ctx.emitActivityState(
2341
- "thinking",
2342
- "context_compacting",
2343
- "assistant_turn",
2344
- reqId,
2345
- "Compacting context",
2346
- );
2347
- let midLoopCompact: Awaited<
2348
- ReturnType<typeof ctx.contextWindowManager.maybeCompact>
2349
- >;
2350
- try {
2351
- midLoopCompact = (await runPipeline<CompactionArgs, CompactionResult>(
2352
- "compaction",
2353
- getMiddlewaresFor("compaction"),
2354
- (args) =>
2355
- defaultCompactionTerminal(args, buildPluginTurnContext(ctx, reqId)),
2356
- {
2357
- messages: ctx.messages,
2358
- signal: abortController.signal,
2359
- options: {
2360
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
2361
- force: true,
2362
- targetInputTokensOverride:
2363
- resolveCurrentContextBudget().preflightBudget,
2364
- conversationOriginChannel:
2365
- getConversationOriginChannel(ctx.conversationId) ?? undefined,
2366
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
2367
- },
2368
- },
2369
- buildPluginTurnContext(ctx, reqId),
2370
- DEFAULT_TIMEOUTS.compaction,
2371
- )) as Awaited<ReturnType<typeof ctx.contextWindowManager.maybeCompact>>;
2372
- } catch (err) {
2373
- if (err instanceof PluginTimeoutError) {
2374
- // Mid-loop compaction timed out. Record the failure for the
2375
- // circuit breaker and escalate to the convergence loop's more
2376
- // aggressive reducer tiers (tool-result truncation, media
2377
- // stubbing, injection downgrade) by flipping the overflow flag
2378
- // and breaking out of the mid-loop retry. The existing
2379
- // "exhausted all attempts" block further down handles the
2380
- // escalation.
2381
- rlog.warn(
2382
- { err, phase: "mid-loop-compact" },
2383
- "Compaction pipeline timed out — escalating to convergence loop",
2384
- );
2385
- await trackCompactionOutcome(ctx, true, onEvent);
2386
- state.contextTooLargeDetected = true;
2387
- break;
2388
- }
2389
- throw err;
2390
- }
2391
- // `force: true` bypasses the cooldown/threshold gates but early returns
2392
- // for "no eligible messages" / "insufficient messages" still leave
2393
- // `summaryFailed` undefined. Only track when the summary LLM actually ran.
2394
- if (midLoopCompact.summaryFailed !== undefined) {
2395
- await trackCompactionOutcome(
2396
- ctx,
2397
- midLoopCompact.summaryFailed,
2398
- onEvent,
2399
- );
2400
- }
2401
- if (midLoopCompact.compacted) {
2402
- await applySuccessfulCompaction(midLoopCompact, rawHistory);
2403
- reducerCompacted = true;
2404
- shouldInjectWorkspace = true;
2405
- }
2406
-
2407
- // Re-inject runtime context and re-enter the agent loop.
2408
- // stripInjectionsForCompaction() unconditionally removed the existing
2409
- // NOW.md block from ctx.messages above, so we must always re-inject
2410
- // the current content regardless of whether compaction actually ran.
2411
- const injection = await applyRuntimeInjections(ctx.messages, {
2412
- ...injectionOpts,
2413
- pkbContext: currentPkbContent,
2414
- memoryV2Static: currentMemoryV2Static,
2415
- nowScratchpad: currentNowContent,
2416
- workspaceTopLevelContext: shouldInjectWorkspace
2417
- ? ctx.workspaceTopLevelContext
2418
- : null,
2419
- // Suppress the chronological-transcript snapshot once the reducer
2420
- // has collapsed `ctx.messages`; the captured snapshot reflects the
2421
- // full persisted transcript and would overwrite compaction.
2422
- slackChronologicalMessages: reducerCompacted
2423
- ? null
2424
- : injectionOpts.slackChronologicalMessages,
2425
- mode: currentInjectionMode,
2426
- turnContext: buildPluginTurnContext(ctx, reqId),
2427
- });
2428
- runMessages = injection.messages;
2429
- if (isTrustedActor && currentInjectionMode !== "minimal") {
2430
- ctx.graphMemory.retrackCachedNodes();
2431
- }
2432
- const midLoopCompactStrip = stripHistoricalWebSearchResults(runMessages);
2433
- if (midLoopCompactStrip.stats.blocksStripped > 0) {
2434
- rlog.info(
2435
- { phase: "mid-loop-compact", ...midLoopCompactStrip.stats },
2436
- "Converted historical web_search_tool_result blocks to text summaries",
2437
- );
2438
- runMessages = midLoopCompactStrip.messages;
2439
- }
2440
- preRepairMessages = runMessages;
2441
- preRunHistoryLength = runMessages.length;
2442
-
2443
- updatedHistory = await ctx.agentLoop.run(
2444
- runMessages,
2445
- eventHandler,
2446
- abortController.signal,
2447
- reqId,
2448
- onCheckpoint,
2449
- turnCallSite,
2450
- loopTurnCtx,
2451
- turnOverrideProfile,
2452
- resolveCurrentMaxInputTokens(),
2453
- resolveCurrentOverrideProfile,
2454
- resolveCurrentMaxInputTokens,
2455
- );
2456
- }
2457
-
2458
- // If mid-loop compaction exhausted all attempts but the agent loop
2459
- // still yielded (yieldedForBudget is true), the turn is incomplete.
2460
- // Escalate to the convergence loop's more aggressive reducer tiers
2461
- // (tool-result truncation, media stubbing, injection downgrade)
2462
- // instead of silently treating an incomplete turn as done.
1693
+ // The loop compacts in place when its budget gate trips and only yields
1694
+ // `exitReason = "budget"` when that inline compaction timed out or
1695
+ // exhausted its retry budget (the `reinject` hook has already restored
1696
+ // runtime context for the productive case). Escalate to the convergence
1697
+ // loop's more aggressive reducer tiers so a half-finished turn doesn't
1698
+ // reach the user.
2463
1699
  if (yieldedForBudget && !abortController.signal.aborted) {
2464
1700
  rlog.warn(
2465
- {
2466
- phase: "mid-loop-compact",
2467
- midLoopCompactAttempts,
2468
- maxAttempts:
2469
- resolveCurrentContextBudget().overflowRecovery.maxAttempts,
2470
- },
2471
- "Mid-loop compaction exhausted all attempts — escalating to convergence loop",
1701
+ { phase: "mid-loop-compact" },
1702
+ "Inline compaction could not get under budget — escalating to convergence loop",
2472
1703
  );
2473
1704
  state.contextTooLargeDetected = true;
2474
1705
  }
2475
1706
 
2476
1707
  // One-shot ordering error retry
2477
- if (
2478
- state.orderingErrorDetected &&
2479
- updatedHistory.length === preRunHistoryLength
2480
- ) {
1708
+ if (state.orderingErrorDetected && !lastRunAppendedNewMessages) {
2481
1709
  rlog.warn(
2482
1710
  { phase: "retry" },
2483
1711
  "Provider ordering error detected, attempting one-shot deep-repair retry",
2484
1712
  );
2485
- // Design note: deep-repair intentionally bypasses the `historyRepair`
2486
- // plugin pipeline. Deep-repair is a recovery-only path triggered by a
2487
- // provider ordering error it must be deterministic and unaffected by
2488
- // user middleware that might have caused (or be unable to recover from)
2489
- // the original drift. Plugins can already observe / override the
2490
- // pre-run repair via the `historyRepair` pipeline above; widening that
2491
- // surface to deep-repair is intentionally deferred until there's a
2492
- // concrete plugin-level use case. Do not route this call through
2493
- // `runPipeline` without first revisiting that contract.
2494
- const retryRepair = deepRepairHistory(runMessages);
1713
+ // Design note: deep-repair intentionally stays a direct call rather
1714
+ // than running through the `user-prompt-submit` hook chain. Deep-repair
1715
+ // is a recovery-only path triggered by a provider ordering error — it
1716
+ // must be deterministic and unaffected by user hooks that might have
1717
+ // caused (or be unable to recover from) the original drift. Plugins can
1718
+ // already observe / transform the pre-run repair via the
1719
+ // `user-prompt-submit` hook (the default history-repair plugin runs
1720
+ // `repairHistory` there); widening that surface to deep-repair is
1721
+ // intentionally deferred until there's a concrete plugin-level use case.
1722
+ const retryRepair = deepRepairHistory(updatedHistory);
2495
1723
  runMessages = retryRepair.messages;
2496
1724
  const retryStrip = stripHistoricalWebSearchResults(runMessages);
2497
1725
  runMessages = retryStrip.messages;
2498
- preRepairMessages = runMessages;
2499
- preRunHistoryLength = runMessages.length;
2500
1726
  state.orderingErrorDetected = false;
2501
1727
  state.deferredOrderingError = null;
2502
1728
 
2503
- updatedHistory = await ctx.agentLoop.run(
2504
- runMessages,
2505
- eventHandler,
2506
- abortController.signal,
2507
- reqId,
2508
- onCheckpoint,
2509
- turnCallSite,
2510
- loopTurnCtx,
2511
- turnOverrideProfile,
2512
- resolveCurrentMaxInputTokens(),
2513
- resolveCurrentOverrideProfile,
2514
- resolveCurrentMaxInputTokens,
2515
- );
1729
+ updatedHistory = await runAgentLoop(runMessages);
2516
1730
 
2517
1731
  if (state.orderingErrorDetected) {
2518
1732
  rlog.error(
@@ -2561,29 +1775,31 @@ export async function runAgentLoopImpl(
2561
1775
  }
2562
1776
  // Can't resize — replace with a text annotation so the model
2563
1777
  // can explain the situation rather than silently dropping context
2564
- return [
2565
- {
2566
- type: "text" as const,
2567
- text: "(An image was attached but could not be sent — its dimensions exceed the provider limit and automatic resize was not available. Please resize the image and try again.)",
2568
- },
2569
- ];
1778
+ return [{ type: "text" as const, text: UNSENDABLE_IMAGE_NOTE }];
2570
1779
  }),
2571
1780
  };
2572
1781
  });
1782
+ // The transform above only mutates ctx.messages for the current retry.
1783
+ // Persist the downgrade for images that can never be sent so the rejected
1784
+ // upload doesn't rehydrate from the DB and resurface on later turns. This
1785
+ // is cleanup for future turns, so a persistence failure must never abort
1786
+ // the retry that is about to run — log it and continue.
1787
+ try {
1788
+ const rewritten = persistUnsendableImageDowngrades(ctx.conversationId);
1789
+ if (rewritten > 0) {
1790
+ rlog.info(
1791
+ { phase: "image-recovery", rewritten },
1792
+ "Persisted unsendable-image downgrades so they cannot resurface",
1793
+ );
1794
+ }
1795
+ } catch (err) {
1796
+ rlog.warn(
1797
+ { phase: "image-recovery", err },
1798
+ "Failed to persist unsendable-image downgrade; continuing with in-memory recovery",
1799
+ );
1800
+ }
2573
1801
  runMessages = ctx.messages;
2574
- updatedHistory = await ctx.agentLoop.run(
2575
- runMessages,
2576
- eventHandler,
2577
- abortController.signal,
2578
- reqId,
2579
- onCheckpoint,
2580
- turnCallSite,
2581
- loopTurnCtx,
2582
- turnOverrideProfile,
2583
- resolveCurrentMaxInputTokens(),
2584
- resolveCurrentOverrideProfile,
2585
- resolveCurrentMaxInputTokens,
2586
- );
1802
+ updatedHistory = await runAgentLoop(runMessages);
2587
1803
  if (state.imageTooLargeDetected) {
2588
1804
  rlog.error(
2589
1805
  { phase: "image-recovery" },
@@ -2610,19 +1826,9 @@ export async function runAgentLoopImpl(
2610
1826
  // limit), incorporate those new messages into ctx.messages so the
2611
1827
  // convergence loop operates on the full (larger) history.
2612
1828
  if (state.contextTooLargeDetected) {
2613
- // Detect whether ctx.messages currently lacks NOW.md so we know if
2614
- // it needs to be re-injected. Mid-loop compaction (line ~1067) may
2615
- // have already stripped injections before escalating here, so we
2616
- // check actual message state rather than tracking mutation sites.
2617
- let convergenceStripped =
2618
- findLastInjectedNowContent(ctx.messages) === null;
2619
-
2620
- if (updatedHistory.length > preRunHistoryLength) {
1829
+ if (lastRunAppendedNewMessages) {
2621
1830
  ctx.messages = stripInjectionsForCompaction(updatedHistory);
2622
- markHistoryStrippedBestEffort(ctx.conversationId, Date.now(), rlog);
2623
- convergenceStripped = true;
2624
- preRepairMessages = updatedHistory;
2625
- preRunHistoryLength = updatedHistory.length;
1831
+ markHistoryStrippedBestEffort(ctx.conversationId);
2626
1832
  }
2627
1833
  if (!reducerState) {
2628
1834
  reducerState = createInitialReducerState();
@@ -2703,15 +1909,13 @@ export async function runAgentLoopImpl(
2703
1909
  "Emergency mid-turn compaction succeeded — bypassing reducer tiers",
2704
1910
  );
2705
1911
  if (emergencyResult.summaryFailed !== undefined) {
2706
- await trackCompactionOutcome(
2707
- ctx,
1912
+ await ctx.agentLoop.compactionCircuit.recordOutcome(
2708
1913
  emergencyResult.summaryFailed,
2709
1914
  onEvent,
2710
1915
  );
2711
1916
  }
2712
1917
  if (emergencyResult.compacted) {
2713
1918
  await applySuccessfulCompaction(emergencyResult, ctx.messages);
2714
- shouldInjectWorkspace = true;
2715
1919
  }
2716
1920
  // Clear the overflow flag and re-run the agent loop with
2717
1921
  // the compacted context.
@@ -2744,12 +1948,9 @@ export async function runAgentLoopImpl(
2744
1948
  "Context too large — applying next reducer tier",
2745
1949
  );
2746
1950
 
2747
- ctx.emitActivityState(
2748
- "thinking",
2749
- "context_compacting",
2750
- "assistant_turn",
2751
- reqId,
2752
- );
1951
+ ctx.emitActivityState("thinking", "context_compacting", {
1952
+ requestId: reqId,
1953
+ });
2753
1954
  const convergenceCompactionBasis = ctx.messages;
2754
1955
  const step = await reduceContextOverflow(
2755
1956
  convergenceCompactionBasis,
@@ -2765,6 +1966,7 @@ export async function runAgentLoopImpl(
2765
1966
  ctx.contextWindowManager.maybeCompact(msgs, signal!, {
2766
1967
  ...(opts ?? {}),
2767
1968
  overrideProfile: resolveCurrentOverrideProfile() ?? null,
1969
+ actorTrustClass: resolveTurnActorTrustClass(ctx),
2768
1970
  }),
2769
1971
  abortController.signal,
2770
1972
  );
@@ -2781,8 +1983,7 @@ export async function runAgentLoopImpl(
2781
1983
  step.compactionResult &&
2782
1984
  step.compactionResult.summaryFailed !== undefined
2783
1985
  ) {
2784
- await trackCompactionOutcome(
2785
- ctx,
1986
+ await ctx.agentLoop.compactionCircuit.recordOutcome(
2786
1987
  step.compactionResult.summaryFailed,
2787
1988
  onEvent,
2788
1989
  );
@@ -2793,22 +1994,17 @@ export async function runAgentLoopImpl(
2793
1994
  step.compactionResult,
2794
1995
  convergenceCompactionBasis,
2795
1996
  );
2796
- shouldInjectWorkspace = true;
2797
- reducerCompacted = true;
1997
+ state.reducerCompacted = true;
2798
1998
  }
2799
1999
 
2800
- // Only re-inject NOW.md when ctx.messages was actually stripped;
2801
- // otherwise the existing NOW.md block is still present and
2802
- // re-injecting would duplicate it.
2000
+ // Only re-inject the memory-static block when ctx.messages was
2001
+ // actually stripped; otherwise the existing block is still present and
2002
+ // re-injecting would duplicate it. (The `<knowledge_base>` and NOW.md
2003
+ // blocks self-gate inside their injectors on whether they are already
2004
+ // present in `ctx.messages`.)
2803
2005
  const injection = await applyRuntimeInjections(ctx.messages, {
2804
2006
  ...injectionOpts,
2805
- pkbContext: currentPkbContent,
2806
- memoryV2Static: convergenceStripped ? currentMemoryV2Static : null,
2807
- nowScratchpad: convergenceStripped ? currentNowContent : null,
2808
- workspaceTopLevelContext: shouldInjectWorkspace
2809
- ? ctx.workspaceTopLevelContext
2810
- : null,
2811
- slackChronologicalMessages: reducerCompacted
2007
+ slackChronologicalMessages: state.reducerCompacted
2812
2008
  ? null
2813
2009
  : injectionOpts.slackChronologicalMessages,
2814
2010
  mode: currentInjectionMode,
@@ -2826,24 +2022,10 @@ export async function runAgentLoopImpl(
2826
2022
  );
2827
2023
  runMessages = convergenceStrip.messages;
2828
2024
  }
2829
- preRepairMessages = runMessages;
2830
- preRunHistoryLength = runMessages.length;
2831
2025
  state.contextTooLargeDetected = false;
2832
2026
  yieldedForBudget = false;
2833
2027
 
2834
- updatedHistory = await ctx.agentLoop.run(
2835
- runMessages,
2836
- eventHandler,
2837
- abortController.signal,
2838
- reqId,
2839
- onCheckpoint,
2840
- turnCallSite,
2841
- loopTurnCtx,
2842
- turnOverrideProfile,
2843
- resolveCurrentMaxInputTokens(),
2844
- resolveCurrentOverrideProfile,
2845
- resolveCurrentMaxInputTokens,
2846
- );
2028
+ updatedHistory = await runAgentLoop(runMessages);
2847
2029
 
2848
2030
  // If the rerun still yields at checkpoint, the turn is still
2849
2031
  // incomplete — continue reducing through the remaining tiers
@@ -2862,12 +2044,9 @@ export async function runAgentLoopImpl(
2862
2044
  // Fold rerun progress into ctx.messages so the next reducer
2863
2045
  // tier operates on up-to-date history instead of stale
2864
2046
  // pre-rerun messages.
2865
- if (updatedHistory.length > preRunHistoryLength) {
2047
+ if (lastRunAppendedNewMessages) {
2866
2048
  ctx.messages = stripInjectionsForCompaction(updatedHistory);
2867
- markHistoryStrippedBestEffort(ctx.conversationId, Date.now(), rlog);
2868
- convergenceStripped = true;
2869
- preRepairMessages = updatedHistory;
2870
- preRunHistoryLength = updatedHistory.length;
2049
+ markHistoryStrippedBestEffort(ctx.conversationId);
2871
2050
  }
2872
2051
  }
2873
2052
  }
@@ -2884,88 +2063,38 @@ export async function runAgentLoopImpl(
2884
2063
 
2885
2064
  if (action === "auto_compress_latest_turn") {
2886
2065
  // Auto-compress without asking — users opt out via the "drop" policy.
2887
- ctx.emitActivityState(
2888
- "thinking",
2889
- "context_compacting",
2890
- "assistant_turn",
2891
- reqId,
2892
- );
2893
- let emergencyCompact: Awaited<
2894
- ReturnType<typeof ctx.contextWindowManager.maybeCompact>
2895
- > | null = null;
2896
- try {
2897
- emergencyCompact = (await runPipeline<
2898
- CompactionArgs,
2899
- CompactionResult
2900
- >(
2901
- "compaction",
2902
- getMiddlewaresFor("compaction"),
2903
- (args) =>
2904
- defaultCompactionTerminal(
2905
- args,
2906
- buildPluginTurnContext(ctx, reqId),
2907
- ),
2908
- {
2909
- messages: ctx.messages,
2910
- signal: abortController.signal,
2911
- options: {
2912
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
2913
- force: true,
2914
- minKeepRecentUserTurns: 0,
2915
- targetInputTokensOverride: correctedTarget,
2916
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
2917
- },
2918
- },
2919
- buildPluginTurnContext(ctx, reqId),
2920
- DEFAULT_TIMEOUTS.compaction,
2921
- )) as Awaited<
2922
- ReturnType<typeof ctx.contextWindowManager.maybeCompact>
2923
- >;
2924
- } catch (err) {
2925
- if (err instanceof PluginTimeoutError) {
2926
- // Emergency compaction timed out. Record the circuit-breaker
2927
- // failure and fall through to the graceful-error path below
2928
- // (the unsuccessful-compaction fallback) rather than hard-
2929
- // failing the turn.
2930
- rlog.warn(
2931
- { err, phase: "emergency-compaction" },
2932
- "Emergency compaction pipeline timed out — continuing with overflow fallback",
2933
- );
2934
- await trackCompactionOutcome(ctx, true, onEvent);
2935
- emergencyCompact = null;
2936
- } else {
2937
- throw err;
2938
- }
2939
- }
2066
+ ctx.emitActivityState("thinking", "context_compacting", {
2067
+ requestId: reqId,
2068
+ });
2069
+ const emergencyCompact = await defaultCompact({
2070
+ manager: ctx.contextWindowManager,
2071
+ messages: ctx.messages,
2072
+ signal: abortController.signal,
2073
+ force: true,
2074
+ minKeepRecentUserTurns: 0,
2075
+ overrideProfile: resolveCurrentOverrideProfile() ?? null,
2076
+ });
2940
2077
  // Only track when the summary LLM actually ran; `force: true`
2941
- // bypasses the cooldown but not the early-return paths.
2942
- if (
2943
- emergencyCompact &&
2944
- emergencyCompact.summaryFailed !== undefined
2945
- ) {
2946
- await trackCompactionOutcome(
2947
- ctx,
2078
+ // bypasses the auto-threshold gate but not the early-return paths.
2079
+ if (emergencyCompact.summaryFailed !== undefined) {
2080
+ await ctx.agentLoop.compactionCircuit.recordOutcome(
2948
2081
  emergencyCompact.summaryFailed,
2949
2082
  onEvent,
2950
2083
  );
2951
2084
  }
2952
- if (emergencyCompact?.compacted) {
2085
+ if (emergencyCompact.compacted) {
2953
2086
  await applySuccessfulCompaction(emergencyCompact, ctx.messages);
2954
- reducerCompacted = true;
2955
- shouldInjectWorkspace = true;
2087
+ state.reducerCompacted = true;
2956
2088
  }
2957
2089
 
2958
- // Only re-inject NOW.md when ctx.messages was actually stripped;
2959
- // otherwise the existing block is still present.
2090
+ // Only re-inject the memory-static block when ctx.messages was
2091
+ // actually stripped; otherwise the existing block is still present.
2092
+ // (The `<knowledge_base>`, NOW.md, and v2 static `<info>` blocks
2093
+ // self-gate inside their injectors on whether they are already
2094
+ // present in `ctx.messages`.)
2960
2095
  const injection = await applyRuntimeInjections(ctx.messages, {
2961
2096
  ...injectionOpts,
2962
- pkbContext: currentPkbContent,
2963
- memoryV2Static: convergenceStripped ? currentMemoryV2Static : null,
2964
- nowScratchpad: convergenceStripped ? currentNowContent : null,
2965
- workspaceTopLevelContext: shouldInjectWorkspace
2966
- ? ctx.workspaceTopLevelContext
2967
- : null,
2968
- slackChronologicalMessages: reducerCompacted
2097
+ slackChronologicalMessages: state.reducerCompacted
2969
2098
  ? null
2970
2099
  : injectionOpts.slackChronologicalMessages,
2971
2100
  mode: currentInjectionMode,
@@ -2983,23 +2112,9 @@ export async function runAgentLoopImpl(
2983
2112
  );
2984
2113
  runMessages = fallbackStrip.messages;
2985
2114
  }
2986
- preRepairMessages = runMessages;
2987
- preRunHistoryLength = runMessages.length;
2988
2115
  state.contextTooLargeDetected = false;
2989
2116
 
2990
- updatedHistory = await ctx.agentLoop.run(
2991
- runMessages,
2992
- eventHandler,
2993
- abortController.signal,
2994
- reqId,
2995
- onCheckpoint,
2996
- turnCallSite,
2997
- loopTurnCtx,
2998
- turnOverrideProfile,
2999
- resolveCurrentMaxInputTokens(),
3000
- resolveCurrentOverrideProfile,
3001
- resolveCurrentMaxInputTokens,
3002
- );
2117
+ updatedHistory = await runAgentLoop(runMessages);
3003
2118
  }
3004
2119
  // action === "fail_gracefully" falls through to the final error below
3005
2120
  }
@@ -3050,44 +2165,11 @@ export async function runAgentLoopImpl(
3050
2165
  onEvent(buildConversationErrorMessage(ctx.conversationId, classified));
3051
2166
  }
3052
2167
 
3053
- // Reconcile synthesized cancellation tool_results
3054
- for (let i = preRunHistoryLength; i < updatedHistory.length; i++) {
3055
- const msg = updatedHistory[i];
3056
- if (msg.role === "user") {
3057
- for (const block of msg.content) {
3058
- if (
3059
- block.type === "tool_result" &&
3060
- !state.pendingToolResults.has(block.tool_use_id) &&
3061
- !state.persistedToolUseIds.has(block.tool_use_id)
3062
- ) {
3063
- state.pendingToolResults.set(block.tool_use_id, {
3064
- content: block.content,
3065
- isError: block.is_error ?? false,
3066
- });
3067
- }
3068
- }
3069
- }
3070
- }
3071
-
3072
- // Flush remaining tool results
2168
+ // Flush remaining tool results. On a normal turn these drain at the next
2169
+ // `message_complete`; an aborted or yielded loop exits with them still
2170
+ // buffered, so finalize the (possibly already on-arrival-reserved) grouped
2171
+ // row here rather than writing a duplicate.
3073
2172
  if (state.pendingToolResults.size > 0) {
3074
- const toolResultBlocks = Array.from(
3075
- state.pendingToolResults.entries(),
3076
- ).map(([toolUseId, result]) => ({
3077
- type: "tool_result",
3078
- tool_use_id: toolUseId,
3079
- content: redactSecrets(result.content),
3080
- is_error: result.isError,
3081
- ...(result.contentBlocks
3082
- ? {
3083
- contentBlocks: result.contentBlocks.map((block) =>
3084
- block.type === "text"
3085
- ? { ...block, text: redactSecrets(block.text) }
3086
- : block,
3087
- ),
3088
- }
3089
- : {}),
3090
- }));
3091
2173
  const toolResultMetadata = {
3092
2174
  ...provenanceFromTrustContext(ctx.trustContext),
3093
2175
  userMessageChannel: capturedTurnChannelContext.userMessageChannel,
@@ -3097,21 +2179,12 @@ export async function runAgentLoopImpl(
3097
2179
  assistantMessageInterface:
3098
2180
  capturedTurnInterfaceContext.assistantMessageInterface,
3099
2181
  };
3100
- await runPipeline<PersistArgs, PersistResult>(
3101
- "persistence",
3102
- getMiddlewaresFor("persistence"),
3103
- defaultPersistenceTerminal,
3104
- {
3105
- op: "add",
3106
- conversationId: ctx.conversationId,
3107
- role: "user",
3108
- content: JSON.stringify(toolResultBlocks),
3109
- metadata: toolResultMetadata,
3110
- },
3111
- buildPluginTurnContext(ctx, reqId),
3112
- DEFAULT_TIMEOUTS.persistence,
2182
+ await finalizePendingToolResultRow(
2183
+ state,
2184
+ ctx.conversationId,
2185
+ toolResultMetadata,
2186
+ rlog,
3113
2187
  );
3114
- state.pendingToolResults.clear();
3115
2188
  }
3116
2189
 
3117
2190
  // Persist the budget_yield_unrecovered notice now that any pending
@@ -3135,24 +2208,13 @@ export async function runAgentLoopImpl(
3135
2208
  };
3136
2209
  let yieldNoticePersistedId: string | null = null;
3137
2210
  try {
3138
- const yieldPersistResult = (await runPipeline<
3139
- PersistArgs,
3140
- PersistResult
3141
- >(
3142
- "persistence",
3143
- getMiddlewaresFor("persistence"),
3144
- defaultPersistenceTerminal,
3145
- {
3146
- op: "add",
3147
- conversationId: ctx.conversationId,
3148
- role: "assistant",
3149
- content: JSON.stringify(yieldNoticeMessage.content),
3150
- metadata: yieldNoticeMetadata,
3151
- },
3152
- buildPluginTurnContext(ctx, reqId),
3153
- DEFAULT_TIMEOUTS.persistence,
3154
- )) as PersistAddResult;
3155
- yieldNoticePersistedId = yieldPersistResult.message.id;
2211
+ const yieldRow = await addMessage(
2212
+ ctx.conversationId,
2213
+ "assistant",
2214
+ JSON.stringify(yieldNoticeMessage.content),
2215
+ { metadata: yieldNoticeMetadata },
2216
+ );
2217
+ yieldNoticePersistedId = yieldRow.id;
3156
2218
  } catch (err) {
3157
2219
  // Non-fatal — a DB hiccup must not escalate a budget-yield exit into
3158
2220
  // a turn-level throw. The live SSE event was already emitted, so the
@@ -3208,7 +2270,7 @@ export async function runAgentLoopImpl(
3208
2270
  }
3209
2271
 
3210
2272
  // Reconstruct history
3211
- const newMessages = updatedHistory.slice(preRunHistoryLength).map((msg) => {
2273
+ const newMessages = lastRunNewMessages.map((msg) => {
3212
2274
  if (msg.role !== "assistant") return msg;
3213
2275
  const { cleanedContent } = cleanAssistantContent(msg.content);
3214
2276
  const cleanedBlocks = cleanedContent as ContentBlock[];
@@ -3239,10 +2301,6 @@ export async function runAgentLoopImpl(
3239
2301
  state.assistantRowAwaitingFinalization &&
3240
2302
  state.lastAssistantMessageId
3241
2303
  ) {
3242
- // Direct `deleteMessageById` (not via the `persistence` pipeline):
3243
- // see the same rationale on the matching cleanup in
3244
- // `handleLlmCallStarted` — an unfinalized reservation has no
3245
- // observable history for plugins.
3246
2304
  try {
3247
2305
  deleteMessageById(state.lastAssistantMessageId);
3248
2306
  } catch (err) {
@@ -3264,20 +2322,12 @@ export async function runAgentLoopImpl(
3264
2322
  const errorAssistantMessage = createAssistantMessage(
3265
2323
  state.providerErrorUserMessage,
3266
2324
  );
3267
- const errorPersistResult = (await runPipeline<PersistArgs, PersistResult>(
3268
- "persistence",
3269
- getMiddlewaresFor("persistence"),
3270
- defaultPersistenceTerminal,
3271
- {
3272
- op: "add",
3273
- conversationId: ctx.conversationId,
3274
- role: "assistant",
3275
- content: JSON.stringify(errorAssistantMessage.content),
3276
- metadata: errChannelMeta,
3277
- },
3278
- buildPluginTurnContext(ctx, reqId),
3279
- DEFAULT_TIMEOUTS.persistence,
3280
- )) as PersistAddResult;
2325
+ const errorRow = await addMessage(
2326
+ ctx.conversationId,
2327
+ "assistant",
2328
+ JSON.stringify(errorAssistantMessage.content),
2329
+ { metadata: errChannelMeta },
2330
+ );
3281
2331
  persistedErrorAssistantMessage = true;
3282
2332
  // Repoint `lastAssistantMessageId` at the synthetic error row so the
3283
2333
  // post-loop sync, attachment resolution, and `message_complete`/
@@ -3286,7 +2336,7 @@ export async function runAgentLoopImpl(
3286
2336
  // above. Mark finalization complete so the next LLM call in this run
3287
2337
  // (or a downstream handler) doesn't try to clean up an id that
3288
2338
  // already corresponds to a finalized row.
3289
- state.lastAssistantMessageId = errorPersistResult.message.id;
2339
+ state.lastAssistantMessageId = errorRow.id;
3290
2340
  state.assistantRowAwaitingFinalization = false;
3291
2341
  newMessages.push(errorAssistantMessage);
3292
2342
  // Pipe the just-assigned message id into any orphaned LLM request log
@@ -3300,10 +2350,7 @@ export async function runAgentLoopImpl(
3300
2350
  // other conversations cannot collide. Non-fatal — a DB hiccup must
3301
2351
  // not escalate a provider rejection into a turn-level throw.
3302
2352
  try {
3303
- backfillMessageIdOnLogs(
3304
- ctx.conversationId,
3305
- errorPersistResult.message.id,
3306
- );
2353
+ backfillMessageIdOnLogs(ctx.conversationId, errorRow.id);
3307
2354
  } catch (err) {
3308
2355
  rlog.warn(
3309
2356
  { err },
@@ -3316,7 +2363,16 @@ export async function runAgentLoopImpl(
3316
2363
  // would create a duplicate plain-text bubble below the alert card.
3317
2364
  }
3318
2365
 
3319
- let restoredHistory = [...preRepairMessages, ...newMessages];
2366
+ // Base persisted into `ctx.messages` is the loop's own returned history
2367
+ // (minus the tail it appended this run), with the cleaned `newMessages`
2368
+ // re-appended on top. Sourcing the base from the loop keeps it in lockstep
2369
+ // with any in-loop compaction without the orchestrator maintaining a
2370
+ // parallel snapshot across re-entry sites.
2371
+ const loopBase = updatedHistory.slice(
2372
+ 0,
2373
+ updatedHistory.length - lastRunNewMessages.length,
2374
+ );
2375
+ let restoredHistory = [...loopBase, ...newMessages];
3320
2376
 
3321
2377
  // Post-turn tool result truncation: save large results to disk and
3322
2378
  // replace in-context content with a prefix/suffix stub + file pointer.
@@ -3389,7 +2445,10 @@ export async function runAgentLoopImpl(
3389
2445
  // so the client can re-enable the UI without delay.
3390
2446
  if (abortController.signal.aborted) {
3391
2447
  syncLastAssistantMessageToDisk();
3392
- ctx.emitActivityState("idle", "generation_cancelled", "global", reqId);
2448
+ ctx.emitActivityState("idle", "generation_cancelled", {
2449
+ anchor: "global",
2450
+ requestId: reqId,
2451
+ });
3393
2452
  ctx.traceEmitter.emit(
3394
2453
  "generation_cancelled",
3395
2454
  "Generation cancelled by user",
@@ -3433,7 +2492,10 @@ export async function runAgentLoopImpl(
3433
2492
  await emitTerminalExit?.("aborted_after_checkpoint");
3434
2493
  pendingCheckpointYield = null;
3435
2494
  }
3436
- ctx.emitActivityState("idle", "generation_cancelled", "global", reqId);
2495
+ ctx.emitActivityState("idle", "generation_cancelled", {
2496
+ anchor: "global",
2497
+ requestId: reqId,
2498
+ });
3437
2499
  ctx.traceEmitter.emit(
3438
2500
  "generation_cancelled",
3439
2501
  "Generation cancelled by user",
@@ -3474,7 +2536,10 @@ export async function runAgentLoopImpl(
3474
2536
  });
3475
2537
  publishLoopMessagesChanged();
3476
2538
  } else {
3477
- ctx.emitActivityState("idle", "message_complete", "global", reqId);
2539
+ ctx.emitActivityState("idle", "message_complete", {
2540
+ anchor: "global",
2541
+ requestId: reqId,
2542
+ });
3478
2543
  ctx.traceEmitter.emit(
3479
2544
  "message_complete",
3480
2545
  "Message processing complete",
@@ -3497,68 +2562,8 @@ export async function runAgentLoopImpl(
3497
2562
  : {}),
3498
2563
  });
3499
2564
  publishLoopMessagesChanged();
3500
-
3501
- // Proactive artifact: fire once when the processed turn was the 4th user message.
3502
- // Only trigger for real user-authored turns (not subagent/system messages).
3503
- {
3504
- const paConv = getConversation(ctx.conversationId);
3505
- if (
3506
- paConv &&
3507
- paConv.conversationType === "standard" &&
3508
- options?.isUserMessage
3509
- ) {
3510
- void (async () => {
3511
- try {
3512
- if (hasProactiveArtifactCompleted()) return;
3513
- const userMsg = getMessageById(
3514
- userMessageId,
3515
- ctx.conversationId,
3516
- );
3517
- if (!userMsg) return;
3518
- if (!tryClaimProactiveArtifactTrigger(userMsg.createdAt))
3519
- return;
3520
- await runProactiveArtifactJob({
3521
- conversationId: ctx.conversationId,
3522
- userMessageCutoff: userMsg.createdAt,
3523
- assistantMessageId: state.lastAssistantMessageId,
3524
- suppressAppBuild: state.appBuildToolUsedThisRun,
3525
- broadcastMessage,
3526
- });
3527
- } catch (err) {
3528
- log.warn(
3529
- { err, conversationId: ctx.conversationId },
3530
- "Proactive artifact trigger failed",
3531
- );
3532
- }
3533
- })();
3534
- }
3535
- }
3536
2565
  }
3537
2566
  }
3538
-
3539
- // Second title pass: after 3 completed turns, re-generate the title
3540
- // using the last 3 messages for better context. Only fires when the
3541
- // current title was auto-generated (isAutoTitle = 1) and the user
3542
- // has not opted out via `conversations.skipAutoRetitling`.
3543
- if (ctx.turnCount === 2 && !getConfig().conversations.skipAutoRetitling) {
3544
- // turnCount is 0-indexed, incremented in finally; 2 = about to become 3rd turn
3545
- queueRegenerateConversationTitle({
3546
- conversationId: ctx.conversationId,
3547
- provider: ctx.provider,
3548
- onTitleUpdated: (title) => {
3549
- onEvent({
3550
- type: "conversation_title_updated",
3551
- conversationId: ctx.conversationId,
3552
- title,
3553
- });
3554
- onEvent({
3555
- type: "sync_changed",
3556
- tags: [conversationMetadataSyncTag(ctx.conversationId)],
3557
- });
3558
- },
3559
- signal: abortController.signal,
3560
- });
3561
- }
3562
2567
  } catch (err) {
3563
2568
  const errorCtx = {
3564
2569
  phase: "agent_loop" as const,
@@ -3569,7 +2574,10 @@ export async function runAgentLoopImpl(
3569
2574
  await emitTerminalExit?.("aborted_after_checkpoint");
3570
2575
  pendingCheckpointYield = null;
3571
2576
  }
3572
- ctx.emitActivityState("idle", "generation_cancelled", "global", reqId);
2577
+ ctx.emitActivityState("idle", "generation_cancelled", {
2578
+ anchor: "global",
2579
+ requestId: reqId,
2580
+ });
3573
2581
  rlog.info("Generation cancelled by user");
3574
2582
  ctx.traceEmitter.emit(
3575
2583
  "generation_cancelled",
@@ -3585,7 +2593,10 @@ export async function runAgentLoopImpl(
3585
2593
  });
3586
2594
  publishLoopMessagesChanged();
3587
2595
  } else {
3588
- ctx.emitActivityState("idle", "error_terminal", "global", reqId);
2596
+ ctx.emitActivityState("idle", "error_terminal", {
2597
+ anchor: "global",
2598
+ requestId: reqId,
2599
+ });
3589
2600
  const message = err instanceof Error ? err.message : String(err);
3590
2601
  const errorClass = err instanceof Error ? err.constructor.name : "Error";
3591
2602
  rlog.error({ err }, "Conversation processing error");
@@ -3612,8 +2623,6 @@ export async function runAgentLoopImpl(
3612
2623
  }
3613
2624
  } finally {
3614
2625
  if (turnStarted) {
3615
- cleanupBootstrapAfterTurnThreshold(ctx.conversationId);
3616
-
3617
2626
  ctx.turnCount++;
3618
2627
  const config = getConfig();
3619
2628
  const maxWait = config.workspaceGit?.turnCommitMaxWaitMs ?? 4000;
@@ -3651,7 +2660,7 @@ export async function runAgentLoopImpl(
3651
2660
  ctx.profiler.emitSummary(ctx.traceEmitter, reqId);
3652
2661
 
3653
2662
  ctx.abortController = null;
3654
- ctx.processing = false;
2663
+ ctx.setProcessing(false);
3655
2664
  ctx.onConfirmationOutcome = undefined;
3656
2665
  ctx.surfaceActionRequestIds.delete(ctx.currentRequestId ?? "");
3657
2666
  ctx.approvedViaPromptThisTurn = false;
@@ -3798,7 +2807,7 @@ export async function applyCompactionResult(
3798
2807
  result.summaryText,
3799
2808
  ctx.contextCompactedMessageCount,
3800
2809
  );
3801
- markHistoryStrippedBestEffort(ctx.conversationId, compactedAt, log);
2810
+ markHistoryStrippedBestEffort(ctx.conversationId);
3802
2811
  if (options.slackContextCompactionWatermarkTs) {
3803
2812
  updateConversationSlackContextWatermark(
3804
2813
  ctx.conversationId,